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.
This commit is contained in:
Roger Far 2021-07-18 17:29:45 -06:00
parent 4daa5ba4dc
commit fa11dc9bc7
33 changed files with 1951 additions and 709 deletions

View file

@ -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",

View file

@ -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",

View file

@ -0,0 +1,123 @@
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px" class="field">
<div fxFlex>
<div class="field">
<label class="label">Torrent file</label>
<div class="file has-name">
<label class="file-label">
<input class="file-input" type="file" name="resume" (change)="pickFile($event)" [disabled]="saving" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Pick a torrent file... </span>
</span>
<span class="file-name">
{{ fileName }}
</span>
</label>
</div>
</div>
<hr />
<div class="field">
<label class="label">Magnet Link</label>
<div class="control">
<textarea
class="textarea"
placeholder="Paste your magnet link here"
[(ngModel)]="magnetLink"
[disabled]="saving"
(blur)="checkFiles()"
></textarea>
</div>
</div>
<hr />
<div class="field">
<label class="label">Download action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="downloadAction">
<option [ngValue]="0">Download all files above a certain size</option>
<option [ngValue]="1">Download all available files on Real Debrid above a certain size</option>
<option [ngValue]="2">Pick files I want to download</option>
</select>
</div>
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
<div class="control">
<div class="field has-addons" style="margin-bottom: 0">
<div class="control is-expanded">
<input class="input" type="number" max="1000" min="0" [(ngModel)]="downloadMinSize" />
</div>
<div class="control">
<a class="button is-static">MB</a>
</div>
</div>
</div>
<p class="help" *ngIf="downloadAction === 2">This setting does not apply to manually selected files.</p>
</div>
<div class="field">
<label class="label">Finished action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="finishedAction">
<option [ngValue]="0">Do nothing</option>
<option [ngValue]="1">Remove torrent from Real Debrid and Real Debrid Client</option>
<option [ngValue]="2">Remove torrent from Real Debrid</option>
</select>
</div>
</div>
<div class="field">
<label class="label">Category</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="category" />
</div>
<p class="help">The category becomes a sub-folder in your main download path.</p>
</div>
</div>
<div fxFlex>
<div class="field">
<label class="label">Available files</label>
<p class="help">
These files are available for immediate download from Real Debrid. <br />
It is possible that there are more files in the torrent, which are not shown here.
</p>
<div class="scroll-container">
<div class="field" *ngIf="downloadAction === 2">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input
type="checkbox"
[checked]="downloadFiles[file.filename]"
(change)="downloadFileChecked(file.filename)"
/>
{{ file.filename }} ({{ file.filesize | filesize }})
</label>
</div>
<div class="field" *ngIf="downloadAction !== 2">
<label class="is-fullwidth-label is-block" *ngFor="let file of availableFiles">
{{ file.filename }} ({{ file.filesize | filesize }})
</label>
</div>
</div>
</div>
</div>
</div>
<div class="field">
<div class="control">
<div class="notification is-danger is-light" *ngIf="error">{{ error }}</div>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
<span>Add Torrent</span>
</button>
</div>
</div>

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -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,
},
],
},
];

View file

@ -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],
})

View file

@ -1,6 +1,6 @@
<app-navbar></app-navbar>
<section class="section main-list">
<div class="container">
<app-torrent-table></app-torrent-table>
<router-outlet></router-outlet>
</div>
</section>

View file

@ -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,

View file

@ -1,77 +0,0 @@
<div class="modal" [class.is-active]="isActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add New Torrent</p>
<button class="delete" aria-label="close" (click)="cancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Magnet Link</label>
<div class="control">
<textarea
class="textarea"
placeholder="Paste your magnet link here"
[(ngModel)]="magnetLink"
[disabled]="saving"
></textarea>
</div>
</div>
<hr />
<div class="field">
<label class="label">Torrent file</label>
<div class="file has-name">
<label class="file-label">
<input class="file-input" type="file" name="resume" (change)="pickFile($event)" [disabled]="saving" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Pick a torrent file... </span>
</span>
<span class="file-name">
{{ fileName }}
</span>
</label>
</div>
</div>
<hr />
<div class="field">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="autoDelete" />
Remove torrent when finished downloading on host
</label>
</div>
<div class="notification is-danger is-light" *ngIf="error">Cannot add torrent: {{ error | json }}</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
<span>Add Torrent</span>
</button>
<button class="button is-info" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="checkFiles()">
<span>Check available files</span>
</button>
<button class="button" (click)="cancel()" [disabled]="saving">Cancel</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isFileModalActive">
<div class="modal-background"></div>
<div class="modal-card file-modal">
<header class="modal-card-head">
<p class="modal-card-title">Available Files</p>
<button class="delete" aria-label="close" (click)="isFileModalActive = false"></button>
</header>
<section class="modal-card-body">
<table class="table" style="width: 100%">
<tbody>
<tr *ngFor="let file of fileList">
<td>{{ file }}</td>
</tr>
</tbody>
</table>
</section>
<footer class="modal-card-foot"></footer>
</div>
</div>

View file

@ -1,8 +0,0 @@
.file-name {
max-width: 24em;
width: 24em;
}
.file-modal {
width: calc(75% - 40px);
}

View file

@ -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<boolean>();
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);
}
}

View file

@ -1,6 +1,6 @@
<nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item">
<a class="navbar-item" routerLink="/">
<img src="../../assets/logo.png" />
</a>
@ -20,13 +20,19 @@
<div class="navbar-menu" [class.is-active]="showMobileMenu">
<div class="navbar-start">
<a class="navbar-item" (click)="this.showNewTorrent = true">
<a class="navbar-item" routerLink="/">
<span class="icon">
<i class="fas fa-table" aria-hidden="true"></i>
</span>
<span>Torrents</span>
</a>
<a class="navbar-item" routerLink="/add">
<span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
<span>Add New Torrent</span>
</a>
<a class="navbar-item" (click)="this.showSettings = true">
<a class="navbar-item" routerLink="/settings">
<span class="icon">
<i class="fas fa-cog" aria-hidden="true"></i>
</span>
@ -55,6 +61,3 @@
</div>
</div>
</nav>
<app-add-new-torrent [(open)]="showNewTorrent"></app-add-new-torrent>
<app-settings [(open)]="showSettings"></app-settings>

View file

@ -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) {}

View file

@ -1,265 +0,0 @@
<div class="modal" [class.is-active]="isActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Settings</p>
<button class="delete" aria-label="close" (click)="cancel()"></button>
</header>
<section class="modal-card-body">
<div class="tabs">
<ul>
<li [ngClass]="{ 'is-active': activeTab === 0 }" (click)="activeTab = 0">
<a>General</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 1 }" (click)="activeTab = 1">
<a>Download Client</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
<a>Radarr/Sonarr</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 3 }" (click)="activeTab = 3">
<a>Tests</a>
</li>
</ul>
</div>
<div *ngIf="activeTab === 0">
<div class="field">
<label class="label">Real-Debrid API Key</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
</div>
<p class="help">
You can find your API key here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a
>.
</p>
</div>
<div class="field">
<label class="label">Log level</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingLogLevel">
<option value="Verbose">Verbose</option>
<option value="Debug">Debug</option>
<option value="Information">Information</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Fatal">Fatal</option>
</select>
</div>
<p class="help">Recommended level is Warning, set to Debug to get the most info.</p>
</div>
<div class="field">
<label class="label">Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingDownloadPath" />
</div>
<p class="help">Path in the docker container to download files to (i.e. /data/downloads).</p>
</div>
<div class="field">
<label class="label">Mapped path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingMappedPath" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field">
<label class="label">Maximum unpack processes</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingUnpackLimit" />
</div>
<p class="help">Maximum amount of downloads that get unpacked on your host at the same time.</p>
</div>
</div>
<div *ngIf="activeTab === 1">
<div class="field">
<label class="label">Download client</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingDownloadClient">
<option value="Simple">Simple Downloader</option>
<option value="MultiPart">Multi Part Downloader</option>
</select>
</div>
<p class="help">
Select which download client to use, see the
<a href="https://github.com/rogerfar/rdt-client/" target="_blank">README</a> for the various options.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Temp Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingTempPath" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field">
<label class="label">Maximum parallel downloads</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingDownloadLimit" />
</div>
<p class="help">Maximum amount of torrents that get downloaded to your host at the same time.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Parallel connections per download</label>
<div class="control">
<input class="input" type="number" max="100" min="0" [(ngModel)]="settingDownloadChunkCount" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Download speed (in MB/s)</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" [(ngModel)]="settingDownloadMaxSpeed" />
</div>
<p class="help">Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Proxy Server</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingProxyServer" />
</div>
<p class="help">Address of a proxy server.</p>
</div>
</div>
<div *ngIf="activeTab === 2">
<div class="field">
<h3 class="subtitle">
The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or
Sonarr.
</h3>
<label class="label">Minimum file size to download</label>
<div class="control">
<div class="field has-addons" style="margin-bottom: 0">
<div class="control is-expanded">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingMinFileSize" />
</div>
<div class="control">
<a class="button is-static">MB</a>
</div>
</div>
<div class="help">
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.
</div>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
Only download available files on RealDebrid
</label>
<div class="help">
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.
</div>
</div>
</div>
</div>
<div *ngIf="activeTab === 3">
<div class="field">
<label class="label">Test download path permissions</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadPath()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testPathError && !testPathSuccess"
>
Test permissions
</button>
<div class="notification is-danger is-light" *ngIf="testPathError">
Could not test your download path<br />
{{ testPathError }}
</div>
<div class="notification is-success is-light" *ngIf="testPathSuccess">Your download path looks good!</div>
</div>
<div class="help">This will check if the download folder has write permissions.</div>
</div>
<div class="field">
<label class="label">Test RealDebrid download speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testDownloadSpeedError && !testDownloadSpeedSuccess"
>
Test download speed
</button>
<div class="notification is-danger is-light" *ngIf="testDownloadSpeedError">
Could not test your download speed<br />
{{ testDownloadSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testDownloadSpeedSuccess">
Download speed {{ testDownloadSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will attempt to download a 10GB file from Real Debrid. When 50MB has been downloaded the test will
stop.
</div>
</div>
<div class="field">
<label class="label">Test download folder write speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testWriteSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testWriteSpeedError && !testWriteSpeedSuccess"
>
Test write speed
</button>
<div class="notification is-danger is-light" *ngIf="testWriteSpeedError">
Could not test your download speed<br />
{{ testWriteSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testWriteSpeedSuccess">
Write speed {{ testWriteSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will write a small file to your download folder to see how fast it can write to it.
</div>
</div>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Settings
</button>
<button class="button" (click)="cancel()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">Cancel</button>
</footer>
</div>
</div>

View file

@ -0,0 +1,254 @@
<div class="tabs">
<ul>
<li [ngClass]="{ 'is-active': activeTab === 0 }" (click)="activeTab = 0">
<a>General</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 1 }" (click)="activeTab = 1">
<a>Download Client</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
<a>Radarr/Sonarr</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 3 }" (click)="activeTab = 3">
<a>Tests</a>
</li>
</ul>
</div>
<div *ngIf="activeTab === 0">
<div class="field">
<label class="label">Real-Debrid API Key</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
</div>
<p class="help">
You can find your API key here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener">https://real-debrid.com/apitoken</a>.
</p>
</div>
<div class="field">
<label class="label">Log level</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingLogLevel">
<option value="Verbose">Verbose</option>
<option value="Debug">Debug</option>
<option value="Information">Information</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Fatal">Fatal</option>
</select>
</div>
<p class="help">Recommended level is Warning, set to Debug to get the most info.</p>
</div>
<div class="field">
<label class="label">Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingDownloadPath" />
</div>
<p class="help">Path in the docker container to download files to (i.e. /data/downloads).</p>
</div>
<div class="field">
<label class="label">Mapped path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingMappedPath" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field">
<label class="label">Maximum unpack processes</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingUnpackLimit" />
</div>
<p class="help">Maximum amount of downloads that get unpacked on your host at the same time.</p>
</div>
</div>
<div *ngIf="activeTab === 1">
<div class="field">
<label class="label">Download client</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingDownloadClient">
<option value="Simple">Simple Downloader</option>
<option value="MultiPart">Multi Part Downloader</option>
</select>
</div>
<p class="help">
Select which download client to use, see the
<a href="https://github.com/rogerfar/rdt-client/" target="_blank">README</a> for the various options.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Temp Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingTempPath" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field">
<label class="label">Maximum parallel downloads</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingDownloadLimit" />
</div>
<p class="help">Maximum amount of torrents that get downloaded to your host at the same time.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Parallel connections per download</label>
<div class="control">
<input class="input" type="number" max="100" min="0" [(ngModel)]="settingDownloadChunkCount" />
</div>
<p class="help">
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.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Download speed (in MB/s)</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" [(ngModel)]="settingDownloadMaxSpeed" />
</div>
<p class="help">Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Proxy Server</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingProxyServer" />
</div>
<p class="help">Address of a proxy server.</p>
</div>
</div>
<div *ngIf="activeTab === 2">
<h3 class="subtitle">
The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or Sonarr.
</h3>
<div class="field">
<label class="label">Minimum file size to download</label>
<div class="control">
<div class="field has-addons" style="margin-bottom: 0">
<div class="control is-expanded">
<input class="input" type="number" max="1000" min="0" [(ngModel)]="settingMinFileSize" />
</div>
<div class="control">
<a class="button is-static">MB</a>
</div>
</div>
<div class="help">
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.
</div>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
Only download available files on RealDebrid
</label>
<div class="help">
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.
</div>
</div>
</div>
</div>
<div *ngIf="activeTab === 3">
<div class="field">
<label class="label">Test download path permissions</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadPath()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testPathError && !testPathSuccess"
>
Test permissions
</button>
<div class="notification is-danger is-light" *ngIf="testPathError">
Could not test your download path<br />
{{ testPathError }}
</div>
<div class="notification is-success is-light" *ngIf="testPathSuccess">Your download path looks good!</div>
</div>
<div class="help">This will check if the download folder has write permissions.</div>
</div>
<div class="field">
<label class="label">Test RealDebrid download speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testDownloadSpeedError && !testDownloadSpeedSuccess"
>
Test download speed
</button>
<div class="notification is-danger is-light" *ngIf="testDownloadSpeedError">
Could not test your download speed<br />
{{ testDownloadSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testDownloadSpeedSuccess">
Download speed {{ testDownloadSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will attempt to download a 10GB file from Real Debrid. When 50MB has been downloaded the test will stop.
</div>
</div>
<div class="field">
<label class="label">Test download folder write speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testWriteSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testWriteSpeedError && !testWriteSpeedSuccess"
>
Test write speed
</button>
<div class="notification is-danger is-light" *ngIf="testWriteSpeedError">
Could not test your download speed<br />
{{ testWriteSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testWriteSpeedSuccess">
Write speed {{ testWriteSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">This will write a small file to your download folder to see how fast it can write to it.</div>
</div>
</div>
<div class="field">
<div class="control">
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
</div>
</div>
<div class="field" *ngIf="activeTab != 3">
<div class="control">
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Settings
</button>
</div>
</div>

View file

@ -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<boolean>();
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);

View file

@ -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<Torrent[]>(`/Api/Torrents`);
}
public uploadMagnet(magnetLink: string, autoDelete: boolean): Observable<void> {
public uploadMagnet(
magnetLink: string,
category: string,
downloadAction: number,
finishedAction: number,
downloadMinSize: number,
downloadManualFiles: string
): Observable<void> {
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
magnetLink,
autoDelete,
category,
downloadAction,
finishedAction,
downloadMinSize,
downloadManualFiles,
});
}
public uploadFile(file: File, autoDelete: boolean): Observable<void> {
public uploadFile(
file: File,
category: string,
downloadAction: number,
finishedAction: number,
downloadMinSize: number,
downloadManualFiles: string
): Observable<void> {
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<void>(`/Api/Torrents/UploadFile`, formData);
}
public checkFilesMagnet(magnetLink: string): Observable<string[]> {
return this.http.post<string[]>(`/Api/Torrents/CheckFilesMagnet`, {
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
magnetLink,
});
}
public checkFiles(file: File): Observable<string[]> {
public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
const formData: FormData = new FormData();
formData.append('file', file);
return this.http.post<string[]>(`/Api/Torrents/CheckFiles`, formData);
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFiles`, formData);
}
public delete(

View file

@ -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<IList<Torrent>> Get();
Task<Torrent> GetById(Guid torrentId);
Task<Torrent> GetByHash(String hash);
Task<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile);
Task<Torrent> 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<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile)
public async Task<Torrent> 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
}
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace RdtClient.Data.Enums
{
public enum TorrentDownloadAction
{
DownloadAll = 0,
DownloadAvailableFiles = 1,
DownloadManual = 2
}
}

View file

@ -0,0 +1,9 @@
namespace RdtClient.Data.Enums
{
public enum TorrentFinishedAction
{
None = 0,
RemoveAllTorrents = 1,
RemoveRealDebrid = 2
}
}

View file

@ -0,0 +1,430 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

View file

@ -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<int>(
name: "FinishedAction",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DownloadAction",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "DownloadManualFiles",
table: "Torrents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
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<int>(
name: "AutoDelete",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
}
}

View file

@ -0,0 +1,433 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

View file

@ -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<DateTimeOffset>(
name: "FilesSelected",
table: "Torrents",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FilesSelected",
table: "Torrents");
}
}
}

View file

@ -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<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");

View file

@ -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<String> ManualFiles
{
get
{
if (String.IsNullOrWhiteSpace(DownloadManualFiles))
{
return new List<String>();
}
return DownloadManualFiles.Split(",");
}
}
}
}

View file

@ -19,8 +19,8 @@ namespace RdtClient.Service.Services
Task<IList<TorrentFileItem>> TorrentFileContents(String hash);
Task<TorrentProperties> 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<IDictionary<String, TorrentCategory>> 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)

View file

@ -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;
}
}
}

View file

@ -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<IList<Torrent>> Get();
Task<Torrent> 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<List<String>> 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<List<TorrentInstantAvailabilityFile>> GetAvailableFiles(String hash);
Task SelectFiles(Guid torrentId, IList<String> fileIds);
Task CheckForLinks(Guid torrentId);
Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles);
Task<String> UnrestrictLink(Guid downloadId);
Task Download(Guid downloadId);
@ -30,6 +44,7 @@ namespace RdtClient.Service.Services
void Reset();
Task<Profile> 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<IList<Torrent>> 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<Torrent> GetById(Guid torrentId)
{
var torrent = await _torrentData.GetById(torrentId);
if (torrent != null)
{
await Update(torrent);
}
return torrent;
}
public async Task<Torrent> 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<List<String>> GetAvailableFiles(String hash)
public async Task<List<TorrentInstantAvailabilityFile>> 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<String> 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<Torrent> 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<String> 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
}
}
}
}
}

View file

@ -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);
}
}

View file

@ -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<ActionResult<IList<Torrent>>> 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);
}
/// <summary>
/// Used for debugging only. Force a tick.
/// Used for debugging only. Force a tick.
/// </summary>
/// <returns></returns>
[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<ActionResult> 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<ActionResult> CheckFiles([FromForm] IFormFile file)
@ -130,7 +136,7 @@ namespace RdtClient.Web.Controllers
return Ok();
}
[HttpPost]
[Route("Retry/{id}")]
public async Task<ActionResult> 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; }
}
}
}