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