diff --git a/client/.editorconfig b/client/.editorconfig index 59d9a3a..c5cf666 100644 --- a/client/.editorconfig +++ b/client/.editorconfig @@ -7,6 +7,7 @@ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 120 [*.ts] quote_type = single diff --git a/client/src/app/file-status.pipe.ts b/client/src/app/file-status.pipe.ts index 16888d4..436dc93 100644 --- a/client/src/app/file-status.pipe.ts +++ b/client/src/app/file-status.pipe.ts @@ -1,29 +1,58 @@ import { Pipe, PipeTransform } from '@angular/core'; +import { FileSizePipe } from 'ngx-filesize'; import { TorrentFile } from './models/torrent.model'; -import { DownloadStatus } from './models/download.model'; @Pipe({ name: 'fileStatus', }) export class FileStatusPipe implements PipeTransform { + constructor(private pipe: FileSizePipe) {} + transform(value: TorrentFile): string { - if ( - !value.download || - value.download.status === DownloadStatus.PendingDownload - ) { + if (!value.download) { + return 'Pending download'; + } + + if (value.download.error) { + return `Error: ${value.download.error}`; + } + + if (value.download.completed != null) { + return 'Finished'; + } + + if (value.download.unpackingFinished) { + return 'Unpacking finished'; + } + + if (value.download.unpackingStarted) { + const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2); + return `Unpacking ${progress || 0}%`; + } + + if (value.download.unpackingQueued) { + return 'Unpacking queued'; + } + + if (value.download.downloadFinished) { + return 'Download finished'; + } + + if (value.download.downloadStarted) { + const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2); + const speed = this.pipe.transform(value.download.speed, 'filesize'); + + return `Downloading ${progress || 0}% (${speed}/s)`; + } + + if (value.download.downloadQueued) { + return 'Download queued'; + } + + if (value.download.added) { return 'Pending'; } - if (value.download.status === DownloadStatus.Downloading) { - const progress = ( - (value.download.bytesDownloaded / value.download.bytesSize) * - 100 - ).toFixed(2); - return `${progress || 0}%`; - } - - if (value.download.status === DownloadStatus.Finished) { - return `Finished`; - } + return ''; } } diff --git a/client/src/app/models/download.model.ts b/client/src/app/models/download.model.ts index 8bfcca3..fe2c712 100644 --- a/client/src/app/models/download.model.ts +++ b/client/src/app/models/download.model.ts @@ -1,24 +1,17 @@ export class Download { public downloadId: string; - public torrentId: string; - public link: string; - public added: Date; - - public status: DownloadStatus; - - public bytesDownloaded: number; - - public bytesSize: number; - + public downloadQueued: Date; + public downloadStarted: Date; + public downloadFinished: Date; + public unpackingQueued: Date; + public unpackingStarted: Date; + public unpackingFinished: Date; + public completed: Date; + public error: string; + public bytesTotal: number; + public bytesDone: number; public speed: number; } - -export enum DownloadStatus { - PendingDownload = 0, - Downloading = 1, - Unpacking = 2, - Finished = 3, -} diff --git a/client/src/app/models/profile.model.ts b/client/src/app/models/profile.model.ts index cc3a993..d84b49b 100644 --- a/client/src/app/models/profile.model.ts +++ b/client/src/app/models/profile.model.ts @@ -1,4 +1,4 @@ export class Profile { - userName: string; - expiration: Date; + public userName: string; + public expiration: Date; } diff --git a/client/src/app/models/torrent.model.ts b/client/src/app/models/torrent.model.ts index d065b29..e44f5c5 100644 --- a/client/src/app/models/torrent.model.ts +++ b/client/src/app/models/torrent.model.ts @@ -1,41 +1,47 @@ import { Download } from './download.model'; export class Torrent { - torrentId: string; - hash: string; - status: TorrentStatus; - rdId: string; - rdName: string; - rdSize: number; - rdHost: string; - rdSplit: number; - rdProgress: number; - rdStatus: string; - rdAdded: Date; - rdEnded: Date; - rdSpeed: number; - rdSeeders: number; - rdFiles: string; + public torrentId: string; + public hash: string; + public category: string; + public added: Date; + public completed: Date; + public autoDownload: boolean; + public autoUnpack: boolean; + public autoDelete: boolean; - files: TorrentFile[]; - downloads: Download[]; + public rdId: string; + public rdName: string; + public rdSize: number; + public rdHost: string; + public rdSplit: number; + public rdProgress: number; + public rdStatus: RealDebridStatus; + public rdStatusRaw: string; + public rdAdded: Date; + public rdEnded: Date; + public rdSpeed: number; + public rdSeeders: number; + public rdFiles: string; + + public files: TorrentFile[]; + public downloads: Download[]; } export class TorrentFile { - id: string; - path: string; - bytes: number; - selected: boolean; + public id: string; + public path: string; + public bytes: number; + public selected: boolean; - download: Download; + public download: Download; } -export enum TorrentStatus { - RealDebrid = 0, - WaitingForDownload = 1, - DownloadQueued = 2, - Downloading = 3, - Finished = 4, +export enum RealDebridStatus { + Processing = 0, + WaitingForFileSelection = 1, + Downloading = 2, + Finished = 3, Error = 99, } diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html index 7e81a8a..87a1675 100644 --- a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html +++ b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html @@ -47,6 +47,10 @@ Download torrent to host when finished downloading on Real-Debrid + + + Unpack files after downloading to host + Remove torrent when finished downloading on host @@ -61,6 +65,7 @@ class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" + (click)="ok()" > Add Torrent diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts index f47607d..9cc7b6e 100644 --- a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts +++ b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { TorrentService } from 'src/app/torrent.service'; @Component({ @@ -25,6 +25,7 @@ export class AddNewTorrentComponent implements OnInit { public fileName: string; public magnetLink: string; public autoDownload: boolean; + public autoUnpack: boolean; public autoDelete: boolean; public saving = false; @@ -67,7 +68,12 @@ export class AddNewTorrentComponent implements OnInit { if (this.magnetLink) { this.torrentService - .uploadMagnet(this.magnetLink, this.autoDownload, this.autoDelete) + .uploadMagnet( + this.magnetLink, + this.autoDownload, + this.autoUnpack, + this.autoDelete + ) .subscribe( () => { this.cancel(); @@ -79,7 +85,12 @@ export class AddNewTorrentComponent implements OnInit { ); } else if (this.selectedFile) { this.torrentService - .uploadFile(this.selectedFile, this.autoDownload, this.autoDelete) + .uploadFile( + this.selectedFile, + this.autoDownload, + this.autoUnpack, + this.autoDelete + ) .subscribe( () => { this.cancel(); diff --git a/client/src/app/navbar/settings/settings.component.ts b/client/src/app/navbar/settings/settings.component.ts index 43dd46f..76ef926 100644 --- a/client/src/app/navbar/settings/settings.component.ts +++ b/client/src/app/navbar/settings/settings.component.ts @@ -42,15 +42,9 @@ export class SettingsComponent implements OnInit { this.settingsService.get().subscribe( (results) => { - this.settingRealDebridApiKey = this.getSetting( - results, - 'RealDebridApiKey' - ); + this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey'); this.settingDownloadFolder = this.getSetting(results, 'DownloadFolder'); - this.settingDownloadLimit = parseInt( - this.getSetting(results, 'DownloadLimit'), - 10 - ); + this.settingDownloadLimit = parseInt(this.getSetting(results, 'DownloadLimit'), 10); }, (err) => { this.error = err.error; diff --git a/client/src/app/torrent-row/torrent-row.component.html b/client/src/app/torrent-row/torrent-row.component.html index 07ceed8..8f22f58 100644 --- a/client/src/app/torrent-row/torrent-row.component.html +++ b/client/src/app/torrent-row/torrent-row.component.html @@ -6,22 +6,12 @@ {{ torrent | status }} - + - + diff --git a/client/src/app/torrent-row/torrent-row.component.ts b/client/src/app/torrent-row/torrent-row.component.ts index 26b4777..b8288ed 100644 --- a/client/src/app/torrent-row/torrent-row.component.ts +++ b/client/src/app/torrent-row/torrent-row.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Torrent, TorrentStatus } from 'src/app/models/torrent.model'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Torrent } from 'src/app/models/torrent.model'; import { TorrentService } from 'src/app/torrent.service'; @Component({ @@ -11,6 +11,9 @@ export class TorrentRowComponent implements OnInit { @Input() public torrent: Torrent; + @Output('delete') + public delete = new EventEmitter(); + public loading = false; constructor(private torrentService: TorrentService) {} @@ -23,14 +26,11 @@ export class TorrentRowComponent implements OnInit { this.loading = true; this.torrentService.download(this.torrent.torrentId).subscribe(() => { this.loading = false; - this.torrent.status = TorrentStatus.Downloading; }); } - public delete(event: Event): void { + public delete1(event: Event): void { event.stopPropagation(); - - this.loading = true; - this.torrentService.delete(this.torrent.torrentId).subscribe(() => {}); + this.delete.emit(this.torrent.torrentId); } } diff --git a/client/src/app/torrent-status.pipe.ts b/client/src/app/torrent-status.pipe.ts index 675ed94..6fe0e2d 100644 --- a/client/src/app/torrent-status.pipe.ts +++ b/client/src/app/torrent-status.pipe.ts @@ -1,7 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core'; import { FileSizePipe } from 'ngx-filesize'; -import { DownloadStatus } from './models/download.model'; -import { Torrent, TorrentStatus } from './models/torrent.model'; +import { RealDebridStatus, Torrent } from './models/torrent.model'; @Pipe({ name: 'status', @@ -10,68 +9,72 @@ export class TorrentStatusPipe implements PipeTransform { constructor(private pipe: FileSizePipe) {} transform(torrent: Torrent): string { + if (torrent.completed) { + return 'Finished'; + } + if (torrent.downloads && torrent.downloads.length > 0) { - const allFinished = torrent.downloads.all( - (m) => m.status === DownloadStatus.Finished - ); + const allFinished = torrent.downloads.all((m) => m.completed != null); if (allFinished) { return 'Finished'; } - const downloading = torrent.downloads.where( - (m) => m.status === DownloadStatus.Downloading - ); - const unpacking = torrent.downloads.where( - (m) => m.status === DownloadStatus.Unpacking - ); + const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished); + const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished); - const allBytesDownloaded = torrent.downloads.sum( - (m) => m.bytesDownloaded - ); - const allBytesSize = torrent.downloads.sum((m) => m.bytesSize); - - let progress = 0; - let allSpeeds = 0; - - if (allBytesSize > 0) { - progress = (allBytesDownloaded / allBytesSize) * 100; - allSpeeds = downloading.sum((m) => m.speed) / downloading.length; - } - - let speed: string | string[] = '0'; - if (allSpeeds > 0) { - speed = this.pipe.transform(allSpeeds, 'filesize'); - } + let downloadText = ''; + let unpackText = ''; if (downloading.length > 0) { - if (allBytesSize > 0) { - return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`; - } + const bytesDone = downloading.sum((m) => m.bytesDone); + const bytesTotal = downloading.sum((m) => m.bytesTotal); + let progress = (bytesDone / bytesTotal) * 100; + let allSpeeds = downloading.sum((m) => m.speed) / downloading.length; - return `Preparing download`; + let speed: string | string[] = '0'; + if (allSpeeds > 0) { + speed = this.pipe.transform(allSpeeds, 'filesize'); + + downloadText = `Downloading (${progress.toFixed(2)}% - ${speed}/s)`; + } } if (unpacking.length > 0) { - return `Unpacking (${progress.toFixed(2)}% - ${speed}/s)`; + const bytesDone = unpacking.sum((m) => m.bytesDone); + const bytesTotal = unpacking.sum((m) => m.bytesTotal); + let progress = (bytesDone / bytesTotal) * 100; + let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length; + + if (allSpeeds > 0) { + downloadText = `Extracting (${progress.toFixed(2)}%)`; + } } - return 'Pending download'; + let result: string[] = []; + if (downloadText) { + result.push(downloadText); + } + if (unpackText) { + result.push(unpackText); + } + + if (result.length > 0) { + return result.join('\r\n'); + } } - switch (torrent.status) { - case TorrentStatus.RealDebrid: + switch (torrent.rdStatus) { + case RealDebridStatus.Downloading: const speed = this.pipe.transform(torrent.rdSpeed, 'filesize'); return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`; - case TorrentStatus.WaitingForDownload: - return `Ready to download, press the download icon to start`; - case TorrentStatus.DownloadQueued: - return `Download queued`; - case TorrentStatus.Downloading: - return `Downloading`; - case TorrentStatus.Finished: - return `Finished`; - case TorrentStatus.Error: - return `Error`; + case RealDebridStatus.Processing: + return `Torrent processing`; + case RealDebridStatus.WaitingForFileSelection: + return `Torrent waiting for file selection`; + case RealDebridStatus.Error: + return `Torrent error: ${torrent.rdStatusRaw}`; + case RealDebridStatus.Finished: + return `Torrent finished`; default: return 'Unknown status'; } diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html index 11c6db2..a599358 100644 --- a/client/src/app/torrent-table/torrent-table.component.html +++ b/client/src/app/torrent-table/torrent-table.component.html @@ -5,11 +5,11 @@ - - - - - + + + + + @@ -26,27 +26,70 @@ app-torrent-row [torrent]="torrent" (click)="selectTorrent(torrent)" + (delete)="showDeleteModal($event)" > - + - 0" - > - + 0"> + + + + + + + Delete torrent + + + + + + + + + Delete Torrent from local RealDebrid Client + + + + + Delete Torrent from RealDebrid + + + + + Delete local files + + + + + Deleting a torrent from RealDebrid will automatically delete it here too. + + 0"> + Error deleting torrent: {{ deleteError }} + + + + + diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts index 239e949..678a15f 100644 --- a/client/src/app/torrent-table/torrent-table.component.ts +++ b/client/src/app/torrent-table/torrent-table.component.ts @@ -1,13 +1,6 @@ -import { - Component, - OnInit, - OnDestroy, - HostListener, - ElementRef, -} from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { Torrent } from '../models/torrent.model'; import { TorrentService } from '../torrent.service'; -import { DownloadStatus } from '../models/download.model'; @Component({ selector: 'app-torrent-table', @@ -19,6 +12,14 @@ export class TorrentTableComponent implements OnInit, OnDestroy { 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; + private timer: any; constructor(private torrentService: TorrentService) {} @@ -50,9 +51,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy { torrent.downloads = result.downloads; torrent.files.forEach((file) => { - const downloads = torrent.downloads.filter( - (m) => m.link === file.path - ); + const downloads = torrent.downloads.filter((m) => m.link === file.path); if (downloads.length > 0) { file.download = downloads[0]; @@ -65,4 +64,30 @@ export class TorrentTableComponent implements OnInit, OnDestroy { public trackByMethod(index: number, el: Torrent): string { return el.torrentId; } + + public showDeleteModal(torrentId: string): void { + 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; + } + ); + } } diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts index 49e81c5..583fe8a 100644 --- a/client/src/app/torrent.service.ts +++ b/client/src/app/torrent.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, Subject } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; import { Torrent } from './models/torrent.model'; @Injectable({ @@ -20,23 +20,21 @@ export class TorrentService { public uploadMagnet( magnetLink: string, autoDownload: boolean, + autoUnpack: boolean, autoDelete: boolean ): Observable { return this.http.post(`/Api/Torrents/UploadMagnet`, { magnetLink, autoDownload, + autoUnpack, autoDelete, }); } - public uploadFile( - file: File, - autoDownload: boolean, - autoDelete: boolean - ): Observable { + public uploadFile(file: File, autoDownload: boolean, autoUnpack: boolean, autoDelete: boolean): Observable { const formData: FormData = new FormData(); formData.append('file', file); - formData.append('formData', JSON.stringify({ autoDownload, autoDelete })); + formData.append('formData', JSON.stringify({ autoDownload, autoUnpack, autoDelete })); return this.http.post(`/Api/Torrents/UploadFile`, formData); } @@ -44,7 +42,16 @@ export class TorrentService { return this.http.get(`/Api/Torrents/Download/${torrentId}`); } - public delete(torrentId: string): Observable { - return this.http.delete(`/Api/Torrents/${torrentId}`); + public delete( + torrentId: string, + deleteData: boolean, + deleteRdTorrent: boolean, + deleteLocalFiles: boolean + ): Observable { + return this.http.post(`/Api/Torrents/Delete/${torrentId}`, { + deleteData, + deleteRdTorrent, + deleteLocalFiles, + }); } } diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs index 0c77857..01b8c12 100644 --- a/server/RdtClient.Data/Data/DataContext.cs +++ b/server/RdtClient.Data/Data/DataContext.cs @@ -50,7 +50,11 @@ namespace RdtClient.Data.Data { SettingId = "DownloadFolder", Type = "String", + #if DEBUG + Value = @"C:\Temp\rdtclient" + #else Value = "/data/downloads" + #endif }, new Setting { diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index ff674d4..9b39d52 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; namespace RdtClient.Data.Data @@ -12,8 +11,14 @@ namespace RdtClient.Data.Data { Task> Get(); Task> GetForTorrent(Guid torrentId); + Task GetById(Guid downloadId); Task Add(Guid torrentId, String link); - Task UpdateStatus(Guid downloadId, DownloadStatus status); + 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 UpdateError(Guid downloadId, String error); Task DeleteForTorrent(Guid torrentId); } @@ -42,6 +47,14 @@ namespace RdtClient.Data.Data .ToListAsync(); } + public async Task GetById(Guid downloadId) + { + return await _dataContext.Downloads + .Include(m => m.Torrent) + .AsNoTracking() + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + } + public async Task Add(Guid torrentId, String link) { var download = new Download @@ -50,7 +63,7 @@ namespace RdtClient.Data.Data TorrentId = torrentId, Link = link, Added = DateTimeOffset.UtcNow, - Status = DownloadStatus.PendingDownload + DownloadQueued = DateTimeOffset.UtcNow }; await _dataContext.Downloads.AddAsync(download); @@ -59,12 +72,64 @@ namespace RdtClient.Data.Data return download; } - - public async Task UpdateStatus(Guid downloadId, DownloadStatus status) + + public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime) { - var download = await _dataContext.Downloads.FirstOrDefaultAsync(m => m.DownloadId == downloadId); + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); - download.Status = status; + dbDownload.DownloadStarted = dateTime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.DownloadFinished = dateTime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.UnpackingQueued = dateTime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.UnpackingStarted = dateTime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.UnpackingFinished = dateTime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateError(Guid downloadId, String error) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.Completed = DateTimeOffset.UtcNow; + dbDownload.Error = error; await _dataContext.SaveChangesAsync(); } diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs index ac594ed..4d7f22c 100644 --- a/server/RdtClient.Data/Data/TorrentData.cs +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; namespace RdtClient.Data.Data @@ -11,13 +9,13 @@ namespace RdtClient.Data.Data public interface ITorrentData { Task> Get(); - Task GetById(Guid id); + Task GetById(Guid torrentId); Task GetByHash(String hash); - Task Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoDelete); + Task Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete); Task UpdateRdData(Torrent torrent); - Task UpdateStatus(Guid torrentId, TorrentStatus status); Task UpdateCategory(Guid torrentId, String category); - Task Delete(Guid id); + Task UpdateComplete(Guid torrentId, DateTimeOffset datetime); + Task Delete(Guid torrentId); } public class TorrentData : ITorrentData @@ -36,20 +34,15 @@ namespace RdtClient.Data.Data .Include(m => m.Downloads) .ToListAsync(); - foreach (var file in results.SelectMany(torrent => torrent.Downloads)) - { - file.Torrent = null; - } - return results; } - public async Task GetById(Guid id) + public async Task GetById(Guid torrentId) { var dbTorrent = await _dataContext.Torrents .AsNoTracking() .Include(m => m.Downloads) - .FirstOrDefaultAsync(m => m.TorrentId == id); + .FirstOrDefaultAsync(m => m.TorrentId == torrentId); if (dbTorrent == null) { @@ -84,15 +77,16 @@ namespace RdtClient.Data.Data return dbTorrent; } - public async Task Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoDelete) + public async Task Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) { var torrent = new Torrent { TorrentId = Guid.NewGuid(), + Added = DateTimeOffset.UtcNow, RdId = realDebridId, Hash = hash.ToLower(), - Status = TorrentStatus.RealDebrid, AutoDownload = autoDownload, + AutoUnpack = autoUnpack, AutoDelete = autoDelete }; @@ -118,6 +112,7 @@ namespace RdtClient.Data.Data dbTorrent.RdSplit = torrent.RdSplit; dbTorrent.RdProgress = torrent.RdProgress; dbTorrent.RdStatus = torrent.RdStatus; + dbTorrent.RdStatusRaw = torrent.RdStatusRaw; dbTorrent.RdAdded = torrent.RdAdded; dbTorrent.RdEnded = torrent.RdEnded; dbTorrent.RdSpeed = torrent.RdSpeed; @@ -130,21 +125,7 @@ namespace RdtClient.Data.Data await _dataContext.SaveChangesAsync(); } - - public async Task UpdateStatus(Guid torrentId, TorrentStatus status) - { - var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); - - if (dbTorrent == null) - { - return; - } - - dbTorrent.Status = status; - - await _dataContext.SaveChangesAsync(); - } - + public async Task UpdateCategory(Guid torrentId, String category) { var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); @@ -159,9 +140,18 @@ namespace RdtClient.Data.Data await _dataContext.SaveChangesAsync(); } - public async Task Delete(Guid id) + public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime) { - var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == id); + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); + + dbTorrent.Completed = datetime; + + await _dataContext.SaveChangesAsync(); + } + + public async Task Delete(Guid torrentId) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); if (dbTorrent == null) { diff --git a/server/RdtClient.Data/Enums/DownloadStatus.cs b/server/RdtClient.Data/Enums/DownloadStatus.cs deleted file mode 100644 index 45c5d38..0000000 --- a/server/RdtClient.Data/Enums/DownloadStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace RdtClient.Data.Enums -{ - public enum DownloadStatus - { - PendingDownload = 0, - Downloading, - Unpacking, - Finished - } -} diff --git a/server/RdtClient.Data/Enums/TorrentStatus.cs b/server/RdtClient.Data/Enums/TorrentStatus.cs index bac9dae..1471da2 100644 --- a/server/RdtClient.Data/Enums/TorrentStatus.cs +++ b/server/RdtClient.Data/Enums/TorrentStatus.cs @@ -1,12 +1,11 @@ namespace RdtClient.Data.Enums { - public enum TorrentStatus + public enum RealDebridStatus { - RealDebrid = 0, - WaitingForDownload = 1, - DownloadQueued = 2, - Downloading = 3, - Finished = 4, + Processing = 0, + WaitingForFileSelection = 1, + Downloading = 2, + Finished = 3, Error = 99 } diff --git a/server/RdtClient.Data/Migrations/20201212204128_Initial.Designer.cs b/server/RdtClient.Data/Migrations/20210110025102_Initial.Designer.cs similarity index 90% rename from server/RdtClient.Data/Migrations/20201212204128_Initial.Designer.cs rename to server/RdtClient.Data/Migrations/20210110025102_Initial.Designer.cs index 9ef6e6b..173e618 100644 --- a/server/RdtClient.Data/Migrations/20201212204128_Initial.Designer.cs +++ b/server/RdtClient.Data/Migrations/20210110025102_Initial.Designer.cs @@ -9,7 +9,7 @@ using RdtClient.Data.Data; namespace RdtClient.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20201212204128_Initial")] + [Migration("20210110025102_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -219,15 +219,36 @@ namespace RdtClient.Data.Migrations b.Property("Added") .HasColumnType("TEXT"); + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + b.Property("Link") .HasColumnType("TEXT"); - b.Property("Status") - .HasColumnType("INTEGER"); - b.Property("TorrentId") .HasColumnType("TEXT"); + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + b.HasKey("DownloadId"); b.HasIndex("TorrentId"); @@ -257,15 +278,24 @@ namespace RdtClient.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("TEXT"); + b.Property("Added") + .HasColumnType("TEXT"); + b.Property("AutoDelete") .HasColumnType("INTEGER"); b.Property("AutoDownload") .HasColumnType("INTEGER"); + b.Property("AutoUnpack") + .HasColumnType("INTEGER"); + b.Property("Category") .HasColumnType("TEXT"); + b.Property("Completed") + .HasColumnType("TEXT"); + b.Property("Hash") .HasColumnType("TEXT"); @@ -302,12 +332,12 @@ namespace RdtClient.Data.Migrations b.Property("RdSplit") .HasColumnType("INTEGER"); - b.Property("RdStatus") - .HasColumnType("TEXT"); - - b.Property("Status") + b.Property("RdStatus") .HasColumnType("INTEGER"); + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + b.HasKey("TorrentId"); b.ToTable("Torrents"); diff --git a/server/RdtClient.Data/Migrations/20201212204128_Initial.cs b/server/RdtClient.Data/Migrations/20210110025102_Initial.cs similarity index 91% rename from server/RdtClient.Data/Migrations/20201212204128_Initial.cs rename to server/RdtClient.Data/Migrations/20210110025102_Initial.cs index c597846..3404bf3 100644 --- a/server/RdtClient.Data/Migrations/20201212204128_Initial.cs +++ b/server/RdtClient.Data/Migrations/20210110025102_Initial.cs @@ -66,16 +66,19 @@ namespace RdtClient.Data.Migrations TorrentId = table.Column(type: "TEXT", nullable: false), Hash = table.Column(type: "TEXT", nullable: true), Category = table.Column(type: "TEXT", nullable: true), + Added = table.Column(type: "TEXT", nullable: false), + Completed = table.Column(type: "TEXT", nullable: true), AutoDownload = table.Column(type: "INTEGER", nullable: false), + AutoUnpack = table.Column(type: "INTEGER", nullable: false), AutoDelete = table.Column(type: "INTEGER", nullable: false), - Status = table.Column(type: "INTEGER", nullable: false), RdId = table.Column(type: "TEXT", nullable: true), RdName = table.Column(type: "TEXT", nullable: true), RdSize = table.Column(type: "INTEGER", nullable: false), RdHost = table.Column(type: "TEXT", nullable: true), RdSplit = table.Column(type: "INTEGER", nullable: false), RdProgress = table.Column(type: "INTEGER", nullable: false), - RdStatus = table.Column(type: "TEXT", nullable: true), + RdStatus = table.Column(type: "INTEGER", nullable: false), + RdStatusRaw = table.Column(type: "TEXT", nullable: true), RdAdded = table.Column(type: "TEXT", nullable: false), RdEnded = table.Column(type: "TEXT", nullable: true), RdSpeed = table.Column(type: "INTEGER", nullable: true), @@ -201,7 +204,14 @@ namespace RdtClient.Data.Migrations TorrentId = table.Column(type: "TEXT", nullable: false), Link = table.Column(type: "TEXT", nullable: true), Added = table.Column(type: "TEXT", nullable: false), - Status = table.Column(type: "INTEGER", nullable: false) + DownloadQueued = table.Column(type: "TEXT", nullable: true), + DownloadStarted = table.Column(type: "TEXT", nullable: true), + DownloadFinished = table.Column(type: "TEXT", nullable: true), + UnpackingQueued = table.Column(type: "TEXT", nullable: true), + UnpackingStarted = table.Column(type: "TEXT", nullable: true), + UnpackingFinished = table.Column(type: "TEXT", nullable: true), + Completed = table.Column(type: "TEXT", nullable: true), + Error = table.Column(type: "TEXT", nullable: true) }, constraints: table => { diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index 412755b..fab3dba 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -217,15 +217,36 @@ namespace RdtClient.Data.Migrations b.Property("Added") .HasColumnType("TEXT"); + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + b.Property("Link") .HasColumnType("TEXT"); - b.Property("Status") - .HasColumnType("INTEGER"); - b.Property("TorrentId") .HasColumnType("TEXT"); + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + b.HasKey("DownloadId"); b.HasIndex("TorrentId"); @@ -255,15 +276,24 @@ namespace RdtClient.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("TEXT"); + b.Property("Added") + .HasColumnType("TEXT"); + b.Property("AutoDelete") .HasColumnType("INTEGER"); b.Property("AutoDownload") .HasColumnType("INTEGER"); + b.Property("AutoUnpack") + .HasColumnType("INTEGER"); + b.Property("Category") .HasColumnType("TEXT"); + b.Property("Completed") + .HasColumnType("TEXT"); + b.Property("Hash") .HasColumnType("TEXT"); @@ -300,12 +330,12 @@ namespace RdtClient.Data.Migrations b.Property("RdSplit") .HasColumnType("INTEGER"); - b.Property("RdStatus") - .HasColumnType("TEXT"); - - b.Property("Status") + b.Property("RdStatus") .HasColumnType("INTEGER"); + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + b.HasKey("TorrentId"); b.ToTable("Torrents"); diff --git a/server/RdtClient.Data/Models/Data/Download.cs b/server/RdtClient.Data/Models/Data/Download.cs index 462705a..bddee89 100644 --- a/server/RdtClient.Data/Models/Data/Download.cs +++ b/server/RdtClient.Data/Models/Data/Download.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using RdtClient.Data.Enums; namespace RdtClient.Data.Models.Data { @@ -15,17 +14,31 @@ namespace RdtClient.Data.Models.Data public String Link { get; set; } public DateTimeOffset Added { get; set; } + + public DateTimeOffset? DownloadQueued { get; set; } - public DownloadStatus Status { get; set; } + public DateTimeOffset? DownloadStarted { get; set; } + + public DateTimeOffset? DownloadFinished { get; set; } + + public DateTimeOffset? UnpackingQueued { get; set; } + + public DateTimeOffset? UnpackingStarted { get; set; } + + public DateTimeOffset? UnpackingFinished { get; set; } + + public DateTimeOffset? Completed { get; set; } + + public String Error { get; set; } [ForeignKey("TorrentId")] public Torrent Torrent { get; set; } [NotMapped] - public Int64 BytesSize { get; set; } + public Int64 BytesTotal { get; set; } [NotMapped] - public Int64 BytesDownloaded { get; set; } + public Int64 BytesDone { get; set; } [NotMapped] public Int64 Speed { get; set; } diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs index e313540..5a7f40c 100644 --- a/server/RdtClient.Data/Models/Data/Torrent.cs +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -16,13 +16,14 @@ namespace RdtClient.Data.Models.Data public String Hash { get; set; } public String Category { get; set; } + + public DateTimeOffset Added { get; set; } + public DateTimeOffset? Completed { get; set; } public Boolean AutoDownload { get; set; } - + public Boolean AutoUnpack { get; set; } public Boolean AutoDelete { get; set; } - - public TorrentStatus Status { get; set; } - + [InverseProperty("Torrent")] public IList Downloads { get; set; } @@ -32,7 +33,8 @@ namespace RdtClient.Data.Models.Data public String RdHost { get; set; } public Int64 RdSplit { get; set; } public Int64 RdProgress { get; set; } - public String RdStatus { get; set; } + public RealDebridStatus RdStatus { get; set; } + public String RdStatusRaw { get; set; } public DateTimeOffset RdAdded { get; set; } public DateTimeOffset? RdEnded { get; set; } public Int64? RdSpeed { get; set; } diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 9080c45..00e9264 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -12,6 +12,8 @@ namespace RdtClient.Service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } } \ No newline at end of file diff --git a/server/RdtClient.Service/RdtClient.Service.csproj.user b/server/RdtClient.Service/RdtClient.Service.csproj.user index cff74a9..c793151 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj.user +++ b/server/RdtClient.Service/RdtClient.Service.csproj.user @@ -2,5 +2,6 @@ IIS Express + false \ No newline at end of file diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs new file mode 100644 index 0000000..e73596f --- /dev/null +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Services +{ + public class DownloadClient + { + public Boolean Finished { get; private set; } + + public String Error { get; private set; } + + public Int64 Speed { get; private set; } + public Int64 BytesTotal { get; private set; } + public Int64 BytesDone { get; private set; } + + private readonly Download _download; + private readonly String _destinationPath; + private readonly Torrent _torrent; + + private Int64 _bytesLastUpdate; + private DateTime _nextUpdate; + + public DownloadClient(Download download, String destinationPath) + { + _download = download; + _destinationPath = destinationPath; + _torrent = download.Torrent; + } + + public async Task Start() + { + BytesDone = 0; + BytesTotal = 0; + Speed = 0; + + try + { + 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(); + var filePath = Path.Combine(torrentPath, fileName); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + await Task.Factory.StartNew(async delegate + { + await Download(uri, filePath); + }); + } + catch (Exception ex) + { + Error = $"An unexpected error occurred preparing download {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + Finished = true; + } + } + + private async Task Download(Uri uri, String filePath) + { + try + { + _bytesLastUpdate = 0; + _nextUpdate = DateTime.UtcNow.AddSeconds(1); + + // Determine the file size + var webRequest = WebRequest.Create(uri); + webRequest.Method = "HEAD"; + Int64 responseLength; + + using (var webResponse = await webRequest.GetResponseAsync()) + { + responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length")); + } + + var request = WebRequest.Create(uri); + using var response = await request.GetResponseAsync(); + + await using var stream = response.GetResponseStream(); + await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write); + var buffer = new Byte[4096]; + + while (fileStream.Length < response.ContentLength) + { + var read = await stream.ReadAsync(buffer, 0, buffer.Length); + + if (read > 0) + { + fileStream.Write(buffer, 0, read); + + BytesDone = fileStream.Length; + BytesTotal = responseLength; + + if (DateTime.UtcNow > _nextUpdate) + { + Speed = fileStream.Length - _bytesLastUpdate; + + _nextUpdate = DateTime.UtcNow.AddSeconds(1); + _bytesLastUpdate = fileStream.Length; + } + } + else + { + break; + } + } + + Speed = 0; + } + catch (Exception ex) + { + Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + } + finally + { + Finished = true; + } + } + } +} diff --git a/server/RdtClient.Service/Services/DownloadManager.cs b/server/RdtClient.Service/Services/DownloadManager.cs deleted file mode 100644 index 5959e1f..0000000 --- a/server/RdtClient.Service/Services/DownloadManager.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using RdtClient.Data.Enums; -using RdtClient.Data.Models.Data; -using SharpCompress.Archives; -using SharpCompress.Archives.Rar; -using SharpCompress.Common; - -namespace RdtClient.Service.Services -{ - public class DownloadManager - { - private Int64 _bytesLastUpdate; - - private DateTime _nextUpdate; - - private RarArchiveEntry _rarCurrentEntry; - private Dictionary _rarfileStatus; - - public DownloadManager() - { - ServicePointManager.Expect100Continue = false; - ServicePointManager.DefaultConnectionLimit = 100; - ServicePointManager.MaxServicePointIdleTime = 1000; - } - - public DownloadStatus? NewStatus { get; set; } - public Download Download { get; set; } - public Int64 Speed { get; private set; } - public Int64 BytesDownloaded { get; private set; } - public Int64 BytesSize { get; private set; } - - private DownloadManager ActiveDownload => TaskRunner.ActiveDownloads[Download.DownloadId]; - - public async Task Start(String destinationFolderPath, String torrentName, DownloadStatus status) - { - ActiveDownload.BytesDownloaded = 0; - ActiveDownload.BytesSize = 0; - ActiveDownload.Speed = 0; - - var fileUrl = Download.Link; - - var uri = new Uri(fileUrl); - var torrentPath = Path.Combine(destinationFolderPath, torrentName); - var filePath = Path.Combine(torrentPath, uri.Segments.Last()); - - if (status == DownloadStatus.Unpacking) - { - await Extract(filePath, destinationFolderPath, torrentName); - - return; - } - - ActiveDownload.NewStatus = DownloadStatus.Downloading; - - _bytesLastUpdate = 0; - _nextUpdate = DateTime.UtcNow.AddSeconds(1); - - if (!Directory.Exists(torrentPath)) - { - Directory.CreateDirectory(torrentPath); - } - - var webRequest = WebRequest.Create(fileUrl); - webRequest.Method = "HEAD"; - Int64 responseLength; - using (var webResponse = await webRequest.GetResponseAsync()) - { - responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length")); - } - - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - - var request = WebRequest.Create(fileUrl); - using (var response = await request.GetResponseAsync()) - { - await using var stream = response.GetResponseStream(); - await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write); - var buffer = new Byte[4096]; - - while (fileStream.Length < response.ContentLength) - { - var read = await stream.ReadAsync(buffer, 0, buffer.Length); - - if (read > 0) - { - fileStream.Write(buffer, 0, read); - - ActiveDownload.BytesDownloaded = fileStream.Length; - ActiveDownload.BytesSize = responseLength; - - if (DateTime.UtcNow > _nextUpdate) - { - ActiveDownload.Speed = fileStream.Length - _bytesLastUpdate; - - _nextUpdate = DateTime.UtcNow.AddSeconds(1); - _bytesLastUpdate = fileStream.Length; - } - } - else - { - break; - } - } - } - - ActiveDownload.Speed = 0; - ActiveDownload.BytesDownloaded = ActiveDownload.BytesSize; - - await Extract(filePath, destinationFolderPath, torrentName); - } - - private async Task Extract(String filePath, String destinationFolderPath, String torrentName) - { - try - { - if (filePath.EndsWith(".rar")) - { - ActiveDownload.NewStatus = DownloadStatus.Unpacking; - - await using (Stream stream = File.OpenRead(filePath)) - { - using var archive = RarArchive.Open(stream); - - ActiveDownload.BytesSize = archive.TotalSize; - - var entries = archive.Entries.Where(entry => !entry.IsDirectory) - .ToList(); - - _rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L); - _rarCurrentEntry = null; - archive.CompressedBytesRead += ArchiveOnCompressedBytesRead; - - var extractPath = destinationFolderPath; - - if (!entries.Any(m => m.Key.StartsWith(torrentName + @"\")) && !entries.Any(m => m.Key.StartsWith(torrentName + @"/"))) - { - extractPath = Path.Combine(destinationFolderPath, torrentName); - } - - if (entries.Any(m => m.Key.Contains(".r00"))) - { - extractPath = Path.Combine(extractPath, "Temp"); - } - - foreach (var entry in entries) - { - _rarCurrentEntry = entry; - - entry.WriteToDirectory(extractPath, - new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - - var retryCount = 0; - while (File.Exists(filePath) && retryCount < 10) - { - retryCount++; - - try - { - File.Delete(filePath); - } - catch - { - await Task.Delay(1000); - } - } - - - } - } - catch - { - // ignored - } - - ActiveDownload.Speed = 0; - ActiveDownload.BytesDownloaded = ActiveDownload.BytesSize; - ActiveDownload.NewStatus = DownloadStatus.Finished; - } - - private void ArchiveOnCompressedBytesRead(Object sender, CompressedBytesReadEventArgs e) - { - if (_rarCurrentEntry == null) - { - return; - } - - _rarfileStatus[_rarCurrentEntry.Key] = e.CompressedBytesRead; - - ActiveDownload.BytesDownloaded = _rarfileStatus.Sum(m => m.Value); - } - } -} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index 803c78c..6b61ffc 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using RdtClient.Data.Data; -using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; namespace RdtClient.Service.Services @@ -11,8 +10,14 @@ namespace RdtClient.Service.Services { Task> Get(); Task> GetForTorrent(Guid torrentId); + Task GetById(Guid downloadId); Task Add(Guid torrentId, String link); - Task UpdateStatus(Guid downloadId, DownloadStatus status); + 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 UpdateError(Guid downloadId, String error); Task DeleteForTorrent(Guid torrentId); } @@ -35,14 +40,44 @@ namespace RdtClient.Service.Services return await _downloadData.GetForTorrent(torrentId); } + public async Task GetById(Guid downloadId) + { + return await _downloadData.GetById(downloadId); + } + public async Task Add(Guid torrentId, String link) { return await _downloadData.Add(torrentId, link); } - public async Task UpdateStatus(Guid downloadId, DownloadStatus status) + public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime) { - await _downloadData.UpdateStatus(downloadId, status); + await _downloadData.UpdateDownloadStarted(downloadId, dateTime); + } + + public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime) + { + await _downloadData.UpdateDownloadFinished(downloadId, dateTime); + } + + public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime) + { + await _downloadData.UpdateUnpackingQueued(downloadId, dateTime); + } + + public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime) + { + await _downloadData.UpdateUnpackingStarted(downloadId, dateTime); + } + + public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime) + { + await _downloadData.UpdateUnpackingFinished(downloadId, dateTime); + } + + public async Task UpdateError(Guid downloadId, String error) + { + await _downloadData.UpdateError(downloadId, error); } public async Task DeleteForTorrent(Guid torrentId) diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 67c40e7..f5ddcd1 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -19,8 +19,8 @@ namespace RdtClient.Service.Services Task> TorrentFileContents(String hash); Task TorrentProperties(String hash); Task TorrentsDelete(String hash, Boolean deleteFiles); - Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete); - Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete); + Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete); + Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete); Task TorrentsSetCategory(String hash, String category); Task> TorrentsCategories(); } @@ -275,14 +275,13 @@ namespace RdtClient.Service.Services result.Uploaded = (Int64) (torrent.RdSize * (torrent.RdProgress / 100.0)); result.UploadedSession = (Int64) (torrent.RdSize * (torrent.RdProgress / 100.0)); result.Upspeed = torrent.RdSpeed ?? 0; - result.State = torrent.Status switch + result.State = torrent.RdStatus switch { - TorrentStatus.RealDebrid => "downloading", - TorrentStatus.WaitingForDownload => "downloading", - TorrentStatus.DownloadQueued => "downloading", - TorrentStatus.Downloading => "downloading", - TorrentStatus.Finished => "pausedUP", - TorrentStatus.Error => "error", + RealDebridStatus.Processing => "downloading", + RealDebridStatus.WaitingForFileSelection => "downloading", + RealDebridStatus.Downloading => "downloading", + RealDebridStatus.Finished => "pausedUP", + RealDebridStatus.Error => "error", _ => throw new ArgumentOutOfRangeException() }; @@ -304,8 +303,11 @@ namespace RdtClient.Service.Services foreach (var file in torrent.Files) { - var result = new TorrentFileItem(); - result.Name = file.Path; + var result = new TorrentFileItem + { + Name = file.Path + }; + results.Add(result); } @@ -340,7 +342,7 @@ namespace RdtClient.Service.Services Peers = 0, PeersTotal = 2, PieceSize = torrent.Files.Count > 0 ? torrent.RdSize / torrent.Files.Count : 0, - PiecesHave = torrent.Downloads.Count(m => m.Status == DownloadStatus.Finished), + PiecesHave = torrent.Downloads.Count(m => m.Completed.HasValue), PiecesNum = torrent.Files.Count, Reannounce = 0, SavePath = savePath, @@ -372,7 +374,7 @@ namespace RdtClient.Service.Services return; } - await _torrents.Delete(torrent.TorrentId); + await _torrents.Delete(torrent.TorrentId, true, true, true); if (deleteFiles) { @@ -387,14 +389,14 @@ namespace RdtClient.Service.Services } } - public async Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete) + public async Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) { - await _torrents.UploadMagnet(magnetLink, autoDownload, autoDelete); + await _torrents.UploadMagnet(magnetLink, autoDownload, autoUnpack, autoDelete); } - public async Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete) + public async Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) { - await _torrents.UploadFile(fileBytes, autoDownload, autoDelete); + await _torrents.UploadFile(fileBytes, autoDownload, autoUnpack, autoDelete); } public async Task TorrentsSetCategory(String hash, String category) diff --git a/server/RdtClient.Service/Services/TaskRunner.cs b/server/RdtClient.Service/Services/TaskRunner.cs index b5859b7..1ae26f4 100644 --- a/server/RdtClient.Service/Services/TaskRunner.cs +++ b/server/RdtClient.Service/Services/TaskRunner.cs @@ -1,157 +1,53 @@ using System; -using System.Collections.Concurrent; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using RdtClient.Data.Enums; namespace RdtClient.Service.Services { public class TaskRunner : BackgroundService { - public static readonly ConcurrentDictionary ActiveDownloads = new ConcurrentDictionary(); - private readonly ILogger _logger; - private readonly IServiceProvider _services; + private readonly IServiceProvider _serviceProvider; - public TaskRunner(ILogger logger, IServiceProvider services) + public TaskRunner(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; - _services = services; + _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { +#if DEBUG + await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); +#else await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken); - +#endif + + using var scope = _serviceProvider.CreateScope(); + var torrentRunner = scope.ServiceProvider.GetRequiredService(); + _logger.LogInformation("TaskRunner started."); + await torrentRunner.Initialize(); + while (!stoppingToken.IsCancellationRequested) { - await DoWork(); + try + { + await torrentRunner.Tick(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick"); + } + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } _logger.LogInformation("TaskRunner stopped."); } - - private async Task DoWork() - { - try - { - using var scope = _services.CreateScope(); - - var downloads = scope.ServiceProvider.GetRequiredService(); - var settings = scope.ServiceProvider.GetRequiredService(); - var torrents = scope.ServiceProvider.GetRequiredService(); - - var rdKey = await settings.GetString("RealDebridApiKey"); - - if (String.IsNullOrWhiteSpace(rdKey)) - { - return; - } - - await torrents.Update(); - - await ProcessAutoDownloads(downloads, settings, torrents); - await ProcessDownloads(downloads, settings, torrents); - await ProcessStatus(downloads, settings, torrents); - } - catch(Exception ex) - { - _logger.LogError(ex, ex.Message); - } - } - - private async Task ProcessAutoDownloads(IDownloads downloads, ISettings settings, ITorrents torrents) - { - var allTorrents = await torrents.Get(); - - allTorrents = allTorrents.Where(m => (m.Status == TorrentStatus.WaitingForDownload && m.AutoDownload && m.Downloads.Count == 0) || m.Status == TorrentStatus.DownloadQueued) - .ToList(); - - foreach (var torrent in allTorrents) - { - await torrents.Download(torrent.TorrentId); - } - } - - private async Task ProcessDownloads(IDownloads downloads, ISettings settings, ITorrents torrents) - { - var allDownloads = await downloads.Get(); - - allDownloads = allDownloads.Where(m => m.Status != DownloadStatus.Finished) - .OrderByDescending(m => m.Status) - .ThenByDescending(m => m.Added) - .ToList(); - - var maxDownloads = await settings.GetNumber("DownloadLimit"); - var destinationFolderPath = await settings.GetString("DownloadFolder"); - - foreach (var download in allDownloads) - { - if (ActiveDownloads.ContainsKey(download.DownloadId)) - { - continue; - } - - if (ActiveDownloads.Count >= maxDownloads) - { - return; - } - - // Prevent circular references - download.Torrent.Downloads = null; - - await torrents.UpdateStatus(download.TorrentId, TorrentStatus.Downloading); - - await Task.Factory.StartNew(async delegate - { - var downloadManager = new DownloadManager(); - - if (ActiveDownloads.TryAdd(download.DownloadId, downloadManager)) - { - downloadManager.Download = download; - await downloadManager.Start(destinationFolderPath, download.Torrent.RdName, download.Status); - } - }); - } - } - - private async Task ProcessStatus(IDownloads downloads, ISettings settings, ITorrents torrents) - { - foreach (var (downloadId, download) in ActiveDownloads) - { - if (download.NewStatus.HasValue) - { - download.Download.Status = download.NewStatus.Value; - download.NewStatus = null; - - await downloads.UpdateStatus(downloadId, download.Download.Status); - - if (download.Download.Status == DownloadStatus.Finished) - { - ActiveDownloads.TryRemove(downloadId, out _); - - // Check if all downloads are completed and update the torrent - var allDownloads = await downloads.GetForTorrent(download.Download.TorrentId); - - if (allDownloads.All(m => m.Status == DownloadStatus.Finished)) - { - await torrents.UpdateStatus(download.Download.TorrentId, TorrentStatus.Finished); - - if (download.Download.Torrent.AutoDelete) - { - await torrents.Delete(download.Download.TorrentId); - } - } - } - } - } - } } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/TorrentDownloadManager.cs b/server/RdtClient.Service/Services/TorrentDownloadManager.cs new file mode 100644 index 0000000..d5ae9aa --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentDownloadManager.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace RdtClient.Service.Services +{ + public interface ITorrentDownloadManager + { + Task Tick(); + } + + public class TorrentDownloadManager : ITorrentDownloadManager + { + public async Task Tick() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs new file mode 100644 index 0000000..92ab390 --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using RdtClient.Data.Enums; + +namespace RdtClient.Service.Services +{ + public interface ITorrentRunner + { + Task Initialize(); + Task Tick(); + } + + public class TorrentRunner : ITorrentRunner + { + private static DateTime _nextUpdate = DateTime.UtcNow; + + public static readonly ConcurrentDictionary ActiveDownloadClients = new ConcurrentDictionary(); + public static readonly ConcurrentDictionary ActiveUnpackClients = new ConcurrentDictionary(); + + private readonly ISettings _settings; + private readonly ITorrents _torrents; + private readonly IDownloads _downloads; + + public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads) + { + _settings = settings; + _torrents = torrents; + _downloads = downloads; + } + + public async Task Initialize() + { + // When starting up reset any pending downloads or unpackings so that they are restarted. + var torrents = await _torrents.Get(); + + var downloads = torrents.SelectMany(m => m.Downloads) + .Where(m => m.DownloadQueued != null && m.DownloadStarted != null && m.DownloadFinished == null) + .OrderBy(m => m.DownloadQueued); + + foreach (var download in downloads) + { + await _downloads.UpdateDownloadStarted(download.DownloadId, null); + } + + var unpacks = torrents.SelectMany(m => m.Downloads) + .Where(m => m.UnpackingQueued != null && m.UnpackingStarted != null && m.UnpackingFinished == null) + .OrderBy(m => m.DownloadQueued); + + foreach (var download in unpacks) + { + await _downloads.UpdateUnpackingStarted(download.DownloadId, null); + } + } + + public async Task Tick() + { + var settingApiKey = await _settings.GetString("RealDebridApiKey"); + + if (String.IsNullOrWhiteSpace(settingApiKey)) + { + return; + } + + // Check if any torrents are finished downloading to the host, remove them from the active download list. + var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList(); + + if (completedActiveDownloads.Count > 0) + { + foreach (var (downloadId, downloadClient) in completedActiveDownloads) + { + if (downloadClient.Error != null) + { + await _downloads.UpdateError(downloadId, downloadClient.Error); + } + else + { + await _downloads.UpdateDownloadFinished(downloadId, DateTimeOffset.UtcNow); + } + + ActiveDownloadClients.TryRemove(downloadId, out _); + } + } + + // Check if any torrents are finished unpacking, remove them from the active unpack list. + var completedUnpacks = ActiveUnpackClients.Where(m => m.Value.Finished).ToList(); + + if (completedUnpacks.Count > 0) + { + foreach (var (downloadId, unpackClient) in completedUnpacks) + { + if (unpackClient.Error != null) + { + await _downloads.UpdateError(downloadId, unpackClient.Error); + } + else + { + await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow); + } + + ActiveUnpackClients.TryRemove(downloadId, out _); + } + } + + // Only poll RealDebrid every 30 seconds + + if (_nextUpdate < DateTime.UtcNow) + { + _nextUpdate = DateTime.UtcNow.AddSeconds(5); + + await _torrents.Update(); + } + + var torrents = await _torrents.Get(); + + torrents = torrents.Where(m => m.Completed == null).ToList(); + + // Check if there are any downloads that are queued and can be started. + var queuedDownloads = torrents.SelectMany(m => m.Downloads) + .Where(m => m.DownloadQueued != null && m.DownloadStarted == null) + .OrderBy(m => m.DownloadQueued); + + foreach (var download in queuedDownloads) + { + await _torrents.Download(download.DownloadId); + } + + // Check if there are any unpacks that are queued and can be started. + var queuedUnpacks = torrents.SelectMany(m => m.Downloads) + .Where(m => m.UnpackingQueued != null && m.UnpackingStarted == null) + .OrderBy(m => m.DownloadQueued); + + foreach (var download in queuedUnpacks) + { + await _torrents.Unpack(download.DownloadId); + } + + foreach (var torrent in torrents) + { + // If torrent is erroring out on the RealDebrid side, skip processing this torrent. + if (torrent.RdStatus == RealDebridStatus.Error) + { + continue; + } + + // RealDebrid is waiting for file selection, select which files to download. + if (torrent.AutoDownload && torrent.RdStatus == RealDebridStatus.WaitingForFileSelection) + { + var fileIds = torrent.Files + .Select(m => m.Id.ToString()) + .ToArray(); + + await _torrents.SelectFiles(torrent.RdId, fileIds); + } + + // If the torrent doesn't have any files at this point, don't process it further. + if (torrent.Files.Count == 0) + { + continue; + } + + // RealDebrid finished downloading the torrent, process the file to host. + if (torrent.AutoDownload && torrent.RdStatus == RealDebridStatus.Finished) + { + // If the torrent doesn't have any Downloads, unrestrict the links and add them to the database. + if (torrent.Downloads.Count == 0) + { + await _torrents.Unrestrict(torrent.TorrentId); + continue; + } + + // If the torrent has any files that need starting to be downloaded, download them. + var downloadsPending = torrent.Downloads + .Where(m => m.DownloadStarted == null && + m.DownloadFinished == null) + .ToList(); + + if (downloadsPending.Count > 0) + { + foreach (var download in downloadsPending) + { + await _torrents.Download(download.DownloadId); + } + + continue; + } + } + + if (torrent.AutoUnpack && torrent.RdStatus == RealDebridStatus.Finished) + { + // If all files are finished downloading, move to the unpacking step. + var unpackingPending = torrent.Downloads + .Where(m => m.DownloadFinished != null && + m.UnpackingStarted == null && + m.UnpackingFinished == null) + .ToList(); + + if (unpackingPending.Count > 0) + { + foreach (var download in unpackingPending) + { + await _torrents.Unpack(download.DownloadId); + } + + continue; + } + } + + // Check if torrent is complete + if (torrent.Downloads.Count > 0) + { + var allComplete = torrent.Downloads.All(m => m.UnpackingFinished != null); + + if (allComplete) + { + await _torrents.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow); + + // Remove the torrent from RealDebrid + if (torrent.AutoDelete) + { + await _torrents.Delete(torrent.TorrentId, true, true, false); + } + } + } + } + } + } +} diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index e7110b2..d30d087 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,24 +17,28 @@ namespace RdtClient.Service.Services public interface ITorrents { Task> Get(); - Task GetById(Guid id); + Task GetById(Guid torrentId); Task GetByHash(String hash); - Task> Update(); - Task UpdateStatus(Guid torrentId, TorrentStatus status); Task UpdateCategory(String hash, String category); - Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete); - Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete); - Task Delete(Guid id); - Task Download(Guid id); + Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete); + Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete); + Task SelectFiles(String torrentId, IList fileIds); + Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles); + Task Unrestrict(Guid torrentId); + Task Download(Guid downloadId); + Task Unpack(Guid downloadId); void Reset(); Task GetProfile(); + Task UpdateComplete(Guid torrentId, DateTimeOffset datetime); + Task Update(); } public class Torrents : ITorrents { - private static RdNetClient _rdtClient; + private static RdNetClient _rdtNetClient; private static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1); + private readonly IDownloads _downloads; private readonly ISettings _settings; private readonly ITorrentData _torrentData; @@ -45,25 +50,22 @@ namespace RdtClient.Service.Services _downloads = downloads; } - private RdNetClient RdNetClient + private RdNetClient GetRdNetClient() { - get + if (_rdtNetClient == null) { - if (_rdtClient == null) + var apiKey = _settings.GetString("RealDebridApiKey") + .Result; + + if (String.IsNullOrWhiteSpace(apiKey)) { - var apiKey = _settings.GetString("RealDebridApiKey") - .Result; - - if (String.IsNullOrWhiteSpace(apiKey)) - { - throw new Exception("RealDebrid API Key not set in the settings"); - } - - _rdtClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey); + throw new Exception("RealDebrid API Key not set in the settings"); } - return _rdtClient; + _rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey); } + + return _rdtNetClient; } public async Task> Get() @@ -74,16 +76,17 @@ namespace RdtClient.Service.Services { foreach (var download in torrent.Downloads) { - if (TaskRunner.ActiveDownloads.TryGetValue(download.DownloadId, out var activeDownload)) + if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - download.Speed = activeDownload.Speed; - download.BytesSize = activeDownload.BytesSize; - download.BytesDownloaded = activeDownload.BytesDownloaded; - - if (activeDownload.NewStatus.HasValue) - { - download.Status = activeDownload.NewStatus.Value; - } + 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; } } } @@ -91,13 +94,13 @@ namespace RdtClient.Service.Services return torrents; } - public async Task GetById(Guid id) + public async Task GetById(Guid torrentId) { - var torrent = await _torrentData.GetById(id); + var torrent = await _torrentData.GetById(torrentId); if (torrent != null) { - var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId); + var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); await Update(torrent, rdTorrent); } @@ -111,7 +114,7 @@ namespace RdtClient.Service.Services if (torrent != null) { - var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId); + var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); await Update(torrent, rdTorrent); } @@ -119,20 +122,224 @@ namespace RdtClient.Service.Services return torrent; } - public async Task> Update() + public async Task UpdateCategory(String hash, String category) + { + var torrent = await _torrentData.GetByHash(hash); + + if (torrent == null) + { + return; + } + + await _torrentData.UpdateCategory(torrent.TorrentId, category); + } + + public async Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) + { + var magnet = MagnetLink.Parse(magnetLink); + + var rdTorrent = await GetRdNetClient().AddTorrentMagnetAsync(magnetLink); + + await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), autoDownload, autoUnpack, autoDelete); + } + + public async Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) + { + var torrent = await MonoTorrent.Torrent.LoadAsync(bytes); + + var rdTorrent = await GetRdNetClient().AddTorrentFileAsync(bytes); + + await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), autoDownload, autoUnpack, autoDelete); + } + + public async Task SelectFiles(String torrentId, IList fileIds) + { + await GetRdNetClient().SelectTorrentFilesAsync(torrentId, fileIds.ToArray()); + } + + public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles) + { + var torrent = await GetById(torrentId); + + if (torrent != null) + { + if (deleteData) + { + await _downloads.DeleteForTorrent(torrent.TorrentId); + await _torrentData.Delete(torrentId); + } + + if (deleteRdTorrent) + { + await GetRdNetClient().DeleteTorrentAsync(torrent.RdId); + } + + if (deleteLocalFiles) + { + // TODO + } + } + } + + public async Task Unrestrict(Guid torrentId) + { + var torrent = await _torrentData.GetById(torrentId); + + var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); + + foreach (var link in rdTorrent.Links) + { + var unrestrictedLink = await GetRdNetClient().UnrestrictLinkAsync(link); + + if (torrent.Downloads.Any(m => m.Link == unrestrictedLink.Download)) + { + continue; + } + + await _downloads.Add(torrent.TorrentId, unrestrictedLink.Download); + } + } + + public async Task Download(Guid downloadId) + { + var settingDownloadLimit = await _settings.GetNumber("DownloadLimit"); + var settingDownloadFolder = await _settings.GetString("DownloadFolder"); + + var download = await _downloads.GetById(downloadId); + + // Check if we have reached the download limit, if so queue the download, but don't start it. + if (TorrentRunner.ActiveDownloadClients.Count >= settingDownloadLimit) + { + return; + } + + download.DownloadStarted = DateTimeOffset.UtcNow; + await _downloads.UpdateDownloadStarted(download.DownloadId, download.DownloadStarted); + + // Start the download process + var downloadClient = new DownloadClient(download, settingDownloadFolder); + + if (TorrentRunner.ActiveDownloadClients.TryAdd(downloadId, downloadClient)) + { + await downloadClient.Start(); + } + } + + public async Task Unpack(Guid downloadId) + { + //var settingUnpackLimit = await _settings.GetNumber("UnpackLimit"); + var settingUnpackLimit = 1; + var settingDownloadFolder = await _settings.GetString("DownloadFolder"); + + var download = await _downloads.GetById(downloadId); + + // Check if the file is even unpackable. + var uri = new Uri(download.Link); + var fileName = uri.Segments.Last(); + var extension = Path.GetExtension(fileName); + + if (extension != ".rar") + { + await _downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow); + await _downloads.UpdateUnpackingStarted(downloadId, DateTimeOffset.UtcNow); + await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow); + + return; + } + + // This property can have a value when it is a retry. + if (download.UnpackingQueued == null) + { + download.UnpackingQueued = DateTimeOffset.UtcNow; + await _downloads.UpdateUnpackingQueued(download.DownloadId, download.UnpackingQueued); + } + + // Check if we have reached the download limit, if so queue the download, but don't start it. + if (TorrentRunner.ActiveUnpackClients.Count >= settingUnpackLimit) + { + return; + } + + download.UnpackingStarted = DateTimeOffset.UtcNow; + await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted); + + // Start the unpacking process + var unpackClient = new UnpackClient(download, settingDownloadFolder); + + if (TorrentRunner.ActiveUnpackClients.TryAdd(downloadId, unpackClient)) + { + await unpackClient.Start(); + } + } + + public void Reset() + { + _rdtNetClient = null; + } + + public async Task GetProfile() + { + if (_rdtNetClient == null) + { + return new Profile(); + } + + var user = await GetRdNetClient().GetUserAsync(); + + var profile = new Profile + { + UserName = user.Username, + Expiration = user.Expiration + }; + + return profile; + } + + private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete) + { + var w = await SemaphoreSlim.WaitAsync(60000); + if (!w) + { + throw new Exception("Unable to add torrent, could not obtain lock"); + } + + try + { + var torrent = await _torrentData.GetByHash(infoHash); + + if (torrent != null) + { + return; + } + + var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoUnpack, autoDelete); + + var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(rdTorrentId); + + await Update(newTorrent, rdTorrent); + } + finally + { + SemaphoreSlim.Release(); + } + } + + public async Task Update() { var w = await SemaphoreSlim.WaitAsync(1); if (!w) { - return await _torrentData.Get(); + + + return; } - var torrents = await _torrentData.Get(); + var torrents = await Get(); try { - var rdTorrents = await RdNetClient.GetTorrentsAsync(0, 100); + var rdTorrents = await GetRdNetClient().GetTorrentsAsync(0, 100); foreach (var rdTorrent in rdTorrents) { @@ -140,13 +347,12 @@ namespace RdtClient.Service.Services if (torrent == null) { - var newTorrent = await _torrentData.Add(rdTorrent.Id, rdTorrent.Hash, false, false); - await GetById(newTorrent.TorrentId); + continue; } - else if (rdTorrent.Files == null) + if (rdTorrent.Files == null) { - var rdTorrent2 = await RdNetClient.GetTorrentInfoAsync(rdTorrent.Id); - await Update(torrent, rdTorrent2); + var rdTorrentInfo = await GetRdNetClient().GetTorrentInfoAsync(rdTorrent.Id); + await Update(torrent, rdTorrentInfo); } else { @@ -164,8 +370,6 @@ namespace RdtClient.Service.Services await _torrentData.Delete(torrent.TorrentId); } } - - return torrents; } finally { @@ -173,128 +377,9 @@ namespace RdtClient.Service.Services } } - public async Task UpdateStatus(Guid torrentId, TorrentStatus status) + public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime) { - await _torrentData.UpdateStatus(torrentId, status); - } - - public async Task UpdateCategory(String hash, String category) - { - var torrent = await _torrentData.GetByHash(hash); - - if (torrent == null) - { - return; - } - - await _torrentData.UpdateCategory(torrent.TorrentId, category); - } - - public async Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete) - { - var magnet = MagnetLink.Parse(magnetLink); - - var rdTorrent = await RdNetClient.AddTorrentMagnetAsync(magnetLink); - - await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), autoDownload, autoDelete); - } - - public async Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete) - { - var torrent = await MonoTorrent.Torrent.LoadAsync(bytes); - - var rdTorrent = await RdNetClient.AddTorrentFileAsync(bytes); - - await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), autoDownload, autoDelete); - } - - public async Task Delete(Guid id) - { - var torrent = await GetById(id); - - if (torrent != null) - { - await _downloads.DeleteForTorrent(torrent.TorrentId); - await _torrentData.Delete(id); - await RdNetClient.DeleteTorrentAsync(torrent.RdId); - } - } - - public async Task Download(Guid id) - { - var torrent = await _torrentData.GetById(id); - - await _downloads.DeleteForTorrent(id); - - await _torrentData.UpdateStatus(id, TorrentStatus.DownloadQueued); - - var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId); - - var existingDownloads = await _downloads.GetForTorrent(id); - - foreach (var link in rdTorrent.Links) - { - var unrestrictedLink = await RdNetClient.UnrestrictLinkAsync(link); - - if (existingDownloads.Any(m => m.Link == unrestrictedLink.Download)) - { - continue; - } - - await _downloads.Add(torrent.TorrentId, unrestrictedLink.Download); - } - } - - public void Reset() - { - _rdtClient = null; - } - - public async Task GetProfile() - { - if (RdNetClient == null) - { - return new Profile(); - } - - var user = await RdNetClient.GetUserAsync(); - - var profile = new Profile - { - UserName = user.Username, - Expiration = user.Expiration - }; - - return profile; - } - - private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoDelete) - { - var w = await SemaphoreSlim.WaitAsync(60000); - if (!w) - { - throw new Exception("Unable to add torrent, could not obtain lock"); - } - - try - { - var torrent = await _torrentData.GetByHash(infoHash); - - if (torrent != null) - { - return; - } - - var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoDelete); - - var rdTorrent = await RdNetClient.GetTorrentInfoAsync(rdTorrentId); - - await Update(newTorrent, rdTorrent); - } - finally - { - SemaphoreSlim.Release(); - } + await _torrentData.UpdateComplete(torrentId, datetime); } private async Task Update(Torrent torrent, RDNET.Torrent rdTorrent) @@ -325,47 +410,29 @@ namespace RdtClient.Service.Services torrent.RdHost = rdTorrent.Host; torrent.RdSplit = rdTorrent.Split; torrent.RdProgress = rdTorrent.Progress; - torrent.RdStatus = rdTorrent.Status; torrent.RdAdded = rdTorrent.Added; torrent.RdEnded = rdTorrent.Ended; torrent.RdSpeed = rdTorrent.Speed; torrent.RdSeeders = rdTorrent.Seeders; + torrent.RdStatusRaw = rdTorrent.Status; + + torrent.RdStatus = rdTorrent.Status switch + { + "magnet_error" => RealDebridStatus.Error, + "magnet_conversion" => RealDebridStatus.Processing, + "waiting_files_selection" => RealDebridStatus.WaitingForFileSelection, + "queued" => RealDebridStatus.Downloading, + "downloading" => RealDebridStatus.Downloading, + "downloaded" => RealDebridStatus.Finished, + "error" => RealDebridStatus.Error, + "virus" => RealDebridStatus.Error, + "compressing" => RealDebridStatus.Downloading, + "uploading" => RealDebridStatus.Finished, + "dead" => RealDebridStatus.Error, + _ => RealDebridStatus.Error + }; await _torrentData.UpdateRdData(torrent); - - if (torrent.Status == TorrentStatus.RealDebrid) - { - if (torrent.Status == TorrentStatus.RealDebrid && torrent.RdProgress == 100) - { - await _torrentData.UpdateStatus(torrent.TorrentId, TorrentStatus.WaitingForDownload); - } - else - { - // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead - torrent.Status = rdTorrent.Status switch - { - "magnet_error" => TorrentStatus.Error, - "error" => TorrentStatus.Error, - "virus" => TorrentStatus.Error, - "dead" => TorrentStatus.Error, - _ => TorrentStatus.RealDebrid - }; - - await _torrentData.UpdateStatus(torrent.TorrentId, torrent.Status); - } - } - - if (rdTorrent.Files != null && rdTorrent.Files.Count > 0) - { - if (!rdTorrent.Files.Any(m => m.Selected)) - { - var fileIds = rdTorrent.Files - .Select(m => m.Id.ToString()) - .ToArray(); - - await RdNetClient.SelectTorrentFilesAsync(rdTorrent.Id, fileIds); - } - } } } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs new file mode 100644 index 0000000..39551db --- /dev/null +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using RdtClient.Data.Models.Data; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; + +namespace RdtClient.Service.Services +{ + public class UnpackClient + { + public Boolean Finished { get; private set; } + + public String Error { get; private set; } + + public Int64 BytesTotal { get; private set; } + public Int64 BytesDone { get; private set; } + + private readonly Download _download; + private readonly String _destinationPath; + private readonly Torrent _torrent; + + private RarArchiveEntry _rarCurrentEntry; + private Dictionary _rarfileStatus; + + public UnpackClient(Download download, String destinationPath) + { + _download = download; + _destinationPath = destinationPath; + _torrent = download.Torrent; + } + + public async Task Start() + { + BytesDone = 0; + BytesTotal = 0; + + try + { + 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); + + var fileName = uri.Segments.Last(); + var filePath = Path.Combine(torrentPath, fileName); + + if (!File.Exists(filePath)) + { + throw new Exception($"File {filePath} could not be extracted because it is missing"); + } + + await Task.Factory.StartNew(async delegate + { + await Unpack(filePath); + }); + } + catch (Exception ex) + { + Error = $"An unexpected error occurred preparing download {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + Finished = true; + } + } + + private async Task Unpack(String filePath) + { + try + { + await using (Stream stream = File.OpenRead(filePath)) + { + using var archive = RarArchive.Open(stream); + + BytesTotal = archive.TotalSize; + + var entries = archive.Entries.Where(entry => !entry.IsDirectory) + .ToList(); + + _rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L); + _rarCurrentEntry = null; + archive.CompressedBytesRead += ArchiveOnCompressedBytesRead; + + var extractPath = _destinationPath; + + if (!entries.Any(m => m.Key.StartsWith(_torrent.RdName + @"\")) && !entries.Any(m => m.Key.StartsWith(_torrent.RdName + @"/"))) + { + extractPath = Path.Combine(_destinationPath, _torrent.RdName); + } + + if (entries.Any(m => m.Key.Contains(".r00"))) + { + extractPath = Path.Combine(extractPath, "Temp"); + } + + foreach (var entry in entries) + { + _rarCurrentEntry = entry; + + entry.WriteToDirectory(extractPath, + new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + + var retryCount = 0; + while (File.Exists(filePath) && retryCount < 10) + { + retryCount++; + + try + { + File.Delete(filePath); + } + catch + { + await Task.Delay(1000); + } + } + } + catch (Exception ex) + { + Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + } + finally + { + Finished = true; + } + } + + private void ArchiveOnCompressedBytesRead(Object sender, CompressedBytesReadEventArgs e) + { + if (_rarCurrentEntry == null) + { + return; + } + + _rarfileStatus[_rarCurrentEntry.Key] = e.CompressedBytesRead; + + BytesDone = _rarfileStatus.Sum(m => m.Value); + } + } +} diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index 9e63fe7..4956fcb 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -245,7 +245,7 @@ namespace RdtClient.Web.Controllers foreach (var url in urls) { - await _qBittorrent.TorrentsAdd(url.Trim(), true, false); + await _qBittorrent.TorrentsAdd(url.Trim(), true, true, false); } return Ok(); @@ -265,7 +265,7 @@ namespace RdtClient.Web.Controllers await file.CopyToAsync(target); var fileBytes = target.ToArray(); - await _qBittorrent.TorrentsAddFile(fileBytes, true, false); + await _qBittorrent.TorrentsAddFile(fileBytes, true, true, false); } } diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index b8b7f54..e703b11 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -26,8 +27,15 @@ namespace RdtClient.Web.Controllers [Route("")] public async Task>> Get() { - var result = await _torrents.Get(); - return Ok(result); + var results = await _torrents.Get(); + + // Prevent infinite recursion when serializing + foreach (var file in results.SelectMany(torrent => torrent.Downloads)) + { + file.Torrent = null; + } + + return Ok(results); } [HttpGet] @@ -63,7 +71,7 @@ namespace RdtClient.Web.Controllers var bytes = memoryStream.ToArray(); - await _torrents.UploadFile(bytes, formData.AutoDownload, formData.AutoDelete); + await _torrents.UploadFile(bytes, formData.AutoDownload, formData.AutoUnpack, formData.AutoDelete); return Ok(); } @@ -72,16 +80,16 @@ namespace RdtClient.Web.Controllers [Route("UploadMagnet")] public async Task UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest request) { - await _torrents.UploadMagnet(request.MagnetLink, request.AutoDownload, request.AutoDelete); + await _torrents.UploadMagnet(request.MagnetLink, request.AutoDownload, request.AutoUnpack, request.AutoDelete); return Ok(); } - [HttpDelete] - [Route("{id}")] - public async Task Delete(Guid id) + [HttpPost] + [Route("Delete/{id}")] + public async Task Delete(Guid id, [FromBody] TorrentControllerDeleteRequest request) { - await _torrents.Delete(id); + await _torrents.Delete(id, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles); return Ok(); } @@ -90,7 +98,7 @@ namespace RdtClient.Web.Controllers [Route("Download/{id}")] public async Task Download(Guid id) { - await _torrents.Download(id); + await _torrents.Unrestrict(id); return Ok(); } @@ -99,6 +107,7 @@ namespace RdtClient.Web.Controllers public class TorrentControllerUploadFileRequest { public Boolean AutoDownload { get; set; } + public Boolean AutoUnpack { get; set; } public Boolean AutoDelete { get; set; } } @@ -106,6 +115,14 @@ namespace RdtClient.Web.Controllers { public String MagnetLink { get; set; } public Boolean AutoDownload { get; set; } + public Boolean AutoUnpack { get; set; } public Boolean AutoDelete { get; set; } } + + public class TorrentControllerDeleteRequest + { + public Boolean DeleteData { get; set; } + public Boolean DeleteRdTorrent { get; set; } + public Boolean DeleteLocalFiles { get; set; } + } } \ No newline at end of file diff --git a/server/RdtClient.Web/Startup.cs b/server/RdtClient.Web/Startup.cs index 3aaf03b..36f7013 100644 --- a/server/RdtClient.Web/Startup.cs +++ b/server/RdtClient.Web/Startup.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder;
Delete torrent