Created a separate page for torrent into, added download retrying per file.

This commit is contained in:
Roger Far 2021-07-18 22:46:17 -06:00
parent fa11dc9bc7
commit 3ee687f615
50 changed files with 1101 additions and 661 deletions

View file

@ -5165,6 +5165,11 @@
"escape-string-regexp": "^1.0.5"
}
},
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"filesize": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",

View file

@ -26,6 +26,7 @@
"@microsoft/signalr": "^5.0.8",
"bulma": "^0.9.3",
"curray": "^1.0.8",
"file-saver": "^2.0.5",
"ngx-filesize": "^2.0.16",
"rxjs": "~7.2.0",
"tslib": "^2.3.0",

View file

@ -40,7 +40,7 @@
<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]="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>
@ -66,8 +66,8 @@
<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>
<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>
@ -84,10 +84,16 @@
<div class="field">
<label class="label">Available files</label>
<p class="help">
These files are available for immediate download from Real Debrid. <br />
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">
<input type="checkbox" [checked]="allSelected" (change)="downloadFileCheckedAll()" />
Select all
</label>
</div>
<div class="field" *ngIf="downloadAction === 2">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input

View file

@ -22,6 +22,7 @@ export class AddNewTorrentComponent implements OnInit {
public availableFiles: TorrentFileAvailability[] = [];
public downloadFiles: { [key: string]: boolean } = {};
public allSelected: boolean;
public saving = false;
public error: string;
@ -50,6 +51,21 @@ export class AddNewTorrentComponent implements OnInit {
public downloadFileChecked(file: string): void {
this.downloadFiles[file] = !this.downloadFiles[file];
this.allSelected = true;
this.availableFiles.forEach((file) => {
if (!this.downloadFiles[file.filename]) {
this.allSelected = false;
}
});
}
public downloadFileCheckedAll(): void {
this.allSelected = !this.allSelected;
this.availableFiles.forEach((file) => {
this.downloadFiles[file.filename] = this.allSelected;
});
}
public ok(): void {
@ -127,6 +143,7 @@ export class AddNewTorrentComponent implements OnInit {
this.error = null;
this.availableFiles = [];
this.downloadFiles = {};
this.allSelected = true;
if (this.magnetLink) {
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe(

View file

@ -7,6 +7,7 @@ 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';
import { TorrentComponent } from './torrent/torrent.component';
const routes: Routes = [
{
@ -28,6 +29,10 @@ const routes: Routes = [
path: '',
component: TorrentTableComponent,
},
{
path: 'torrent/:id',
component: TorrentComponent,
},
{
path: 'add',
component: AddNewTorrentComponent,

View file

@ -1,3 +1,4 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
@ -15,11 +16,10 @@ import { MainLayoutComponent } from './main-layout/main-layout.component';
import { NavbarComponent } from './navbar/navbar.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';
import { TorrentRowComponent } from './torrent-row/torrent-row.component';
import { TorrentStatusPipe } from './torrent-status.pipe';
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component';
import { DecodeURIPipe } from './decode-uri.pipe';
curray();
@ -30,16 +30,23 @@ curray();
NavbarComponent,
AddNewTorrentComponent,
TorrentTableComponent,
TorrentRowComponent,
TorrentFileComponent,
SettingsComponent,
TorrentStatusPipe,
DownloadStatusPipe,
LoginComponent,
SetupComponent,
TorrentDownloadComponent,
TorrentComponent,
DecodeURIPipe,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
NgxFilesizeModule,
FlexLayoutModule,
ClipboardModule,
],
imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule, FlexLayoutModule],
providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent],
})

View file

@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'decodeURI',
})
export class DecodeURIPipe implements PipeTransform {
transform(input: any) {
return decodeURI(input);
}
}

View file

@ -14,7 +14,7 @@ export class DownloadStatusPipe implements PipeTransform {
}
if (value.error) {
return `Error: ${value.error}`;
return `Error`;
}
if (value.completed != null) {
@ -26,8 +26,13 @@ export class DownloadStatusPipe implements PipeTransform {
}
if (value.unpackingStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
return `Unpacking ${progress || 0}%`;
let progress = (value.bytesDone / value.bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
return `Unpacking ${progress.toFixed(2)}%`;
}
if (value.unpackingQueued) {
@ -39,10 +44,15 @@ export class DownloadStatusPipe implements PipeTransform {
}
if (value.downloadStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
let progress = (value.bytesDone / value.bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
const speed = this.pipe.transform(value.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`;
return `Downloading ${progress.toFixed(2)}% (${speed}/s)`;
}
if (value.downloadQueued) {

View file

@ -4,9 +4,17 @@ export class Torrent {
public torrentId: string;
public hash: string;
public category: string;
public downloadAction: number;
public finishedAction: number;
public downloadMinSize: number;
public downloadManualFiles: string;
public added: Date;
public filesSelected: Date;
public completed: Date;
public autoDelete: boolean;
public fileOrMagnet: string;
public isFile: boolean;
public rdId: string;
public rdName: string;

View file

@ -146,7 +146,7 @@
<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.
Real-Debrid having to re-download the torrent.
</div>
</div>
</div>
@ -155,10 +155,10 @@
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
Only download available files on RealDebrid
Only download available files on Real-Debrid
</label>
<div class="help">
When selected, it will only download files in the torrent that have been download by Real Debrid. You can use
When selected, it will only download files in the torrent that have been download by Real-Debrid. You can use
this in combination with the Min File size setting above.
</div>
</div>
@ -189,7 +189,7 @@
</div>
<div class="field">
<label class="label">Test RealDebrid download speed</label>
<label class="label">Test Real-Debrid download speed</label>
<div class="control">
<button
class="button is-warning"
@ -210,7 +210,7 @@
</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.
This will attempt to download a 10GB file from Real-Debrid. When 50MB has been downloaded the test will stop.
</div>
</div>

View file

@ -6,7 +6,7 @@
<img src="../../assets/logo.png" />
<div class="box" *ngIf="step === 1">
<div class="notification is-primary">
Welcome to RealDebrid Client. Please create your account by entering a username and password.
Welcome to Real-Debrid Client. Please create your account by entering a username and password.
</div>
<form (ngSubmit)="setup()">
<div class="field">
@ -44,14 +44,14 @@
<div class="box" *ngIf="step === 2">
<div class="notification is-primary">
To be able to use the RealDebrid Client you need a premium subscription to download torrents.
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
<br /><br />
Not premium yet?
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
>Use this link to sign up to RealDebrid.</a
>Use this link to sign up to Real-Debrid.</a
>
<br /><br />
To connect to RealDebrid please enter your Personal Prive API Token found here:
To connect to Real-Debrid please enter your Personal Prive API Token found here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a
>.
@ -82,7 +82,7 @@
<div class="box" *ngIf="step === 3">
<div class="notification is-primary">
You are now connected to RealDebrid! To setup Radarr or Sonarr, please following the instructions here:
You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here:
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
>https://github.com/rogerfar/rdt-client/</a
>.

View file

@ -1,12 +0,0 @@
<td colspan="4">
<i class="fas fa-download" aria-hidden="true"></i>
<span *ngIf="download.link">{{ download.link }}</span>
<span *ngIf="!download.link">{{ download.path }}</span>
</td>
<td>
{{ download.bytesTotal | filesize }}
</td>
<td>
{{ download | downloadStatus }}
</td>
<td></td>

View file

@ -1,3 +0,0 @@
td {
font-size: smaller;
}

View file

@ -1,16 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { Download } from '../models/download.model';
@Component({
selector: '[app-torrent-download]',
templateUrl: './torrent-download.component.html',
styleUrls: ['./torrent-download.component.scss'],
})
export class TorrentDownloadComponent implements OnInit {
@Input()
public download: Download;
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,6 +0,0 @@
<td colspan="4">
<i class="fas fa-file" aria-hidden="true"></i>
{{ file.path }}
</td>
<td>{{ file.bytes | filesize }}</td>
<td colspan="10"></td>

View file

@ -1,3 +0,0 @@
td {
font-size: smaller;
}

View file

@ -1,16 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { TorrentFile } from '../models/torrent.model';
@Component({
selector: '[app-torrent-file]',
templateUrl: './torrent-file.component.html',
styleUrls: ['./torrent-file.component.scss'],
})
export class TorrentFileComponent implements OnInit {
@Input()
public file: TorrentFile;
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,27 +0,0 @@
<td>{{ torrent.rdName }}</td>
<td>
{{ torrent.files.length | number }}
</td>
<td>
{{ torrent.downloads.length | number }}
</td>
<td class="auto">
<i class="fas fa-trash" *ngIf="torrent.autoDelete" title="Auto delete"></i>
</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent | status }}
</td>
<td>
<span class="icon sync-icon" (click)="retryClick($event)" title="Retry" *ngIf="!loading">
<i class="fas fa-sync"></i>
</span>
<span class="icon delete-icon" (click)="deleteClick($event)" title="Delete torrent" *ngIf="!loading">
<i class="fas fa-times"></i>
</span>
<span class="icon loading-icon" *ngIf="loading">
<i class="fas fa-spinner fa-pulse"></i>
</span>
</td>

View file

@ -1,8 +0,0 @@
.delete-icon {
color: red;
cursor: pointer;
}
.auto .fas {
margin-right: 4px;
}

View file

@ -1,34 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Torrent } from 'src/app/models/torrent.model';
@Component({
selector: '[app-torrent-row]',
templateUrl: './torrent-row.component.html',
styleUrls: ['./torrent-row.component.scss'],
})
export class TorrentRowComponent implements OnInit {
@Input()
public torrent: Torrent;
@Output('delete')
public delete = new EventEmitter();
@Output('retry')
public retry = new EventEmitter();
public loading = false;
constructor() {}
ngOnInit(): void {}
public deleteClick(event: Event): void {
event.stopPropagation();
this.delete.emit(this.torrent.torrentId);
}
public retryClick(event: Event): void {
event.stopPropagation();
this.retry.emit(this.torrent.torrentId);
}
}

View file

@ -28,6 +28,11 @@ export class TorrentStatusPipe implements PipeTransform {
const bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
let speed: string | string[] = '0';
@ -44,6 +49,11 @@ export class TorrentStatusPipe implements PipeTransform {
const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (allSpeeds > 0) {

View file

@ -7,126 +7,34 @@
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Files</th>
<th>Downloads</th>
<th>Auto</th>
<th>Size</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let torrent of torrents; trackBy: trackByMethod">
<tr
app-torrent-row
[torrent]="torrent"
(click)="selectTorrent(torrent)"
(delete)="showDeleteModal($event)"
(retry)="showRetryModal($event)"
></tr>
<ng-container *ngIf="showFiles[torrent.torrentId]">
<tr class="separator">
<td colspan="10">Downloads</td>
</tr>
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr>
<tr class="separator">
<td colspan="10">Files in torrent</td>
</tr>
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
</ng-container>
</ng-container>
<tr (click)="selectTorrent(torrent.torrentId)" *ngFor="let torrent of torrents; trackBy: trackByMethod">
<td>
{{ torrent.rdName }}
</td>
<td>
{{ torrent.category }}
</td>
<td>
{{ torrent.files.length | number }}
</td>
<td>
{{ torrent.downloads.length | number }}
</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent | status }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal" [class.is-active]="isDeleteModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete torrent</p>
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
Delete Torrent from local RealDebrid Client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
Delete Torrent from RealDebrid
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
Delete local files
</label>
</div>
</div>
<div class="notification is-primary">
Deleting a torrent from RealDebrid will automatically delete it here too.
</div>
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
Error deleting torrent: {{ deleteError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="deleteOk()"
[disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }"
>
Delete
</button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry torrent</p>
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Retry</label>
<div class="control">
<label class="radio">
<input type="radio" name="retry" [value]="0" [(ngModel)]="retry" />
Retry on Real-Debrid
</label>
<br />
<label class="radio">
<input type="radio" name="retry" [value]="1" [(ngModel)]="retry" />
Retry downloading locally
</label>
</div>
</div>
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
Error retrying torrent: {{ retryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="retryOk()"
[disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }"
>
Retry
</button>
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
Cancel
</button>
</footer>
</div>
</div>

View file

@ -2,11 +2,4 @@ table {
tr {
cursor: pointer;
}
tr.separator {
td {
font-size: smaller;
font-weight: bold;
}
}
}

View file

@ -1,4 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
@ -10,23 +11,8 @@ import { TorrentService } from '../torrent.service';
export class TorrentTableComponent implements OnInit, OnDestroy {
public torrents: Torrent[] = [];
public error: string;
public showFiles: { [key: string]: boolean } = {};
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
public deleteTorrentId: string;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
public isRetryModalActive: boolean;
public retryError: string;
public retrying: boolean;
public retryTorrentId: string;
public retry: number;
constructor(private torrentService: TorrentService) {}
constructor(private router: Router, private torrentService: TorrentService) {}
ngOnInit(): void {
this.torrentService.getList().subscribe(
@ -49,67 +35,11 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
this.torrentService.disconnect();
}
public selectTorrent(torrent: Torrent): void {
this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId];
public selectTorrent(torrentId: string): void {
this.router.navigate([`/torrent/${torrentId}`]);
}
public trackByMethod(index: number, el: Torrent): string {
return el.torrentId;
}
public showDeleteModal(torrentId: string): void {
this.deleteData = false;
this.deleteRdTorrent = false;
this.deleteLocalFiles = false;
this.deleteTorrentId = torrentId;
this.isDeleteModalActive = true;
}
public deleteCancel(): void {
this.isDeleteModalActive = false;
}
public deleteOk(): void {
this.deleting = true;
this.torrentService
.delete(this.deleteTorrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
.subscribe(
() => {
this.isDeleteModalActive = false;
this.deleting = false;
},
(err) => {
this.deleteError = err.error;
this.deleting = false;
}
);
}
public showRetryModal(torrentId: string): void {
this.retry = 0;
this.retryTorrentId = torrentId;
this.isRetryModalActive = true;
}
public retryCancel(): void {
this.isRetryModalActive = false;
}
public retryOk(): void {
this.retrying = true;
this.torrentService.retry(this.retryTorrentId, this.retry).subscribe(
() => {
this.isRetryModalActive = false;
this.retrying = false;
},
(err) => {
this.retryError = err.error;
this.retrying = false;
}
);
}
}

View file

@ -15,6 +15,10 @@ export class TorrentService {
private connection: signalR.HubConnection;
public connect(): void {
if (this.connection != null) {
return;
}
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
this.connection.start().catch((err) => console.error(err));
@ -31,6 +35,10 @@ export class TorrentService {
return this.http.get<Torrent[]>(`/Api/Torrents`);
}
public get(torrentId: string): Observable<Torrent> {
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`);
}
public uploadMagnet(
magnetLink: string,
category: string,
@ -91,9 +99,11 @@ export class TorrentService {
});
}
public retry(torrentId: string, retry: number): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {
retry,
});
public retry(torrentId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
}
public retryDownload(downloadId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/RetryDownload/${downloadId}`, {});
}
}

View file

@ -0,0 +1,439 @@
<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>Torrent Files</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
<a>Downloads</a>
</li>
</ul>
</div>
<div *ngIf="torrent == null">
<div class="fa-3x">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div *ngIf="torrent != null">
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px" *ngIf="activeTab === 0">
<div fxFlex>
<div class="field is-grouped">
<div class="control">
<button class="button is-danger" (click)="showDeleteModal()">Delete Torrent</button>
</div>
<div class="control">
<button class="button is-primary" (click)="showRetryModal()">Retry Torrent</button>
</div>
</div>
<div class="field">
<label class="label">Status</label>
{{ torrent | status }}
</div>
<div class="field">
<label class="label">Hash</label>
{{ torrent.hash }}
</div>
<div class="field">
<label class="label">Category</label>
{{ torrent.category || "(no category set)" }}
</div>
<div class="field">
<label class="label">Download action</label>
<ng-container [ngSwitch]="torrent.downloadAction">
<ng-container *ngSwitchCase="0">Download all files above a certain size</ng-container>
<ng-container *ngSwitchCase="1"
>Download all available files on Real-Debrid above a certain size</ng-container
>
<ng-container *ngSwitchCase="2">Pick files I want to download</ng-container>
</ng-container>
</div>
<div class="field">
<label class="label">Finished action</label>
<ng-container [ngSwitch]="torrent.finishedAction">
<ng-container *ngSwitchCase="0">Do nothing</ng-container>
<ng-container *ngSwitchCase="1">Remove torrent from Real-Debrid and Real-Debrid Client</ng-container>
<ng-container *ngSwitchCase="2">Remove torrent from Real-Debrid</ng-container>
</ng-container>
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
{{ torrent.downloadMinSize }}MB
</div>
<div class="field" *ngIf="!torrent.isFile">
<label class="label">Magnet</label>
<span [cdkCopyToClipboard]="torrent.fileOrMagnet" (click)="copied = true" *ngIf="!copied"
>Click to copy magnet link to clipboard</span
>
<span *ngIf="copied">Link copied to clipboard!</span>
</div>
<div class="field" *ngIf="torrent.isFile">
<label class="label">Torrent file</label>
<span (click)="download()">Click to download torrent file</span>
</div>
<div class="field">
<label class="label">Added on</label>
{{ torrent.added | date: "fullDate" }} {{ torrent.added | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Files selected on</label>
<ng-container *ngIf="torrent.filesSelected == null">(no files selected yet) </ng-container>
<ng-container *ngIf="torrent.filesSelected != null">
{{ torrent.filesSelected | date: "fullDate" }} {{ torrent.filesSelected | date: "mediumTime" }}
</ng-container>
</div>
<div class="field">
<label class="label">Completed on</label>
<ng-container *ngIf="torrent.completed == null">(not completed yet) </ng-container>
<ng-container *ngIf="torrent.completed != null">
{{ torrent.completed | date: "fullDate" }} {{ torrent.completed | date: "mediumTime" }}
</ng-container>
</div>
</div>
<div fxFlex>
<div class="field">
<label class="label">Real-Debrid ID</label>
{{ torrent.rdId }}
</div>
<div class="field">
<label class="label">Real-Debrid Name</label>
{{ torrent.rdName }}
</div>
<div class="field">
<label class="label">Real-Debrid Size</label>
{{ torrent.rdSize | filesize }}
</div>
<div class="field">
<label class="label">Real-Debrid Host</label>
{{ torrent.rdHost }}
</div>
<div class="field">
<label class="label">Real-Debrid Split</label>
{{ torrent.rdSplit }}
</div>
<div class="field">
<label class="label">Real-Debrid Progress</label>
{{ torrent.rdProgress || 0 }}%
</div>
<div class="field">
<label class="label">Real-Debrid Status</label>
<ng-container [ngSwitch]="torrent.rdStatus">
<ng-container *ngSwitchCase="0">Processing</ng-container>
<ng-container *ngSwitchCase="1">Waiting For File Selection</ng-container>
<ng-container *ngSwitchCase="2">Downloading</ng-container>
<ng-container *ngSwitchCase="3">Finished</ng-container>
<ng-container *ngSwitchCase="99">Error</ng-container>
</ng-container>
({{ torrent.rdStatusRaw }})
</div>
<div class="field">
<label class="label">Real-Debrid Added</label>
{{ torrent.rdAdded | date: "fullDate" }} {{ torrent.rdAdded | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Real-Debrid Ended</label>
{{ torrent.rdEnded | date: "fullDate" }} {{ torrent.rdEnded | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Real-Debrid Speed</label>
{{ torrent.rdSpeed || 0 }}
</div>
<div class="field">
<label class="label">Real-Debrid Seeders</label>
{{ torrent.rdSeeders || "0" }}
</div>
</div>
</div>
<div *ngIf="activeTab === 1">
<div class="field">
<table class="table is-fullwidth">
<thead>
<tr>
<th>ID</th>
<th>Path</th>
<th>Size</th>
<th>Selected</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let file of torrent.files">
<td>
{{ file.id }}
</td>
<td>
{{ file.path }}
</td>
<td>
{{ file.bytes | filesize }}
</td>
<td>
<i class="fas fa-check" *ngIf="file.selected" style="color: green"></i>
<i class="fas fa-times" *ngIf="!file.selected" style="color: red"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div *ngIf="activeTab === 2">
<div class="field">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th style="width: 35px"></th>
<th>Link</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let download of torrent.downloads">
<tr (click)="downloadExpanded[download.downloadId] = !downloadExpanded[download.downloadId]">
<td style="width: 35px">
<i class="fas fa-caret-right" *ngIf="!downloadExpanded[download.downloadId]"></i>
<i class="fas fa-caret-down" *ngIf="downloadExpanded[download.downloadId]"></i>
</td>
<td>
<ng-container *ngIf="download.link">
{{ download.link | decodeURI }}
</ng-container>
</td>
<td>
{{ download.bytesTotal | filesize }}
</td>
<td>
{{ download | downloadStatus }}
</td>
</tr>
<tr *ngIf="downloadExpanded[download.downloadId]" class="separator">
<td style="width: 35px"></td>
<td colspan="5">
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px">
<div fxFlex>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
Retry Download
</button>
</div>
</div>
<div class="field" *ngIf="download.error">
<label class="label">Error</label>
{{ download.error }}
</div>
<div class="field">
<label class="label">Real-Debrid Unrestricted Link</label>
<a href="{{ download.link }}" target="_blank" *ngIf="download.link">
{{ download.link | decodeURI }}</a
>
</div>
<div class="field">
<label class="label">Real-Debrid Link</label>
{{ download.path }}
</div>
<div class="field">
<label class="label">Download</label>
{{ download.bytesDone | filesize }} / {{ download.bytesTotal | filesize }} ({{
download.speed | filesize
}}/s)
</div>
<div class="field">
<label class="label">Retry Count</label>
{{ download.retryCount }}
</div>
</div>
<div fxFlex>
<div class="field">
<label class="label">Added</label>
<ng-container *ngIf="download.added">
{{ download.added | date: "fullDate" }} {{ download.added | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.added">(not added yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Queued</label>
<ng-container *ngIf="download.downloadQueued">
{{ download.downloadQueued | date: "fullDate" }}
{{ download.downloadQueued | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadQueued">(not queued for downloading yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Started</label>
<ng-container *ngIf="download.downloadStarted">
{{ download.downloadStarted | date: "fullDate" }}
{{ download.downloadStarted | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadStarted">(not started downloading yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Finished</label>
<ng-container *ngIf="download.downloadFinished">
{{ download.downloadFinished | date: "fullDate" }}
{{ download.downloadFinished | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadFinished">(not finished yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Queued</label>
<ng-container *ngIf="download.unpackingQueued">
{{ download.unpackingQueued | date: "fullDate" }}
{{ download.unpackingQueued | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingQueued">(not queued for unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Started</label>
<ng-container *ngIf="download.unpackingStarted">
{{ download.unpackingStarted | date: "fullDate" }}
{{ download.unpackingStarted | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingStarted">(not started unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Finished</label>
<ng-container *ngIf="download.unpackingFinished">
{{ download.unpackingFinished | date: "fullDate" }}
{{ download.unpackingFinished | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingFinished">(not finished unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Completed</label>
<ng-container *ngIf="download.completed">
{{ download.completed | date: "fullDate" }}
{{ download.completed | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.completed">(not completed yet) </ng-container>
</div>
</div>
</div>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal" [class.is-active]="isDeleteModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete torrent</p>
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to delete this torrent?</p>
<div class="field">
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
Delete Torrent from local Real-Debrid Client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
Delete Torrent from Real-Debrid
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
Delete local files
</label>
</div>
</div>
<div class="notification is-primary">
Deleting a torrent from Real-Debrid will automatically delete it here too.
</div>
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
Error deleting torrent: {{ deleteError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="deleteOk()"
[disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }"
>
Delete
</button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry torrent</p>
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to retry this torrent?</p>
<p>
This action will delete all the torrent data + all local downloads. Then it will re-add the original magnet link
or torrent file to Real-Debrid.
</p>
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
Error retrying torrent: {{ retryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="retryOk()"
[disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }"
>
Retry
</button>
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isDownloadRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry download</p>
<button class="delete" aria-label="close" (click)="downloadRetryCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to retry this download?</p>
<p>This action will remove the local download and re-download the file from Real-Debrid.</p>
<div class="notification is-danger is-light" *ngIf="downloadRetryError?.length > 0">
Error retrying download: {{ downloadRetryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="downloadRetryOk()"
[disabled]="downloadRetrying"
[ngClass]="{ 'is-loading': downloadRetrying }"
>
Retry
</button>
<button
class="button"
(click)="downloadRetryCancel()"
[disabled]="downloadRetrying"
[ngClass]="{ 'is-loading': downloadRetrying }"
>
Cancel
</button>
</footer>
</div>
</div>

View file

@ -0,0 +1,13 @@
.label {
margin-bottom: 0;
}
table {
tr {
cursor: pointer;
}
}
.fa-download {
margin-left :12px;
}

View file

@ -0,0 +1,168 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { saveAs } from 'file-saver';
import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
@Component({
selector: 'app-torrent',
templateUrl: './torrent.component.html',
styleUrls: ['./torrent.component.scss'],
})
export class TorrentComponent implements OnInit {
public torrent: Torrent;
public activeTab: number = 0;
public copied: boolean = false;
public downloadExpanded: { [downloadId: string]: boolean } = {};
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
public isRetryModalActive: boolean;
public retryError: string;
public retrying: boolean;
public isDownloadRetryModalActive: boolean;
public downloadRetryError: string;
public downloadRetrying: boolean;
public downloadRetryId: string;
constructor(private activatedRoute: ActivatedRoute, private router: Router, private torrentService: TorrentService) {}
ngOnInit(): void {
this.activatedRoute.params.subscribe((params) => {
const torrentId = params['id'];
this.torrentService.get(torrentId).subscribe(
(torrent) => {
this.torrent = torrent;
this.torrentService.connect();
this.torrentService.update$.subscribe((result) => {
this.update(result);
});
},
() => {
this.router.navigate(['/']);
}
);
});
}
public update(torrents: Torrent[]): void {
const updatedTorrent = torrents.firstOrDefault((m) => m.torrentId === this.torrent.torrentId);
if (updatedTorrent == null) {
return;
}
this.torrent = updatedTorrent;
}
public download(): void {
const byteArray = new Uint8Array(
window
.atob(this.torrent.fileOrMagnet)
.split('')
.map(function (c) {
return c.charCodeAt(0);
})
);
var blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
saveAs(blob, `${this.torrent.rdName}.torrent`);
}
public showDeleteModal(): void {
this.deleteData = false;
this.deleteRdTorrent = false;
this.deleteLocalFiles = false;
this.deleteError = null;
this.isDeleteModalActive = true;
}
public deleteCancel(): void {
this.isDeleteModalActive = false;
}
public deleteOk(): void {
this.deleting = true;
this.torrentService
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
.subscribe(
() => {
this.isDeleteModalActive = false;
this.deleting = false;
this.router.navigate(['/']);
},
(err) => {
this.deleteError = err.error;
this.deleting = false;
}
);
}
public showRetryModal(): void {
this.retryError = null;
this.isRetryModalActive = true;
}
public retryCancel(): void {
this.isRetryModalActive = false;
}
public retryOk(): void {
this.retrying = true;
this.torrentService.retry(this.torrent.torrentId).subscribe(
() => {
this.isRetryModalActive = false;
this.retrying = false;
this.router.navigate(['/']);
},
(err) => {
this.retryError = err.error;
this.retrying = false;
}
);
}
public showDownloadRetryModal(downloadId: string): void {
this.downloadRetryId = downloadId;
this.downloadRetryError = null;
this.isDownloadRetryModalActive = true;
}
public downloadRetryCancel(): void {
this.isDownloadRetryModalActive = false;
}
public downloadRetryOk(): void {
this.downloadRetrying = true;
this.torrentService.retryDownload(this.downloadRetryId).subscribe(
() => {
this.isDownloadRetryModalActive = false;
this.downloadRetrying = false;
},
(err) => {
this.downloadRetryError = err.error;
this.downloadRetrying = false;
}
);
}
}

View file

@ -6,29 +6,12 @@ using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Data.Data
{
public interface IDownloadData
{
Task<Download> GetById(Guid downloadId);
Task<Download> Get(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, String path);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateError(Guid downloadId, String error);
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
Task DeleteForTorrent(Guid torrentId);
}
public class DownloadData : IDownloadData
public class DownloadData
{
private readonly DataContext _dataContext;
private readonly ITorrentData _torrentData;
private readonly TorrentData _torrentData;
public DownloadData(DataContext dataContext, ITorrentData torrentData)
public DownloadData(DataContext dataContext, TorrentData torrentData)
{
_dataContext = dataContext;
_torrentData = torrentData;
@ -44,7 +27,9 @@ namespace RdtClient.Data.Data
public async Task<Download> Get(Guid torrentId, String path)
{
return await _dataContext.Downloads.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
return await _dataContext.Downloads
.AsNoTracking()
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
}
public async Task<Download> Add(Guid torrentId, String path)
@ -193,5 +178,27 @@ namespace RdtClient.Data.Data
await _torrentData.VoidCache();
}
public async Task Reset(Guid downloadId)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.RetryCount = 0;
dbDownload.Link = null;
dbDownload.Added = DateTimeOffset.UtcNow;
dbDownload.DownloadQueued = DateTimeOffset.UtcNow;
dbDownload.DownloadStarted = null;
dbDownload.DownloadFinished = null;
dbDownload.UnpackingQueued = null;
dbDownload.UnpackingStarted = null;
dbDownload.UnpackingFinished = null;
dbDownload.Completed = null;
dbDownload.Error = null;
await _dataContext.SaveChangesAsync();
await _torrentData.VoidCache();
}
}
}

View file

@ -8,14 +8,7 @@ using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data
{
public interface ISettingData
{
Task<IList<Setting>> GetAll();
Task Update(IList<Setting> settings);
Task UpdateString(String key, String value);
}
public class SettingData : ISettingData
public class SettingData
{
private static IList<Setting> _settingCache;
private static readonly SemaphoreSlim _settingCacheLock = new(1);

View file

@ -9,31 +9,7 @@ using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data
{
public interface ITorrentData
{
Task<IList<Torrent>> Get();
Task<Torrent> GetById(Guid torrentId);
Task<Torrent> GetByHash(String hash);
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();
}
public class TorrentData : ITorrentData
public class TorrentData
{
private static IList<Torrent> _torrentCache;
private static readonly SemaphoreSlim _torrentCacheLock = new(1);

View file

@ -4,12 +4,7 @@ using Microsoft.EntityFrameworkCore;
namespace RdtClient.Data.Data
{
public interface IUserData
{
Task<IdentityUser> GetUser();
}
public class UserData : IUserData
public class UserData
{
private readonly DataContext _dataContext;

View file

@ -7,10 +7,10 @@ namespace RdtClient.Data
{
public static void Config(IServiceCollection services)
{
services.AddScoped<IDownloadData, DownloadData>();
services.AddScoped<ISettingData, SettingData>();
services.AddScoped<ITorrentData, TorrentData>();
services.AddScoped<IUserData, UserData>();
services.AddScoped<DownloadData>();
services.AddScoped<SettingData>();
services.AddScoped<TorrentData>();
services.AddScoped<UserData>();
}
}
}

View file

@ -7,13 +7,13 @@ namespace RdtClient.Service
{
public static void Config(IServiceCollection services)
{
services.AddScoped<IAuthentication, Authentication>();
services.AddScoped<IDownloads, Downloads>();
services.AddScoped<IQBittorrent, QBittorrent>();
services.AddScoped<IRemoteService, RemoteService>();
services.AddScoped<ISettings, Settings>();
services.AddScoped<ITorrents, Torrents>();
services.AddScoped<ITorrentRunner, TorrentRunner>();
services.AddScoped<Authentication>();
services.AddScoped<Downloads>();
services.AddScoped<QBittorrent>();
services.AddScoped<RemoteService>();
services.AddScoped<Settings>();
services.AddScoped<Torrents>();
services.AddScoped<TorrentRunner>();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.IO;
using System.Linq;
using System.Web;
using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Helpers
{
public static class DownloadHelper
{
public static String GetDownloadPath(String downloadPath, Torrent torrent, Download download)
{
var fileUrl = download.Link;
if (String.IsNullOrWhiteSpace(fileUrl))
{
return null;
}
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(downloadPath, torrent.RdName);
if (!Directory.Exists(torrentPath))
{
Directory.CreateDirectory(torrentPath);
}
var fileName = uri.Segments.Last();
fileName = HttpUtility.UrlDecode(fileName);
var filePath = Path.Combine(torrentPath, fileName);
return filePath;
}
}
}

View file

@ -5,21 +5,13 @@ using RdtClient.Data.Data;
namespace RdtClient.Service.Services
{
public interface IAuthentication
{
Task<IdentityResult> Register(String userName, String password);
Task<SignInResult> Login(String userName, String password);
Task<IdentityUser> GetUser();
Task Logout();
}
public class Authentication : IAuthentication
public class Authentication
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly IUserData _userData;
private readonly UserData _userData;
public Authentication(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IUserData userData)
public Authentication(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, UserData userData)
{
_signInManager = signInManager;
_userManager = userManager;

View file

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using Downloader;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
@ -44,36 +42,24 @@ namespace RdtClient.Service.Services
try
{
var downloadClientSetting = settings.GetString("DownloadClient");
var filePath = DownloadHelper.GetDownloadPath(_destinationPath, _torrent, _download);
if (filePath == null)
{
throw new Exception("Invalid download path");
}
var fileUrl = _download.Link;
if (String.IsNullOrWhiteSpace(fileUrl))
{
throw new Exception("File URL is empty");
}
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(_destinationPath, _torrent.RdName);
if (!Directory.Exists(torrentPath))
{
Directory.CreateDirectory(torrentPath);
}
var fileName = uri.Segments.Last();
fileName = HttpUtility.UrlDecode(fileName);
var filePath = Path.Combine(torrentPath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
var uri = new Uri(_download.Link);
await Task.Factory.StartNew(async delegate
{
var downloadClientSetting = settings.GetString("DownloadClient");
switch (downloadClientSetting)
{
case "Simple":

View file

@ -5,28 +5,11 @@ using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Service.Services
{
public interface IDownloads
public class Downloads
{
Task<Download> GetById(Guid downloadId);
Task<Download> Get(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, String path);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateError(Guid downloadId, String error);
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
Task DeleteForTorrent(Guid torrentId);
}
private readonly DownloadData _downloadData;
public class Downloads : IDownloads
{
private readonly IDownloadData _downloadData;
public Downloads(IDownloadData downloadData)
public Downloads(DownloadData downloadData)
{
_downloadData = downloadData;
}
@ -95,5 +78,10 @@ namespace RdtClient.Service.Services
{
await _downloadData.DeleteForTorrent(torrentId);
}
public async Task Reset(Guid downloadId)
{
await _downloadData.Reset(downloadId);
}
}
}

View file

@ -9,31 +9,13 @@ using RdtClient.Service.Models.QBittorrent.QuickType;
namespace RdtClient.Service.Services
{
public interface IQBittorrent
public class QBittorrent
{
Task<Boolean> AuthLogin(String userName, String password);
Task AuthLogout();
Task<AppPreferences> AppPreferences();
Task<String> AppDefaultSavePath();
Task<IList<TorrentInfo>> TorrentInfo();
Task<IList<TorrentFileItem>> TorrentFileContents(String hash);
Task<TorrentProperties> TorrentProperties(String hash);
Task TorrentsDelete(String hash, Boolean deleteFiles);
Task TorrentsAddMagnet(String magnetLink, String category);
Task TorrentsAddFile(Byte[] fileBytes, String category);
Task TorrentsSetCategory(String hash, String category);
Task<IDictionary<String, TorrentCategory>> TorrentsCategories();
Task CategoryCreate(String category);
Task CategoryRemove(String category);
}
private readonly Authentication _authentication;
private readonly Settings _settings;
private readonly Torrents _torrents;
public class QBittorrent : IQBittorrent
{
private readonly IAuthentication _authentication;
private readonly ISettings _settings;
private readonly ITorrents _torrents;
public QBittorrent(ISettings settings, IAuthentication authentication, ITorrents torrents)
public QBittorrent(Settings settings, Authentication authentication, Torrents torrents)
{
_settings = settings;
_authentication = authentication;

View file

@ -5,17 +5,12 @@ using Microsoft.AspNetCore.SignalR;
namespace RdtClient.Service.Services
{
public interface IRemoteService
{
Task Update();
}
public class RemoteService : IRemoteService
public class RemoteService
{
private readonly IHubContext<RdtHub> _hub;
private readonly ITorrents _torrents;
private readonly Torrents _torrents;
public RemoteService(IHubContext<RdtHub> hub, ITorrents torrents)
public RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
{
_hub = hub;
_torrents = torrents;

View file

@ -10,24 +10,11 @@ using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Services
{
public interface ISettings
public class Settings
{
Task<IList<Setting>> GetAll();
Task Update(IList<Setting> settings);
Task UpdateString(String key, String value);
Task<String> GetString(String key);
Task<Int32> GetNumber(String key);
Task TestPath(String path);
Task<Double> TestDownloadSpeed(CancellationToken cancellationToken);
Task<Double> TestWriteSpeed();
void Clean();
}
private readonly SettingData _settingData;
public class Settings : ISettings
{
private readonly ISettingData _settingData;
public Settings(ISettingData settingData)
public Settings(SettingData settingData)
{
_settingData = settingData;
}

View file

@ -23,7 +23,7 @@ namespace RdtClient.Service.Services
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
using var scope = _serviceProvider.CreateScope();
var torrentRunner = scope.ServiceProvider.GetRequiredService<ITorrentRunner>();
var torrentRunner = scope.ServiceProvider.GetRequiredService<TorrentRunner>();
_logger.LogInformation("TaskRunner started.");
@ -33,12 +33,18 @@ namespace RdtClient.Service.Services
{
try
{
await Torrents.TorrentResetLock.WaitAsync(stoppingToken);
await torrentRunner.Tick();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick");
}
finally
{
Torrents.TorrentResetLock.Release();
}
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
}

View file

@ -11,13 +11,7 @@ using Serilog;
namespace RdtClient.Service.Services
{
public interface ITorrentRunner
{
Task Initialize();
Task Tick();
}
public class TorrentRunner : ITorrentRunner
public class TorrentRunner
{
private const Int32 RetryCount = 3;
@ -25,13 +19,13 @@ namespace RdtClient.Service.Services
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
private readonly IDownloads _downloads;
private readonly IRemoteService _remoteService;
private readonly Downloads _downloads;
private readonly RemoteService _remoteService;
private readonly ISettings _settings;
private readonly ITorrents _torrents;
private readonly Settings _settings;
private readonly Torrents _torrents;
public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads, IRemoteService remoteService)
public TorrentRunner(Settings settings, Torrents torrents, Downloads downloads, RemoteService remoteService)
{
_settings = settings;
_torrents = torrents;
@ -174,10 +168,10 @@ namespace RdtClient.Service.Services
Log.Debug($"Found {torrents.Count} torrents");
// Only poll RealDebrid every second when a hub is connected, otherwise every 30 seconds
// Only poll Real-Debrid every second when a hub is connected, otherwise every 30 seconds
if (_nextUpdate < DateTime.UtcNow && torrents.Count > 0)
{
Log.Debug($"Updating torrent info from RealDebrid");
Log.Debug($"Updating torrent info from Real-Debrid");
var updateTime = 30;
@ -193,7 +187,7 @@ namespace RdtClient.Service.Services
// Re-get torrents to account for updated info
torrents = await _torrents.Get();
Log.Debug($"Finished updating torrent info from RealDebrid, next update in {updateTime} seconds");
Log.Debug($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds");
}
torrents = torrents.Where(m => m.Completed == null).ToList();
@ -279,6 +273,13 @@ namespace RdtClient.Service.Services
var torrentDownload = torrents.First(m => m.TorrentId == download.TorrentId);
if (download.Link == null)
{
await _downloads.UpdateError(download.DownloadId, "Download Link cannot be null");
continue;
}
// Check if the unpacking process is even needed
var uri = new Uri(download.Link);
var fileName = uri.Segments.Last();
@ -346,7 +347,7 @@ namespace RdtClient.Service.Services
foreach (var torrent in torrents)
{
// If torrent is erroring out on the RealDebrid side, skip processing this torrent.
// If torrent is erroring out on the Real-Debrid side, skip processing this torrent.
if (torrent.RdStatus == RealDebridStatus.Error)
{
Log.Debug($"Torrent {torrent.RdId} has an error: {torrent.RdStatusRaw}, not processing further");
@ -354,14 +355,15 @@ namespace RdtClient.Service.Services
continue;
}
// The files are selected but there are no downloads yet, check if Real Debrid has generated links yet.
// The files are selected but there are no downloads yet, check if Real-Debrid has generated links yet.
if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null)
{
await _torrents.CheckForLinks(torrent.TorrentId);
}
// RealDebrid is waiting for file selection, select which files to download.
// Real-Debrid is waiting for file selection, select which files to download.
if ((torrent.RdStatus == RealDebridStatus.WaitingForFileSelection || torrent.RdStatus == RealDebridStatus.Finished) &&
torrent.FilesSelected == null &&
torrent.Downloads.Count == 0 &&
torrent.FilesSelected == null)
{
@ -420,7 +422,7 @@ namespace RdtClient.Service.Services
await _torrents.UpdateFilesSelected(torrent.TorrentId, DateTime.UtcNow);
}
// RealDebrid finished downloading the torrent, process the file to host.
// Real-Debrid finished downloading the torrent, process the file to host.
if (torrent.RdStatus == RealDebridStatus.Finished)
{
// If the torrent has any files that need starting to be downloaded, download them.
@ -483,11 +485,11 @@ namespace RdtClient.Service.Services
switch (torrent.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
Log.Debug($"Torrent {torrent.RdId} removing torrents from Real Debrid and Real Debrid Client, no files");
Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid and Real-Debrid Client, no files");
await _torrents.Delete(torrent.TorrentId, true, true, false);
break;
case TorrentFinishedAction.RemoveRealDebrid:
Log.Debug($"Torrent {torrent.RdId} removing torrents from Real Debrid, no files");
Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid, no files");
await _torrents.Delete(torrent.TorrentId, false, true, false);
break;
case TorrentFinishedAction.None:

View file

@ -9,57 +9,25 @@ using Newtonsoft.Json;
using RDNET;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Service.Helpers;
using RdtClient.Service.Models;
using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services
{
public interface ITorrents
public class Torrents
{
Task<IList<Torrent>> Get();
Task<Torrent> GetByHash(String hash);
Task UpdateCategory(String hash, String category);
public static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
Task UploadMagnet(String magnetLink,
String category,
TorrentDownloadAction downloadAction,
TorrentFinishedAction finishedAction,
Int32 downloadMinSize,
String downloadManualFiles);
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
Task UploadFile(Byte[] bytes,
String category,
TorrentDownloadAction downloadAction,
TorrentFinishedAction finishedAction,
Int32 downloadMinSize,
String downloadManualFiles);
Task<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);
Task Unpack(Guid downloadId);
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);
}
public class Torrents : ITorrents
{
private static RdNetClient _rdtNetClient;
private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1);
private readonly Downloads _downloads;
private readonly Settings _settings;
private readonly TorrentData _torrentData;
private readonly IDownloads _downloads;
private readonly ISettings _settings;
private readonly ITorrentData _torrentData;
public Torrents(ITorrentData torrentData, ISettings settings, IDownloads downloads)
public Torrents(TorrentData torrentData, Settings settings, Downloads downloads)
{
_torrentData = torrentData;
_settings = settings;
@ -271,6 +239,47 @@ namespace RdtClient.Service.Services
}
}
private async Task DeleteDownload(Guid torrentId, Guid downloadId)
{
var torrent = await GetById(torrentId);
var download = await _downloads.GetById(downloadId);
var downloadPath = await DownloadPath(torrent);
var filePath = DownloadHelper.GetDownloadPath(downloadPath, torrent, download);
if (filePath == null)
{
return;
}
if (File.Exists(filePath))
{
var retry = 0;
while (true)
{
try
{
File.Delete(filePath);
break;
}
catch
{
retry++;
if (retry >= 3)
{
throw;
}
await Task.Delay(1000);
}
}
}
}
public async Task<String> UnrestrictLink(Guid downloadId)
{
var download = await _downloads.GetById(downloadId);
@ -322,12 +331,7 @@ namespace RdtClient.Service.Services
public async Task Update()
{
var w = await SemaphoreSlim.WaitAsync(1);
if (!w)
{
return;
}
await RealDebridUpdateLock.WaitAsync();
var torrents = await Get();
@ -360,23 +364,42 @@ namespace RdtClient.Service.Services
}
finally
{
SemaphoreSlim.Release();
RealDebridUpdateLock.Release();
}
}
public async Task Retry(Guid id, Int32 retry)
public async Task RetryTorrent(Guid torrentId)
{
var torrent = await _torrentData.GetById(id);
var torrent = await _torrentData.GetById(torrentId);
if (retry == 0)
foreach (var download in torrent.Downloads)
{
await Delete(id, true, true, true);
if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet))
while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
throw new Exception($"Cannot re-add this torrent, original magnet or file not found");
downloadClient.Cancel();
await Task.Delay(100);
}
while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
unpackClient.Cancel();
await Task.Delay(100);
}
}
await Delete(torrentId, true, true, true);
if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet))
{
throw new Exception($"Cannot re-add this torrent, original magnet or file not found");
}
await TorrentResetLock.WaitAsync();
try
{
if (torrent.IsFile)
{
var bytes = Convert.FromBase64String(torrent.FileOrMagnet);
@ -393,19 +416,52 @@ namespace RdtClient.Service.Services
torrent.DownloadManualFiles);
}
}
else if (retry == 1)
finally
{
await Delete(id, false, false, true);
await _torrentData.UpdateComplete(id, null);
await _downloads.DeleteForTorrent(id);
}
else
{
throw new Exception($"Invalid retry option {retry}");
TorrentResetLock.Release();
}
}
public async Task RetryDownload(Guid downloadId)
{
var download = await _downloads.GetById(downloadId);
if (download == null)
{
return;
}
while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
downloadClient.Cancel();
await Task.Delay(100);
}
while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
unpackClient.Cancel();
await Task.Delay(100);
}
await TorrentResetLock.WaitAsync();
try
{
await DeleteDownload(download.TorrentId, download.DownloadId);
await _torrentData.UpdateComplete(download.TorrentId, null);
await _downloads.Reset(downloadId);
}
finally
{
TorrentResetLock.Release();
}
}
public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime)
{
await _torrentData.UpdateComplete(torrentId, datetime);
@ -425,7 +481,7 @@ namespace RdtClient.Service.Services
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new Exception("RealDebrid API Key not set in the settings");
throw new Exception("Real-Debrid API Key not set in the settings");
}
_rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey);
@ -441,6 +497,22 @@ namespace RdtClient.Service.Services
if (torrent != null)
{
await Update(torrent);
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = downloadClient.Speed;
download.BytesTotal = downloadClient.BytesTotal;
download.BytesDone = downloadClient.BytesDone;
}
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
download.BytesTotal = unpackClient.BytesTotal;
download.BytesDone = unpackClient.BytesDone;
}
}
}
return torrent;
@ -456,13 +528,8 @@ namespace RdtClient.Service.Services
String fileOrMagnetContents,
Boolean isFile)
{
var w = await SemaphoreSlim.WaitAsync(60000);
if (!w)
{
throw new Exception("Unable to add torrent, could not obtain lock");
}
await RealDebridUpdateLock.WaitAsync();
try
{
var torrent = await _torrentData.GetByHash(infoHash);
@ -486,7 +553,7 @@ namespace RdtClient.Service.Services
}
finally
{
SemaphoreSlim.Release();
RealDebridUpdateLock.Release();
}
}

View file

@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
@ -24,7 +24,7 @@ namespace RdtClient.Service.Services
private readonly String _destinationPath;
private readonly Torrent _torrent;
private Boolean _cancelled = false;
private Boolean _cancelled;
private RarArchiveEntry _rarCurrentEntry;
private Dictionary<String, Int64> _rarfileStatus;
@ -43,25 +43,11 @@ namespace RdtClient.Service.Services
try
{
var fileUrl = _download.Link;
var filePath = DownloadHelper.GetDownloadPath(_destinationPath, _torrent, _download);
if (String.IsNullOrWhiteSpace(fileUrl))
if (filePath == null)
{
throw new Exception("File URL is empty");
}
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(_destinationPath, _torrent.RdName);
var fileName = uri.Segments.Last();
fileName = HttpUtility.UrlDecode(fileName);
var filePath = Path.Combine(torrentPath, fileName);
if (!File.Exists(filePath))
{
throw new Exception($"File {filePath} could not be extracted because it is missing");
throw new Exception("Invalid download path");
}
await Task.Factory.StartNew(async delegate
@ -88,6 +74,11 @@ namespace RdtClient.Service.Services
{
try
{
if (!File.Exists(filePath))
{
return;
}
await using (Stream stream = File.OpenRead(filePath))
{
using var archive = RarArchive.Open(stream);
@ -97,7 +88,7 @@ namespace RdtClient.Service.Services
var entries = archive.Entries.Where(entry => !entry.IsDirectory)
.ToList();
_rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L);
_rarfileStatus = entries.ToDictionary(entry => entry.Key, _ => 0L);
_rarCurrentEntry = null;
archive.CompressedBytesRead += ArchiveOnCompressedBytesRead;
@ -148,7 +139,7 @@ namespace RdtClient.Service.Services
}
catch (Exception ex)
{
Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
Error = $"An unexpected error occurred unpacking {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
}
finally
{

View file

@ -10,9 +10,9 @@ namespace RdtClient.Web.Controllers
[Route("Api/Authentication")]
public class AuthController : Controller
{
private readonly IAuthentication _authentication;
private readonly Authentication _authentication;
public AuthController(IAuthentication authentication)
public AuthController(Authentication authentication)
{
_authentication = authentication;
}

View file

@ -20,9 +20,9 @@ namespace RdtClient.Web.Controllers
[Route("api/v2")]
public class QBittorrentController : Controller
{
private readonly IQBittorrent _qBittorrent;
private readonly QBittorrent _qBittorrent;
public QBittorrentController(IQBittorrent qBittorrent)
public QBittorrentController(QBittorrent qBittorrent)
{
_qBittorrent = qBittorrent;
}

View file

@ -15,10 +15,10 @@ namespace RdtClient.Web.Controllers
[Route("Api/Settings")]
public class SettingsController : Controller
{
private readonly ISettings _settings;
private readonly ITorrents _torrents;
private readonly Settings _settings;
private readonly Torrents _torrents;
public SettingsController(ISettings settings, ITorrents torrents)
public SettingsController(Settings settings, Torrents torrents)
{
_settings = settings;
_torrents = torrents;

View file

@ -18,10 +18,10 @@ namespace RdtClient.Web.Controllers
[Route("Api/Torrents")]
public class TorrentsController : Controller
{
private readonly ITorrentRunner _torrentRunner;
private readonly ITorrents _torrents;
private readonly TorrentRunner _torrentRunner;
private readonly Torrents _torrents;
public TorrentsController(ITorrents torrents, ITorrentRunner torrentRunner)
public TorrentsController(Torrents torrents, TorrentRunner torrentRunner)
{
_torrents = torrents;
_torrentRunner = torrentRunner;
@ -29,7 +29,7 @@ namespace RdtClient.Web.Controllers
[HttpGet]
[Route("")]
public async Task<ActionResult<IList<Torrent>>> Get()
public async Task<ActionResult<IList<Torrent>>> GetAll()
{
var results = await _torrents.Get();
@ -42,6 +42,23 @@ namespace RdtClient.Web.Controllers
return Ok(results);
}
[HttpGet]
[Route("Get/{torrentId:guid}")]
public async Task<ActionResult<Torrent>> GetById(Guid torrentId)
{
var torrent = await _torrents.GetById(torrentId);
if (torrent?.Downloads != null)
{
foreach (var file in torrent.Downloads)
{
file.Torrent = null;
}
}
return Ok(torrent);
}
/// <summary>
/// Used for debugging only. Force a tick.
/// </summary>
@ -129,19 +146,28 @@ namespace RdtClient.Web.Controllers
}
[HttpPost]
[Route("Delete/{id}")]
public async Task<ActionResult> Delete(Guid id, [FromBody] TorrentControllerDeleteRequest request)
[Route("Delete/{torrentId:guid}")]
public async Task<ActionResult> Delete(Guid torrentId, [FromBody] TorrentControllerDeleteRequest request)
{
await _torrents.Delete(id, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles);
await _torrents.Delete(torrentId, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles);
return Ok();
}
[HttpPost]
[Route("Retry/{id}")]
public async Task<ActionResult> Retry(Guid id, [FromBody] TorrentControllerRetryRequest request)
[Route("Retry/{torrentId:guid}")]
public async Task<ActionResult> Retry(Guid torrentId)
{
await _torrents.Retry(id, request.Retry);
await _torrents.RetryTorrent(torrentId);
return Ok();
}
[HttpPost]
[Route("RetryDownload/{downloadId:guid}")]
public async Task<ActionResult> RetryDownload(Guid downloadId)
{
await _torrents.RetryDownload(downloadId);
return Ok();
}
@ -173,11 +199,6 @@ namespace RdtClient.Web.Controllers
public Boolean DeleteLocalFiles { get; set; }
}
public class TorrentControllerRetryRequest
{
public Int32 Retry { get; set; }
}
public class TorrentControllerCheckFilesRequest
{
public String MagnetLink { get; set; }

View file

@ -91,7 +91,7 @@ namespace RdtClient.Web
Service.DiConfig.Config(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, ISettings settings)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, Settings settings)
{
if (env.IsDevelopment())
{