Refactored download processor.

This commit is contained in:
Roger Far 2021-01-09 19:51:36 -07:00
parent ec02b3d8bb
commit a27bd37991
38 changed files with 1413 additions and 824 deletions

View file

@ -7,6 +7,7 @@ indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
[*.ts]
quote_type = single

View file

@ -1,29 +1,58 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { TorrentFile } from './models/torrent.model';
import { DownloadStatus } from './models/download.model';
@Pipe({
name: 'fileStatus',
})
export class FileStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {}
transform(value: TorrentFile): string {
if (
!value.download ||
value.download.status === DownloadStatus.PendingDownload
) {
if (!value.download) {
return 'Pending download';
}
if (value.download.error) {
return `Error: ${value.download.error}`;
}
if (value.download.completed != null) {
return 'Finished';
}
if (value.download.unpackingFinished) {
return 'Unpacking finished';
}
if (value.download.unpackingStarted) {
const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2);
return `Unpacking ${progress || 0}%`;
}
if (value.download.unpackingQueued) {
return 'Unpacking queued';
}
if (value.download.downloadFinished) {
return 'Download finished';
}
if (value.download.downloadStarted) {
const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2);
const speed = this.pipe.transform(value.download.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`;
}
if (value.download.downloadQueued) {
return 'Download queued';
}
if (value.download.added) {
return 'Pending';
}
if (value.download.status === DownloadStatus.Downloading) {
const progress = (
(value.download.bytesDownloaded / value.download.bytesSize) *
100
).toFixed(2);
return `${progress || 0}%`;
}
if (value.download.status === DownloadStatus.Finished) {
return `Finished`;
}
return '';
}
}

View file

@ -1,24 +1,17 @@
export class Download {
public downloadId: string;
public torrentId: string;
public link: string;
public added: Date;
public status: DownloadStatus;
public bytesDownloaded: number;
public bytesSize: number;
public downloadQueued: Date;
public downloadStarted: Date;
public downloadFinished: Date;
public unpackingQueued: Date;
public unpackingStarted: Date;
public unpackingFinished: Date;
public completed: Date;
public error: string;
public bytesTotal: number;
public bytesDone: number;
public speed: number;
}
export enum DownloadStatus {
PendingDownload = 0,
Downloading = 1,
Unpacking = 2,
Finished = 3,
}

View file

@ -1,4 +1,4 @@
export class Profile {
userName: string;
expiration: Date;
public userName: string;
public expiration: Date;
}

View file

@ -1,41 +1,47 @@
import { Download } from './download.model';
export class Torrent {
torrentId: string;
hash: string;
status: TorrentStatus;
rdId: string;
rdName: string;
rdSize: number;
rdHost: string;
rdSplit: number;
rdProgress: number;
rdStatus: string;
rdAdded: Date;
rdEnded: Date;
rdSpeed: number;
rdSeeders: number;
rdFiles: string;
public torrentId: string;
public hash: string;
public category: string;
public added: Date;
public completed: Date;
public autoDownload: boolean;
public autoUnpack: boolean;
public autoDelete: boolean;
files: TorrentFile[];
downloads: Download[];
public rdId: string;
public rdName: string;
public rdSize: number;
public rdHost: string;
public rdSplit: number;
public rdProgress: number;
public rdStatus: RealDebridStatus;
public rdStatusRaw: string;
public rdAdded: Date;
public rdEnded: Date;
public rdSpeed: number;
public rdSeeders: number;
public rdFiles: string;
public files: TorrentFile[];
public downloads: Download[];
}
export class TorrentFile {
id: string;
path: string;
bytes: number;
selected: boolean;
public id: string;
public path: string;
public bytes: number;
public selected: boolean;
download: Download;
public download: Download;
}
export enum TorrentStatus {
RealDebrid = 0,
WaitingForDownload = 1,
DownloadQueued = 2,
Downloading = 3,
Finished = 4,
export enum RealDebridStatus {
Processing = 0,
WaitingForFileSelection = 1,
Downloading = 2,
Finished = 3,
Error = 99,
}

View file

@ -47,6 +47,10 @@
<input type="checkbox" [(ngModel)]="autoDownload" />
Download torrent to host when finished downloading on Real-Debrid
</label>
<label class="checkbox">
<input type="checkbox" [(ngModel)]="autoUnpack" />
Unpack files after downloading to host
</label>
<label class="checkbox">
<input type="checkbox" [(ngModel)]="autoDelete" />
Remove torrent when finished downloading on host
@ -61,6 +65,7 @@
class="button is-success"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
(click)="ok()"
>
<span>Add Torrent</span>
</button>

View file

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { TorrentService } from 'src/app/torrent.service';
@Component({
@ -25,6 +25,7 @@ export class AddNewTorrentComponent implements OnInit {
public fileName: string;
public magnetLink: string;
public autoDownload: boolean;
public autoUnpack: boolean;
public autoDelete: boolean;
public saving = false;
@ -67,7 +68,12 @@ export class AddNewTorrentComponent implements OnInit {
if (this.magnetLink) {
this.torrentService
.uploadMagnet(this.magnetLink, this.autoDownload, this.autoDelete)
.uploadMagnet(
this.magnetLink,
this.autoDownload,
this.autoUnpack,
this.autoDelete
)
.subscribe(
() => {
this.cancel();
@ -79,7 +85,12 @@ export class AddNewTorrentComponent implements OnInit {
);
} else if (this.selectedFile) {
this.torrentService
.uploadFile(this.selectedFile, this.autoDownload, this.autoDelete)
.uploadFile(
this.selectedFile,
this.autoDownload,
this.autoUnpack,
this.autoDelete
)
.subscribe(
() => {
this.cancel();

View file

@ -42,15 +42,9 @@ export class SettingsComponent implements OnInit {
this.settingsService.get().subscribe(
(results) => {
this.settingRealDebridApiKey = this.getSetting(
results,
'RealDebridApiKey'
);
this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey');
this.settingDownloadFolder = this.getSetting(results, 'DownloadFolder');
this.settingDownloadLimit = parseInt(
this.getSetting(results, 'DownloadLimit'),
10
);
this.settingDownloadLimit = parseInt(this.getSetting(results, 'DownloadLimit'), 10);
},
(err) => {
this.error = err.error;

View file

@ -6,22 +6,12 @@
{{ torrent | status }}
</td>
<td>
<span
class="icon download-icon"
(click)="download($event)"
title="Download to disk"
*ngIf="!loading && torrent.status === 1"
>
<span class="icon download-icon" (click)="download($event)" title="Download to disk" *ngIf="!loading">
<i class="fas fa-download"></i>
</span>
</td>
<td>
<span
class="icon delete-icon"
(click)="delete($event)"
title="Delete torrent"
*ngIf="!loading"
>
<span class="icon delete-icon" (click)="delete1($event)" title="Delete torrent" *ngIf="!loading">
<i class="fas fa-times"></i>
</span>
<span class="icon loading-icon" *ngIf="loading">

View file

@ -1,5 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { Torrent, TorrentStatus } from 'src/app/models/torrent.model';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Torrent } from 'src/app/models/torrent.model';
import { TorrentService } from 'src/app/torrent.service';
@Component({
@ -11,6 +11,9 @@ export class TorrentRowComponent implements OnInit {
@Input()
public torrent: Torrent;
@Output('delete')
public delete = new EventEmitter();
public loading = false;
constructor(private torrentService: TorrentService) {}
@ -23,14 +26,11 @@ export class TorrentRowComponent implements OnInit {
this.loading = true;
this.torrentService.download(this.torrent.torrentId).subscribe(() => {
this.loading = false;
this.torrent.status = TorrentStatus.Downloading;
});
}
public delete(event: Event): void {
public delete1(event: Event): void {
event.stopPropagation();
this.loading = true;
this.torrentService.delete(this.torrent.torrentId).subscribe(() => {});
this.delete.emit(this.torrent.torrentId);
}
}

View file

@ -1,7 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { DownloadStatus } from './models/download.model';
import { Torrent, TorrentStatus } from './models/torrent.model';
import { RealDebridStatus, Torrent } from './models/torrent.model';
@Pipe({
name: 'status',
@ -10,68 +9,72 @@ export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {}
transform(torrent: Torrent): string {
if (torrent.completed) {
return 'Finished';
}
if (torrent.downloads && torrent.downloads.length > 0) {
const allFinished = torrent.downloads.all(
(m) => m.status === DownloadStatus.Finished
);
const allFinished = torrent.downloads.all((m) => m.completed != null);
if (allFinished) {
return 'Finished';
}
const downloading = torrent.downloads.where(
(m) => m.status === DownloadStatus.Downloading
);
const unpacking = torrent.downloads.where(
(m) => m.status === DownloadStatus.Unpacking
);
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished);
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished);
const allBytesDownloaded = torrent.downloads.sum(
(m) => m.bytesDownloaded
);
const allBytesSize = torrent.downloads.sum((m) => m.bytesSize);
let progress = 0;
let allSpeeds = 0;
if (allBytesSize > 0) {
progress = (allBytesDownloaded / allBytesSize) * 100;
allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
}
let speed: string | string[] = '0';
if (allSpeeds > 0) {
speed = this.pipe.transform(allSpeeds, 'filesize');
}
let downloadText = '';
let unpackText = '';
if (downloading.length > 0) {
if (allBytesSize > 0) {
return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`;
}
const bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
return `Preparing download`;
let speed: string | string[] = '0';
if (allSpeeds > 0) {
speed = this.pipe.transform(allSpeeds, 'filesize');
downloadText = `Downloading (${progress.toFixed(2)}% - ${speed}/s)`;
}
}
if (unpacking.length > 0) {
return `Unpacking (${progress.toFixed(2)}% - ${speed}/s)`;
const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (allSpeeds > 0) {
downloadText = `Extracting (${progress.toFixed(2)}%)`;
}
}
return 'Pending download';
let result: string[] = [];
if (downloadText) {
result.push(downloadText);
}
if (unpackText) {
result.push(unpackText);
}
if (result.length > 0) {
return result.join('\r\n');
}
}
switch (torrent.status) {
case TorrentStatus.RealDebrid:
switch (torrent.rdStatus) {
case RealDebridStatus.Downloading:
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`;
case TorrentStatus.WaitingForDownload:
return `Ready to download, press the download icon to start`;
case TorrentStatus.DownloadQueued:
return `Download queued`;
case TorrentStatus.Downloading:
return `Downloading`;
case TorrentStatus.Finished:
return `Finished`;
case TorrentStatus.Error:
return `Error`;
case RealDebridStatus.Processing:
return `Torrent processing`;
case RealDebridStatus.WaitingForFileSelection:
return `Torrent waiting for file selection`;
case RealDebridStatus.Error:
return `Torrent error: ${torrent.rdStatusRaw}`;
case RealDebridStatus.Finished:
return `Torrent finished`;
default:
return 'Unknown status';
}

View file

@ -5,11 +5,11 @@
<div class="table-container">
<table class="table is-fullwidth is-hoverable">
<colgroup>
<col style="width: 50%;" />
<col style="width: 15%;" />
<col style="width: 35%;" />
<col style="width: 50px;" />
<col style="width: 50px;" />
<col style="width: 50%" />
<col style="width: 15%" />
<col style="width: 35%" />
<col style="width: 50px" />
<col style="width: 50px" />
</colgroup>
<thead>
<tr>
@ -26,27 +26,70 @@
app-torrent-row
[torrent]="torrent"
(click)="selectTorrent(torrent)"
(delete)="showDeleteModal($event)"
></tr>
<ng-container
*ngIf="showFiles[torrent.torrentId] && torrent?.files.length === 0"
>
<ng-container *ngIf="showFiles[torrent.torrentId] && torrent?.files.length === 0">
<tr>
<td colspan="20">
<i class="fas fa-spinner fa-pulse"></i>
</td>
</tr>
</ng-container>
<ng-container
*ngIf="showFiles[torrent.torrentId] && torrent?.files.length > 0"
>
<tr
app-torrent-file
[file]="file"
*ngFor="let file of torrent.files"
></tr>
<ng-container *ngIf="showFiles[torrent.torrentId] && torrent?.files.length > 0">
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
</ng-container>
</ng-container>
</tbody>
</table>
</div>
<div class="modal" [class.is-active]="isDeleteModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete torrent</p>
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
Delete Torrent from local RealDebrid Client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
Delete Torrent from RealDebrid
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
Delete local files
</label>
</div>
</div>
<div class="notification is-primary">
Deleting a torrent from RealDebrid will automatically delete it here too.
</div>
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
Error deleting torrent: {{ deleteError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="deleteOk()"
[disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }"
>
Delete
</button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
Cancel
</button>
</footer>
</div>
</div>

View file

@ -1,13 +1,6 @@
import {
Component,
OnInit,
OnDestroy,
HostListener,
ElementRef,
} from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
import { DownloadStatus } from '../models/download.model';
@Component({
selector: 'app-torrent-table',
@ -19,6 +12,14 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
public error: string;
public showFiles: { [key: string]: boolean } = {};
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
public deleteTorrentId: string;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
private timer: any;
constructor(private torrentService: TorrentService) {}
@ -50,9 +51,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
torrent.downloads = result.downloads;
torrent.files.forEach((file) => {
const downloads = torrent.downloads.filter(
(m) => m.link === file.path
);
const downloads = torrent.downloads.filter((m) => m.link === file.path);
if (downloads.length > 0) {
file.download = downloads[0];
@ -65,4 +64,30 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
public trackByMethod(index: number, el: Torrent): string {
return el.torrentId;
}
public showDeleteModal(torrentId: string): void {
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;
}
);
}
}

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Torrent } from './models/torrent.model';
@Injectable({
@ -20,23 +20,21 @@ export class TorrentService {
public uploadMagnet(
magnetLink: string,
autoDownload: boolean,
autoUnpack: boolean,
autoDelete: boolean
): Observable<void> {
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
magnetLink,
autoDownload,
autoUnpack,
autoDelete,
});
}
public uploadFile(
file: File,
autoDownload: boolean,
autoDelete: boolean
): Observable<void> {
public uploadFile(file: File, autoDownload: boolean, autoUnpack: boolean, autoDelete: boolean): Observable<void> {
const formData: FormData = new FormData();
formData.append('file', file);
formData.append('formData', JSON.stringify({ autoDownload, autoDelete }));
formData.append('formData', JSON.stringify({ autoDownload, autoUnpack, autoDelete }));
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
}
@ -44,7 +42,16 @@ export class TorrentService {
return this.http.get<void>(`/Api/Torrents/Download/${torrentId}`);
}
public delete(torrentId: string): Observable<void> {
return this.http.delete<void>(`/Api/Torrents/${torrentId}`);
public delete(
torrentId: string,
deleteData: boolean,
deleteRdTorrent: boolean,
deleteLocalFiles: boolean
): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Delete/${torrentId}`, {
deleteData,
deleteRdTorrent,
deleteLocalFiles,
});
}
}

View file

@ -50,7 +50,11 @@ namespace RdtClient.Data.Data
{
SettingId = "DownloadFolder",
Type = "String",
#if DEBUG
Value = @"C:\Temp\rdtclient"
#else
Value = "/data/downloads"
#endif
},
new Setting
{

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data
@ -12,8 +11,14 @@ namespace RdtClient.Data.Data
{
Task<IList<Download>> Get();
Task<IList<Download>> GetForTorrent(Guid torrentId);
Task<Download> GetById(Guid downloadId);
Task<Download> Add(Guid torrentId, String link);
Task UpdateStatus(Guid downloadId, DownloadStatus status);
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 UpdateError(Guid downloadId, String error);
Task DeleteForTorrent(Guid torrentId);
}
@ -42,6 +47,14 @@ namespace RdtClient.Data.Data
.ToListAsync();
}
public async Task<Download> GetById(Guid downloadId)
{
return await _dataContext.Downloads
.Include(m => m.Torrent)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
}
public async Task<Download> Add(Guid torrentId, String link)
{
var download = new Download
@ -50,7 +63,7 @@ namespace RdtClient.Data.Data
TorrentId = torrentId,
Link = link,
Added = DateTimeOffset.UtcNow,
Status = DownloadStatus.PendingDownload
DownloadQueued = DateTimeOffset.UtcNow
};
await _dataContext.Downloads.AddAsync(download);
@ -59,12 +72,64 @@ namespace RdtClient.Data.Data
return download;
}
public async Task UpdateStatus(Guid downloadId, DownloadStatus status)
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
{
var download = await _dataContext.Downloads.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
download.Status = status;
dbDownload.DownloadStarted = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.DownloadFinished = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingQueued = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingStarted = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingFinished = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateError(Guid downloadId, String error)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Completed = DateTimeOffset.UtcNow;
dbDownload.Error = error;
await _dataContext.SaveChangesAsync();
}

View file

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data
@ -11,13 +9,13 @@ namespace RdtClient.Data.Data
public interface ITorrentData
{
Task<IList<Torrent>> Get();
Task<Torrent> GetById(Guid id);
Task<Torrent> GetById(Guid torrentId);
Task<Torrent> GetByHash(String hash);
Task<Torrent> Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoDelete);
Task<Torrent> Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete);
Task UpdateRdData(Torrent torrent);
Task UpdateStatus(Guid torrentId, TorrentStatus status);
Task UpdateCategory(Guid torrentId, String category);
Task Delete(Guid id);
Task UpdateComplete(Guid torrentId, DateTimeOffset datetime);
Task Delete(Guid torrentId);
}
public class TorrentData : ITorrentData
@ -36,20 +34,15 @@ namespace RdtClient.Data.Data
.Include(m => m.Downloads)
.ToListAsync();
foreach (var file in results.SelectMany(torrent => torrent.Downloads))
{
file.Torrent = null;
}
return results;
}
public async Task<Torrent> GetById(Guid id)
public async Task<Torrent> GetById(Guid torrentId)
{
var dbTorrent = await _dataContext.Torrents
.AsNoTracking()
.Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.TorrentId == id);
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
@ -84,15 +77,16 @@ namespace RdtClient.Data.Data
return dbTorrent;
}
public async Task<Torrent> Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoDelete)
public async Task<Torrent> Add(String realDebridId, String hash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
var torrent = new Torrent
{
TorrentId = Guid.NewGuid(),
Added = DateTimeOffset.UtcNow,
RdId = realDebridId,
Hash = hash.ToLower(),
Status = TorrentStatus.RealDebrid,
AutoDownload = autoDownload,
AutoUnpack = autoUnpack,
AutoDelete = autoDelete
};
@ -118,6 +112,7 @@ namespace RdtClient.Data.Data
dbTorrent.RdSplit = torrent.RdSplit;
dbTorrent.RdProgress = torrent.RdProgress;
dbTorrent.RdStatus = torrent.RdStatus;
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
dbTorrent.RdAdded = torrent.RdAdded;
dbTorrent.RdEnded = torrent.RdEnded;
dbTorrent.RdSpeed = torrent.RdSpeed;
@ -130,21 +125,7 @@ namespace RdtClient.Data.Data
await _dataContext.SaveChangesAsync();
}
public async Task UpdateStatus(Guid torrentId, TorrentStatus status)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.Status = status;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateCategory(Guid torrentId, String category)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
@ -159,9 +140,18 @@ namespace RdtClient.Data.Data
await _dataContext.SaveChangesAsync();
}
public async Task Delete(Guid id)
public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == id);
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
dbTorrent.Completed = datetime;
await _dataContext.SaveChangesAsync();
}
public async Task Delete(Guid torrentId)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{

View file

@ -1,10 +0,0 @@
namespace RdtClient.Data.Enums
{
public enum DownloadStatus
{
PendingDownload = 0,
Downloading,
Unpacking,
Finished
}
}

View file

@ -1,12 +1,11 @@
namespace RdtClient.Data.Enums
{
public enum TorrentStatus
public enum RealDebridStatus
{
RealDebrid = 0,
WaitingForDownload = 1,
DownloadQueued = 2,
Downloading = 3,
Finished = 4,
Processing = 0,
WaitingForFileSelection = 1,
Downloading = 2,
Finished = 3,
Error = 99
}

View file

@ -9,7 +9,7 @@ using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20201212204128_Initial")]
[Migration("20210110025102_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -219,15 +219,36 @@ namespace RdtClient.Data.Migrations
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
@ -257,15 +278,24 @@ namespace RdtClient.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDownload")
.HasColumnType("INTEGER");
b.Property<bool>("AutoUnpack")
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<string>("Hash")
.HasColumnType("TEXT");
@ -302,12 +332,12 @@ namespace RdtClient.Data.Migrations
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<string>("RdStatus")
.HasColumnType("TEXT");
b.Property<int>("Status")
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");

View file

@ -66,16 +66,19 @@ namespace RdtClient.Data.Migrations
TorrentId = table.Column<Guid>(type: "TEXT", nullable: false),
Hash = table.Column<string>(type: "TEXT", nullable: true),
Category = table.Column<string>(type: "TEXT", nullable: true),
Added = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Completed = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
AutoDownload = table.Column<bool>(type: "INTEGER", nullable: false),
AutoUnpack = table.Column<bool>(type: "INTEGER", nullable: false),
AutoDelete = table.Column<bool>(type: "INTEGER", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
RdId = table.Column<string>(type: "TEXT", nullable: true),
RdName = table.Column<string>(type: "TEXT", nullable: true),
RdSize = table.Column<long>(type: "INTEGER", nullable: false),
RdHost = table.Column<string>(type: "TEXT", nullable: true),
RdSplit = table.Column<long>(type: "INTEGER", nullable: false),
RdProgress = table.Column<long>(type: "INTEGER", nullable: false),
RdStatus = table.Column<string>(type: "TEXT", nullable: true),
RdStatus = table.Column<int>(type: "INTEGER", nullable: false),
RdStatusRaw = table.Column<string>(type: "TEXT", nullable: true),
RdAdded = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
RdEnded = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
RdSpeed = table.Column<long>(type: "INTEGER", nullable: true),
@ -201,7 +204,14 @@ namespace RdtClient.Data.Migrations
TorrentId = table.Column<Guid>(type: "TEXT", nullable: false),
Link = table.Column<string>(type: "TEXT", nullable: true),
Added = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false)
DownloadQueued = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
DownloadStarted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
DownloadFinished = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
UnpackingQueued = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
UnpackingStarted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
UnpackingFinished = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Completed = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Error = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{

View file

@ -217,15 +217,36 @@ namespace RdtClient.Data.Migrations
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
@ -255,15 +276,24 @@ namespace RdtClient.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDownload")
.HasColumnType("INTEGER");
b.Property<bool>("AutoUnpack")
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<string>("Hash")
.HasColumnType("TEXT");
@ -300,12 +330,12 @@ namespace RdtClient.Data.Migrations
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<string>("RdStatus")
.HasColumnType("TEXT");
b.Property<int>("Status")
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");

View file

@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using RdtClient.Data.Enums;
namespace RdtClient.Data.Models.Data
{
@ -15,17 +14,31 @@ namespace RdtClient.Data.Models.Data
public String Link { get; set; }
public DateTimeOffset Added { get; set; }
public DateTimeOffset? DownloadQueued { get; set; }
public DownloadStatus Status { get; set; }
public DateTimeOffset? DownloadStarted { get; set; }
public DateTimeOffset? DownloadFinished { get; set; }
public DateTimeOffset? UnpackingQueued { get; set; }
public DateTimeOffset? UnpackingStarted { get; set; }
public DateTimeOffset? UnpackingFinished { get; set; }
public DateTimeOffset? Completed { get; set; }
public String Error { get; set; }
[ForeignKey("TorrentId")]
public Torrent Torrent { get; set; }
[NotMapped]
public Int64 BytesSize { get; set; }
public Int64 BytesTotal { get; set; }
[NotMapped]
public Int64 BytesDownloaded { get; set; }
public Int64 BytesDone { get; set; }
[NotMapped]
public Int64 Speed { get; set; }

View file

@ -16,13 +16,14 @@ namespace RdtClient.Data.Models.Data
public String Hash { get; set; }
public String Category { get; set; }
public DateTimeOffset Added { get; set; }
public DateTimeOffset? Completed { get; set; }
public Boolean AutoDownload { get; set; }
public Boolean AutoUnpack { get; set; }
public Boolean AutoDelete { get; set; }
public TorrentStatus Status { get; set; }
[InverseProperty("Torrent")]
public IList<Download> Downloads { get; set; }
@ -32,7 +33,8 @@ namespace RdtClient.Data.Models.Data
public String RdHost { get; set; }
public Int64 RdSplit { get; set; }
public Int64 RdProgress { get; set; }
public String RdStatus { get; set; }
public RealDebridStatus RdStatus { get; set; }
public String RdStatusRaw { get; set; }
public DateTimeOffset RdAdded { get; set; }
public DateTimeOffset? RdEnded { get; set; }
public Int64? RdSpeed { get; set; }

View file

@ -12,6 +12,8 @@ namespace RdtClient.Service
services.AddScoped<IQBittorrent, QBittorrent>();
services.AddScoped<ISettings, Settings>();
services.AddScoped<ITorrents, Torrents>();
services.AddScoped<ITorrentRunner, TorrentRunner>();
services.AddScoped<ITorrentDownloadManager, TorrentDownloadManager>();
}
}
}

View file

@ -2,5 +2,6 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>IIS Express</ActiveDebugProfile>
<ShowAllFiles>false</ShowAllFiles>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,138 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Services
{
public class DownloadClient
{
public Boolean Finished { get; private set; }
public String Error { get; private set; }
public Int64 Speed { get; private set; }
public Int64 BytesTotal { get; private set; }
public Int64 BytesDone { get; private set; }
private readonly Download _download;
private readonly String _destinationPath;
private readonly Torrent _torrent;
private Int64 _bytesLastUpdate;
private DateTime _nextUpdate;
public DownloadClient(Download download, String destinationPath)
{
_download = download;
_destinationPath = destinationPath;
_torrent = download.Torrent;
}
public async Task Start()
{
BytesDone = 0;
BytesTotal = 0;
Speed = 0;
try
{
var fileUrl = _download.Link;
if (String.IsNullOrWhiteSpace(fileUrl))
{
throw new Exception("File URL is empty");
}
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(_destinationPath, _torrent.RdName);
if (!Directory.Exists(torrentPath))
{
Directory.CreateDirectory(torrentPath);
}
var fileName = uri.Segments.Last();
var filePath = Path.Combine(torrentPath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
await Task.Factory.StartNew(async delegate
{
await Download(uri, filePath);
});
}
catch (Exception ex)
{
Error = $"An unexpected error occurred preparing download {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
Finished = true;
}
}
private async Task Download(Uri uri, String filePath)
{
try
{
_bytesLastUpdate = 0;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
// Determine the file size
var webRequest = WebRequest.Create(uri);
webRequest.Method = "HEAD";
Int64 responseLength;
using (var webResponse = await webRequest.GetResponseAsync())
{
responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length"));
}
var request = WebRequest.Create(uri);
using var response = await request.GetResponseAsync();
await using var stream = response.GetResponseStream();
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write);
var buffer = new Byte[4096];
while (fileStream.Length < response.ContentLength)
{
var read = await stream.ReadAsync(buffer, 0, buffer.Length);
if (read > 0)
{
fileStream.Write(buffer, 0, read);
BytesDone = fileStream.Length;
BytesTotal = responseLength;
if (DateTime.UtcNow > _nextUpdate)
{
Speed = fileStream.Length - _bytesLastUpdate;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
_bytesLastUpdate = fileStream.Length;
}
}
else
{
break;
}
}
Speed = 0;
}
catch (Exception ex)
{
Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
}
finally
{
Finished = true;
}
}
}
}

View file

@ -1,206 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
namespace RdtClient.Service.Services
{
public class DownloadManager
{
private Int64 _bytesLastUpdate;
private DateTime _nextUpdate;
private RarArchiveEntry _rarCurrentEntry;
private Dictionary<String, Int64> _rarfileStatus;
public DownloadManager()
{
ServicePointManager.Expect100Continue = false;
ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.MaxServicePointIdleTime = 1000;
}
public DownloadStatus? NewStatus { get; set; }
public Download Download { get; set; }
public Int64 Speed { get; private set; }
public Int64 BytesDownloaded { get; private set; }
public Int64 BytesSize { get; private set; }
private DownloadManager ActiveDownload => TaskRunner.ActiveDownloads[Download.DownloadId];
public async Task Start(String destinationFolderPath, String torrentName, DownloadStatus status)
{
ActiveDownload.BytesDownloaded = 0;
ActiveDownload.BytesSize = 0;
ActiveDownload.Speed = 0;
var fileUrl = Download.Link;
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(destinationFolderPath, torrentName);
var filePath = Path.Combine(torrentPath, uri.Segments.Last());
if (status == DownloadStatus.Unpacking)
{
await Extract(filePath, destinationFolderPath, torrentName);
return;
}
ActiveDownload.NewStatus = DownloadStatus.Downloading;
_bytesLastUpdate = 0;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
if (!Directory.Exists(torrentPath))
{
Directory.CreateDirectory(torrentPath);
}
var webRequest = WebRequest.Create(fileUrl);
webRequest.Method = "HEAD";
Int64 responseLength;
using (var webResponse = await webRequest.GetResponseAsync())
{
responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length"));
}
if (File.Exists(filePath))
{
File.Delete(filePath);
}
var request = WebRequest.Create(fileUrl);
using (var response = await request.GetResponseAsync())
{
await using var stream = response.GetResponseStream();
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write);
var buffer = new Byte[4096];
while (fileStream.Length < response.ContentLength)
{
var read = await stream.ReadAsync(buffer, 0, buffer.Length);
if (read > 0)
{
fileStream.Write(buffer, 0, read);
ActiveDownload.BytesDownloaded = fileStream.Length;
ActiveDownload.BytesSize = responseLength;
if (DateTime.UtcNow > _nextUpdate)
{
ActiveDownload.Speed = fileStream.Length - _bytesLastUpdate;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
_bytesLastUpdate = fileStream.Length;
}
}
else
{
break;
}
}
}
ActiveDownload.Speed = 0;
ActiveDownload.BytesDownloaded = ActiveDownload.BytesSize;
await Extract(filePath, destinationFolderPath, torrentName);
}
private async Task Extract(String filePath, String destinationFolderPath, String torrentName)
{
try
{
if (filePath.EndsWith(".rar"))
{
ActiveDownload.NewStatus = DownloadStatus.Unpacking;
await using (Stream stream = File.OpenRead(filePath))
{
using var archive = RarArchive.Open(stream);
ActiveDownload.BytesSize = archive.TotalSize;
var entries = archive.Entries.Where(entry => !entry.IsDirectory)
.ToList();
_rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L);
_rarCurrentEntry = null;
archive.CompressedBytesRead += ArchiveOnCompressedBytesRead;
var extractPath = destinationFolderPath;
if (!entries.Any(m => m.Key.StartsWith(torrentName + @"\")) && !entries.Any(m => m.Key.StartsWith(torrentName + @"/")))
{
extractPath = Path.Combine(destinationFolderPath, torrentName);
}
if (entries.Any(m => m.Key.Contains(".r00")))
{
extractPath = Path.Combine(extractPath, "Temp");
}
foreach (var entry in entries)
{
_rarCurrentEntry = entry;
entry.WriteToDirectory(extractPath,
new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
}
}
var retryCount = 0;
while (File.Exists(filePath) && retryCount < 10)
{
retryCount++;
try
{
File.Delete(filePath);
}
catch
{
await Task.Delay(1000);
}
}
}
}
catch
{
// ignored
}
ActiveDownload.Speed = 0;
ActiveDownload.BytesDownloaded = ActiveDownload.BytesSize;
ActiveDownload.NewStatus = DownloadStatus.Finished;
}
private void ArchiveOnCompressedBytesRead(Object sender, CompressedBytesReadEventArgs e)
{
if (_rarCurrentEntry == null)
{
return;
}
_rarfileStatus[_rarCurrentEntry.Key] = e.CompressedBytesRead;
ActiveDownload.BytesDownloaded = _rarfileStatus.Sum(m => m.Value);
}
}
}

View file

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Services
@ -11,8 +10,14 @@ namespace RdtClient.Service.Services
{
Task<IList<Download>> Get();
Task<IList<Download>> GetForTorrent(Guid torrentId);
Task<Download> GetById(Guid downloadId);
Task<Download> Add(Guid torrentId, String link);
Task UpdateStatus(Guid downloadId, DownloadStatus status);
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 UpdateError(Guid downloadId, String error);
Task DeleteForTorrent(Guid torrentId);
}
@ -35,14 +40,44 @@ namespace RdtClient.Service.Services
return await _downloadData.GetForTorrent(torrentId);
}
public async Task<Download> GetById(Guid downloadId)
{
return await _downloadData.GetById(downloadId);
}
public async Task<Download> Add(Guid torrentId, String link)
{
return await _downloadData.Add(torrentId, link);
}
public async Task UpdateStatus(Guid downloadId, DownloadStatus status)
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateStatus(downloadId, status);
await _downloadData.UpdateDownloadStarted(downloadId, dateTime);
}
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateDownloadFinished(downloadId, dateTime);
}
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateUnpackingQueued(downloadId, dateTime);
}
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateUnpackingStarted(downloadId, dateTime);
}
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateUnpackingFinished(downloadId, dateTime);
}
public async Task UpdateError(Guid downloadId, String error)
{
await _downloadData.UpdateError(downloadId, error);
}
public async Task DeleteForTorrent(Guid torrentId)

View file

@ -19,8 +19,8 @@ namespace RdtClient.Service.Services
Task<IList<TorrentFileItem>> TorrentFileContents(String hash);
Task<TorrentProperties> TorrentProperties(String hash);
Task TorrentsDelete(String hash, Boolean deleteFiles);
Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete);
Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete);
Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete);
Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete);
Task TorrentsSetCategory(String hash, String category);
Task<IDictionary<String, TorrentCategory>> TorrentsCategories();
}
@ -275,14 +275,13 @@ namespace RdtClient.Service.Services
result.Uploaded = (Int64) (torrent.RdSize * (torrent.RdProgress / 100.0));
result.UploadedSession = (Int64) (torrent.RdSize * (torrent.RdProgress / 100.0));
result.Upspeed = torrent.RdSpeed ?? 0;
result.State = torrent.Status switch
result.State = torrent.RdStatus switch
{
TorrentStatus.RealDebrid => "downloading",
TorrentStatus.WaitingForDownload => "downloading",
TorrentStatus.DownloadQueued => "downloading",
TorrentStatus.Downloading => "downloading",
TorrentStatus.Finished => "pausedUP",
TorrentStatus.Error => "error",
RealDebridStatus.Processing => "downloading",
RealDebridStatus.WaitingForFileSelection => "downloading",
RealDebridStatus.Downloading => "downloading",
RealDebridStatus.Finished => "pausedUP",
RealDebridStatus.Error => "error",
_ => throw new ArgumentOutOfRangeException()
};
@ -304,8 +303,11 @@ namespace RdtClient.Service.Services
foreach (var file in torrent.Files)
{
var result = new TorrentFileItem();
result.Name = file.Path;
var result = new TorrentFileItem
{
Name = file.Path
};
results.Add(result);
}
@ -340,7 +342,7 @@ namespace RdtClient.Service.Services
Peers = 0,
PeersTotal = 2,
PieceSize = torrent.Files.Count > 0 ? torrent.RdSize / torrent.Files.Count : 0,
PiecesHave = torrent.Downloads.Count(m => m.Status == DownloadStatus.Finished),
PiecesHave = torrent.Downloads.Count(m => m.Completed.HasValue),
PiecesNum = torrent.Files.Count,
Reannounce = 0,
SavePath = savePath,
@ -372,7 +374,7 @@ namespace RdtClient.Service.Services
return;
}
await _torrents.Delete(torrent.TorrentId);
await _torrents.Delete(torrent.TorrentId, true, true, true);
if (deleteFiles)
{
@ -387,14 +389,14 @@ namespace RdtClient.Service.Services
}
}
public async Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete)
public async Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
await _torrents.UploadMagnet(magnetLink, autoDownload, autoDelete);
await _torrents.UploadMagnet(magnetLink, autoDownload, autoUnpack, autoDelete);
}
public async Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete)
public async Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
await _torrents.UploadFile(fileBytes, autoDownload, autoDelete);
await _torrents.UploadFile(fileBytes, autoDownload, autoUnpack, autoDelete);
}
public async Task TorrentsSetCategory(String hash, String category)

View file

@ -1,157 +1,53 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
namespace RdtClient.Service.Services
{
public class TaskRunner : BackgroundService
{
public static readonly ConcurrentDictionary<Guid, DownloadManager> ActiveDownloads = new ConcurrentDictionary<Guid, DownloadManager>();
private readonly ILogger<TaskRunner> _logger;
private readonly IServiceProvider _services;
private readonly IServiceProvider _serviceProvider;
public TaskRunner(ILogger<TaskRunner> logger, IServiceProvider services)
public TaskRunner(ILogger<TaskRunner> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_services = services;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
#if DEBUG
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
#else
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
#endif
using var scope = _serviceProvider.CreateScope();
var torrentRunner = scope.ServiceProvider.GetRequiredService<ITorrentRunner>();
_logger.LogInformation("TaskRunner started.");
await torrentRunner.Initialize();
while (!stoppingToken.IsCancellationRequested)
{
await DoWork();
try
{
await torrentRunner.Tick();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
_logger.LogInformation("TaskRunner stopped.");
}
private async Task DoWork()
{
try
{
using var scope = _services.CreateScope();
var downloads = scope.ServiceProvider.GetRequiredService<IDownloads>();
var settings = scope.ServiceProvider.GetRequiredService<ISettings>();
var torrents = scope.ServiceProvider.GetRequiredService<ITorrents>();
var rdKey = await settings.GetString("RealDebridApiKey");
if (String.IsNullOrWhiteSpace(rdKey))
{
return;
}
await torrents.Update();
await ProcessAutoDownloads(downloads, settings, torrents);
await ProcessDownloads(downloads, settings, torrents);
await ProcessStatus(downloads, settings, torrents);
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
private async Task ProcessAutoDownloads(IDownloads downloads, ISettings settings, ITorrents torrents)
{
var allTorrents = await torrents.Get();
allTorrents = allTorrents.Where(m => (m.Status == TorrentStatus.WaitingForDownload && m.AutoDownload && m.Downloads.Count == 0) || m.Status == TorrentStatus.DownloadQueued)
.ToList();
foreach (var torrent in allTorrents)
{
await torrents.Download(torrent.TorrentId);
}
}
private async Task ProcessDownloads(IDownloads downloads, ISettings settings, ITorrents torrents)
{
var allDownloads = await downloads.Get();
allDownloads = allDownloads.Where(m => m.Status != DownloadStatus.Finished)
.OrderByDescending(m => m.Status)
.ThenByDescending(m => m.Added)
.ToList();
var maxDownloads = await settings.GetNumber("DownloadLimit");
var destinationFolderPath = await settings.GetString("DownloadFolder");
foreach (var download in allDownloads)
{
if (ActiveDownloads.ContainsKey(download.DownloadId))
{
continue;
}
if (ActiveDownloads.Count >= maxDownloads)
{
return;
}
// Prevent circular references
download.Torrent.Downloads = null;
await torrents.UpdateStatus(download.TorrentId, TorrentStatus.Downloading);
await Task.Factory.StartNew(async delegate
{
var downloadManager = new DownloadManager();
if (ActiveDownloads.TryAdd(download.DownloadId, downloadManager))
{
downloadManager.Download = download;
await downloadManager.Start(destinationFolderPath, download.Torrent.RdName, download.Status);
}
});
}
}
private async Task ProcessStatus(IDownloads downloads, ISettings settings, ITorrents torrents)
{
foreach (var (downloadId, download) in ActiveDownloads)
{
if (download.NewStatus.HasValue)
{
download.Download.Status = download.NewStatus.Value;
download.NewStatus = null;
await downloads.UpdateStatus(downloadId, download.Download.Status);
if (download.Download.Status == DownloadStatus.Finished)
{
ActiveDownloads.TryRemove(downloadId, out _);
// Check if all downloads are completed and update the torrent
var allDownloads = await downloads.GetForTorrent(download.Download.TorrentId);
if (allDownloads.All(m => m.Status == DownloadStatus.Finished))
{
await torrents.UpdateStatus(download.Download.TorrentId, TorrentStatus.Finished);
if (download.Download.Torrent.AutoDelete)
{
await torrents.Delete(download.Download.TorrentId);
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,17 @@
using System.Threading.Tasks;
namespace RdtClient.Service.Services
{
public interface ITorrentDownloadManager
{
Task Tick();
}
public class TorrentDownloadManager : ITorrentDownloadManager
{
public async Task Tick()
{
throw new System.NotImplementedException();
}
}
}

View file

@ -0,0 +1,229 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using RdtClient.Data.Enums;
namespace RdtClient.Service.Services
{
public interface ITorrentRunner
{
Task Initialize();
Task Tick();
}
public class TorrentRunner : ITorrentRunner
{
private static DateTime _nextUpdate = DateTime.UtcNow;
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new ConcurrentDictionary<Guid, DownloadClient>();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new ConcurrentDictionary<Guid, UnpackClient>();
private readonly ISettings _settings;
private readonly ITorrents _torrents;
private readonly IDownloads _downloads;
public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads)
{
_settings = settings;
_torrents = torrents;
_downloads = downloads;
}
public async Task Initialize()
{
// When starting up reset any pending downloads or unpackings so that they are restarted.
var torrents = await _torrents.Get();
var downloads = torrents.SelectMany(m => m.Downloads)
.Where(m => m.DownloadQueued != null && m.DownloadStarted != null && m.DownloadFinished == null)
.OrderBy(m => m.DownloadQueued);
foreach (var download in downloads)
{
await _downloads.UpdateDownloadStarted(download.DownloadId, null);
}
var unpacks = torrents.SelectMany(m => m.Downloads)
.Where(m => m.UnpackingQueued != null && m.UnpackingStarted != null && m.UnpackingFinished == null)
.OrderBy(m => m.DownloadQueued);
foreach (var download in unpacks)
{
await _downloads.UpdateUnpackingStarted(download.DownloadId, null);
}
}
public async Task Tick()
{
var settingApiKey = await _settings.GetString("RealDebridApiKey");
if (String.IsNullOrWhiteSpace(settingApiKey))
{
return;
}
// Check if any torrents are finished downloading to the host, remove them from the active download list.
var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList();
if (completedActiveDownloads.Count > 0)
{
foreach (var (downloadId, downloadClient) in completedActiveDownloads)
{
if (downloadClient.Error != null)
{
await _downloads.UpdateError(downloadId, downloadClient.Error);
}
else
{
await _downloads.UpdateDownloadFinished(downloadId, DateTimeOffset.UtcNow);
}
ActiveDownloadClients.TryRemove(downloadId, out _);
}
}
// Check if any torrents are finished unpacking, remove them from the active unpack list.
var completedUnpacks = ActiveUnpackClients.Where(m => m.Value.Finished).ToList();
if (completedUnpacks.Count > 0)
{
foreach (var (downloadId, unpackClient) in completedUnpacks)
{
if (unpackClient.Error != null)
{
await _downloads.UpdateError(downloadId, unpackClient.Error);
}
else
{
await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow);
}
ActiveUnpackClients.TryRemove(downloadId, out _);
}
}
// Only poll RealDebrid every 30 seconds
if (_nextUpdate < DateTime.UtcNow)
{
_nextUpdate = DateTime.UtcNow.AddSeconds(5);
await _torrents.Update();
}
var torrents = await _torrents.Get();
torrents = torrents.Where(m => m.Completed == null).ToList();
// Check if there are any downloads that are queued and can be started.
var queuedDownloads = torrents.SelectMany(m => m.Downloads)
.Where(m => m.DownloadQueued != null && m.DownloadStarted == null)
.OrderBy(m => m.DownloadQueued);
foreach (var download in queuedDownloads)
{
await _torrents.Download(download.DownloadId);
}
// Check if there are any unpacks that are queued and can be started.
var queuedUnpacks = torrents.SelectMany(m => m.Downloads)
.Where(m => m.UnpackingQueued != null && m.UnpackingStarted == null)
.OrderBy(m => m.DownloadQueued);
foreach (var download in queuedUnpacks)
{
await _torrents.Unpack(download.DownloadId);
}
foreach (var torrent in torrents)
{
// If torrent is erroring out on the RealDebrid side, skip processing this torrent.
if (torrent.RdStatus == RealDebridStatus.Error)
{
continue;
}
// RealDebrid is waiting for file selection, select which files to download.
if (torrent.AutoDownload && torrent.RdStatus == RealDebridStatus.WaitingForFileSelection)
{
var fileIds = torrent.Files
.Select(m => m.Id.ToString())
.ToArray();
await _torrents.SelectFiles(torrent.RdId, fileIds);
}
// If the torrent doesn't have any files at this point, don't process it further.
if (torrent.Files.Count == 0)
{
continue;
}
// RealDebrid finished downloading the torrent, process the file to host.
if (torrent.AutoDownload && torrent.RdStatus == RealDebridStatus.Finished)
{
// If the torrent doesn't have any Downloads, unrestrict the links and add them to the database.
if (torrent.Downloads.Count == 0)
{
await _torrents.Unrestrict(torrent.TorrentId);
continue;
}
// If the torrent has any files that need starting to be downloaded, download them.
var downloadsPending = torrent.Downloads
.Where(m => m.DownloadStarted == null &&
m.DownloadFinished == null)
.ToList();
if (downloadsPending.Count > 0)
{
foreach (var download in downloadsPending)
{
await _torrents.Download(download.DownloadId);
}
continue;
}
}
if (torrent.AutoUnpack && torrent.RdStatus == RealDebridStatus.Finished)
{
// If all files are finished downloading, move to the unpacking step.
var unpackingPending = torrent.Downloads
.Where(m => m.DownloadFinished != null &&
m.UnpackingStarted == null &&
m.UnpackingFinished == null)
.ToList();
if (unpackingPending.Count > 0)
{
foreach (var download in unpackingPending)
{
await _torrents.Unpack(download.DownloadId);
}
continue;
}
}
// Check if torrent is complete
if (torrent.Downloads.Count > 0)
{
var allComplete = torrent.Downloads.All(m => m.UnpackingFinished != null);
if (allComplete)
{
await _torrents.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow);
// Remove the torrent from RealDebrid
if (torrent.AutoDelete)
{
await _torrents.Delete(torrent.TorrentId, true, true, false);
}
}
}
}
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -16,24 +17,28 @@ namespace RdtClient.Service.Services
public interface ITorrents
{
Task<IList<Torrent>> Get();
Task<Torrent> GetById(Guid id);
Task<Torrent> GetById(Guid torrentId);
Task<Torrent> GetByHash(String hash);
Task<IList<Torrent>> Update();
Task UpdateStatus(Guid torrentId, TorrentStatus status);
Task UpdateCategory(String hash, String category);
Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete);
Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete);
Task Delete(Guid id);
Task Download(Guid id);
Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete);
Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete);
Task SelectFiles(String torrentId, IList<String> fileIds);
Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles);
Task Unrestrict(Guid torrentId);
Task Download(Guid downloadId);
Task Unpack(Guid downloadId);
void Reset();
Task<Profile> GetProfile();
Task UpdateComplete(Guid torrentId, DateTimeOffset datetime);
Task Update();
}
public class Torrents : ITorrents
{
private static RdNetClient _rdtClient;
private static RdNetClient _rdtNetClient;
private static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1);
private readonly IDownloads _downloads;
private readonly ISettings _settings;
private readonly ITorrentData _torrentData;
@ -45,25 +50,22 @@ namespace RdtClient.Service.Services
_downloads = downloads;
}
private RdNetClient RdNetClient
private RdNetClient GetRdNetClient()
{
get
if (_rdtNetClient == null)
{
if (_rdtClient == null)
var apiKey = _settings.GetString("RealDebridApiKey")
.Result;
if (String.IsNullOrWhiteSpace(apiKey))
{
var apiKey = _settings.GetString("RealDebridApiKey")
.Result;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new Exception("RealDebrid API Key not set in the settings");
}
_rdtClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey);
throw new Exception("RealDebrid API Key not set in the settings");
}
return _rdtClient;
_rdtNetClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey);
}
return _rdtNetClient;
}
public async Task<IList<Torrent>> Get()
@ -74,16 +76,17 @@ namespace RdtClient.Service.Services
{
foreach (var download in torrent.Downloads)
{
if (TaskRunner.ActiveDownloads.TryGetValue(download.DownloadId, out var activeDownload))
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = activeDownload.Speed;
download.BytesSize = activeDownload.BytesSize;
download.BytesDownloaded = activeDownload.BytesDownloaded;
if (activeDownload.NewStatus.HasValue)
{
download.Status = activeDownload.NewStatus.Value;
}
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;
}
}
}
@ -91,13 +94,13 @@ namespace RdtClient.Service.Services
return torrents;
}
public async Task<Torrent> GetById(Guid id)
public async Task<Torrent> GetById(Guid torrentId)
{
var torrent = await _torrentData.GetById(id);
var torrent = await _torrentData.GetById(torrentId);
if (torrent != null)
{
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId);
await Update(torrent, rdTorrent);
}
@ -111,7 +114,7 @@ namespace RdtClient.Service.Services
if (torrent != null)
{
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId);
await Update(torrent, rdTorrent);
}
@ -119,20 +122,224 @@ namespace RdtClient.Service.Services
return torrent;
}
public async Task<IList<Torrent>> Update()
public async Task UpdateCategory(String hash, String category)
{
var torrent = await _torrentData.GetByHash(hash);
if (torrent == null)
{
return;
}
await _torrentData.UpdateCategory(torrent.TorrentId, category);
}
public async Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
var magnet = MagnetLink.Parse(magnetLink);
var rdTorrent = await GetRdNetClient().AddTorrentMagnetAsync(magnetLink);
await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), autoDownload, autoUnpack, autoDelete);
}
public async Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
var rdTorrent = await GetRdNetClient().AddTorrentFileAsync(bytes);
await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), autoDownload, autoUnpack, autoDelete);
}
public async Task SelectFiles(String torrentId, IList<String> fileIds)
{
await GetRdNetClient().SelectTorrentFilesAsync(torrentId, fileIds.ToArray());
}
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
{
var torrent = await GetById(torrentId);
if (torrent != null)
{
if (deleteData)
{
await _downloads.DeleteForTorrent(torrent.TorrentId);
await _torrentData.Delete(torrentId);
}
if (deleteRdTorrent)
{
await GetRdNetClient().DeleteTorrentAsync(torrent.RdId);
}
if (deleteLocalFiles)
{
// TODO
}
}
}
public async Task Unrestrict(Guid torrentId)
{
var torrent = await _torrentData.GetById(torrentId);
var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(torrent.RdId);
foreach (var link in rdTorrent.Links)
{
var unrestrictedLink = await GetRdNetClient().UnrestrictLinkAsync(link);
if (torrent.Downloads.Any(m => m.Link == unrestrictedLink.Download))
{
continue;
}
await _downloads.Add(torrent.TorrentId, unrestrictedLink.Download);
}
}
public async Task Download(Guid downloadId)
{
var settingDownloadLimit = await _settings.GetNumber("DownloadLimit");
var settingDownloadFolder = await _settings.GetString("DownloadFolder");
var download = await _downloads.GetById(downloadId);
// Check if we have reached the download limit, if so queue the download, but don't start it.
if (TorrentRunner.ActiveDownloadClients.Count >= settingDownloadLimit)
{
return;
}
download.DownloadStarted = DateTimeOffset.UtcNow;
await _downloads.UpdateDownloadStarted(download.DownloadId, download.DownloadStarted);
// Start the download process
var downloadClient = new DownloadClient(download, settingDownloadFolder);
if (TorrentRunner.ActiveDownloadClients.TryAdd(downloadId, downloadClient))
{
await downloadClient.Start();
}
}
public async Task Unpack(Guid downloadId)
{
//var settingUnpackLimit = await _settings.GetNumber("UnpackLimit");
var settingUnpackLimit = 1;
var settingDownloadFolder = await _settings.GetString("DownloadFolder");
var download = await _downloads.GetById(downloadId);
// Check if the file is even unpackable.
var uri = new Uri(download.Link);
var fileName = uri.Segments.Last();
var extension = Path.GetExtension(fileName);
if (extension != ".rar")
{
await _downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow);
await _downloads.UpdateUnpackingStarted(downloadId, DateTimeOffset.UtcNow);
await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow);
return;
}
// This property can have a value when it is a retry.
if (download.UnpackingQueued == null)
{
download.UnpackingQueued = DateTimeOffset.UtcNow;
await _downloads.UpdateUnpackingQueued(download.DownloadId, download.UnpackingQueued);
}
// Check if we have reached the download limit, if so queue the download, but don't start it.
if (TorrentRunner.ActiveUnpackClients.Count >= settingUnpackLimit)
{
return;
}
download.UnpackingStarted = DateTimeOffset.UtcNow;
await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted);
// Start the unpacking process
var unpackClient = new UnpackClient(download, settingDownloadFolder);
if (TorrentRunner.ActiveUnpackClients.TryAdd(downloadId, unpackClient))
{
await unpackClient.Start();
}
}
public void Reset()
{
_rdtNetClient = null;
}
public async Task<Profile> GetProfile()
{
if (_rdtNetClient == null)
{
return new Profile();
}
var user = await GetRdNetClient().GetUserAsync();
var profile = new Profile
{
UserName = user.Username,
Expiration = user.Expiration
};
return profile;
}
private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoUnpack, Boolean autoDelete)
{
var w = await SemaphoreSlim.WaitAsync(60000);
if (!w)
{
throw new Exception("Unable to add torrent, could not obtain lock");
}
try
{
var torrent = await _torrentData.GetByHash(infoHash);
if (torrent != null)
{
return;
}
var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoUnpack, autoDelete);
var rdTorrent = await GetRdNetClient().GetTorrentInfoAsync(rdTorrentId);
await Update(newTorrent, rdTorrent);
}
finally
{
SemaphoreSlim.Release();
}
}
public async Task Update()
{
var w = await SemaphoreSlim.WaitAsync(1);
if (!w)
{
return await _torrentData.Get();
return;
}
var torrents = await _torrentData.Get();
var torrents = await Get();
try
{
var rdTorrents = await RdNetClient.GetTorrentsAsync(0, 100);
var rdTorrents = await GetRdNetClient().GetTorrentsAsync(0, 100);
foreach (var rdTorrent in rdTorrents)
{
@ -140,13 +347,12 @@ namespace RdtClient.Service.Services
if (torrent == null)
{
var newTorrent = await _torrentData.Add(rdTorrent.Id, rdTorrent.Hash, false, false);
await GetById(newTorrent.TorrentId);
continue;
}
else if (rdTorrent.Files == null)
if (rdTorrent.Files == null)
{
var rdTorrent2 = await RdNetClient.GetTorrentInfoAsync(rdTorrent.Id);
await Update(torrent, rdTorrent2);
var rdTorrentInfo = await GetRdNetClient().GetTorrentInfoAsync(rdTorrent.Id);
await Update(torrent, rdTorrentInfo);
}
else
{
@ -164,8 +370,6 @@ namespace RdtClient.Service.Services
await _torrentData.Delete(torrent.TorrentId);
}
}
return torrents;
}
finally
{
@ -173,128 +377,9 @@ namespace RdtClient.Service.Services
}
}
public async Task UpdateStatus(Guid torrentId, TorrentStatus status)
public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime)
{
await _torrentData.UpdateStatus(torrentId, status);
}
public async Task UpdateCategory(String hash, String category)
{
var torrent = await _torrentData.GetByHash(hash);
if (torrent == null)
{
return;
}
await _torrentData.UpdateCategory(torrent.TorrentId, category);
}
public async Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete)
{
var magnet = MagnetLink.Parse(magnetLink);
var rdTorrent = await RdNetClient.AddTorrentMagnetAsync(magnetLink);
await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), autoDownload, autoDelete);
}
public async Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete)
{
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
var rdTorrent = await RdNetClient.AddTorrentFileAsync(bytes);
await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), autoDownload, autoDelete);
}
public async Task Delete(Guid id)
{
var torrent = await GetById(id);
if (torrent != null)
{
await _downloads.DeleteForTorrent(torrent.TorrentId);
await _torrentData.Delete(id);
await RdNetClient.DeleteTorrentAsync(torrent.RdId);
}
}
public async Task Download(Guid id)
{
var torrent = await _torrentData.GetById(id);
await _downloads.DeleteForTorrent(id);
await _torrentData.UpdateStatus(id, TorrentStatus.DownloadQueued);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
var existingDownloads = await _downloads.GetForTorrent(id);
foreach (var link in rdTorrent.Links)
{
var unrestrictedLink = await RdNetClient.UnrestrictLinkAsync(link);
if (existingDownloads.Any(m => m.Link == unrestrictedLink.Download))
{
continue;
}
await _downloads.Add(torrent.TorrentId, unrestrictedLink.Download);
}
}
public void Reset()
{
_rdtClient = null;
}
public async Task<Profile> GetProfile()
{
if (RdNetClient == null)
{
return new Profile();
}
var user = await RdNetClient.GetUserAsync();
var profile = new Profile
{
UserName = user.Username,
Expiration = user.Expiration
};
return profile;
}
private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoDelete)
{
var w = await SemaphoreSlim.WaitAsync(60000);
if (!w)
{
throw new Exception("Unable to add torrent, could not obtain lock");
}
try
{
var torrent = await _torrentData.GetByHash(infoHash);
if (torrent != null)
{
return;
}
var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoDelete);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(rdTorrentId);
await Update(newTorrent, rdTorrent);
}
finally
{
SemaphoreSlim.Release();
}
await _torrentData.UpdateComplete(torrentId, datetime);
}
private async Task Update(Torrent torrent, RDNET.Torrent rdTorrent)
@ -325,47 +410,29 @@ namespace RdtClient.Service.Services
torrent.RdHost = rdTorrent.Host;
torrent.RdSplit = rdTorrent.Split;
torrent.RdProgress = rdTorrent.Progress;
torrent.RdStatus = rdTorrent.Status;
torrent.RdAdded = rdTorrent.Added;
torrent.RdEnded = rdTorrent.Ended;
torrent.RdSpeed = rdTorrent.Speed;
torrent.RdSeeders = rdTorrent.Seeders;
torrent.RdStatusRaw = rdTorrent.Status;
torrent.RdStatus = rdTorrent.Status switch
{
"magnet_error" => RealDebridStatus.Error,
"magnet_conversion" => RealDebridStatus.Processing,
"waiting_files_selection" => RealDebridStatus.WaitingForFileSelection,
"queued" => RealDebridStatus.Downloading,
"downloading" => RealDebridStatus.Downloading,
"downloaded" => RealDebridStatus.Finished,
"error" => RealDebridStatus.Error,
"virus" => RealDebridStatus.Error,
"compressing" => RealDebridStatus.Downloading,
"uploading" => RealDebridStatus.Finished,
"dead" => RealDebridStatus.Error,
_ => RealDebridStatus.Error
};
await _torrentData.UpdateRdData(torrent);
if (torrent.Status == TorrentStatus.RealDebrid)
{
if (torrent.Status == TorrentStatus.RealDebrid && torrent.RdProgress == 100)
{
await _torrentData.UpdateStatus(torrent.TorrentId, TorrentStatus.WaitingForDownload);
}
else
{
// Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead
torrent.Status = rdTorrent.Status switch
{
"magnet_error" => TorrentStatus.Error,
"error" => TorrentStatus.Error,
"virus" => TorrentStatus.Error,
"dead" => TorrentStatus.Error,
_ => TorrentStatus.RealDebrid
};
await _torrentData.UpdateStatus(torrent.TorrentId, torrent.Status);
}
}
if (rdTorrent.Files != null && rdTorrent.Files.Count > 0)
{
if (!rdTorrent.Files.Any(m => m.Selected))
{
var fileIds = rdTorrent.Files
.Select(m => m.Id.ToString())
.ToArray();
await RdNetClient.SelectTorrentFilesAsync(rdTorrent.Id, fileIds);
}
}
}
}
}

View file

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using RdtClient.Data.Models.Data;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
namespace RdtClient.Service.Services
{
public class UnpackClient
{
public Boolean Finished { get; private set; }
public String Error { get; private set; }
public Int64 BytesTotal { get; private set; }
public Int64 BytesDone { get; private set; }
private readonly Download _download;
private readonly String _destinationPath;
private readonly Torrent _torrent;
private RarArchiveEntry _rarCurrentEntry;
private Dictionary<String, Int64> _rarfileStatus;
public UnpackClient(Download download, String destinationPath)
{
_download = download;
_destinationPath = destinationPath;
_torrent = download.Torrent;
}
public async Task Start()
{
BytesDone = 0;
BytesTotal = 0;
try
{
var fileUrl = _download.Link;
if (String.IsNullOrWhiteSpace(fileUrl))
{
throw new Exception("File URL is empty");
}
var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(_destinationPath, _torrent.RdName);
var fileName = uri.Segments.Last();
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 Unpack(filePath);
});
}
catch (Exception ex)
{
Error = $"An unexpected error occurred preparing download {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
Finished = true;
}
}
private async Task Unpack(String filePath)
{
try
{
await using (Stream stream = File.OpenRead(filePath))
{
using var archive = RarArchive.Open(stream);
BytesTotal = archive.TotalSize;
var entries = archive.Entries.Where(entry => !entry.IsDirectory)
.ToList();
_rarfileStatus = entries.ToDictionary(entry => entry.Key, entry => 0L);
_rarCurrentEntry = null;
archive.CompressedBytesRead += ArchiveOnCompressedBytesRead;
var extractPath = _destinationPath;
if (!entries.Any(m => m.Key.StartsWith(_torrent.RdName + @"\")) && !entries.Any(m => m.Key.StartsWith(_torrent.RdName + @"/")))
{
extractPath = Path.Combine(_destinationPath, _torrent.RdName);
}
if (entries.Any(m => m.Key.Contains(".r00")))
{
extractPath = Path.Combine(extractPath, "Temp");
}
foreach (var entry in entries)
{
_rarCurrentEntry = entry;
entry.WriteToDirectory(extractPath,
new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
}
}
var retryCount = 0;
while (File.Exists(filePath) && retryCount < 10)
{
retryCount++;
try
{
File.Delete(filePath);
}
catch
{
await Task.Delay(1000);
}
}
}
catch (Exception ex)
{
Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
}
finally
{
Finished = true;
}
}
private void ArchiveOnCompressedBytesRead(Object sender, CompressedBytesReadEventArgs e)
{
if (_rarCurrentEntry == null)
{
return;
}
_rarfileStatus[_rarCurrentEntry.Key] = e.CompressedBytesRead;
BytesDone = _rarfileStatus.Sum(m => m.Value);
}
}
}

View file

@ -245,7 +245,7 @@ namespace RdtClient.Web.Controllers
foreach (var url in urls)
{
await _qBittorrent.TorrentsAdd(url.Trim(), true, false);
await _qBittorrent.TorrentsAdd(url.Trim(), true, true, false);
}
return Ok();
@ -265,7 +265,7 @@ namespace RdtClient.Web.Controllers
await file.CopyToAsync(target);
var fileBytes = target.ToArray();
await _qBittorrent.TorrentsAddFile(fileBytes, true, false);
await _qBittorrent.TorrentsAddFile(fileBytes, true, true, false);
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -26,8 +27,15 @@ namespace RdtClient.Web.Controllers
[Route("")]
public async Task<ActionResult<IList<Torrent>>> Get()
{
var result = await _torrents.Get();
return Ok(result);
var results = await _torrents.Get();
// Prevent infinite recursion when serializing
foreach (var file in results.SelectMany(torrent => torrent.Downloads))
{
file.Torrent = null;
}
return Ok(results);
}
[HttpGet]
@ -63,7 +71,7 @@ namespace RdtClient.Web.Controllers
var bytes = memoryStream.ToArray();
await _torrents.UploadFile(bytes, formData.AutoDownload, formData.AutoDelete);
await _torrents.UploadFile(bytes, formData.AutoDownload, formData.AutoUnpack, formData.AutoDelete);
return Ok();
}
@ -72,16 +80,16 @@ namespace RdtClient.Web.Controllers
[Route("UploadMagnet")]
public async Task<ActionResult> UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest request)
{
await _torrents.UploadMagnet(request.MagnetLink, request.AutoDownload, request.AutoDelete);
await _torrents.UploadMagnet(request.MagnetLink, request.AutoDownload, request.AutoUnpack, request.AutoDelete);
return Ok();
}
[HttpDelete]
[Route("{id}")]
public async Task<ActionResult> Delete(Guid id)
[HttpPost]
[Route("Delete/{id}")]
public async Task<ActionResult> Delete(Guid id, [FromBody] TorrentControllerDeleteRequest request)
{
await _torrents.Delete(id);
await _torrents.Delete(id, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles);
return Ok();
}
@ -90,7 +98,7 @@ namespace RdtClient.Web.Controllers
[Route("Download/{id}")]
public async Task<ActionResult> Download(Guid id)
{
await _torrents.Download(id);
await _torrents.Unrestrict(id);
return Ok();
}
@ -99,6 +107,7 @@ namespace RdtClient.Web.Controllers
public class TorrentControllerUploadFileRequest
{
public Boolean AutoDownload { get; set; }
public Boolean AutoUnpack { get; set; }
public Boolean AutoDelete { get; set; }
}
@ -106,6 +115,14 @@ namespace RdtClient.Web.Controllers
{
public String MagnetLink { get; set; }
public Boolean AutoDownload { get; set; }
public Boolean AutoUnpack { get; set; }
public Boolean AutoDelete { get; set; }
}
public class TorrentControllerDeleteRequest
{
public Boolean DeleteData { get; set; }
public Boolean DeleteRdTorrent { get; set; }
public Boolean DeleteLocalFiles { get; set; }
}
}

View file

@ -1,4 +1,3 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;