From fa11dc9bc7e1e6e262e6b1c3870dc8a543ce422d Mon Sep 17 00:00:00 2001 From: Roger Far Date: Sun, 18 Jul 2021 17:29:45 -0600 Subject: [PATCH] Big update on the add download and setting screens which are now proper pages instead of modals. Fixed issue where it would slow down on waiting for RealDebrid generating links for downloads. --- client/package-lock.json | 25 + client/package.json | 2 + .../add-new-torrent.component.html | 123 +++++ .../add-new-torrent.component.scss | 19 + .../add-new-torrent.component.ts | 164 +++++++ client/src/app/app-routing.module.ts | 17 + client/src/app/app.module.ts | 7 +- .../main-layout/main-layout.component.html | 2 +- client/src/app/models/torrent.model.ts | 5 + .../add-new-torrent.component.html | 77 ---- .../add-new-torrent.component.scss | 8 - .../add-new-torrent.component.ts | 131 ------ client/src/app/navbar/navbar.component.html | 15 +- client/src/app/navbar/navbar.component.ts | 10 +- .../navbar/settings/settings.component.html | 265 ----------- .../src/app/settings/settings.component.html | 254 ++++++++++ .../settings/settings.component.scss | 0 .../settings/settings.component.ts | 32 +- client/src/app/torrent.service.ts | 39 +- server/RdtClient.Data/Data/TorrentData.cs | 50 +- .../Enums/TorrentDownloadAction.cs | 9 + .../Enums/TorrentFinishedAction.cs | 9 + ...718222602_Torrents_Add_Actions.Designer.cs | 430 +++++++++++++++++ .../20210718222602_Torrents_Add_Actions.cs | 67 +++ ...758_Torrents_Add_FilesSelected.Designer.cs | 433 ++++++++++++++++++ ...210718231758_Torrents_Add_FilesSelected.cs | 24 + .../Migrations/DataContextModelSnapshot.cs | 20 +- server/RdtClient.Data/Models/Data/Torrent.cs | 28 +- .../RdtClient.Service/Services/QBittorrent.cs | 22 +- .../Services/TorrentRunner.cs | 80 ++-- server/RdtClient.Service/Services/Torrents.cs | 251 +++++----- .../Controllers/QBittorrentController.cs | 6 +- .../Controllers/TorrentsController.cs | 36 +- 33 files changed, 1951 insertions(+), 709 deletions(-) create mode 100644 client/src/app/add-new-torrent/add-new-torrent.component.html create mode 100644 client/src/app/add-new-torrent/add-new-torrent.component.scss create mode 100644 client/src/app/add-new-torrent/add-new-torrent.component.ts delete mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.html delete mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss delete mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts delete mode 100644 client/src/app/navbar/settings/settings.component.html create mode 100644 client/src/app/settings/settings.component.html rename client/src/app/{navbar => }/settings/settings.component.scss (100%) rename client/src/app/{navbar => }/settings/settings.component.ts (91%) create mode 100644 server/RdtClient.Data/Enums/TorrentDownloadAction.cs create mode 100644 server/RdtClient.Data/Enums/TorrentFinishedAction.cs create mode 100644 server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs create mode 100644 server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.cs create mode 100644 server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs create mode 100644 server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.cs diff --git a/client/package-lock.json b/client/package-lock.json index c2af9d2..1bc9c26 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -291,6 +291,23 @@ "tslib": "^2.2.0" } }, + "@angular/cdk": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.1.2.tgz", + "integrity": "sha512-ALupZejZDsVYcbNZcEH1cV8SDgVBL40FAwDnlSZxCgd0HOBHH0ZqQV+8z0uCQeMatoNM+SwmJ8Y1JXYh9Bqfiw==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + } + } + }, "@angular/cli": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-12.1.2.tgz", @@ -469,6 +486,14 @@ "tslib": "^2.2.0" } }, + "@angular/flex-layout": { + "version": "12.0.0-beta.34", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-12.0.0-beta.34.tgz", + "integrity": "sha512-nLwKovXpyG+/U3Lbmfwt+q4ARupxtbPmFTZD0Y8oItFAV6/Oh9l+QQsNQa2VhOHAOrVagyDwcEM+SePtB5EmrQ==", + "requires": { + "tslib": "^2.1.0" + } + }, "@angular/forms": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.1.2.tgz", diff --git a/client/package.json b/client/package.json index 8aa2fa3..1afb77b 100644 --- a/client/package.json +++ b/client/package.json @@ -13,9 +13,11 @@ "private": true, "dependencies": { "@angular/animations": "~12.1.2", + "@angular/cdk": "^12.1.2", "@angular/common": "~12.1.2", "@angular/compiler": "~12.1.2", "@angular/core": "~12.1.2", + "@angular/flex-layout": "^12.0.0-beta.34", "@angular/forms": "~12.1.2", "@angular/platform-browser": "~12.1.2", "@angular/platform-browser-dynamic": "~12.1.2", 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 new file mode 100644 index 0000000..bd5af13 --- /dev/null +++ b/client/src/app/add-new-torrent/add-new-torrent.component.html @@ -0,0 +1,123 @@ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+
+ MB +
+
+
+

This setting does not apply to manually selected files.

+
+ +
+ +
+ +
+
+ +
+ +
+ +
+

The category becomes a sub-folder in your main download path.

+
+
+
+
+ +

+ 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. +

+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
{{ error }}
+
+
+ +
+
+ +
+
diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.scss b/client/src/app/add-new-torrent/add-new-torrent.component.scss new file mode 100644 index 0000000..64f7f7d --- /dev/null +++ b/client/src/app/add-new-torrent/add-new-torrent.component.scss @@ -0,0 +1,19 @@ +.file-name { + max-width: 24em; + width: 24em; +} + +.scroll-container { + width: 100%; + overflow-x: auto; + overflow-y: auto; + max-height: 603px; +} + +.is-fullwidth-label { + width: 100%; +} + +.is-block { + display: block; +} diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.ts b/client/src/app/add-new-torrent/add-new-torrent.component.ts new file mode 100644 index 0000000..2a384fe --- /dev/null +++ b/client/src/app/add-new-torrent/add-new-torrent.component.ts @@ -0,0 +1,164 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { TorrentService } from 'src/app/torrent.service'; +import { TorrentFileAvailability } from '../models/torrent.model'; + +@Component({ + selector: 'app-add-new-torrent', + templateUrl: './add-new-torrent.component.html', + styleUrls: ['./add-new-torrent.component.scss'], +}) +export class AddNewTorrentComponent implements OnInit { + public fileName: string; + public magnetLink: string; + private currentTorrentFile: string; + + public category: string; + + public downloadAction: number = 0; + public finishedAction: number = 0; + + public downloadMinSize: number = 0; + + public availableFiles: TorrentFileAvailability[] = []; + public downloadFiles: { [key: string]: boolean } = {}; + + public saving = false; + public error: string; + + private selectedFile: File; + + constructor(private router: Router, private torrentService: TorrentService) {} + + ngOnInit(): void {} + + public pickFile(evt: Event): void { + const files = (evt.target as HTMLInputElement).files; + + if (files.length === 0) { + return; + } + + const file = files[0]; + + this.fileName = file.name; + + this.selectedFile = file; + + this.checkFiles(); + } + + public downloadFileChecked(file: string): void { + this.downloadFiles[file] = !this.downloadFiles[file]; + } + + public ok(): void { + this.saving = true; + this.error = null; + + let downloadManualFiles: string = null; + + if (this.downloadAction === 2) { + const selectedFiles = []; + for (let filePath in this.downloadFiles) { + if (this.downloadFiles[filePath] === true) { + selectedFiles.push(filePath); + } + } + + if (selectedFiles.length === 0) { + this.error = 'No files have been selected to download'; + return; + } + + downloadManualFiles = selectedFiles.join(','); + } + + if (this.magnetLink) { + this.torrentService + .uploadMagnet( + this.magnetLink, + this.category, + this.downloadAction, + this.finishedAction, + this.downloadMinSize, + downloadManualFiles + ) + .subscribe( + () => { + this.router.navigate(['/']); + }, + (err) => { + this.error = err.error; + this.saving = false; + } + ); + } else if (this.selectedFile) { + this.torrentService + .uploadFile( + this.selectedFile, + this.category, + this.downloadAction, + this.finishedAction, + this.downloadMinSize, + downloadManualFiles + ) + .subscribe( + () => { + this.router.navigate(['/']); + }, + (err) => { + this.error = err.error; + this.saving = false; + } + ); + } else { + this.error = 'No magnet or file uploaded'; + this.saving = false; + } + } + + public checkFiles(): void { + if (this.magnetLink && this.magnetLink === this.currentTorrentFile) { + return; + } + + this.saving = true; + this.error = null; + this.availableFiles = []; + this.downloadFiles = {}; + + if (this.magnetLink) { + this.torrentService.checkFilesMagnet(this.magnetLink).subscribe( + (result) => { + this.saving = false; + this.availableFiles = result; + this.currentTorrentFile = this.magnetLink; + result.forEach((file) => { + this.downloadFiles[file.filename] = true; + }); + }, + (err) => { + this.error = err.error; + this.saving = false; + } + ); + } else if (this.selectedFile) { + this.torrentService.checkFiles(this.selectedFile).subscribe( + (result) => { + this.saving = false; + this.availableFiles = result; + result.forEach((file) => { + this.downloadFiles[file.filename] = true; + }); + }, + (err) => { + this.error = err.error; + this.saving = false; + } + ); + } else { + this.saving = false; + } + } +} diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index f16c983..9047896 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,9 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component'; import { AuthResolverService } from './auth-resolver.service'; import { LoginComponent } from './login/login.component'; import { MainLayoutComponent } from './main-layout/main-layout.component'; +import { SettingsComponent } from './settings/settings.component'; import { SetupComponent } from './setup/setup.component'; +import { TorrentTableComponent } from './torrent-table/torrent-table.component'; const routes: Routes = [ { @@ -20,6 +23,20 @@ const routes: Routes = [ resolve: { isLoggedIn: AuthResolverService, }, + children: [ + { + path: '', + component: TorrentTableComponent, + }, + { + path: 'add', + component: AddNewTorrentComponent, + }, + { + path: 'settings', + component: SettingsComponent, + }, + ], }, ]; diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 036ff92..8b35ce8 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -1,18 +1,19 @@ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { curray } from 'curray'; import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize'; +import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AuthInterceptor } from './auth.interceptor'; import { DownloadStatusPipe } from './download-status.pipe'; import { LoginComponent } from './login/login.component'; import { MainLayoutComponent } from './main-layout/main-layout.component'; -import { AddNewTorrentComponent } from './navbar/add-new-torrent/add-new-torrent.component'; import { NavbarComponent } from './navbar/navbar.component'; -import { SettingsComponent } from './navbar/settings/settings.component'; +import { SettingsComponent } from './settings/settings.component'; import { SetupComponent } from './setup/setup.component'; import { TorrentDownloadComponent } from './torrent-download/torrent-download.component'; import { TorrentFileComponent } from './torrent-file/torrent-file.component'; @@ -38,7 +39,7 @@ curray(); SetupComponent, TorrentDownloadComponent, ], - imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule], + imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule, FlexLayoutModule], providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }], bootstrap: [AppComponent], }) diff --git a/client/src/app/main-layout/main-layout.component.html b/client/src/app/main-layout/main-layout.component.html index 1ad0a23..6df30e1 100644 --- a/client/src/app/main-layout/main-layout.component.html +++ b/client/src/app/main-layout/main-layout.component.html @@ -1,6 +1,6 @@
- +
diff --git a/client/src/app/models/torrent.model.ts b/client/src/app/models/torrent.model.ts index 68d8c2a..1a16267 100644 --- a/client/src/app/models/torrent.model.ts +++ b/client/src/app/models/torrent.model.ts @@ -35,6 +35,11 @@ export class TorrentFile { public download: Download; } +export class TorrentFileAvailability { + public filename: string; + public filesize: number; +} + export enum RealDebridStatus { Processing = 0, WaitingForFileSelection = 1, 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 deleted file mode 100644 index 04832a0..0000000 --- a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss deleted file mode 100644 index 4b5fc83..0000000 --- a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -.file-name { - max-width: 24em; - width: 24em; -} - -.file-modal { - width: calc(75% - 40px); -} 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 deleted file mode 100644 index 8d1ce08..0000000 --- a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { TorrentService } from 'src/app/torrent.service'; - -@Component({ - selector: 'app-add-new-torrent', - templateUrl: './add-new-torrent.component.html', - styleUrls: ['./add-new-torrent.component.scss'], -}) -export class AddNewTorrentComponent implements OnInit { - @Input() - public get open(): boolean { - return this.isActive; - } - - public set open(val: boolean) { - this.reset(); - this.isActive = val; - } - - @Output() - public openChange = new EventEmitter(); - - public isActive = false; - public isFileModalActive = false; - - public fileName: string; - public magnetLink: string; - public autoDelete: boolean; - - public fileList: string[]; - - public saving = false; - public error: string; - - private selectedFile: File; - - constructor(private torrentService: TorrentService) {} - - ngOnInit(): void {} - - public reset(): void { - this.fileName = ''; - this.magnetLink = ''; - this.autoDelete = false; - - this.saving = false; - this.selectedFile = null; - this.error = null; - } - - public pickFile(evt: Event): void { - const files = (evt.target as HTMLInputElement).files; - - if (files.length === 0) { - return; - } - - const file = files[0]; - - this.fileName = file.name; - - this.selectedFile = file; - } - - public ok(): void { - this.saving = true; - this.error = null; - - if (this.magnetLink) { - this.torrentService.uploadMagnet(this.magnetLink, this.autoDelete).subscribe( - () => { - this.cancel(); - }, - (err) => { - this.error = err.error; - this.saving = false; - } - ); - } else if (this.selectedFile) { - this.torrentService.uploadFile(this.selectedFile, this.autoDelete).subscribe( - () => { - this.cancel(); - }, - (err) => { - this.error = err.error; - this.saving = false; - } - ); - } else { - this.cancel(); - } - } - - public checkFiles(): void { - this.saving = true; - this.error = null; - - if (this.magnetLink) { - this.torrentService.checkFilesMagnet(this.magnetLink).subscribe( - (result) => { - this.saving = false; - this.isFileModalActive = true; - this.fileList = result; - }, - (err) => { - this.error = err.error; - this.saving = false; - } - ); - } else if (this.selectedFile) { - this.torrentService.checkFiles(this.selectedFile).subscribe( - (result) => { - this.saving = false; - this.isFileModalActive = true; - this.fileList = result; - }, - (err) => { - this.error = err.error; - this.saving = false; - } - ); - } else { - this.cancel(); - } - } - - public cancel(): void { - this.isActive = false; - this.openChange.emit(this.open); - } -} diff --git a/client/src/app/navbar/navbar.component.html b/client/src/app/navbar/navbar.component.html index 5cded01..50f9f04 100644 --- a/client/src/app/navbar/navbar.component.html +++ b/client/src/app/navbar/navbar.component.html @@ -1,6 +1,6 @@ - - - diff --git a/client/src/app/navbar/navbar.component.ts b/client/src/app/navbar/navbar.component.ts index e6bfedc..0aa7162 100644 --- a/client/src/app/navbar/navbar.component.ts +++ b/client/src/app/navbar/navbar.component.ts @@ -1,9 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { TorrentService } from '../torrent.service'; -import { SettingsService } from '../settings.service'; -import { Profile } from '../models/profile.model'; -import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; +import { AuthService } from '../auth.service'; +import { Profile } from '../models/profile.model'; +import { SettingsService } from '../settings.service'; @Component({ selector: 'app-navbar', @@ -13,9 +12,6 @@ import { Router } from '@angular/router'; export class NavbarComponent implements OnInit { public showMobileMenu = false; - public showNewTorrent = false; - public showSettings = false; - public profile: Profile; constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {} diff --git a/client/src/app/navbar/settings/settings.component.html b/client/src/app/navbar/settings/settings.component.html deleted file mode 100644 index 5435135..0000000 --- a/client/src/app/navbar/settings/settings.component.html +++ /dev/null @@ -1,265 +0,0 @@ - diff --git a/client/src/app/settings/settings.component.html b/client/src/app/settings/settings.component.html new file mode 100644 index 0000000..c2cfce4 --- /dev/null +++ b/client/src/app/settings/settings.component.html @@ -0,0 +1,254 @@ +
+ +
+ +
+
+ +
+ +
+

+ You can find your API key here: + https://real-debrid.com/apitoken. +

+
+ +
+ +
+ +
+

Recommended level is Warning, set to Debug to get the most info.

+
+
+ +
+ +
+

Path in the docker container to download files to (i.e. /data/downloads).

+
+
+ +
+ +
+

+ Path where files are downloaded to on your host (i.e. D:\Downloads). This path is used for Radarr and Sonarr to + find your downloads. +

+
+ +
+ +
+ +
+

Maximum amount of downloads that get unpacked on your host at the same time.

+
+
+ +
+
+ +
+ +
+

+ Select which download client to use, see the + README for the various options. +

+
+ +
+ +
+ +
+

+ Path in the docker container to temporarily download to (i.e. /data/temp). Make sure the docker container has + enough disk space if using a path inside the container. +

+
+ +
+ +
+ +
+

Maximum amount of torrents that get downloaded to your host at the same time.

+
+ +
+ +
+ +
+

+ Maximum amount of parallel threads that are used to download a single torrent to your host. If set to 1 no + parallel downloading will be done. +

+
+
+ +
+ +
+

Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.

+
+
+ +
+ +
+

Address of a proxy server.

+
+
+ +
+

+ The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or Sonarr. +

+
+ +
+
+
+ +
+
+ MB +
+
+
+ Files that are smaller than this setting are skipped and not downloaded. When set to 0 all files are downloaded. + When downloading from Radarr or Sonarr it's recommended to keep this setting at atleast a few MB to avoid + RealDebrid having to re-download the torrent. +
+
+
+ +
+
+ +
+ 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. +
+
+
+
+ +
+
+ +
+ +
+ Could not test your download path
+ {{ testPathError }} +
+ +
Your download path looks good!
+
+
This will check if the download folder has write permissions.
+
+ +
+ +
+ +
+ Could not test your download speed
+ {{ testDownloadSpeedError }} +
+ +
+ Download speed {{ testDownloadSpeedSuccess | filesize }}/s +
+
+
+ This will attempt to download a 10GB file from Real Debrid. When 50MB has been downloaded the test will stop. +
+
+ +
+ +
+ +
+ Could not test your download speed
+ {{ testWriteSpeedError }} +
+ +
+ Write speed {{ testWriteSpeedSuccess | filesize }}/s +
+
+
This will write a small file to your download folder to see how fast it can write to it.
+
+
+ +
+
+
Error saving settings: {{ error }}
+
+
+ +
+
+ +
+
diff --git a/client/src/app/navbar/settings/settings.component.scss b/client/src/app/settings/settings.component.scss similarity index 100% rename from client/src/app/navbar/settings/settings.component.scss rename to client/src/app/settings/settings.component.scss diff --git a/client/src/app/navbar/settings/settings.component.ts b/client/src/app/settings/settings.component.ts similarity index 91% rename from client/src/app/navbar/settings/settings.component.ts rename to client/src/app/settings/settings.component.ts index 985a773..dcf0c73 100644 --- a/client/src/app/navbar/settings/settings.component.ts +++ b/client/src/app/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Setting } from 'src/app/models/setting.model'; import { SettingsService } from 'src/app/settings.service'; @@ -8,23 +8,8 @@ import { SettingsService } from 'src/app/settings.service'; styleUrls: ['./settings.component.scss'], }) export class SettingsComponent implements OnInit { - @Input() - public get open(): boolean { - return this.isActive; - } - - public set open(val: boolean) { - this.reset(); - this.isActive = val; - } - - @Output() - public openChange = new EventEmitter(); - public activeTab = 0; - public isActive = false; - public saving = false; public error: string; @@ -53,7 +38,9 @@ export class SettingsComponent implements OnInit { constructor(private settingsService: SettingsService) {} - ngOnInit(): void {} + ngOnInit(): void { + this.reset(); + } public reset(): void { this.saving = false; @@ -142,10 +129,12 @@ export class SettingsComponent implements OnInit { this.settingsService.update(settings).subscribe( () => { - this.isActive = false; - this.openChange.emit(this.open); + setTimeout(() => { + this.saving = false; + }, 1000); }, (err) => { + this.saving = false; this.error = err; } ); @@ -201,11 +190,6 @@ export class SettingsComponent implements OnInit { ); } - public cancel(): void { - this.isActive = false; - this.openChange.emit(this.open); - } - private getSetting(settings: Setting[], key: string): string { const setting = settings.filter((m) => m.settingId === key); diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts index 0360f8d..18edb14 100644 --- a/client/src/app/torrent.service.ts +++ b/client/src/app/torrent.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import * as signalR from '@microsoft/signalr'; import { Observable, Subject } from 'rxjs'; -import { Torrent } from './models/torrent.model'; +import { Torrent, TorrentFileAvailability } from './models/torrent.model'; @Injectable({ providedIn: 'root', @@ -31,30 +31,51 @@ export class TorrentService { return this.http.get(`/Api/Torrents`); } - public uploadMagnet(magnetLink: string, autoDelete: boolean): Observable { + public uploadMagnet( + magnetLink: string, + category: string, + downloadAction: number, + finishedAction: number, + downloadMinSize: number, + downloadManualFiles: string + ): Observable { return this.http.post(`/Api/Torrents/UploadMagnet`, { magnetLink, - autoDelete, + category, + downloadAction, + finishedAction, + downloadMinSize, + downloadManualFiles, }); } - public uploadFile(file: File, autoDelete: boolean): Observable { + public uploadFile( + file: File, + category: string, + downloadAction: number, + finishedAction: number, + downloadMinSize: number, + downloadManualFiles: string + ): Observable { const formData: FormData = new FormData(); formData.append('file', file); - formData.append('formData', JSON.stringify({ autoDelete })); + formData.append( + 'formData', + JSON.stringify({ category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles }) + ); return this.http.post(`/Api/Torrents/UploadFile`, formData); } - public checkFilesMagnet(magnetLink: string): Observable { - return this.http.post(`/Api/Torrents/CheckFilesMagnet`, { + public checkFilesMagnet(magnetLink: string): Observable { + return this.http.post(`/Api/Torrents/CheckFilesMagnet`, { magnetLink, }); } - public checkFiles(file: File): Observable { + public checkFiles(file: File): Observable { const formData: FormData = new FormData(); formData.append('file', file); - return this.http.post(`/Api/Torrents/CheckFiles`, formData); + return this.http.post(`/Api/Torrents/CheckFiles`, formData); } public delete( diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs index 0bd73d6..8d6103c 100644 --- a/server/RdtClient.Data/Data/TorrentData.cs +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; namespace RdtClient.Data.Data @@ -13,10 +14,21 @@ namespace RdtClient.Data.Data Task> Get(); Task GetById(Guid torrentId); Task GetByHash(String hash); - Task Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile); + + 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(); } @@ -95,7 +107,15 @@ namespace RdtClient.Data.Data return dbTorrent; } - public async Task Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile) + public async Task Add(String realDebridId, + String hash, + String category, + TorrentDownloadAction downloadAction, + TorrentFinishedAction finishedAction, + Int32 downloadMinSize, + String downloadManualFiles, + String fileOrMagnetContents, + Boolean isFile) { var torrent = new Torrent { @@ -104,7 +124,10 @@ namespace RdtClient.Data.Data RdId = realDebridId, Hash = hash.ToLower(), Category = category, - AutoDelete = autoDelete, + DownloadAction = downloadAction, + FinishedAction = finishedAction, + DownloadMinSize = downloadMinSize, + DownloadManualFiles = downloadManualFiles, FileOrMagnet = fileOrMagnetContents, IsFile = isFile }; @@ -148,7 +171,7 @@ namespace RdtClient.Data.Data await VoidCache(); } - + public async Task UpdateCategory(Guid torrentId, String category) { var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); @@ -161,7 +184,7 @@ namespace RdtClient.Data.Data dbTorrent.Category = category; await _dataContext.SaveChangesAsync(); - + await VoidCache(); } @@ -172,7 +195,18 @@ namespace RdtClient.Data.Data dbTorrent.Completed = datetime; await _dataContext.SaveChangesAsync(); - + + await VoidCache(); + } + + public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); + + dbTorrent.FilesSelected = datetime; + + await _dataContext.SaveChangesAsync(); + await VoidCache(); } @@ -188,7 +222,7 @@ namespace RdtClient.Data.Data _dataContext.Torrents.Remove(dbTorrent); await _dataContext.SaveChangesAsync(); - + await VoidCache(); } @@ -206,4 +240,4 @@ namespace RdtClient.Data.Data } } } -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Enums/TorrentDownloadAction.cs b/server/RdtClient.Data/Enums/TorrentDownloadAction.cs new file mode 100644 index 0000000..7210567 --- /dev/null +++ b/server/RdtClient.Data/Enums/TorrentDownloadAction.cs @@ -0,0 +1,9 @@ +namespace RdtClient.Data.Enums +{ + public enum TorrentDownloadAction + { + DownloadAll = 0, + DownloadAvailableFiles = 1, + DownloadManual = 2 + } +} diff --git a/server/RdtClient.Data/Enums/TorrentFinishedAction.cs b/server/RdtClient.Data/Enums/TorrentFinishedAction.cs new file mode 100644 index 0000000..2643419 --- /dev/null +++ b/server/RdtClient.Data/Enums/TorrentFinishedAction.cs @@ -0,0 +1,9 @@ +namespace RdtClient.Data.Enums +{ + public enum TorrentFinishedAction + { + None = 0, + RemoveAllTorrents = 1, + RemoveRealDebrid = 2 + } +} diff --git a/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs b/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs new file mode 100644 index 0000000..c2c9a5d --- /dev/null +++ b/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs @@ -0,0 +1,430 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210718222602_Torrents_Add_Actions")] + partial class Torrents_Add_Actions + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + 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("Path") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .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"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Torrent"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Navigation("Downloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.cs b/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.cs new file mode 100644 index 0000000..54218d4 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RdtClient.Data.Migrations +{ + public partial class Torrents_Add_Actions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AutoDelete", + table: "Torrents"); + + migrationBuilder.AddColumn( + name: "FinishedAction", + table: "Torrents", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DownloadAction", + table: "Torrents", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DownloadManualFiles", + table: "Torrents", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "DownloadMinSize", + table: "Torrents", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DownloadAction", + table: "Torrents"); + + migrationBuilder.DropColumn( + name: "DownloadManualFiles", + table: "Torrents"); + + migrationBuilder.DropColumn( + name: "DownloadMinSize", + table: "Torrents"); + + migrationBuilder.DropColumn( + name: "FinishedAction", + table: "Torrents"); + + migrationBuilder.AddColumn( + name: "AutoDelete", + table: "Torrents", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs b/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs new file mode 100644 index 0000000..2e26a27 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs @@ -0,0 +1,433 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210718231758_Torrents_Add_FilesSelected")] + partial class Torrents_Add_FilesSelected + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + 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("Path") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .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"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Torrent"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Navigation("Downloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.cs b/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.cs new file mode 100644 index 0000000..fde01aa --- /dev/null +++ b/server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RdtClient.Data.Migrations +{ + public partial class Torrents_Add_FilesSelected : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FilesSelected", + table: "Torrents", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FilesSelected", + table: "Torrents"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index b614ada..9cc3b17 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace RdtClient.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.3"); + .HasAnnotation("ProductVersion", "5.0.8"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -285,18 +285,30 @@ namespace RdtClient.Data.Migrations b.Property("Added") .HasColumnType("TEXT"); - b.Property("AutoDelete") - .HasColumnType("INTEGER"); - b.Property("Category") .HasColumnType("TEXT"); b.Property("Completed") .HasColumnType("TEXT"); + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + b.Property("FileOrMagnet") .HasColumnType("TEXT"); + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + b.Property("Hash") .HasColumnType("TEXT"); diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs index 165c7a9..2a0466e 100644 --- a/server/RdtClient.Data/Models/Data/Torrent.cs +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -17,17 +17,15 @@ namespace RdtClient.Data.Models.Data public String Category { get; set; } + public TorrentDownloadAction DownloadAction { get; set; } + public TorrentFinishedAction FinishedAction { get; set; } + public Int32 DownloadMinSize { get; set; } + public String DownloadManualFiles { get; set; } + public DateTimeOffset Added { get; set; } + public DateTimeOffset? FilesSelected { get; set; } public DateTimeOffset? Completed { get; set; } - public Boolean DownloadOnlyAvailableFiles { get; set; } - - public String DownloadFiles { get; set; } - - public Int32 MinimumFileSize { get; set; } - - public Boolean AutoDelete { get; set; } - public String FileOrMagnet { get; set; } public Boolean IsFile { get; set; } @@ -68,5 +66,19 @@ namespace RdtClient.Data.Models.Data } } } + + [NotMapped] + public IList ManualFiles + { + get + { + if (String.IsNullOrWhiteSpace(DownloadManualFiles)) + { + return new List(); + } + + return DownloadManualFiles.Split(","); + } + } } } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 1af7223..c554759 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 TorrentsAddMagnet(String magnetLink, String category, Boolean autoDelete); - Task TorrentsAddFile(Byte[] fileBytes, String category, Boolean autoDelete); + Task TorrentsAddMagnet(String magnetLink, String category); + Task TorrentsAddFile(Byte[] fileBytes, String category); Task TorrentsSetCategory(String hash, String category); Task> TorrentsCategories(); Task CategoryCreate(String category); @@ -432,14 +432,24 @@ namespace RdtClient.Service.Services await _torrents.Delete(torrent.TorrentId, true, true, deleteFiles); } - public async Task TorrentsAddMagnet(String magnetLink, String category, Boolean autoDelete) + public async Task TorrentsAddMagnet(String magnetLink, String category) { - await _torrents.UploadMagnet(magnetLink, category, autoDelete); + var onlyDownloadAvailableFilesSetting = await _settings.GetNumber("OnlyDownloadAvailableFiles"); + var minFileSizeSetting = await _settings.GetNumber("MinFileSize"); + + var downloadAction = onlyDownloadAvailableFilesSetting == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll; + + await _torrents.UploadMagnet(magnetLink, category, downloadAction, TorrentFinishedAction.None, minFileSizeSetting, null); } - public async Task TorrentsAddFile(Byte[] fileBytes, String category, Boolean autoDelete) + public async Task TorrentsAddFile(Byte[] fileBytes, String category) { - await _torrents.UploadFile(fileBytes, category, autoDelete); + var onlyDownloadAvailableFilesSetting = await _settings.GetNumber("OnlyDownloadAvailableFiles"); + var minFileSizeSetting = await _settings.GetNumber("MinFileSize"); + + var downloadAction = onlyDownloadAvailableFilesSetting == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll; + + await _torrents.UploadFile(fileBytes, category, downloadAction, TorrentFinishedAction.None, minFileSizeSetting, null); } public async Task TorrentsSetCategory(String hash, String category) diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 762b546..3340ad7 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -80,18 +80,7 @@ namespace RdtClient.Service.Services Log.Debug($"No RealDebridApiKey set!"); return; } - - var settingMinFileSize = settings.GetNumber("MinFileSize"); - if (settingMinFileSize <= 0) - { - settingMinFileSize = 0; - } - - settingMinFileSize = settingMinFileSize * 1024 * 1024; - - var settingOnlyDownloadAvailableFilesRaw = settings.GetNumber("OnlyDownloadAvailableFiles"); - var settingOnlyDownloadAvailableFiles = settingOnlyDownloadAvailableFilesRaw == 1; - + var settingDownloadLimit = settings.GetNumber("DownloadLimit"); if (settingDownloadLimit < 1) { @@ -365,35 +354,52 @@ namespace RdtClient.Service.Services continue; } + // 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. if ((torrent.RdStatus == RealDebridStatus.WaitingForFileSelection || torrent.RdStatus == RealDebridStatus.Finished) && - torrent.Downloads.Count == 0) + torrent.Downloads.Count == 0 && + torrent.FilesSelected == null) { Log.Debug($"Torrent {torrent.RdId} selecting files"); var files = torrent.Files; - if (settingOnlyDownloadAvailableFiles) + if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles) { Log.Debug($"Torrent {torrent.RdId} determining which files are already available"); var availableFiles = await _torrents.GetAvailableFiles(torrent.Hash); - Log.Debug($"Found {availableFiles.Count} available files for torrent {torrent.RdId}"); + Log.Debug($"Found {files.Count}/{torrent.Files.Count} available files for torrent {torrent.RdId}"); - if (availableFiles.Count > 0) - { - files = torrent.Files.Where(m => availableFiles.Any(f => m.Path.EndsWith(f))).ToList(); - - Log.Debug($"Selecting {files.Count} available files for torrent {torrent.RdId}"); - } + files = torrent.Files.Where(m => availableFiles.Any(f => m.Path.EndsWith(f.Filename))).ToList(); + } + else if (torrent.DownloadAction == TorrentDownloadAction.DownloadAll) + { + Log.Debug("Selecting all files"); + files = torrent.Files.ToList(); + } + else if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual) + { + Log.Debug("Selecting manual selected files"); + files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList(); } - if (settingMinFileSize > 0) - { - Log.Debug($"Torrent {torrent.RdId} determining which files are over {settingMinFileSize} bytes"); + Log.Debug($"Selecting {files.Count}/{torrent.Files.Count} files for torrent {torrent.RdId}"); - files = files.Where(m => m.Bytes > settingMinFileSize).ToList(); + if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0) + { + var minFileSize = torrent.DownloadMinSize * 1024 * 1024; + + Log.Debug($"Torrent {torrent.RdId} determining which files are over {minFileSize} bytes"); + + files = files.Where(m => m.Bytes > minFileSize) + .ToList(); Log.Debug($"Found {files.Count} files that match the minimum file size criterea for torrent {torrent.RdId}"); } @@ -410,13 +416,8 @@ namespace RdtClient.Service.Services Log.Debug($"Selecting files for torrent {torrent.RdId}: {String.Join(", ", fileIds)}"); await _torrents.SelectFiles(torrent.TorrentId, fileIds); - } - // If the torrent doesn't have any files at this point, don't process it further. - if (torrent.Files.Count == 0) - { - Log.Debug($"No files found for torrent {torrent.RdId}!"); - continue; + await _torrents.UpdateFilesSelected(torrent.TorrentId, DateTime.UtcNow); } // RealDebrid finished downloading the torrent, process the file to host. @@ -479,12 +480,19 @@ namespace RdtClient.Service.Services await _torrents.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow); - // Remove the torrent from RealDebrid - if (torrent.AutoDelete) + switch (torrent.FinishedAction) { - Log.Debug($"Torrent {torrent.RdId} removing"); - - await _torrents.Delete(torrent.TorrentId, true, true, false); + case TorrentFinishedAction.RemoveAllTorrents: + 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"); + await _torrents.Delete(torrent.TorrentId, false, true, false); + break; + case TorrentFinishedAction.None: + Log.Debug($"Torrent {torrent.RdId} not removing torrents or files"); + break; } } } diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index deb90c5..c0aefdb 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -7,9 +7,9 @@ using System.Threading.Tasks; using MonoTorrent; using Newtonsoft.Json; using RDNET; +using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Service.Models; -using ITorrentData = RdtClient.Data.Data.ITorrentData; using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services @@ -19,10 +19,24 @@ namespace RdtClient.Service.Services Task> Get(); Task GetByHash(String hash); Task UpdateCategory(String hash, String category); - Task UploadMagnet(String magnetLink, String category, Boolean autoDelete); - Task UploadFile(Byte[] bytes, String category, Boolean autoDelete); - Task> GetAvailableFiles(String hash); + + Task UploadMagnet(String magnetLink, + String category, + TorrentDownloadAction downloadAction, + TorrentFinishedAction finishedAction, + Int32 downloadMinSize, + String downloadManualFiles); + + 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); @@ -30,6 +44,7 @@ namespace RdtClient.Service.Services void Reset(); Task GetProfile(); Task UpdateComplete(Guid torrentId, DateTimeOffset datetime); + Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime); Task Update(); Task Retry(Guid id, Int32 retry); } @@ -39,7 +54,7 @@ namespace RdtClient.Service.Services private static RdNetClient _rdtNetClient; private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1); - + private readonly IDownloads _downloads; private readonly ISettings _settings; private readonly ITorrentData _torrentData; @@ -51,28 +66,10 @@ namespace RdtClient.Service.Services _downloads = downloads; } - private RdNetClient GetRdNetClient() - { - if (_rdtNetClient == null) - { - var apiKey = _settings.GetString("RealDebridApiKey") - .Result; - - if (String.IsNullOrWhiteSpace(apiKey)) - { - throw new Exception("RealDebrid API Key not set in the settings"); - } - - _rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey); - } - - return _rdtNetClient; - } - public async Task> Get() { var torrents = await _torrentData.Get(); - + foreach (var torrent in torrents) { foreach (var download in torrent.Downloads) @@ -83,7 +80,7 @@ namespace RdtClient.Service.Services download.BytesTotal = downloadClient.BytesTotal; download.BytesDone = downloadClient.BytesDone; } - + if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) { download.BytesTotal = unpackClient.BytesTotal; @@ -95,18 +92,6 @@ namespace RdtClient.Service.Services return torrents; } - public async Task GetById(Guid torrentId) - { - var torrent = await _torrentData.GetById(torrentId); - - if (torrent != null) - { - await Update(torrent); - } - - return torrent; - } - public async Task GetByHash(String hash) { var torrent = await _torrentData.GetByHash(hash); @@ -131,7 +116,12 @@ namespace RdtClient.Service.Services await _torrentData.UpdateCategory(torrent.TorrentId, category); } - public async Task UploadMagnet(String magnetLink, String category, Boolean autoDelete) + public async Task UploadMagnet(String magnetLink, + String category, + TorrentDownloadAction downloadAction, + TorrentFinishedAction finishedAction, + Int32 downloadMinSize, + String downloadManualFiles) { MagnetLink magnet; @@ -146,10 +136,15 @@ namespace RdtClient.Service.Services var rdTorrent = await GetRdNetClient().AddTorrentMagnetAsync(magnetLink); - await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, autoDelete, magnetLink, false); + await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false); } - public async Task UploadFile(Byte[] bytes, String category, Boolean autoDelete) + public async Task UploadFile(Byte[] bytes, + String category, + TorrentDownloadAction downloadAction, + TorrentFinishedAction finishedAction, + Int32 downloadMinSize, + String downloadManualFiles) { MonoTorrent.Torrent torrent; @@ -165,44 +160,36 @@ namespace RdtClient.Service.Services } var rdTorrent = await GetRdNetClient().AddTorrentFileAsync(bytes); - - await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, autoDelete, fileAsBase64, true); + + await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true); } - public async Task> GetAvailableFiles(String hash) + public async Task> GetAvailableFiles(String hash) { var result = await GetRdNetClient().GetAvailableFiles(hash); var files = result.SelectMany(m => m.Value).SelectMany(m => m.Value).SelectMany(m => m.Values); - var groups = files.GroupBy(m => m.Filename); + var groups = files.GroupBy(m => $"{m.Filename}-{m.Filesize}"); - return groups.Select(m => m.Key).ToList(); + return groups.Select(m => m.First()).ToList(); } public async Task SelectFiles(Guid torrentId, IList fileIds) { var torrent = await GetById(torrentId); - RDNET.Torrent rdTorrent = null; + await GetRdNetClient().SelectTorrentFilesAsync(torrent.RdId, fileIds.ToArray()); + } - for (var i = 0; i < 5; i++) - { - await GetRdNetClient().SelectTorrentFilesAsync(torrent.RdId, fileIds.ToArray()); + public async Task CheckForLinks(Guid torrentId) + { + var torrent = await GetById(torrentId); - await Task.Delay(5000); + var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); - rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); - - if (fileIds.Count == rdTorrent.Links.Count) - { - break; - } - - await Task.Delay(10000); - } - - if (rdTorrent == null) + // Sometimes RD will give you 1 rar with all files, sometimes it will give you 1 link per file. + if (torrent.Files.Count != rdTorrent.Links.Count && rdTorrent.Links.Count != 1) { return; } @@ -254,7 +241,7 @@ namespace RdtClient.Service.Services { var downloadPath = await DownloadPath(torrent); downloadPath = Path.Combine(downloadPath, torrent.RdName); - + if (Directory.Exists(downloadPath)) { var retry = 0; @@ -270,6 +257,7 @@ namespace RdtClient.Service.Services catch { retry++; + if (retry >= 3) { throw; @@ -332,33 +320,6 @@ namespace RdtClient.Service.Services return profile; } - private async Task Add(String rdTorrentId, String infoHash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile) - { - 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, category, autoDelete, fileOrMagnetContents, isFile); - - await Update(newTorrent); - } - finally - { - SemaphoreSlim.Release(); - } - } - public async Task Update() { var w = await SemaphoreSlim.WaitAsync(1); @@ -382,7 +343,7 @@ namespace RdtClient.Service.Services { continue; } - + await Update(torrent); } @@ -420,11 +381,16 @@ namespace RdtClient.Service.Services { var bytes = Convert.FromBase64String(torrent.FileOrMagnet); - await UploadFile(bytes, torrent.Category, torrent.AutoDelete); + await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles); } else { - await UploadMagnet(torrent.FileOrMagnet, torrent.Category, torrent.AutoDelete); + await UploadMagnet(torrent.FileOrMagnet, + torrent.Category, + torrent.DownloadAction, + torrent.FinishedAction, + torrent.DownloadMinSize, + torrent.DownloadManualFiles); } } else if (retry == 1) @@ -445,10 +411,89 @@ namespace RdtClient.Service.Services await _torrentData.UpdateComplete(torrentId, datetime); } + public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime) + { + await _torrentData.UpdateFilesSelected(torrentId, datetime); + } + + private RdNetClient GetRdNetClient() + { + if (_rdtNetClient == null) + { + var apiKey = _settings.GetString("RealDebridApiKey") + .Result; + + if (String.IsNullOrWhiteSpace(apiKey)) + { + throw new Exception("RealDebrid API Key not set in the settings"); + } + + _rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey); + } + + return _rdtNetClient; + } + + public async Task GetById(Guid torrentId) + { + var torrent = await _torrentData.GetById(torrentId); + + if (torrent != null) + { + await Update(torrent); + } + + return torrent; + } + + private async Task Add(String rdTorrentId, + String infoHash, + String category, + TorrentDownloadAction downloadAction, + TorrentFinishedAction finishedAction, + Int32 downloadMinSize, + String downloadManualFiles, + String fileOrMagnetContents, + Boolean isFile) + { + 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, + category, + downloadAction, + finishedAction, + downloadMinSize, + downloadManualFiles, + fileOrMagnetContents, + isFile); + + await Update(newTorrent); + } + finally + { + SemaphoreSlim.Release(); + } + } + private async Task DownloadPath(Torrent torrent) { var settingDownloadPath = await _settings.GetString("DownloadPath"); - + if (!String.IsNullOrWhiteSpace(torrent.Category)) { settingDownloadPath = Path.Combine(settingDownloadPath, torrent.Category); @@ -459,10 +504,11 @@ namespace RdtClient.Service.Services private async Task Update(Torrent torrent) { - var originalTorrent = JsonConvert.SerializeObject(torrent, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); + var originalTorrent = JsonConvert.SerializeObject(torrent, + new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId); @@ -515,10 +561,11 @@ namespace RdtClient.Service.Services _ => RealDebridStatus.Error }; - var newTorrent = JsonConvert.SerializeObject(torrent, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); + var newTorrent = JsonConvert.SerializeObject(torrent, + new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); if (originalTorrent != newTorrent) { @@ -526,4 +573,4 @@ namespace RdtClient.Service.Services } } } -} \ No newline at end of file +} diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index 7b63f96..377fc14 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -248,13 +248,13 @@ namespace RdtClient.Web.Controllers { if (url.StartsWith("magnet")) { - await _qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, false); + await _qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category); } else if (url.StartsWith("http")) { var httpClient = new HttpClient(); var result = await httpClient.GetByteArrayAsync(url); - await _qBittorrent.TorrentsAddFile(result, request.Category, false); + await _qBittorrent.TorrentsAddFile(result, request.Category); } else { @@ -279,7 +279,7 @@ namespace RdtClient.Web.Controllers await file.CopyToAsync(target); var fileBytes = target.ToArray(); - await _qBittorrent.TorrentsAddFile(fileBytes, request.Category, false); + await _qBittorrent.TorrentsAddFile(fileBytes, request.Category); } } diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index 1ac28d9..d28b0e0 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using MonoTorrent; +using RdtClient.Data.Enums; using RdtClient.Service.Helpers; using RdtClient.Service.Services; using Torrent = RdtClient.Data.Models.Data.Torrent; @@ -17,8 +18,8 @@ namespace RdtClient.Web.Controllers [Route("Api/Torrents")] public class TorrentsController : Controller { - private readonly ITorrents _torrents; private readonly ITorrentRunner _torrentRunner; + private readonly ITorrents _torrents; public TorrentsController(ITorrents torrents, ITorrentRunner torrentRunner) { @@ -31,18 +32,18 @@ namespace RdtClient.Web.Controllers public async Task>> Get() { 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); } /// - /// Used for debugging only. Force a tick. + /// Used for debugging only. Force a tick. /// /// [HttpGet] @@ -73,7 +74,7 @@ namespace RdtClient.Web.Controllers var bytes = memoryStream.ToArray(); - await _torrents.UploadFile(bytes, null, formData.AutoDelete); + await _torrents.UploadFile(bytes, formData.Category, formData.DownloadAction, formData.FinishedAction, formData.DownloadMinSize, formData.DownloadManualFiles); return Ok(); } @@ -82,11 +83,16 @@ namespace RdtClient.Web.Controllers [Route("UploadMagnet")] public async Task UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest request) { - await _torrents.UploadMagnet(request.MagnetLink, null, request.AutoDelete); + await _torrents.UploadMagnet(request.MagnetLink, + request.Category, + request.DownloadAction, + request.FinishedAction, + request.DownloadMinSize, + request.DownloadManualFiles); return Ok(); } - + [HttpPost] [Route("CheckFiles")] public async Task CheckFiles([FromForm] IFormFile file) @@ -130,7 +136,7 @@ namespace RdtClient.Web.Controllers return Ok(); } - + [HttpPost] [Route("Retry/{id}")] public async Task Retry(Guid id, [FromBody] TorrentControllerRetryRequest request) @@ -143,13 +149,21 @@ namespace RdtClient.Web.Controllers public class TorrentControllerUploadFileRequest { - public Boolean AutoDelete { get; set; } + public String Category { get; set; } + public TorrentDownloadAction DownloadAction { get; set; } + public TorrentFinishedAction FinishedAction { get; set; } + public Int32 DownloadMinSize { get; set; } + public String DownloadManualFiles { get; set; } } public class TorrentControllerUploadMagnetRequest { public String MagnetLink { get; set; } - public Boolean AutoDelete { get; set; } + public String Category { get; set; } + public TorrentDownloadAction DownloadAction { get; set; } + public TorrentFinishedAction FinishedAction { get; set; } + public Int32 DownloadMinSize { get; set; } + public String DownloadManualFiles { get; set; } } public class TorrentControllerDeleteRequest @@ -168,4 +182,4 @@ namespace RdtClient.Web.Controllers { public String MagnetLink { get; set; } } -} \ No newline at end of file +}