diff --git a/client/package-lock.json b/client/package-lock.json index 1bc9c26..da51458 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -5165,6 +5165,11 @@ "escape-string-regexp": "^1.0.5" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "filesize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", diff --git a/client/package.json b/client/package.json index 1afb77b..7bbf205 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ "@microsoft/signalr": "^5.0.8", "bulma": "^0.9.3", "curray": "^1.0.8", + "file-saver": "^2.0.5", "ngx-filesize": "^2.0.16", "rxjs": "~7.2.0", "tslib": "^2.3.0", diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.html b/client/src/app/add-new-torrent/add-new-torrent.component.html index bd5af13..7f7b275 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.html +++ b/client/src/app/add-new-torrent/add-new-torrent.component.html @@ -40,7 +40,7 @@
@@ -66,8 +66,8 @@
@@ -84,10 +84,16 @@

- These files are available for immediate download from Real Debrid.
+ These files are available for immediate download from Real-Debrid.
It is possible that there are more files in the torrent, which are not shown here.

+
+ +
@@ -155,10 +155,10 @@
- When selected, it will only download files in the torrent that have been download by Real Debrid. You can use + When selected, it will only download files in the torrent that have been download by Real-Debrid. You can use this in combination with the Min File size setting above.
@@ -189,7 +189,7 @@
- +
- This will attempt to download a 10GB file from Real Debrid. When 50MB has been downloaded the test will stop. + This will attempt to download a 10GB file from Real-Debrid. When 50MB has been downloaded the test will stop.
diff --git a/client/src/app/setup/setup.component.html b/client/src/app/setup/setup.component.html index 5f53e35..cbe8fac 100644 --- a/client/src/app/setup/setup.component.html +++ b/client/src/app/setup/setup.component.html @@ -6,7 +6,7 @@
- Welcome to RealDebrid Client. Please create your account by entering a username and password. + Welcome to Real-Debrid Client. Please create your account by entering a username and password.
@@ -44,14 +44,14 @@
- To be able to use the RealDebrid Client you need a premium subscription to download torrents. + To be able to use the Real-Debrid Client you need a premium subscription to download torrents.

Not premium yet? Use this link to sign up to RealDebrid.Use this link to sign up to Real-Debrid.

- To connect to RealDebrid please enter your Personal Prive API Token found here: + To connect to Real-Debrid please enter your Personal Prive API Token found here: https://real-debrid.com/apitoken. @@ -82,7 +82,7 @@
- You are now connected to RealDebrid! To setup Radarr or Sonarr, please following the instructions here: + You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here: https://github.com/rogerfar/rdt-client/. diff --git a/client/src/app/torrent-download/torrent-download.component.html b/client/src/app/torrent-download/torrent-download.component.html deleted file mode 100644 index c4fbf06..0000000 --- a/client/src/app/torrent-download/torrent-download.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - - {{ download.link }} - {{ download.path }} - - - {{ download.bytesTotal | filesize }} - - - {{ download | downloadStatus }} - - diff --git a/client/src/app/torrent-download/torrent-download.component.scss b/client/src/app/torrent-download/torrent-download.component.scss deleted file mode 100644 index 03bcfa7..0000000 --- a/client/src/app/torrent-download/torrent-download.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -td { - font-size: smaller; -} diff --git a/client/src/app/torrent-download/torrent-download.component.ts b/client/src/app/torrent-download/torrent-download.component.ts deleted file mode 100644 index 85787b6..0000000 --- a/client/src/app/torrent-download/torrent-download.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Download } from '../models/download.model'; - -@Component({ - selector: '[app-torrent-download]', - templateUrl: './torrent-download.component.html', - styleUrls: ['./torrent-download.component.scss'], -}) -export class TorrentDownloadComponent implements OnInit { - @Input() - public download: Download; - - constructor() {} - - ngOnInit(): void {} -} diff --git a/client/src/app/torrent-file/torrent-file.component.html b/client/src/app/torrent-file/torrent-file.component.html deleted file mode 100644 index 267193d..0000000 --- a/client/src/app/torrent-file/torrent-file.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - {{ file.path }} - -{{ file.bytes | filesize }} - diff --git a/client/src/app/torrent-file/torrent-file.component.scss b/client/src/app/torrent-file/torrent-file.component.scss deleted file mode 100644 index 03bcfa7..0000000 --- a/client/src/app/torrent-file/torrent-file.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -td { - font-size: smaller; -} diff --git a/client/src/app/torrent-file/torrent-file.component.ts b/client/src/app/torrent-file/torrent-file.component.ts deleted file mode 100644 index 4ae3346..0000000 --- a/client/src/app/torrent-file/torrent-file.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { TorrentFile } from '../models/torrent.model'; - -@Component({ - selector: '[app-torrent-file]', - templateUrl: './torrent-file.component.html', - styleUrls: ['./torrent-file.component.scss'], -}) -export class TorrentFileComponent implements OnInit { - @Input() - public file: TorrentFile; - - constructor() {} - - ngOnInit(): void {} -} diff --git a/client/src/app/torrent-row/torrent-row.component.html b/client/src/app/torrent-row/torrent-row.component.html deleted file mode 100644 index b69355e..0000000 --- a/client/src/app/torrent-row/torrent-row.component.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ torrent.rdName }} - - {{ torrent.files.length | number }} - - - {{ torrent.downloads.length | number }} - - - - - - {{ torrent.rdSize | filesize }} - - - {{ torrent | status }} - - - - - - - - - - - - diff --git a/client/src/app/torrent-row/torrent-row.component.scss b/client/src/app/torrent-row/torrent-row.component.scss deleted file mode 100644 index 6a01b12..0000000 --- a/client/src/app/torrent-row/torrent-row.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -.delete-icon { - color: red; - cursor: pointer; -} - -.auto .fas { - margin-right: 4px; -} \ No newline at end of file diff --git a/client/src/app/torrent-row/torrent-row.component.ts b/client/src/app/torrent-row/torrent-row.component.ts deleted file mode 100644 index 76554c5..0000000 --- a/client/src/app/torrent-row/torrent-row.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Torrent } from 'src/app/models/torrent.model'; - -@Component({ - selector: '[app-torrent-row]', - templateUrl: './torrent-row.component.html', - styleUrls: ['./torrent-row.component.scss'], -}) -export class TorrentRowComponent implements OnInit { - @Input() - public torrent: Torrent; - - @Output('delete') - public delete = new EventEmitter(); - - @Output('retry') - public retry = new EventEmitter(); - - public loading = false; - - constructor() {} - - ngOnInit(): void {} - - public deleteClick(event: Event): void { - event.stopPropagation(); - this.delete.emit(this.torrent.torrentId); - } - - public retryClick(event: Event): void { - event.stopPropagation(); - this.retry.emit(this.torrent.torrentId); - } -} diff --git a/client/src/app/torrent-status.pipe.ts b/client/src/app/torrent-status.pipe.ts index 5d81a0a..2b9b7d6 100644 --- a/client/src/app/torrent-status.pipe.ts +++ b/client/src/app/torrent-status.pipe.ts @@ -28,6 +28,11 @@ export class TorrentStatusPipe implements PipeTransform { const bytesDone = downloading.sum((m) => m.bytesDone); const bytesTotal = downloading.sum((m) => m.bytesTotal); let progress = (bytesDone / bytesTotal) * 100; + + if (isNaN(progress)) { + progress = 0; + } + let allSpeeds = downloading.sum((m) => m.speed) / downloading.length; let speed: string | string[] = '0'; @@ -44,6 +49,11 @@ export class TorrentStatusPipe implements PipeTransform { const bytesDone = unpacking.sum((m) => m.bytesDone); const bytesTotal = unpacking.sum((m) => m.bytesTotal); let progress = (bytesDone / bytesTotal) * 100; + + if (isNaN(progress)) { + progress = 0; + } + let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length; if (allSpeeds > 0) { diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html index ce51221..4f37dc2 100644 --- a/client/src/app/torrent-table/torrent-table.component.html +++ b/client/src/app/torrent-table/torrent-table.component.html @@ -7,126 +7,34 @@ Name + Category Files Downloads - Auto Size Status - Action - - - - - Downloads - - - - Files in torrent - - - - + + + {{ torrent.rdName }} + + + {{ torrent.category }} + + + {{ torrent.files.length | number }} + + + {{ torrent.downloads.length | number }} + + + {{ torrent.rdSize | filesize }} + + + {{ torrent | status }} + +
- - - - diff --git a/client/src/app/torrent-table/torrent-table.component.scss b/client/src/app/torrent-table/torrent-table.component.scss index fd54999..41c6f23 100644 --- a/client/src/app/torrent-table/torrent-table.component.scss +++ b/client/src/app/torrent-table/torrent-table.component.scss @@ -2,11 +2,4 @@ table { tr { cursor: pointer; } - - tr.separator { - td { - font-size: smaller; - font-weight: bold; - } - } } diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts index e59569b..6a5a8c9 100644 --- a/client/src/app/torrent-table/torrent-table.component.ts +++ b/client/src/app/torrent-table/torrent-table.component.ts @@ -1,4 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { Torrent } from '../models/torrent.model'; import { TorrentService } from '../torrent.service'; @@ -10,23 +11,8 @@ import { TorrentService } from '../torrent.service'; export class TorrentTableComponent implements OnInit, OnDestroy { public torrents: Torrent[] = []; public error: string; - public showFiles: { [key: string]: boolean } = {}; - public isDeleteModalActive: boolean; - public deleteError: string; - public deleting: boolean; - public deleteTorrentId: string; - public deleteData: boolean; - public deleteRdTorrent: boolean; - public deleteLocalFiles: boolean; - - public isRetryModalActive: boolean; - public retryError: string; - public retrying: boolean; - public retryTorrentId: string; - public retry: number; - - constructor(private torrentService: TorrentService) {} + constructor(private router: Router, private torrentService: TorrentService) {} ngOnInit(): void { this.torrentService.getList().subscribe( @@ -49,67 +35,11 @@ export class TorrentTableComponent implements OnInit, OnDestroy { this.torrentService.disconnect(); } - public selectTorrent(torrent: Torrent): void { - this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId]; + public selectTorrent(torrentId: string): void { + this.router.navigate([`/torrent/${torrentId}`]); } public trackByMethod(index: number, el: Torrent): string { return el.torrentId; } - - public showDeleteModal(torrentId: string): void { - this.deleteData = false; - this.deleteRdTorrent = false; - this.deleteLocalFiles = false; - - this.deleteTorrentId = torrentId; - this.isDeleteModalActive = true; - } - - public deleteCancel(): void { - this.isDeleteModalActive = false; - } - - public deleteOk(): void { - this.deleting = true; - - this.torrentService - .delete(this.deleteTorrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles) - .subscribe( - () => { - this.isDeleteModalActive = false; - this.deleting = false; - }, - (err) => { - this.deleteError = err.error; - this.deleting = false; - } - ); - } - - public showRetryModal(torrentId: string): void { - this.retry = 0; - - this.retryTorrentId = torrentId; - this.isRetryModalActive = true; - } - - public retryCancel(): void { - this.isRetryModalActive = false; - } - - public retryOk(): void { - this.retrying = true; - - this.torrentService.retry(this.retryTorrentId, this.retry).subscribe( - () => { - this.isRetryModalActive = false; - this.retrying = false; - }, - (err) => { - this.retryError = err.error; - this.retrying = false; - } - ); - } } diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts index 18edb14..4b8f22b 100644 --- a/client/src/app/torrent.service.ts +++ b/client/src/app/torrent.service.ts @@ -15,6 +15,10 @@ export class TorrentService { private connection: signalR.HubConnection; public connect(): void { + if (this.connection != null) { + return; + } + this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build(); this.connection.start().catch((err) => console.error(err)); @@ -31,6 +35,10 @@ export class TorrentService { return this.http.get(`/Api/Torrents`); } + public get(torrentId: string): Observable { + return this.http.get(`/Api/Torrents/Get/${torrentId}`); + } + public uploadMagnet( magnetLink: string, category: string, @@ -91,9 +99,11 @@ export class TorrentService { }); } - public retry(torrentId: string, retry: number): Observable { - return this.http.post(`/Api/Torrents/Retry/${torrentId}`, { - retry, - }); + public retry(torrentId: string): Observable { + return this.http.post(`/Api/Torrents/Retry/${torrentId}`, {}); + } + + public retryDownload(downloadId: string): Observable { + return this.http.post(`/Api/Torrents/RetryDownload/${downloadId}`, {}); } } diff --git a/client/src/app/torrent/torrent.component.html b/client/src/app/torrent/torrent.component.html new file mode 100644 index 0000000..9b59205 --- /dev/null +++ b/client/src/app/torrent/torrent.component.html @@ -0,0 +1,439 @@ +
+ +
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+ + {{ torrent | status }} +
+
+ + {{ torrent.hash }} +
+
+ + {{ torrent.category || "(no category set)" }} +
+
+ + + Download all files above a certain size + Download all available files on Real-Debrid above a certain size + Pick files I want to download + +
+
+ + + Do nothing + Remove torrent from Real-Debrid and Real-Debrid Client + Remove torrent from Real-Debrid + +
+
+ + {{ torrent.downloadMinSize }}MB +
+
+ + Click to copy magnet link to clipboard + Link copied to clipboard! +
+
+ + Click to download torrent file +
+
+ + {{ torrent.added | date: "fullDate" }} {{ torrent.added | date: "mediumTime" }} +
+
+ + (no files selected yet) + + {{ torrent.filesSelected | date: "fullDate" }} {{ torrent.filesSelected | date: "mediumTime" }} + +
+
+ + (not completed yet) + + {{ torrent.completed | date: "fullDate" }} {{ torrent.completed | date: "mediumTime" }} + +
+
+
+
+ + {{ torrent.rdId }} +
+
+ + {{ torrent.rdName }} +
+
+ + {{ torrent.rdSize | filesize }} +
+
+ + {{ torrent.rdHost }} +
+
+ + {{ torrent.rdSplit }} +
+
+ + {{ torrent.rdProgress || 0 }}% +
+
+ + + Processing + Waiting For File Selection + Downloading + Finished + Error + + ({{ torrent.rdStatusRaw }}) +
+
+ + {{ torrent.rdAdded | date: "fullDate" }} {{ torrent.rdAdded | date: "mediumTime" }} +
+
+ + {{ torrent.rdEnded | date: "fullDate" }} {{ torrent.rdEnded | date: "mediumTime" }} +
+
+ + {{ torrent.rdSpeed || 0 }} +
+
+ + {{ torrent.rdSeeders || "0" }} +
+
+
+
+
+ + + + + + + + + + + + + + + + + +
IDPathSizeSelected
+ {{ file.id }} + + {{ file.path }} + + {{ file.bytes | filesize }} + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
LinkSizeStatus
+ + + + + {{ download.link | decodeURI }} + + + {{ download.bytesTotal | filesize }} + + {{ download | downloadStatus }} +
+
+
+
+
+ +
+
+
+ + {{ download.error }} +
+
+ + + {{ download.link | decodeURI }} +
+
+ + {{ download.path }} +
+
+ + {{ download.bytesDone | filesize }} / {{ download.bytesTotal | filesize }} ({{ + download.speed | filesize + }}/s) +
+
+ + {{ download.retryCount }} +
+
+
+
+ + + {{ download.added | date: "fullDate" }} {{ download.added | date: "mediumTime" }} + + (not added yet) +
+
+ + + {{ download.downloadQueued | date: "fullDate" }} + {{ download.downloadQueued | date: "mediumTime" }} + + (not queued for downloading yet) +
+
+ + + {{ download.downloadStarted | date: "fullDate" }} + {{ download.downloadStarted | date: "mediumTime" }} + + (not started downloading yet) +
+
+ + + {{ download.downloadFinished | date: "fullDate" }} + {{ download.downloadFinished | date: "mediumTime" }} + + (not finished yet) +
+
+ + + {{ download.unpackingQueued | date: "fullDate" }} + {{ download.unpackingQueued | date: "mediumTime" }} + + (not queued for unpacking yet) +
+
+ + + {{ download.unpackingStarted | date: "fullDate" }} + {{ download.unpackingStarted | date: "mediumTime" }} + + (not started unpacking yet) +
+
+ + + {{ download.unpackingFinished | date: "fullDate" }} + {{ download.unpackingFinished | date: "mediumTime" }} + + (not finished unpacking yet) +
+
+ + + {{ download.completed | date: "fullDate" }} + {{ download.completed | date: "mediumTime" }} + + (not completed yet) +
+
+
+
+
+
+
+ + + + + + diff --git a/client/src/app/torrent/torrent.component.scss b/client/src/app/torrent/torrent.component.scss new file mode 100644 index 0000000..d81c596 --- /dev/null +++ b/client/src/app/torrent/torrent.component.scss @@ -0,0 +1,13 @@ +.label { + margin-bottom: 0; +} + +table { + tr { + cursor: pointer; + } +} + +.fa-download { + margin-left :12px; +} \ No newline at end of file diff --git a/client/src/app/torrent/torrent.component.ts b/client/src/app/torrent/torrent.component.ts new file mode 100644 index 0000000..9e276a0 --- /dev/null +++ b/client/src/app/torrent/torrent.component.ts @@ -0,0 +1,168 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { saveAs } from 'file-saver'; +import { Torrent } from '../models/torrent.model'; +import { TorrentService } from '../torrent.service'; + +@Component({ + selector: 'app-torrent', + templateUrl: './torrent.component.html', + styleUrls: ['./torrent.component.scss'], +}) +export class TorrentComponent implements OnInit { + public torrent: Torrent; + + public activeTab: number = 0; + + public copied: boolean = false; + + public downloadExpanded: { [downloadId: string]: boolean } = {}; + + public isDeleteModalActive: boolean; + public deleteError: string; + public deleting: boolean; + public deleteData: boolean; + public deleteRdTorrent: boolean; + public deleteLocalFiles: boolean; + + public isRetryModalActive: boolean; + public retryError: string; + public retrying: boolean; + + public isDownloadRetryModalActive: boolean; + public downloadRetryError: string; + public downloadRetrying: boolean; + public downloadRetryId: string; + + constructor(private activatedRoute: ActivatedRoute, private router: Router, private torrentService: TorrentService) {} + + ngOnInit(): void { + this.activatedRoute.params.subscribe((params) => { + const torrentId = params['id']; + + this.torrentService.get(torrentId).subscribe( + (torrent) => { + this.torrent = torrent; + + this.torrentService.connect(); + + this.torrentService.update$.subscribe((result) => { + this.update(result); + }); + }, + () => { + this.router.navigate(['/']); + } + ); + }); + } + + public update(torrents: Torrent[]): void { + const updatedTorrent = torrents.firstOrDefault((m) => m.torrentId === this.torrent.torrentId); + + if (updatedTorrent == null) { + return; + } + + this.torrent = updatedTorrent; + } + + public download(): void { + const byteArray = new Uint8Array( + window + .atob(this.torrent.fileOrMagnet) + .split('') + .map(function (c) { + return c.charCodeAt(0); + }) + ); + + var blob = new Blob([byteArray], { type: 'application/x-bittorrent' }); + saveAs(blob, `${this.torrent.rdName}.torrent`); + } + + public showDeleteModal(): void { + this.deleteData = false; + this.deleteRdTorrent = false; + this.deleteLocalFiles = false; + this.deleteError = null; + + this.isDeleteModalActive = true; + } + + public deleteCancel(): void { + this.isDeleteModalActive = false; + } + + public deleteOk(): void { + this.deleting = true; + + this.torrentService + .delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles) + .subscribe( + () => { + this.isDeleteModalActive = false; + this.deleting = false; + + this.router.navigate(['/']); + }, + (err) => { + this.deleteError = err.error; + this.deleting = false; + } + ); + } + + public showRetryModal(): void { + this.retryError = null; + + this.isRetryModalActive = true; + } + + public retryCancel(): void { + this.isRetryModalActive = false; + } + + public retryOk(): void { + this.retrying = true; + + this.torrentService.retry(this.torrent.torrentId).subscribe( + () => { + this.isRetryModalActive = false; + this.retrying = false; + + this.router.navigate(['/']); + }, + (err) => { + this.retryError = err.error; + this.retrying = false; + } + ); + } + + public showDownloadRetryModal(downloadId: string): void { + this.downloadRetryId = downloadId; + this.downloadRetryError = null; + + this.isDownloadRetryModalActive = true; + } + + public downloadRetryCancel(): void { + this.isDownloadRetryModalActive = false; + } + + public downloadRetryOk(): void { + this.downloadRetrying = true; + + this.torrentService.retryDownload(this.downloadRetryId).subscribe( + () => { + this.isDownloadRetryModalActive = false; + this.downloadRetrying = false; + }, + (err) => { + this.downloadRetryError = err.error; + this.downloadRetrying = false; + } + ); + } +} diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index 1767ce0..ad06880 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -6,29 +6,12 @@ using Download = RdtClient.Data.Models.Data.Download; namespace RdtClient.Data.Data { - public interface IDownloadData - { - Task GetById(Guid downloadId); - Task Get(Guid torrentId, String path); - Task Add(Guid torrentId, String path); - Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink); - Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateError(Guid downloadId, String error); - Task UpdateRetryCount(Guid downloadId, Int32 retryCount); - Task DeleteForTorrent(Guid torrentId); - } - - public class DownloadData : IDownloadData + public class DownloadData { private readonly DataContext _dataContext; - private readonly ITorrentData _torrentData; + private readonly TorrentData _torrentData; - public DownloadData(DataContext dataContext, ITorrentData torrentData) + public DownloadData(DataContext dataContext, TorrentData torrentData) { _dataContext = dataContext; _torrentData = torrentData; @@ -44,7 +27,9 @@ namespace RdtClient.Data.Data public async Task Get(Guid torrentId, String path) { - return await _dataContext.Downloads.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path); + return await _dataContext.Downloads + .AsNoTracking() + .FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path); } public async Task Add(Guid torrentId, String path) @@ -193,5 +178,27 @@ namespace RdtClient.Data.Data await _torrentData.VoidCache(); } + + public async Task Reset(Guid downloadId) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.RetryCount = 0; + dbDownload.Link = null; + dbDownload.Added = DateTimeOffset.UtcNow; + dbDownload.DownloadQueued = DateTimeOffset.UtcNow; + dbDownload.DownloadStarted = null; + dbDownload.DownloadFinished = null; + dbDownload.UnpackingQueued = null; + dbDownload.UnpackingStarted = null; + dbDownload.UnpackingFinished = null; + dbDownload.Completed = null; + dbDownload.Error = null; + + await _dataContext.SaveChangesAsync(); + + await _torrentData.VoidCache(); + } } } \ No newline at end of file diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs index 2db5c99..043fafa 100644 --- a/server/RdtClient.Data/Data/SettingData.cs +++ b/server/RdtClient.Data/Data/SettingData.cs @@ -8,14 +8,7 @@ using RdtClient.Data.Models.Data; namespace RdtClient.Data.Data { - public interface ISettingData - { - Task> GetAll(); - Task Update(IList settings); - Task UpdateString(String key, String value); - } - - public class SettingData : ISettingData + public class SettingData { private static IList _settingCache; private static readonly SemaphoreSlim _settingCacheLock = new(1); diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs index 8d6103c..c411fd0 100644 --- a/server/RdtClient.Data/Data/TorrentData.cs +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -9,31 +9,7 @@ using RdtClient.Data.Models.Data; namespace RdtClient.Data.Data { - public interface ITorrentData - { - Task> Get(); - Task GetById(Guid torrentId); - Task GetByHash(String hash); - - Task Add(String realDebridId, - String hash, - String category, - TorrentDownloadAction downloadAction, - TorrentFinishedAction finishedAction, - Int32 downloadMinSize, - String downloadManualFiles, - String fileOrMagnetContents, - Boolean isFile); - - Task UpdateRdData(Torrent torrent); - Task UpdateCategory(Guid torrentId, String category); - Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime); - Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime); - Task Delete(Guid torrentId); - Task VoidCache(); - } - - public class TorrentData : ITorrentData + public class TorrentData { private static IList _torrentCache; private static readonly SemaphoreSlim _torrentCacheLock = new(1); diff --git a/server/RdtClient.Data/Data/UserData.cs b/server/RdtClient.Data/Data/UserData.cs index 6cd44ae..0ddbb9e 100644 --- a/server/RdtClient.Data/Data/UserData.cs +++ b/server/RdtClient.Data/Data/UserData.cs @@ -4,12 +4,7 @@ using Microsoft.EntityFrameworkCore; namespace RdtClient.Data.Data { - public interface IUserData - { - Task GetUser(); - } - - public class UserData : IUserData + public class UserData { private readonly DataContext _dataContext; diff --git a/server/RdtClient.Data/DiConfig.cs b/server/RdtClient.Data/DiConfig.cs index 369abb8..c96f6ca 100644 --- a/server/RdtClient.Data/DiConfig.cs +++ b/server/RdtClient.Data/DiConfig.cs @@ -7,10 +7,10 @@ namespace RdtClient.Data { public static void Config(IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } } diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index d2a890c..29a4d0e 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -7,13 +7,13 @@ namespace RdtClient.Service { public static void Config(IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } } \ No newline at end of file diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs new file mode 100644 index 0000000..1989727 --- /dev/null +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; +using System.Linq; +using System.Web; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Helpers +{ + public static class DownloadHelper + { + public static String GetDownloadPath(String downloadPath, Torrent torrent, Download download) + { + var fileUrl = download.Link; + + if (String.IsNullOrWhiteSpace(fileUrl)) + { + return null; + } + + var uri = new Uri(fileUrl); + var torrentPath = Path.Combine(downloadPath, torrent.RdName); + + if (!Directory.Exists(torrentPath)) + { + Directory.CreateDirectory(torrentPath); + } + + var fileName = uri.Segments.Last(); + + fileName = HttpUtility.UrlDecode(fileName); + + var filePath = Path.Combine(torrentPath, fileName); + + return filePath; + } + } +} diff --git a/server/RdtClient.Service/Services/Authentication.cs b/server/RdtClient.Service/Services/Authentication.cs index ada92a7..a8a0a22 100644 --- a/server/RdtClient.Service/Services/Authentication.cs +++ b/server/RdtClient.Service/Services/Authentication.cs @@ -5,21 +5,13 @@ using RdtClient.Data.Data; namespace RdtClient.Service.Services { - public interface IAuthentication - { - Task Register(String userName, String password); - Task Login(String userName, String password); - Task GetUser(); - Task Logout(); - } - - public class Authentication : IAuthentication + public class Authentication { private readonly SignInManager _signInManager; private readonly UserManager _userManager; - private readonly IUserData _userData; + private readonly UserData _userData; - public Authentication(SignInManager signInManager, UserManager userManager, IUserData userData) + public Authentication(SignInManager signInManager, UserManager userManager, UserData userData) { _signInManager = signInManager; _userManager = userManager; diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 879e4d2..a0b32d3 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Threading.Tasks; -using System.Web; using Downloader; using RdtClient.Data.Models.Data; using RdtClient.Service.Helpers; @@ -44,36 +42,24 @@ namespace RdtClient.Service.Services try { - var downloadClientSetting = settings.GetString("DownloadClient"); + var filePath = DownloadHelper.GetDownloadPath(_destinationPath, _torrent, _download); + + if (filePath == null) + { + throw new Exception("Invalid download path"); + } - var fileUrl = _download.Link; - - if (String.IsNullOrWhiteSpace(fileUrl)) - { - throw new Exception("File URL is empty"); - } - - var uri = new Uri(fileUrl); - var torrentPath = Path.Combine(_destinationPath, _torrent.RdName); - - if (!Directory.Exists(torrentPath)) - { - Directory.CreateDirectory(torrentPath); - } - - var fileName = uri.Segments.Last(); - - fileName = HttpUtility.UrlDecode(fileName); - - var filePath = Path.Combine(torrentPath, fileName); - if (File.Exists(filePath)) { File.Delete(filePath); } + var uri = new Uri(_download.Link); + await Task.Factory.StartNew(async delegate { + var downloadClientSetting = settings.GetString("DownloadClient"); + switch (downloadClientSetting) { case "Simple": diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index 245a526..e2c3aae 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -5,28 +5,11 @@ using Download = RdtClient.Data.Models.Data.Download; namespace RdtClient.Service.Services { - public interface IDownloads + public class Downloads { - Task GetById(Guid downloadId); - Task Get(Guid torrentId, String path); - Task Add(Guid torrentId, String path); - Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink); - Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime); - Task UpdateError(Guid downloadId, String error); - Task UpdateRetryCount(Guid downloadId, Int32 retryCount); - Task DeleteForTorrent(Guid torrentId); - } + private readonly DownloadData _downloadData; - public class Downloads : IDownloads - { - private readonly IDownloadData _downloadData; - - public Downloads(IDownloadData downloadData) + public Downloads(DownloadData downloadData) { _downloadData = downloadData; } @@ -95,5 +78,10 @@ namespace RdtClient.Service.Services { await _downloadData.DeleteForTorrent(torrentId); } + + public async Task Reset(Guid downloadId) + { + await _downloadData.Reset(downloadId); + } } } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index c554759..aae8bba 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -9,31 +9,13 @@ using RdtClient.Service.Models.QBittorrent.QuickType; namespace RdtClient.Service.Services { - public interface IQBittorrent + public class QBittorrent { - Task AuthLogin(String userName, String password); - Task AuthLogout(); - Task AppPreferences(); - Task AppDefaultSavePath(); - Task> TorrentInfo(); - Task> TorrentFileContents(String hash); - Task TorrentProperties(String hash); - Task TorrentsDelete(String hash, Boolean deleteFiles); - Task TorrentsAddMagnet(String magnetLink, String category); - Task TorrentsAddFile(Byte[] fileBytes, String category); - Task TorrentsSetCategory(String hash, String category); - Task> TorrentsCategories(); - Task CategoryCreate(String category); - Task CategoryRemove(String category); - } + private readonly Authentication _authentication; + private readonly Settings _settings; + private readonly Torrents _torrents; - public class QBittorrent : IQBittorrent - { - private readonly IAuthentication _authentication; - private readonly ISettings _settings; - private readonly ITorrents _torrents; - - public QBittorrent(ISettings settings, IAuthentication authentication, ITorrents torrents) + public QBittorrent(Settings settings, Authentication authentication, Torrents torrents) { _settings = settings; _authentication = authentication; diff --git a/server/RdtClient.Service/Services/RemoteService.cs b/server/RdtClient.Service/Services/RemoteService.cs index 60f26c5..5ff41b7 100644 --- a/server/RdtClient.Service/Services/RemoteService.cs +++ b/server/RdtClient.Service/Services/RemoteService.cs @@ -5,17 +5,12 @@ using Microsoft.AspNetCore.SignalR; namespace RdtClient.Service.Services { - public interface IRemoteService - { - Task Update(); - } - - public class RemoteService : IRemoteService + public class RemoteService { private readonly IHubContext _hub; - private readonly ITorrents _torrents; + private readonly Torrents _torrents; - public RemoteService(IHubContext hub, ITorrents torrents) + public RemoteService(IHubContext hub, Torrents torrents) { _hub = hub; _torrents = torrents; diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index 56e206b..8aadd3a 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -10,24 +10,11 @@ using RdtClient.Data.Models.Data; namespace RdtClient.Service.Services { - public interface ISettings + public class Settings { - Task> GetAll(); - Task Update(IList settings); - Task UpdateString(String key, String value); - Task GetString(String key); - Task GetNumber(String key); - Task TestPath(String path); - Task TestDownloadSpeed(CancellationToken cancellationToken); - Task TestWriteSpeed(); - void Clean(); - } + private readonly SettingData _settingData; - public class Settings : ISettings - { - private readonly ISettingData _settingData; - - public Settings(ISettingData settingData) + public Settings(SettingData settingData) { _settingData = settingData; } diff --git a/server/RdtClient.Service/Services/TaskRunner.cs b/server/RdtClient.Service/Services/TaskRunner.cs index 5db1ad4..cd6d224 100644 --- a/server/RdtClient.Service/Services/TaskRunner.cs +++ b/server/RdtClient.Service/Services/TaskRunner.cs @@ -23,7 +23,7 @@ namespace RdtClient.Service.Services await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); using var scope = _serviceProvider.CreateScope(); - var torrentRunner = scope.ServiceProvider.GetRequiredService(); + var torrentRunner = scope.ServiceProvider.GetRequiredService(); _logger.LogInformation("TaskRunner started."); @@ -33,12 +33,18 @@ namespace RdtClient.Service.Services { try { + await Torrents.TorrentResetLock.WaitAsync(stoppingToken); + await torrentRunner.Tick(); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick"); } + finally + { + Torrents.TorrentResetLock.Release(); + } await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 3340ad7..4b2c934 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -11,13 +11,7 @@ using Serilog; namespace RdtClient.Service.Services { - public interface ITorrentRunner - { - Task Initialize(); - Task Tick(); - } - - public class TorrentRunner : ITorrentRunner + public class TorrentRunner { private const Int32 RetryCount = 3; @@ -25,13 +19,13 @@ namespace RdtClient.Service.Services public static readonly ConcurrentDictionary ActiveDownloadClients = new(); public static readonly ConcurrentDictionary ActiveUnpackClients = new(); - private readonly IDownloads _downloads; - private readonly IRemoteService _remoteService; + private readonly Downloads _downloads; + private readonly RemoteService _remoteService; - private readonly ISettings _settings; - private readonly ITorrents _torrents; + private readonly Settings _settings; + private readonly Torrents _torrents; - public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads, IRemoteService remoteService) + public TorrentRunner(Settings settings, Torrents torrents, Downloads downloads, RemoteService remoteService) { _settings = settings; _torrents = torrents; @@ -174,10 +168,10 @@ namespace RdtClient.Service.Services Log.Debug($"Found {torrents.Count} torrents"); - // Only poll RealDebrid every second when a hub is connected, otherwise every 30 seconds + // Only poll Real-Debrid every second when a hub is connected, otherwise every 30 seconds if (_nextUpdate < DateTime.UtcNow && torrents.Count > 0) { - Log.Debug($"Updating torrent info from RealDebrid"); + Log.Debug($"Updating torrent info from Real-Debrid"); var updateTime = 30; @@ -193,7 +187,7 @@ namespace RdtClient.Service.Services // Re-get torrents to account for updated info torrents = await _torrents.Get(); - Log.Debug($"Finished updating torrent info from RealDebrid, next update in {updateTime} seconds"); + Log.Debug($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds"); } torrents = torrents.Where(m => m.Completed == null).ToList(); @@ -279,6 +273,13 @@ namespace RdtClient.Service.Services var torrentDownload = torrents.First(m => m.TorrentId == download.TorrentId); + if (download.Link == null) + { + await _downloads.UpdateError(download.DownloadId, "Download Link cannot be null"); + + continue; + } + // Check if the unpacking process is even needed var uri = new Uri(download.Link); var fileName = uri.Segments.Last(); @@ -346,7 +347,7 @@ namespace RdtClient.Service.Services foreach (var torrent in torrents) { - // If torrent is erroring out on the RealDebrid side, skip processing this torrent. + // If torrent is erroring out on the Real-Debrid side, skip processing this torrent. if (torrent.RdStatus == RealDebridStatus.Error) { Log.Debug($"Torrent {torrent.RdId} has an error: {torrent.RdStatusRaw}, not processing further"); @@ -354,14 +355,15 @@ namespace RdtClient.Service.Services continue; } - // The files are selected but there are no downloads yet, check if Real Debrid has generated links yet. + // The files are selected but there are no downloads yet, check if Real-Debrid has generated links yet. if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null) { await _torrents.CheckForLinks(torrent.TorrentId); } - // RealDebrid is waiting for file selection, select which files to download. + // Real-Debrid is waiting for file selection, select which files to download. if ((torrent.RdStatus == RealDebridStatus.WaitingForFileSelection || torrent.RdStatus == RealDebridStatus.Finished) && + torrent.FilesSelected == null && torrent.Downloads.Count == 0 && torrent.FilesSelected == null) { @@ -420,7 +422,7 @@ namespace RdtClient.Service.Services await _torrents.UpdateFilesSelected(torrent.TorrentId, DateTime.UtcNow); } - // RealDebrid finished downloading the torrent, process the file to host. + // Real-Debrid finished downloading the torrent, process the file to host. if (torrent.RdStatus == RealDebridStatus.Finished) { // If the torrent has any files that need starting to be downloaded, download them. @@ -483,11 +485,11 @@ namespace RdtClient.Service.Services switch (torrent.FinishedAction) { case TorrentFinishedAction.RemoveAllTorrents: - Log.Debug($"Torrent {torrent.RdId} removing torrents from Real Debrid and Real Debrid Client, no files"); + Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid and Real-Debrid Client, no files"); await _torrents.Delete(torrent.TorrentId, true, true, false); break; case TorrentFinishedAction.RemoveRealDebrid: - Log.Debug($"Torrent {torrent.RdId} removing torrents from Real Debrid, no files"); + Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid, no files"); await _torrents.Delete(torrent.TorrentId, false, true, false); break; case TorrentFinishedAction.None: diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index c0aefdb..fced88d 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -9,57 +9,25 @@ using Newtonsoft.Json; using RDNET; using RdtClient.Data.Data; using RdtClient.Data.Enums; +using RdtClient.Service.Helpers; using RdtClient.Service.Models; using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services { - public interface ITorrents + public class Torrents { - Task> Get(); - Task GetByHash(String hash); - Task UpdateCategory(String hash, String category); + public static readonly SemaphoreSlim TorrentResetLock = new(1, 1); - Task UploadMagnet(String magnetLink, - String category, - TorrentDownloadAction downloadAction, - TorrentFinishedAction finishedAction, - Int32 downloadMinSize, - String downloadManualFiles); + private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); - Task UploadFile(Byte[] bytes, - String category, - TorrentDownloadAction downloadAction, - TorrentFinishedAction finishedAction, - Int32 downloadMinSize, - String downloadManualFiles); - - Task> GetAvailableFiles(String hash); - Task SelectFiles(Guid torrentId, IList fileIds); - Task CheckForLinks(Guid torrentId); - Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles); - Task UnrestrictLink(Guid downloadId); - Task Download(Guid downloadId); - Task Unpack(Guid downloadId); - void Reset(); - Task GetProfile(); - Task UpdateComplete(Guid torrentId, DateTimeOffset datetime); - Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime); - Task Update(); - Task Retry(Guid id, Int32 retry); - } - - public class Torrents : ITorrents - { private static RdNetClient _rdtNetClient; - private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1); + private readonly Downloads _downloads; + private readonly Settings _settings; + private readonly TorrentData _torrentData; - private readonly IDownloads _downloads; - private readonly ISettings _settings; - private readonly ITorrentData _torrentData; - - public Torrents(ITorrentData torrentData, ISettings settings, IDownloads downloads) + public Torrents(TorrentData torrentData, Settings settings, Downloads downloads) { _torrentData = torrentData; _settings = settings; @@ -271,6 +239,47 @@ namespace RdtClient.Service.Services } } + private async Task DeleteDownload(Guid torrentId, Guid downloadId) + { + var torrent = await GetById(torrentId); + var download = await _downloads.GetById(downloadId); + + var downloadPath = await DownloadPath(torrent); + + var filePath = DownloadHelper.GetDownloadPath(downloadPath, torrent, download); + + if (filePath == null) + { + return; + } + + if (File.Exists(filePath)) + { + var retry = 0; + + while (true) + { + try + { + File.Delete(filePath); + + break; + } + catch + { + retry++; + + if (retry >= 3) + { + throw; + } + + await Task.Delay(1000); + } + } + } + } + public async Task UnrestrictLink(Guid downloadId) { var download = await _downloads.GetById(downloadId); @@ -322,12 +331,7 @@ namespace RdtClient.Service.Services public async Task Update() { - var w = await SemaphoreSlim.WaitAsync(1); - - if (!w) - { - return; - } + await RealDebridUpdateLock.WaitAsync(); var torrents = await Get(); @@ -360,23 +364,42 @@ namespace RdtClient.Service.Services } finally { - SemaphoreSlim.Release(); + RealDebridUpdateLock.Release(); } } - public async Task Retry(Guid id, Int32 retry) + public async Task RetryTorrent(Guid torrentId) { - var torrent = await _torrentData.GetById(id); + var torrent = await _torrentData.GetById(torrentId); - if (retry == 0) + foreach (var download in torrent.Downloads) { - await Delete(id, true, true, true); - - if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet)) + while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - throw new Exception($"Cannot re-add this torrent, original magnet or file not found"); + downloadClient.Cancel(); + + await Task.Delay(100); } + while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) + { + unpackClient.Cancel(); + + await Task.Delay(100); + } + } + + await Delete(torrentId, true, true, true); + + if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet)) + { + throw new Exception($"Cannot re-add this torrent, original magnet or file not found"); + } + + await TorrentResetLock.WaitAsync(); + + try + { if (torrent.IsFile) { var bytes = Convert.FromBase64String(torrent.FileOrMagnet); @@ -393,19 +416,52 @@ namespace RdtClient.Service.Services torrent.DownloadManualFiles); } } - else if (retry == 1) + finally { - await Delete(id, false, false, true); - - await _torrentData.UpdateComplete(id, null); - await _downloads.DeleteForTorrent(id); - } - else - { - throw new Exception($"Invalid retry option {retry}"); + TorrentResetLock.Release(); } } + public async Task RetryDownload(Guid downloadId) + { + var download = await _downloads.GetById(downloadId); + + if (download == null) + { + return; + } + + while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + { + downloadClient.Cancel(); + + await Task.Delay(100); + } + + while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) + { + unpackClient.Cancel(); + + await Task.Delay(100); + } + + await TorrentResetLock.WaitAsync(); + + try + { + + await DeleteDownload(download.TorrentId, download.DownloadId); + + await _torrentData.UpdateComplete(download.TorrentId, null); + + await _downloads.Reset(downloadId); + } + finally + { + TorrentResetLock.Release(); + } + } + public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime) { await _torrentData.UpdateComplete(torrentId, datetime); @@ -425,7 +481,7 @@ namespace RdtClient.Service.Services if (String.IsNullOrWhiteSpace(apiKey)) { - throw new Exception("RealDebrid API Key not set in the settings"); + throw new Exception("Real-Debrid API Key not set in the settings"); } _rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey); @@ -441,6 +497,22 @@ namespace RdtClient.Service.Services if (torrent != null) { await Update(torrent); + + foreach (var download in torrent.Downloads) + { + if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + { + download.Speed = downloadClient.Speed; + download.BytesTotal = downloadClient.BytesTotal; + download.BytesDone = downloadClient.BytesDone; + } + + if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) + { + download.BytesTotal = unpackClient.BytesTotal; + download.BytesDone = unpackClient.BytesDone; + } + } } return torrent; @@ -456,13 +528,8 @@ namespace RdtClient.Service.Services String fileOrMagnetContents, Boolean isFile) { - var w = await SemaphoreSlim.WaitAsync(60000); - - if (!w) - { - throw new Exception("Unable to add torrent, could not obtain lock"); - } - + await RealDebridUpdateLock.WaitAsync(); + try { var torrent = await _torrentData.GetByHash(infoHash); @@ -486,7 +553,7 @@ namespace RdtClient.Service.Services } finally { - SemaphoreSlim.Release(); + RealDebridUpdateLock.Release(); } } diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs index ae0ac96..d2e8be7 100644 --- a/server/RdtClient.Service/Services/UnpackClient.cs +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Web; using RdtClient.Data.Models.Data; +using RdtClient.Service.Helpers; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Common; @@ -24,7 +24,7 @@ namespace RdtClient.Service.Services private readonly String _destinationPath; private readonly Torrent _torrent; - private Boolean _cancelled = false; + private Boolean _cancelled; private RarArchiveEntry _rarCurrentEntry; private Dictionary _rarfileStatus; @@ -43,25 +43,11 @@ namespace RdtClient.Service.Services try { - var fileUrl = _download.Link; + var filePath = DownloadHelper.GetDownloadPath(_destinationPath, _torrent, _download); - if (String.IsNullOrWhiteSpace(fileUrl)) + if (filePath == null) { - throw new Exception("File URL is empty"); - } - - var uri = new Uri(fileUrl); - var torrentPath = Path.Combine(_destinationPath, _torrent.RdName); - - var fileName = uri.Segments.Last(); - - fileName = HttpUtility.UrlDecode(fileName); - - var filePath = Path.Combine(torrentPath, fileName); - - if (!File.Exists(filePath)) - { - throw new Exception($"File {filePath} could not be extracted because it is missing"); + throw new Exception("Invalid download path"); } await Task.Factory.StartNew(async delegate @@ -88,6 +74,11 @@ namespace RdtClient.Service.Services { try { + if (!File.Exists(filePath)) + { + return; + } + await using (Stream stream = File.OpenRead(filePath)) { using var archive = RarArchive.Open(stream); @@ -97,7 +88,7 @@ namespace RdtClient.Service.Services var entries = archive.Entries.Where(entry => !entry.IsDirectory) .ToList(); - _rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L); + _rarfileStatus = entries.ToDictionary(entry => entry.Key, _ => 0L); _rarCurrentEntry = null; archive.CompressedBytesRead += ArchiveOnCompressedBytesRead; @@ -148,7 +139,7 @@ namespace RdtClient.Service.Services } catch (Exception ex) { - Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + Error = $"An unexpected error occurred unpacking {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; } finally { diff --git a/server/RdtClient.Web/Controllers/AuthController.cs b/server/RdtClient.Web/Controllers/AuthController.cs index 233fc07..279a406 100644 --- a/server/RdtClient.Web/Controllers/AuthController.cs +++ b/server/RdtClient.Web/Controllers/AuthController.cs @@ -10,9 +10,9 @@ namespace RdtClient.Web.Controllers [Route("Api/Authentication")] public class AuthController : Controller { - private readonly IAuthentication _authentication; + private readonly Authentication _authentication; - public AuthController(IAuthentication authentication) + public AuthController(Authentication authentication) { _authentication = authentication; } diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index 377fc14..2d47927 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -20,9 +20,9 @@ namespace RdtClient.Web.Controllers [Route("api/v2")] public class QBittorrentController : Controller { - private readonly IQBittorrent _qBittorrent; + private readonly QBittorrent _qBittorrent; - public QBittorrentController(IQBittorrent qBittorrent) + public QBittorrentController(QBittorrent qBittorrent) { _qBittorrent = qBittorrent; } diff --git a/server/RdtClient.Web/Controllers/SettingsController.cs b/server/RdtClient.Web/Controllers/SettingsController.cs index a84a7bf..7d6904c 100644 --- a/server/RdtClient.Web/Controllers/SettingsController.cs +++ b/server/RdtClient.Web/Controllers/SettingsController.cs @@ -15,10 +15,10 @@ namespace RdtClient.Web.Controllers [Route("Api/Settings")] public class SettingsController : Controller { - private readonly ISettings _settings; - private readonly ITorrents _torrents; + private readonly Settings _settings; + private readonly Torrents _torrents; - public SettingsController(ISettings settings, ITorrents torrents) + public SettingsController(Settings settings, Torrents torrents) { _settings = settings; _torrents = torrents; diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index d28b0e0..21d48b3 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -18,10 +18,10 @@ namespace RdtClient.Web.Controllers [Route("Api/Torrents")] public class TorrentsController : Controller { - private readonly ITorrentRunner _torrentRunner; - private readonly ITorrents _torrents; + private readonly TorrentRunner _torrentRunner; + private readonly Torrents _torrents; - public TorrentsController(ITorrents torrents, ITorrentRunner torrentRunner) + public TorrentsController(Torrents torrents, TorrentRunner torrentRunner) { _torrents = torrents; _torrentRunner = torrentRunner; @@ -29,7 +29,7 @@ namespace RdtClient.Web.Controllers [HttpGet] [Route("")] - public async Task>> Get() + public async Task>> GetAll() { var results = await _torrents.Get(); @@ -42,6 +42,23 @@ namespace RdtClient.Web.Controllers return Ok(results); } + [HttpGet] + [Route("Get/{torrentId:guid}")] + public async Task> GetById(Guid torrentId) + { + var torrent = await _torrents.GetById(torrentId); + + if (torrent?.Downloads != null) + { + foreach (var file in torrent.Downloads) + { + file.Torrent = null; + } + } + + return Ok(torrent); + } + /// /// Used for debugging only. Force a tick. /// @@ -129,19 +146,28 @@ namespace RdtClient.Web.Controllers } [HttpPost] - [Route("Delete/{id}")] - public async Task Delete(Guid id, [FromBody] TorrentControllerDeleteRequest request) + [Route("Delete/{torrentId:guid}")] + public async Task Delete(Guid torrentId, [FromBody] TorrentControllerDeleteRequest request) { - await _torrents.Delete(id, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles); + await _torrents.Delete(torrentId, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles); return Ok(); } [HttpPost] - [Route("Retry/{id}")] - public async Task Retry(Guid id, [FromBody] TorrentControllerRetryRequest request) + [Route("Retry/{torrentId:guid}")] + public async Task Retry(Guid torrentId) { - await _torrents.Retry(id, request.Retry); + await _torrents.RetryTorrent(torrentId); + + return Ok(); + } + + [HttpPost] + [Route("RetryDownload/{downloadId:guid}")] + public async Task RetryDownload(Guid downloadId) + { + await _torrents.RetryDownload(downloadId); return Ok(); } @@ -173,11 +199,6 @@ namespace RdtClient.Web.Controllers public Boolean DeleteLocalFiles { get; set; } } - public class TorrentControllerRetryRequest - { - public Int32 Retry { get; set; } - } - public class TorrentControllerCheckFilesRequest { public String MagnetLink { get; set; } diff --git a/server/RdtClient.Web/Startup.cs b/server/RdtClient.Web/Startup.cs index f02e8e9..6242eda 100644 --- a/server/RdtClient.Web/Startup.cs +++ b/server/RdtClient.Web/Startup.cs @@ -91,7 +91,7 @@ namespace RdtClient.Web Service.DiConfig.Config(services); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger, ISettings settings) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger, Settings settings) { if (env.IsDevelopment()) {