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" "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": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",

View file

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

View file

@ -40,7 +40,7 @@
<div class="control select is-fullwidth"> <div class="control select is-fullwidth">
<select [(ngModel)]="downloadAction"> <select [(ngModel)]="downloadAction">
<option [ngValue]="0">Download all files above a certain size</option> <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> <option [ngValue]="2">Pick files I want to download</option>
</select> </select>
</div> </div>
@ -66,8 +66,8 @@
<div class="control select is-fullwidth"> <div class="control select is-fullwidth">
<select [(ngModel)]="finishedAction"> <select [(ngModel)]="finishedAction">
<option [ngValue]="0">Do nothing</option> <option [ngValue]="0">Do nothing</option>
<option [ngValue]="1">Remove torrent from Real Debrid and Real Debrid Client</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]="2">Remove torrent from Real-Debrid</option>
</select> </select>
</div> </div>
</div> </div>
@ -84,10 +84,16 @@
<div class="field"> <div class="field">
<label class="label">Available files</label> <label class="label">Available files</label>
<p class="help"> <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. It is possible that there are more files in the torrent, which are not shown here.
</p> </p>
<div class="scroll-container"> <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"> <div class="field" *ngIf="downloadAction === 2">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles"> <label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input <input

View file

@ -22,6 +22,7 @@ export class AddNewTorrentComponent implements OnInit {
public availableFiles: TorrentFileAvailability[] = []; public availableFiles: TorrentFileAvailability[] = [];
public downloadFiles: { [key: string]: boolean } = {}; public downloadFiles: { [key: string]: boolean } = {};
public allSelected: boolean;
public saving = false; public saving = false;
public error: string; public error: string;
@ -50,6 +51,21 @@ export class AddNewTorrentComponent implements OnInit {
public downloadFileChecked(file: string): void { public downloadFileChecked(file: string): void {
this.downloadFiles[file] = !this.downloadFiles[file]; 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 { public ok(): void {
@ -127,6 +143,7 @@ export class AddNewTorrentComponent implements OnInit {
this.error = null; this.error = null;
this.availableFiles = []; this.availableFiles = [];
this.downloadFiles = {}; this.downloadFiles = {};
this.allSelected = true;
if (this.magnetLink) { if (this.magnetLink) {
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe( 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 { SettingsComponent } from './settings/settings.component';
import { SetupComponent } from './setup/setup.component'; import { SetupComponent } from './setup/setup.component';
import { TorrentTableComponent } from './torrent-table/torrent-table.component'; import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -28,6 +29,10 @@ const routes: Routes = [
path: '', path: '',
component: TorrentTableComponent, component: TorrentTableComponent,
}, },
{
path: 'torrent/:id',
component: TorrentComponent,
},
{ {
path: 'add', path: 'add',
component: AddNewTorrentComponent, component: AddNewTorrentComponent,

View file

@ -1,3 +1,4 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout'; 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 { NavbarComponent } from './navbar/navbar.component';
import { SettingsComponent } from './settings/settings.component'; import { SettingsComponent } from './settings/settings.component';
import { SetupComponent } from './setup/setup.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 { TorrentStatusPipe } from './torrent-status.pipe';
import { TorrentTableComponent } from './torrent-table/torrent-table.component'; import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component';
import { DecodeURIPipe } from './decode-uri.pipe';
curray(); curray();
@ -30,16 +30,23 @@ curray();
NavbarComponent, NavbarComponent,
AddNewTorrentComponent, AddNewTorrentComponent,
TorrentTableComponent, TorrentTableComponent,
TorrentRowComponent,
TorrentFileComponent,
SettingsComponent, SettingsComponent,
TorrentStatusPipe, TorrentStatusPipe,
DownloadStatusPipe, DownloadStatusPipe,
LoginComponent, LoginComponent,
SetupComponent, 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 }], providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent], 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) { if (value.error) {
return `Error: ${value.error}`; return `Error`;
} }
if (value.completed != null) { if (value.completed != null) {
@ -26,8 +26,13 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.unpackingStarted) { if (value.unpackingStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2); let progress = (value.bytesDone / value.bytesTotal) * 100;
return `Unpacking ${progress || 0}%`;
if (isNaN(progress)) {
progress = 0;
}
return `Unpacking ${progress.toFixed(2)}%`;
} }
if (value.unpackingQueued) { if (value.unpackingQueued) {
@ -39,10 +44,15 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.downloadStarted) { 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'); const speed = this.pipe.transform(value.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`; return `Downloading ${progress.toFixed(2)}% (${speed}/s)`;
} }
if (value.downloadQueued) { if (value.downloadQueued) {

View file

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

View file

@ -146,7 +146,7 @@
<div class="help"> <div class="help">
Files that are smaller than this setting are skipped and not downloaded. When set to 0 all files are downloaded. 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 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> </div>
</div> </div>
@ -155,10 +155,10 @@
<div class="control"> <div class="control">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" /> <input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
Only download available files on RealDebrid Only download available files on Real-Debrid
</label> </label>
<div class="help"> <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. this in combination with the Min File size setting above.
</div> </div>
</div> </div>
@ -189,7 +189,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label">Test RealDebrid download speed</label> <label class="label">Test Real-Debrid download speed</label>
<div class="control"> <div class="control">
<button <button
class="button is-warning" class="button is-warning"
@ -210,7 +210,7 @@
</div> </div>
</div> </div>
<div class="help"> <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>
</div> </div>

View file

@ -6,7 +6,7 @@
<img src="../../assets/logo.png" /> <img src="../../assets/logo.png" />
<div class="box" *ngIf="step === 1"> <div class="box" *ngIf="step === 1">
<div class="notification is-primary"> <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> </div>
<form (ngSubmit)="setup()"> <form (ngSubmit)="setup()">
<div class="field"> <div class="field">
@ -44,14 +44,14 @@
<div class="box" *ngIf="step === 2"> <div class="box" *ngIf="step === 2">
<div class="notification is-primary"> <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 /> <br /><br />
Not premium yet? Not premium yet?
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener" <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 /> <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" <a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a >https://real-debrid.com/apitoken</a
>. >.
@ -82,7 +82,7 @@
<div class="box" *ngIf="step === 3"> <div class="box" *ngIf="step === 3">
<div class="notification is-primary"> <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" <a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
>https://github.com/rogerfar/rdt-client/</a >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 bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal); const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100; let progress = (bytesDone / bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length; let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
let speed: string | string[] = '0'; let speed: string | string[] = '0';
@ -44,6 +49,11 @@ export class TorrentStatusPipe implements PipeTransform {
const bytesDone = unpacking.sum((m) => m.bytesDone); const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal); const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100; let progress = (bytesDone / bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length; let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (allSpeeds > 0) { if (allSpeeds > 0) {

View file

@ -7,126 +7,34 @@
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Category</th>
<th>Files</th> <th>Files</th>
<th>Downloads</th> <th>Downloads</th>
<th>Auto</th>
<th>Size</th> <th>Size</th>
<th>Status</th> <th>Status</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ng-container *ngFor="let torrent of torrents; trackBy: trackByMethod"> <tr (click)="selectTorrent(torrent.torrentId)" *ngFor="let torrent of torrents; trackBy: trackByMethod">
<tr <td>
app-torrent-row {{ torrent.rdName }}
[torrent]="torrent" </td>
(click)="selectTorrent(torrent)" <td>
(delete)="showDeleteModal($event)" {{ torrent.category }}
(retry)="showRetryModal($event)" </td>
></tr> <td>
<ng-container *ngIf="showFiles[torrent.torrentId]"> {{ torrent.files.length | number }}
<tr class="separator"> </td>
<td colspan="10">Downloads</td> <td>
</tr> {{ torrent.downloads.length | number }}
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr> </td>
<tr class="separator"> <td>
<td colspan="10">Files in torrent</td> {{ torrent.rdSize | filesize }}
</tr> </td>
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr> <td>
</ng-container> {{ torrent | status }}
</ng-container> </td>
</tr>
</tbody> </tbody>
</table> </table>
</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">
<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 { tr {
cursor: pointer; 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 { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model'; import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service'; import { TorrentService } from '../torrent.service';
@ -10,23 +11,8 @@ import { TorrentService } from '../torrent.service';
export class TorrentTableComponent implements OnInit, OnDestroy { export class TorrentTableComponent implements OnInit, OnDestroy {
public torrents: Torrent[] = []; public torrents: Torrent[] = [];
public error: string; public error: string;
public showFiles: { [key: string]: boolean } = {};
public isDeleteModalActive: boolean; constructor(private router: Router, private torrentService: TorrentService) {}
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) {}
ngOnInit(): void { ngOnInit(): void {
this.torrentService.getList().subscribe( this.torrentService.getList().subscribe(
@ -49,67 +35,11 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
this.torrentService.disconnect(); this.torrentService.disconnect();
} }
public selectTorrent(torrent: Torrent): void { public selectTorrent(torrentId: string): void {
this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId]; this.router.navigate([`/torrent/${torrentId}`]);
} }
public trackByMethod(index: number, el: Torrent): string { public trackByMethod(index: number, el: Torrent): string {
return el.torrentId; 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; private connection: signalR.HubConnection;
public connect(): void { public connect(): void {
if (this.connection != null) {
return;
}
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build(); this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
this.connection.start().catch((err) => console.error(err)); this.connection.start().catch((err) => console.error(err));
@ -31,6 +35,10 @@ export class TorrentService {
return this.http.get<Torrent[]>(`/Api/Torrents`); return this.http.get<Torrent[]>(`/Api/Torrents`);
} }
public get(torrentId: string): Observable<Torrent> {
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`);
}
public uploadMagnet( public uploadMagnet(
magnetLink: string, magnetLink: string,
category: string, category: string,
@ -91,9 +99,11 @@ export class TorrentService {
}); });
} }
public retry(torrentId: string, retry: number): Observable<void> { public retry(torrentId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, { return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
retry, }
});
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 namespace RdtClient.Data.Data
{ {
public interface IDownloadData public class DownloadData
{
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
{ {
private readonly DataContext _dataContext; 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; _dataContext = dataContext;
_torrentData = torrentData; _torrentData = torrentData;
@ -44,7 +27,9 @@ namespace RdtClient.Data.Data
public async Task<Download> Get(Guid torrentId, String path) 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) public async Task<Download> Add(Guid torrentId, String path)
@ -193,5 +178,27 @@ namespace RdtClient.Data.Data
await _torrentData.VoidCache(); 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 namespace RdtClient.Data.Data
{ {
public interface ISettingData public class SettingData
{
Task<IList<Setting>> GetAll();
Task Update(IList<Setting> settings);
Task UpdateString(String key, String value);
}
public class SettingData : ISettingData
{ {
private static IList<Setting> _settingCache; private static IList<Setting> _settingCache;
private static readonly SemaphoreSlim _settingCacheLock = new(1); private static readonly SemaphoreSlim _settingCacheLock = new(1);

View file

@ -9,31 +9,7 @@ using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data
{ {
public interface ITorrentData public class TorrentData
{
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
{ {
private static IList<Torrent> _torrentCache; private static IList<Torrent> _torrentCache;
private static readonly SemaphoreSlim _torrentCacheLock = new(1); private static readonly SemaphoreSlim _torrentCacheLock = new(1);

View file

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

View file

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

View file

@ -7,13 +7,13 @@ namespace RdtClient.Service
{ {
public static void Config(IServiceCollection services) public static void Config(IServiceCollection services)
{ {
services.AddScoped<IAuthentication, Authentication>(); services.AddScoped<Authentication>();
services.AddScoped<IDownloads, Downloads>(); services.AddScoped<Downloads>();
services.AddScoped<IQBittorrent, QBittorrent>(); services.AddScoped<QBittorrent>();
services.AddScoped<IRemoteService, RemoteService>(); services.AddScoped<RemoteService>();
services.AddScoped<ISettings, Settings>(); services.AddScoped<Settings>();
services.AddScoped<ITorrents, Torrents>(); services.AddScoped<Torrents>();
services.AddScoped<ITorrentRunner, TorrentRunner>(); 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 namespace RdtClient.Service.Services
{ {
public interface IAuthentication public class Authentication
{
Task<IdentityResult> Register(String userName, String password);
Task<SignInResult> Login(String userName, String password);
Task<IdentityUser> GetUser();
Task Logout();
}
public class Authentication : IAuthentication
{ {
private readonly SignInManager<IdentityUser> _signInManager; private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager; 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; _signInManager = signInManager;
_userManager = userManager; _userManager = userManager;

View file

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

View file

@ -5,28 +5,11 @@ using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Service.Services namespace RdtClient.Service.Services
{ {
public interface IDownloads public class Downloads
{ {
Task<Download> GetById(Guid downloadId); private readonly DownloadData _downloadData;
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 Downloads : IDownloads public Downloads(DownloadData downloadData)
{
private readonly IDownloadData _downloadData;
public Downloads(IDownloadData downloadData)
{ {
_downloadData = downloadData; _downloadData = downloadData;
} }
@ -95,5 +78,10 @@ namespace RdtClient.Service.Services
{ {
await _downloadData.DeleteForTorrent(torrentId); 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 namespace RdtClient.Service.Services
{ {
public interface IQBittorrent public class QBittorrent
{ {
Task<Boolean> AuthLogin(String userName, String password); private readonly Authentication _authentication;
Task AuthLogout(); private readonly Settings _settings;
Task<AppPreferences> AppPreferences(); private readonly Torrents _torrents;
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);
}
public class QBittorrent : IQBittorrent public QBittorrent(Settings settings, Authentication authentication, Torrents torrents)
{
private readonly IAuthentication _authentication;
private readonly ISettings _settings;
private readonly ITorrents _torrents;
public QBittorrent(ISettings settings, IAuthentication authentication, ITorrents torrents)
{ {
_settings = settings; _settings = settings;
_authentication = authentication; _authentication = authentication;

View file

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

View file

@ -10,24 +10,11 @@ using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Services namespace RdtClient.Service.Services
{ {
public interface ISettings public class Settings
{ {
Task<IList<Setting>> GetAll(); private readonly SettingData _settingData;
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();
}
public class Settings : ISettings public Settings(SettingData settingData)
{
private readonly ISettingData _settingData;
public Settings(ISettingData settingData)
{ {
_settingData = settingData; _settingData = settingData;
} }

View file

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

View file

@ -11,13 +11,7 @@ using Serilog;
namespace RdtClient.Service.Services namespace RdtClient.Service.Services
{ {
public interface ITorrentRunner public class TorrentRunner
{
Task Initialize();
Task Tick();
}
public class TorrentRunner : ITorrentRunner
{ {
private const Int32 RetryCount = 3; 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, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new(); public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
private readonly IDownloads _downloads; private readonly Downloads _downloads;
private readonly IRemoteService _remoteService; private readonly RemoteService _remoteService;
private readonly ISettings _settings; private readonly Settings _settings;
private readonly ITorrents _torrents; 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; _settings = settings;
_torrents = torrents; _torrents = torrents;
@ -174,10 +168,10 @@ namespace RdtClient.Service.Services
Log.Debug($"Found {torrents.Count} torrents"); 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) 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; var updateTime = 30;
@ -193,7 +187,7 @@ namespace RdtClient.Service.Services
// Re-get torrents to account for updated info // Re-get torrents to account for updated info
torrents = await _torrents.Get(); 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(); 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); 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 // Check if the unpacking process is even needed
var uri = new Uri(download.Link); var uri = new Uri(download.Link);
var fileName = uri.Segments.Last(); var fileName = uri.Segments.Last();
@ -346,7 +347,7 @@ namespace RdtClient.Service.Services
foreach (var torrent in torrents) 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) if (torrent.RdStatus == RealDebridStatus.Error)
{ {
Log.Debug($"Torrent {torrent.RdId} has an error: {torrent.RdStatusRaw}, not processing further"); Log.Debug($"Torrent {torrent.RdId} has an error: {torrent.RdStatusRaw}, not processing further");
@ -354,14 +355,15 @@ namespace RdtClient.Service.Services
continue; 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) if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null)
{ {
await _torrents.CheckForLinks(torrent.TorrentId); 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) && if ((torrent.RdStatus == RealDebridStatus.WaitingForFileSelection || torrent.RdStatus == RealDebridStatus.Finished) &&
torrent.FilesSelected == null &&
torrent.Downloads.Count == 0 && torrent.Downloads.Count == 0 &&
torrent.FilesSelected == null) torrent.FilesSelected == null)
{ {
@ -420,7 +422,7 @@ namespace RdtClient.Service.Services
await _torrents.UpdateFilesSelected(torrent.TorrentId, DateTime.UtcNow); 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 (torrent.RdStatus == RealDebridStatus.Finished)
{ {
// If the torrent has any files that need starting to be downloaded, download them. // 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) switch (torrent.FinishedAction)
{ {
case TorrentFinishedAction.RemoveAllTorrents: 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); await _torrents.Delete(torrent.TorrentId, true, true, false);
break; break;
case TorrentFinishedAction.RemoveRealDebrid: 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); await _torrents.Delete(torrent.TorrentId, false, true, false);
break; break;
case TorrentFinishedAction.None: case TorrentFinishedAction.None:

View file

@ -9,57 +9,25 @@ using Newtonsoft.Json;
using RDNET; using RDNET;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Service.Helpers;
using RdtClient.Service.Models; using RdtClient.Service.Models;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services namespace RdtClient.Service.Services
{ {
public interface ITorrents public class Torrents
{ {
Task<IList<Torrent>> Get(); public static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
Task<Torrent> GetByHash(String hash);
Task UpdateCategory(String hash, String category);
Task UploadMagnet(String magnetLink, private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
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);
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 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; public Torrents(TorrentData torrentData, Settings settings, Downloads downloads)
private readonly ISettings _settings;
private readonly ITorrentData _torrentData;
public Torrents(ITorrentData torrentData, ISettings settings, IDownloads downloads)
{ {
_torrentData = torrentData; _torrentData = torrentData;
_settings = settings; _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) public async Task<String> UnrestrictLink(Guid downloadId)
{ {
var download = await _downloads.GetById(downloadId); var download = await _downloads.GetById(downloadId);
@ -322,12 +331,7 @@ namespace RdtClient.Service.Services
public async Task Update() public async Task Update()
{ {
var w = await SemaphoreSlim.WaitAsync(1); await RealDebridUpdateLock.WaitAsync();
if (!w)
{
return;
}
var torrents = await Get(); var torrents = await Get();
@ -360,23 +364,42 @@ namespace RdtClient.Service.Services
} }
finally 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); while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet))
{ {
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) if (torrent.IsFile)
{ {
var bytes = Convert.FromBase64String(torrent.FileOrMagnet); var bytes = Convert.FromBase64String(torrent.FileOrMagnet);
@ -393,16 +416,49 @@ namespace RdtClient.Service.Services
torrent.DownloadManualFiles); torrent.DownloadManualFiles);
} }
} }
else if (retry == 1) finally
{ {
await Delete(id, false, false, true); TorrentResetLock.Release();
await _torrentData.UpdateComplete(id, null);
await _downloads.DeleteForTorrent(id);
} }
else }
public async Task RetryDownload(Guid downloadId)
{
var download = await _downloads.GetById(downloadId);
if (download == null)
{ {
throw new Exception($"Invalid retry option {retry}"); 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();
} }
} }
@ -425,7 +481,7 @@ namespace RdtClient.Service.Services
if (String.IsNullOrWhiteSpace(apiKey)) 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); _rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey);
@ -441,6 +497,22 @@ namespace RdtClient.Service.Services
if (torrent != null) if (torrent != null)
{ {
await Update(torrent); 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; return torrent;
@ -456,12 +528,7 @@ namespace RdtClient.Service.Services
String fileOrMagnetContents, String fileOrMagnetContents,
Boolean isFile) Boolean isFile)
{ {
var w = await SemaphoreSlim.WaitAsync(60000); await RealDebridUpdateLock.WaitAsync();
if (!w)
{
throw new Exception("Unable to add torrent, could not obtain lock");
}
try try
{ {
@ -486,7 +553,7 @@ namespace RdtClient.Service.Services
} }
finally finally
{ {
SemaphoreSlim.Release(); RealDebridUpdateLock.Release();
} }
} }

View file

@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Rar; using SharpCompress.Archives.Rar;
using SharpCompress.Common; using SharpCompress.Common;
@ -24,7 +24,7 @@ namespace RdtClient.Service.Services
private readonly String _destinationPath; private readonly String _destinationPath;
private readonly Torrent _torrent; private readonly Torrent _torrent;
private Boolean _cancelled = false; private Boolean _cancelled;
private RarArchiveEntry _rarCurrentEntry; private RarArchiveEntry _rarCurrentEntry;
private Dictionary<String, Int64> _rarfileStatus; private Dictionary<String, Int64> _rarfileStatus;
@ -43,25 +43,11 @@ namespace RdtClient.Service.Services
try 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"); throw new Exception("Invalid download path");
}
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");
} }
await Task.Factory.StartNew(async delegate await Task.Factory.StartNew(async delegate
@ -88,6 +74,11 @@ namespace RdtClient.Service.Services
{ {
try try
{ {
if (!File.Exists(filePath))
{
return;
}
await using (Stream stream = File.OpenRead(filePath)) await using (Stream stream = File.OpenRead(filePath))
{ {
using var archive = RarArchive.Open(stream); using var archive = RarArchive.Open(stream);
@ -97,7 +88,7 @@ namespace RdtClient.Service.Services
var entries = archive.Entries.Where(entry => !entry.IsDirectory) var entries = archive.Entries.Where(entry => !entry.IsDirectory)
.ToList(); .ToList();
_rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L); _rarfileStatus = entries.ToDictionary(entry => entry.Key, _ => 0L);
_rarCurrentEntry = null; _rarCurrentEntry = null;
archive.CompressedBytesRead += ArchiveOnCompressedBytesRead; archive.CompressedBytesRead += ArchiveOnCompressedBytesRead;
@ -148,7 +139,7 @@ namespace RdtClient.Service.Services
} }
catch (Exception ex) 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 finally
{ {

View file

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

View file

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

View file

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

View file

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

View file

@ -91,7 +91,7 @@ namespace RdtClient.Web
Service.DiConfig.Config(services); 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()) if (env.IsDevelopment())
{ {