Created a separate page for torrent into, added download retrying per file.
This commit is contained in:
parent
fa11dc9bc7
commit
3ee687f615
50 changed files with 1101 additions and 661 deletions
5
client/package-lock.json
generated
5
client/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
10
client/src/app/decode-uri.pipe.ts
Normal file
10
client/src/app/decode-uri.pipe.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
>.
|
>.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
td {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
td {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
.delete-icon {
|
|
||||||
color: red;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto .fas {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,4 @@ table {
|
||||||
tr {
|
tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.separator {
|
|
||||||
td {
|
|
||||||
font-size: smaller;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
439
client/src/app/torrent/torrent.component.html
Normal file
439
client/src/app/torrent/torrent.component.html
Normal 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>
|
||||||
13
client/src/app/torrent/torrent.component.scss
Normal file
13
client/src/app/torrent/torrent.component.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-download {
|
||||||
|
margin-left :12px;
|
||||||
|
}
|
||||||
168
client/src/app/torrent/torrent.component.ts
Normal file
168
client/src/app/torrent/torrent.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
37
server/RdtClient.Service/Helpers/DownloadHelper.cs
Normal file
37
server/RdtClient.Service/Helpers/DownloadHelper.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue