Switched out RDNET Dll with Nuget package, update Angular. Rewrote Download handler and removed references to Hangfire.

This commit is contained in:
Roger Far 2020-04-10 16:48:33 -06:00
parent 76b8513d6b
commit bfba5c99fb
39 changed files with 1183 additions and 856 deletions

10
.gitignore vendored
View file

@ -33,11 +33,7 @@ client/e2e/*.map
# .NET Core build folders
server/**/bin
server/**/obj
server/*.user
# Common node modules locations
server/wwwroot/
server/.vs
server/**/*.user
server/**/.vs
server/RdtClient.Web/wwwroot/
server/RdtClient.Web/Properties/PublishProfiles/FolderProfile.pubxml.user
*.user

1183
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,33 +11,34 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.0",
"@angular/common": "~9.1.0",
"@angular/compiler": "~9.1.0",
"@angular/core": "~9.1.0",
"@angular/forms": "~9.1.0",
"@angular/platform-browser": "~9.1.0",
"@angular/platform-browser-dynamic": "~9.1.0",
"@angular/router": "~9.1.0",
"@angular/animations": "~9.1.1",
"@angular/common": "~9.1.1",
"@angular/compiler": "~9.1.1",
"@angular/core": "~9.1.1",
"@angular/forms": "~9.1.1",
"@angular/platform-browser": "~9.1.1",
"@angular/platform-browser-dynamic": "~9.1.1",
"@angular/router": "~9.1.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"bulma": "^0.8.1",
"curray": "^1.0.7",
"ngx-filesize": "^2.0.13",
"rxjs": "~6.5.5",
"tslib": "^1.11.1",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.0",
"@angular/cli": "~9.1.0",
"@angular/compiler-cli": "~9.1.0",
"@angular/language-service": "~9.1.0",
"@types/node": "^13.11.0",
"@angular-devkit/build-angular": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.1",
"@angular/language-service": "~9.1.1",
"@types/node": "^13.11.1",
"@types/jasmine": "~3.5.10",
"@types/jasminewd2": "~2.0.8",
"codelyzer": "^5.2.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.1",
"karma": "~4.4.1",
"karma": "~5.0.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~3.1.1",

View file

@ -17,6 +17,9 @@ import { TorrentStatusPipe } from './torrent-status.pipe';
import { FileStatusPipe } from './file-status.pipe';
import { LoginComponent } from './login/login.component';
import { AuthInterceptor } from './auth.interceptor';
import { curray } from 'curray';
curray();
@NgModule({
declarations: [

View file

@ -15,7 +15,11 @@ export class FileStatusPipe implements PipeTransform {
}
if (value.download.status === DownloadStatus.Downloading) {
return `${value.download.progress}%`;
const progress = (
(value.download.bytesDownloaded / value.download.bytesSize) *
100
).toFixed(2);
return `${progress || 0}%`;
}
if (value.download.status === DownloadStatus.Finished) {

View file

@ -9,13 +9,16 @@ export class Download {
public status: DownloadStatus;
public progress: number;
public bytesDownloaded: number;
public bytesSize: number;
public speed: number;
}
export enum DownloadStatus {
PendingDownload = 0,
Downloading,
Finished,
Downloading = 1,
Unpacking = 2,
Finished = 3,
}

View file

@ -19,7 +19,6 @@ export class Torrent {
files: TorrentFile[];
downloads: Download[];
activeDownload: Download;
}
export class TorrentFile {
@ -33,9 +32,10 @@ export class TorrentFile {
export enum TorrentStatus {
RealDebrid = 0,
WaitingForDownload,
Downloading,
Finished,
WaitingForDownload = 1,
DownloadQueued = 2,
Downloading = 3,
Finished = 4,
Error = 99,
}

View file

@ -24,4 +24,7 @@
>
<i class="fas fa-times"></i>
</span>
<span class="icon loading-icon" *ngIf="loading">
<i class="fas fa-spinner fa-pulse"></i>
</span>
</td>

View file

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Torrent, TorrentStatus } from './models/torrent.model';
import { FileSizePipe } from 'ngx-filesize';
import { DownloadStatus } from './models/download.model';
@Pipe({
name: 'status',
@ -9,23 +10,59 @@ export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {}
transform(torrent: Torrent): string {
switch (torrent.status) {
case TorrentStatus.RealDebrid: {
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
return `Downloading from RD (${torrent.rdProgress}% - ${speed}/s)`;
if (torrent.downloads && torrent.downloads.length > 0) {
const allFinished = torrent.downloads.all(
(m) => m.status === DownloadStatus.Finished
);
if (allFinished) {
return 'Finished';
}
const downloading = torrent.downloads.where(
(m) => m.status === DownloadStatus.Downloading
);
const unpacking = torrent.downloads.where(
(m) => m.status === DownloadStatus.Unpacking
);
if (downloading.length > 0) {
const allBytesDownloaded = torrent.downloads.sum(
(m) => m.bytesDownloaded
);
const allBytesSize = torrent.downloads.sum((m) => m.bytesSize);
if (allBytesSize > 0) {
const progress = ((allBytesDownloaded / allBytesSize) * 100).toFixed(
2
);
const allSpeeds =
downloading.sum((m) => m.speed) / downloading.length;
const speed = this.pipe.transform(allSpeeds, 'filesize');
return `Downloading (${progress || 0}% - ${speed}/s)`;
}
return `Preparing download`;
}
if (unpacking.length > 0) {
return `Unpacking`;
}
return 'Pending download';
}
switch (torrent.status) {
case TorrentStatus.RealDebrid:
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`;
case TorrentStatus.WaitingForDownload:
return `Waiting to download`;
case TorrentStatus.Downloading: {
if (torrent.activeDownload != null) {
const speed = this.pipe.transform(
torrent.activeDownload.speed,
'filesize'
);
return `Downloading (${torrent.activeDownload.progress}% - ${speed}/s)`;
}
case TorrentStatus.DownloadQueued:
return `Download queued`;
case TorrentStatus.Downloading:
return `Downloading`;
}
case TorrentStatus.Finished:
return `Finished`;
case TorrentStatus.Error:

View file

@ -1,3 +1,7 @@
<div class="notification is-danger is-light" *ngIf="error && error.length > 0">
An error has occured: {{ error }}<br />
Please refresh the screen after fixing this error.
</div>
<div class="table-container">
<table class="table is-fullwidth is-hoverable">
<colgroup>

View file

@ -16,7 +16,7 @@ import { DownloadStatus } from '../models/download.model';
})
export class TorrentTableComponent implements OnInit, OnDestroy {
public torrents: Torrent[] = [];
public error: string;
public showFiles: { [key: string]: boolean } = {};
private timer: any;
@ -25,18 +25,15 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.timer = setInterval(() => {
this.torrentService.getList().subscribe((result) => {
this.torrents = result;
this.torrents.forEach((torrent) => {
const activeDownloads = torrent.downloads.filter(
(m) => m.status === DownloadStatus.Downloading
);
if (activeDownloads.length > 0) {
torrent.activeDownload = activeDownloads[0];
}
});
});
this.torrentService.getList().subscribe(
(result) => {
this.torrents = result;
},
(err) => {
this.error = err.error;
clearInterval(this.timer);
}
);
}, 1000);
}

View file

@ -2,10 +2,7 @@
"extends": "tslint:recommended",
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
"options": ["parameters", "statements"]
},
"array-type": false,
"arrow-return-shorthand": true,
@ -16,34 +13,16 @@
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"directive-selector": [true, "attribute", "app", "camelCase"],
"component-selector": [true, "element", "app", "kebab-case"],
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-blacklist": [true, "rxjs/Rx"],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
"options": ["spaces"]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"max-line-length": [true, 140],
"member-ordering": [
true,
{
@ -55,35 +34,17 @@
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-inferrable-types": [true, "ignore-params"],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"object-literal-key-quotes": [true, "as-needed"],
"quotemark": [true, "single"],
"semicolon": {
"options": [
"always"
]
"options": ["always"]
},
"space-before-function-paren": {
"options": {
@ -113,11 +74,7 @@
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
"options": ["ban-keywords", "check-format", "allow-pascal-case"]
},
"whitespace": {
"options": [
@ -142,7 +99,5 @@
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}
"rulesDirectory": ["codelyzer"]
}

View file

@ -11,6 +11,7 @@ namespace RdtClient.Data.Data
public interface IDownloadData
{
Task<IList<Download>> Get();
Task<IList<Download>> GetForTorrent(Guid torrentId);
Task<Download> Add(Guid torrentId, String link);
Task UpdateStatus(Guid downloadId, DownloadStatus status);
Task DeleteForTorrent(Guid torrentId);
@ -33,6 +34,14 @@ namespace RdtClient.Data.Data
.ToListAsync();
}
public async Task<IList<Download>> GetForTorrent(Guid torrentId)
{
return await _dataContext.Downloads
.AsNoTracking()
.Where(m => m.TorrentId == torrentId)
.ToListAsync();
}
public async Task<Download> Add(Guid torrentId, String link)
{
var download = new Download

View file

@ -101,8 +101,6 @@ namespace RdtClient.Data.Data
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
dbTorrent.Status = torrent.Status;
dbTorrent.RdName = torrent.RdName;
dbTorrent.RdSize = torrent.RdSize;
dbTorrent.RdHost = torrent.RdHost;
@ -114,6 +112,11 @@ namespace RdtClient.Data.Data
dbTorrent.RdSpeed = torrent.RdSpeed;
dbTorrent.RdSeeders = torrent.RdSeeders;
if (torrent.Files != null)
{
dbTorrent.RdFiles = torrent.RdFiles;
}
await _dataContext.SaveChangesAsync();
}

View file

@ -3,9 +3,10 @@
public enum TorrentStatus
{
RealDebrid = 0,
WaitingForDownload,
Downloading,
Finished,
WaitingForDownload = 1,
DownloadQueued = 2,
Downloading = 3,
Finished = 4,
Error = 99
}

View file

@ -9,7 +9,7 @@ using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20200407174750_Initial")]
[Migration("20200408224831_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -277,6 +277,12 @@ namespace RdtClient.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDownload")
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasColumnType("TEXT");

View file

@ -66,6 +66,8 @@ namespace RdtClient.Data.Migrations
TorrentId = table.Column<Guid>(nullable: false),
Hash = table.Column<string>(nullable: true),
Category = table.Column<string>(nullable: true),
AutoDownload = table.Column<bool>(nullable: false),
AutoDelete = table.Column<bool>(nullable: false),
Status = table.Column<int>(nullable: false),
RdId = table.Column<string>(nullable: true),
RdName = table.Column<string>(nullable: true),

View file

@ -275,6 +275,12 @@ namespace RdtClient.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDownload")
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasColumnType("TEXT");

View file

@ -22,15 +22,12 @@ namespace RdtClient.Data.Models.Data
public Torrent Torrent { get; set; }
[NotMapped]
public Int32 Progress { get; set; }
public Int64 BytesSize { get; set; }
[NotMapped]
public Int64 BytesDownloaded { get; set; }
[NotMapped]
public Int64 Speed { get; set; }
[NotMapped]
public DateTime NextUpdate { get; set; }
[NotMapped]
public Int64 BytesLastUpdate { get; set; }
}
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
using RDNET.Models;
using RDNET;
using RdtClient.Data.Enums;
namespace RdtClient.Data.Models.Data
@ -17,6 +17,10 @@ namespace RdtClient.Data.Models.Data
public String Category { get; set; }
public Boolean AutoDownload { get; set; }
public Boolean AutoDelete { get; set; }
public TorrentStatus Status { get; set; }
[InverseProperty("Torrent")]

View file

@ -4,31 +4,6 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Migrations\20200402172935_AspNetIdentity.cs" />
<Compile Remove="Migrations\20200402172935_AspNetIdentity.Designer.cs" />
<Compile Remove="Migrations\20200402173119_Torrents.cs" />
<Compile Remove="Migrations\20200402173119_Torrents.Designer.cs" />
<Compile Remove="Migrations\20200402190746_Torrents.cs" />
<Compile Remove="Migrations\20200402190746_Torrents.Designer.cs" />
<Compile Remove="Migrations\20200402194059_Torrents.cs" />
<Compile Remove="Migrations\20200402194059_Torrents.Designer.cs" />
<Compile Remove="Migrations\20200402212417_Torrents.cs" />
<Compile Remove="Migrations\20200402212417_Torrents.Designer.cs" />
<Compile Remove="Migrations\20200403143200_Torrent.cs" />
<Compile Remove="Migrations\20200403143200_Torrent.Designer.cs" />
<Compile Remove="Migrations\20200403143240_Settings.cs" />
<Compile Remove="Migrations\20200403143240_Settings.Designer.cs" />
<Compile Remove="Migrations\20200403145651_Settings.cs" />
<Compile Remove="Migrations\20200403145651_Settings.Designer.cs" />
<Compile Remove="Migrations\20200403164143_Downloads.cs" />
<Compile Remove="Migrations\20200403164143_Downloads.Designer.cs" />
<Compile Remove="Migrations\20200403174830_Initia.cs" />
<Compile Remove="Migrations\20200403174830_Initia.Designer.cs" />
<Compile Remove="Migrations\20200403174846_Initial.cs" />
<Compile Remove="Migrations\20200403174846_Initial.Designer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="3.1.3" />
@ -39,6 +14,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RD.NET" Version="1.0.0" />
</ItemGroup>
<ItemGroup>

View file

@ -10,9 +10,10 @@ namespace RdtClient.Service
services.AddScoped<IAuthentication, Authentication>();
services.AddScoped<IDownloads, Downloads>();
services.AddScoped<IQBittorrent, QBittorrent>();
services.AddScoped<IScheduler, Scheduler>();
services.AddScoped<ISettings, Settings>();
services.AddScoped<ITorrents, Torrents>();
services.AddHostedService<TaskRunner>();
}
}
}

View file

@ -6,9 +6,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire.Core" Version="1.7.10" />
<PackageReference Include="MonoTorrent" Version="1.0.19" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RD.NET" Version="1.0.0" />
<PackageReference Include="SharpCompress" Version="0.25.0" />
</ItemGroup>
@ -16,10 +16,4 @@
<ProjectReference Include="..\RdtClient.Data\RdtClient.Data.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="RDNET">
<HintPath>..\libs\RDNET.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using SharpCompress.Common;
@ -14,32 +10,37 @@ using SharpCompress.Readers;
namespace RdtClient.Service.Services
{
public static class DownloadManager
public class DownloadManager
{
public static readonly Dictionary<Guid, Download> ActiveDownloads = new Dictionary<Guid, Download>();
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; }
static DownloadManager()
private DateTime _nextUpdate;
private Int64 _bytesLastUpdate;
public DownloadManager()
{
ServicePointManager.Expect100Continue = false;
ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.MaxServicePointIdleTime = 1000;
}
public static async Task Download(Download download, String destinationFolderPath)
private DownloadManager ActiveDownload => TaskRunner.ActiveDownloads[Download.DownloadId];
public async Task Start(String destinationFolderPath)
{
await UpdateStatus(download.DownloadId, DownloadStatus.Downloading, TorrentStatus.Downloading);
ActiveDownload.NewStatus = DownloadStatus.Downloading;
ActiveDownload.BytesDownloaded = 0;
ActiveDownload.BytesSize = 0;
ActiveDownload.Speed = 0;
_bytesLastUpdate = 0;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
if (!ActiveDownloads.TryAdd(download.DownloadId, download))
{
return;
}
download.Progress = 0;
download.BytesLastUpdate = 0;
download.NextUpdate = DateTime.UtcNow.AddSeconds(1);
download.Speed = 0;
var fileUrl = download.Link;
var fileUrl = Download.Link;
var uri = new Uri(fileUrl);
var filePath = Path.Combine(destinationFolderPath, uri.Segments.Last());
@ -77,13 +78,15 @@ namespace RdtClient.Service.Services
{
fileStream.Write(buffer, 0, read);
ActiveDownloads[download.DownloadId].Progress = (Int32) (fileStream.Length * 100 / responseLength);
ActiveDownload.BytesDownloaded = fileStream.Length;
ActiveDownload.BytesSize = responseLength;
if (DateTime.UtcNow > ActiveDownloads[download.DownloadId].NextUpdate)
if (DateTime.UtcNow > _nextUpdate)
{
ActiveDownloads[download.DownloadId].Speed = fileStream.Length - ActiveDownloads[download.DownloadId].BytesLastUpdate;
ActiveDownloads[download.DownloadId].NextUpdate = DateTime.UtcNow.AddSeconds(1);
ActiveDownloads[download.DownloadId].BytesLastUpdate = fileStream.Length;
ActiveDownload.Speed = fileStream.Length - _bytesLastUpdate;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
_bytesLastUpdate = fileStream.Length;
}
}
else
@ -93,13 +96,14 @@ namespace RdtClient.Service.Services
}
}
ActiveDownloads[download.DownloadId].Speed = 0;
ActiveDownload.Speed = 0;
ActiveDownload.BytesDownloaded = ActiveDownload.BytesSize;
try
{
if (filePath.EndsWith(".rar"))
{
await UpdateStatus(download.DownloadId, DownloadStatus.Unpacking, TorrentStatus.Downloading);
ActiveDownload.NewStatus = DownloadStatus.Unpacking;
await using (Stream stream = File.OpenRead(filePath))
{
@ -141,43 +145,7 @@ namespace RdtClient.Service.Services
// ignored
}
await UpdateStatus(download.DownloadId, DownloadStatus.Finished, TorrentStatus.Finished);
ActiveDownloads.Remove(download.DownloadId);
}
private static async Task UpdateStatus(Guid downloadId, DownloadStatus downloadStatus, TorrentStatus torrentStatus)
{
await using var context = new DataContext();
var download = await context.Downloads.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
download.Status = downloadStatus;
await context.SaveChangesAsync();
var torrent = await context.Torrents.FirstOrDefaultAsync(m => m.TorrentId == download.TorrentId);
if (torrentStatus == TorrentStatus.Finished)
{
var allDownloads = await context.Downloads.Where(m => m.TorrentId == download.TorrentId)
.ToListAsync();
if (allDownloads.All(m => m.Status == DownloadStatus.Finished))
{
torrent.Status = TorrentStatus.Finished;
}
else
{
torrent.Status = TorrentStatus.Downloading;
}
}
else
{
torrent.Status = torrentStatus;
}
await context.SaveChangesAsync();
ActiveDownload.NewStatus = DownloadStatus.Finished;
}
}
}

View file

@ -10,6 +10,7 @@ namespace RdtClient.Service.Services
public interface IDownloads
{
Task<IList<Download>> Get();
Task<IList<Download>> GetForTorrent(Guid torrentId);
Task<Download> Add(Guid torrentId, String link);
Task UpdateStatus(Guid downloadId, DownloadStatus status);
Task DeleteForTorrent(Guid torrentId);
@ -18,12 +19,10 @@ namespace RdtClient.Service.Services
public class Downloads : IDownloads
{
private readonly IDownloadData _downloadData;
private readonly ISettings _settings;
public Downloads(IDownloadData downloadData, ISettings settings)
public Downloads(IDownloadData downloadData)
{
_downloadData = downloadData;
_settings = settings;
}
public async Task<IList<Download>> Get()
@ -31,6 +30,11 @@ namespace RdtClient.Service.Services
return await _downloadData.Get();
}
public async Task<IList<Download>> GetForTorrent(Guid torrentId)
{
return await _downloadData.GetForTorrent(torrentId);
}
public async Task<Download> Add(Guid torrentId, String link)
{
return await _downloadData.Add(torrentId, link);

View file

@ -18,7 +18,7 @@ namespace RdtClient.Service.Services
Task<IList<TorrentInfo>> TorrentInfo();
Task<TorrentProperties> TorrentProperties(String hash);
Task TorrentsDelete(String hash, Boolean deleteFiles);
Task TorrentsAdd(String url);
Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete);
Task TorrentsSetCategory(String hash, String category);
Task<IDictionary<String, TorrentCategory>> TorrentsCategories();
}
@ -363,9 +363,9 @@ namespace RdtClient.Service.Services
}
}
public async Task TorrentsAdd(String url)
public async Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete)
{
await _torrents.UploadMagnet(url);
await _torrents.UploadMagnet(magnetLink, autoDownload, autoDelete);
}
public async Task TorrentsSetCategory(String hash, String category)

View file

@ -1,62 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using RdtClient.Data.Enums;
namespace RdtClient.Service.Services
{
public interface IScheduler
{
void Start();
Task Process();
}
public class Scheduler : IScheduler
{
private readonly IDownloads _downloads;
private readonly ISettings _settings;
private readonly ITorrents _torrents;
public Scheduler(ITorrents torrents, IDownloads downloads, ISettings settings)
{
_torrents = torrents;
_downloads = downloads;
_settings = settings;
}
public void Start()
{
RecurringJob.AddOrUpdate(() => Process(), "* * * * *");
BackgroundJob.Enqueue(() => Process());
}
[DisableConcurrentExecution(5)]
public async Task Process()
{
await _torrents.Update();
var downloads = await _downloads.Get();
downloads = downloads.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 downloads)
{
if (DownloadManager.ActiveDownloads.Count >= maxDownloads)
{
return;
}
download.Torrent = null;
BackgroundJob.Enqueue(() => DownloadManager.Download(download, destinationFolderPath));
await Task.Delay(1000);
}
}
}
}

View file

@ -0,0 +1,180 @@
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 : IHostedService, IDisposable
{
public static readonly ConcurrentDictionary<Guid, DownloadManager> ActiveDownloads = new ConcurrentDictionary<Guid, DownloadManager>();
private readonly ILogger<TaskRunner> _logger;
private readonly IServiceProvider _services;
private Timer _timer;
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
public TaskRunner(ILogger<TaskRunner> logger, IServiceProvider services)
{
_logger = logger;
_services = services;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service running.");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
private async void DoWork(Object state)
{
// Make sure only 1 process enters the lock
var obtainLock = await _semaphoreSlim.WaitAsync(100);
if (!obtainLock)
{
return;
}
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 ProcessAutoDownloads(downloads, settings, torrents);
await ProcessDownloads(downloads, settings, torrents);
await ProcessStatus(downloads, settings, torrents);
}
}
finally
{
_semaphoreSlim.Release(1);
}
}
private async Task ProcessAutoDownloads(IDownloads downloads, ISettings settings, ITorrents torrents)
{
await torrents.Update();
var allTorrents = await torrents.Get();
allTorrents = allTorrents.Where(m => m.Status == TorrentStatus.WaitingForDownload && m.AutoDownload && m.Downloads.Count == 0)
.ToList();
foreach (var torrent in allTorrents)
{
await torrents.Download(torrent.TorrentId);
}
}
private async Task ProcessDownloads(IDownloads downloads, ISettings settings, ITorrents torrents)
{
await torrents.Update();
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 Task.Factory.StartNew(async delegate
{
var downloadManager = new DownloadManager();
if (ActiveDownloads.TryAdd(download.DownloadId, downloadManager))
{
downloadManager.Download = download;
await downloadManager.Start(destinationFolderPath);
await torrents.UpdateStatus(download.TorrentId, TorrentStatus.Downloading);
}
});
}
}
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

@ -19,9 +19,10 @@ namespace RdtClient.Service.Services
Task<Torrent> GetById(Guid id);
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);
Task UploadFile(Byte[] bytes);
Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete);
Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete);
Task Delete(Guid id);
Task Download(Guid id);
void Reset();
@ -60,7 +61,7 @@ namespace RdtClient.Service.Services
throw new Exception("RealDebrid API Key not set in the settings");
}
_rdtClient = new RdNetClient("X245A4XAIBGVM", null, null, null, null, apiKey);
_rdtClient = new RdNetClient("X245A4XAIBGVM", null, null, null, apiKey);
}
return _rdtClient;
@ -82,10 +83,11 @@ namespace RdtClient.Service.Services
{
foreach (var download in torrent.Downloads)
{
if (DownloadManager.ActiveDownloads.TryGetValue(download.DownloadId, out var activeDownload))
if (TaskRunner.ActiveDownloads.TryGetValue(download.DownloadId, out var activeDownload))
{
download.Speed = activeDownload.Speed;
download.Progress = activeDownload.Progress;
download.BytesSize = activeDownload.BytesSize;
download.BytesDownloaded = activeDownload.BytesDownloaded;
}
}
}
@ -99,7 +101,7 @@ namespace RdtClient.Service.Services
if (torrent != null)
{
var rdTorrent = await RdNetClient.TorrentInfoAsync(torrent.RdId);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
await Update(torrent, rdTorrent);
}
@ -113,7 +115,7 @@ namespace RdtClient.Service.Services
if (torrent != null)
{
var rdTorrent = await RdNetClient.TorrentInfoAsync(torrent.RdId);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
await Update(torrent, rdTorrent);
}
@ -133,7 +135,7 @@ namespace RdtClient.Service.Services
try
{
var rdTorrents = await RdNetClient.TorrentsAsync(0, 100);
var rdTorrents = await RdNetClient.GetTorrentsAsync(0, 100);
foreach (var rdTorrent in rdTorrents)
{
@ -169,6 +171,11 @@ namespace RdtClient.Service.Services
}
}
public async Task UpdateStatus(Guid torrentId, TorrentStatus status)
{
await _torrentData.UpdateStatus(torrentId, status);
}
public async Task UpdateCategory(String hash, String category)
{
var torrent = await _torrentData.GetByHash(hash);
@ -181,22 +188,22 @@ namespace RdtClient.Service.Services
await _torrentData.UpdateCategory(torrent.TorrentId, category);
}
public async Task UploadMagnet(String magnetLink)
public async Task UploadMagnet(String magnetLink, Boolean autoDownload, Boolean autoDelete)
{
var magnet = MagnetLink.Parse(magnetLink);
var rdTorrent = await RdNetClient.TorrentAddMagnet(magnetLink);
var rdTorrent = await RdNetClient.AddTorrentMagnetAsync(magnetLink);
await Add(rdTorrent.Id, magnet.InfoHash.ToHex());
await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), autoDownload, autoDelete);
}
public async Task UploadFile(Byte[] bytes)
public async Task UploadFile(Byte[] bytes, Boolean autoDownload, Boolean autoDelete)
{
var torrent = MonoTorrent.Torrent.Load(bytes);
var rdTorrent = await RdNetClient.TorrentAddFile(bytes);
var rdTorrent = await RdNetClient.AddTorrentFileAsync(bytes);
await Add(rdTorrent.Id, torrent.InfoHash.ToHex());
await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), autoDownload, autoDelete);
}
public async Task Delete(Guid id)
@ -207,7 +214,7 @@ namespace RdtClient.Service.Services
{
await _downloads.DeleteForTorrent(torrent.TorrentId);
await _torrentData.Delete(id);
await RdNetClient.TorrentDelete(torrent.RdId);
await RdNetClient.DeleteTorrentAsync(torrent.RdId);
}
}
@ -217,7 +224,9 @@ namespace RdtClient.Service.Services
await _downloads.DeleteForTorrent(id);
var rdTorrent = await RdNetClient.TorrentInfoAsync(torrent.RdId);
await _torrentData.UpdateStatus(id, TorrentStatus.DownloadQueued);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(torrent.RdId);
foreach (var link in rdTorrent.Links)
{
@ -235,7 +244,12 @@ namespace RdtClient.Service.Services
public async Task<Profile> GetProfile()
{
var user = await _rdtClient.UserAsync();
if (_rdtClient == null)
{
return new Profile();
}
var user = await _rdtClient.GetUserAsync();
var profile = new Profile
{
@ -246,11 +260,11 @@ namespace RdtClient.Service.Services
return profile;
}
private async Task Add(String rdTorrentId, String infoHash)
private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoDelete)
{
var newTorrent = await _torrentData.Add(rdTorrentId, infoHash);
var rdTorrent = await RdNetClient.TorrentInfoAsync(rdTorrentId);
var rdTorrent = await RdNetClient.GetTorrentInfoAsync(rdTorrentId);
if (rdTorrent.Files != null && rdTorrent.Files.Count > 0)
{
@ -259,14 +273,14 @@ namespace RdtClient.Service.Services
var fileIds = rdTorrent.Files.Select(m => m.Id.ToString())
.ToArray();
await RdNetClient.TorrentSelectFiles(rdTorrentId, fileIds);
await RdNetClient.SelectTorrentFilesAsync(rdTorrentId, fileIds);
}
}
await Update(newTorrent, rdTorrent);
}
private async Task Update(Torrent torrent, RDNET.Models.Torrent rdTorrent)
private async Task Update(Torrent torrent, RDNET.Torrent rdTorrent)
{
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{
@ -300,11 +314,13 @@ namespace RdtClient.Service.Services
torrent.RdSpeed = rdTorrent.Speed;
torrent.RdSeeders = rdTorrent.Seeders;
await _torrentData.UpdateRdData(torrent);
if (torrent.Status == TorrentStatus.RealDebrid)
{
if (torrent.Status == TorrentStatus.RealDebrid && torrent.RdProgress == 100)
{
torrent.Status = TorrentStatus.WaitingForDownload;
await _torrentData.UpdateStatus(torrent.TorrentId, TorrentStatus.WaitingForDownload);
}
else
{
@ -317,10 +333,10 @@ namespace RdtClient.Service.Services
"dead" => TorrentStatus.Error,
_ => TorrentStatus.RealDebrid
};
await _torrentData.UpdateStatus(torrent.TorrentId, torrent.Status);
}
}
await _torrentData.UpdateRdData(torrent);
}
}
}

View file

@ -272,7 +272,7 @@ namespace RdtClient.Web.Controllers
foreach (var url in urls)
{
await _qBittorrent.TorrentsAdd(url.Trim());
await _qBittorrent.TorrentsAdd(url.Trim(), true, true);
}
return Ok();

View file

@ -33,7 +33,7 @@ namespace RdtClient.Web.Controllers
}
catch (Exception ex)
{
return BadRequest(ex);
return BadRequest(ex.Message);
}
}
@ -50,7 +50,7 @@ namespace RdtClient.Web.Controllers
}
catch (Exception ex)
{
return BadRequest(ex);
return BadRequest(ex.Message);
}
}
@ -65,7 +65,7 @@ namespace RdtClient.Web.Controllers
}
catch (Exception ex)
{
return BadRequest(ex);
return BadRequest(ex.Message);
}
}
}

View file

@ -15,12 +15,10 @@ namespace RdtClient.Web.Controllers
public class TorrentsController : Controller
{
private readonly ITorrents _torrents;
private readonly IScheduler _scheduler;
public TorrentsController(ITorrents torrents, IScheduler scheduler)
public TorrentsController(ITorrents torrents)
{
_torrents = torrents;
_scheduler = scheduler;
}
[HttpGet]
@ -34,7 +32,7 @@ namespace RdtClient.Web.Controllers
}
catch (Exception ex)
{
return BadRequest(ex);
return BadRequest(ex.Message);
}
}
@ -55,7 +53,7 @@ namespace RdtClient.Web.Controllers
}
catch (Exception ex)
{
return BadRequest(ex);
return BadRequest(ex.Message);
}
}
@ -78,7 +76,7 @@ namespace RdtClient.Web.Controllers
var bytes = memoryStream.ToArray();
await _torrents.UploadFile(bytes);
await _torrents.UploadFile(bytes, false, false);
return Ok();
}
@ -94,7 +92,7 @@ namespace RdtClient.Web.Controllers
{
try
{
await _torrents.UploadMagnet(request.MagnetLink);
await _torrents.UploadMagnet(request.MagnetLink, false, false);
return Ok();
}
@ -127,7 +125,6 @@ namespace RdtClient.Web.Controllers
try
{
await _torrents.Download(id);
await _scheduler.Process();
return Ok();
}

View file

@ -4,7 +4,6 @@ using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using RdtClient.Data.Models.Internal;
namespace RdtClient.Web
{

View file

@ -5,9 +5,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.10" />
<PackageReference Include="Hangfire.Core" Version="1.7.10" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.3" />
@ -17,7 +14,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="3.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NReco.Logging.File" Version="1.0.5" />

View file

@ -1,6 +1,4 @@
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -14,7 +12,6 @@ using Microsoft.Extensions.Logging;
using RdtClient.Data;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
namespace RdtClient.Web
{
@ -83,19 +80,11 @@ namespace RdtClient.Web
options.Cookie.Name = "SID";
});
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage());
services.AddHangfireServer();
DiConfig.Config(services);
Service.DiConfig.Config(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, DataContext dataContext, IScheduler scheduler)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, DataContext dataContext)
{
if (env.IsDevelopment())
{
@ -107,9 +96,9 @@ namespace RdtClient.Web
{
await next.Invoke();
if (context.Response.StatusCode >= 404)
if (context.Response.StatusCode != 200)
{
logger.LogWarning($"404: {context.Request.Path.Value}");
logger.LogWarning($"{context.Response.StatusCode}: {context.Request.Path.Value}");
}
});
@ -118,9 +107,7 @@ namespace RdtClient.Web
app.UseAuthentication();
app.UseAuthorization();
app.UseHangfireServer();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
@ -134,8 +121,6 @@ namespace RdtClient.Web
});
dataContext.Migrate();
scheduler.Start();
}
}
}

View file

@ -9,5 +9,5 @@
<link rel="stylesheet" href="styles.8d1160aad8efbde1424d.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.c9afb3256f2870e161de.js" type="module"></script><script src="runtime-es5.c9afb3256f2870e161de.js" nomodule defer></script><script src="polyfills-es5.0e4e1968447fab48e788.js" nomodule defer></script><script src="polyfills-es2015.bbb42ff2e1c488ff52d5.js" type="module"></script><script src="main-es2015.949c41a1b6780ff9f3a9.js" type="module"></script><script src="main-es5.949c41a1b6780ff9f3a9.js" nomodule defer></script></body>
<script src="runtime-es2015.1eba213af0b233498d9d.js" type="module"></script><script src="runtime-es5.1eba213af0b233498d9d.js" nomodule defer></script><script src="polyfills-es5.9e286f6d9247438cbb02.js" nomodule defer></script><script src="polyfills-es2015.690002c25ea8557bb4b0.js" type="module"></script><script src="main-es2015.86b880448cfc913cfe80.js" type="module"></script><script src="main-es5.86b880448cfc913cfe80.js" nomodule defer></script></body>
</html>

View file

@ -1,63 +0,0 @@
{
"runtimeTarget": {
"name": ".NETStandard,Version=v2.0/",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETStandard,Version=v2.0": {},
".NETStandard,Version=v2.0/": {
"RDNET/1.0.0": {
"dependencies": {
"NETStandard.Library": "2.0.3",
"Newtonsoft.Json": "12.0.3"
},
"runtime": {
"RDNET.dll": {}
}
},
"Microsoft.NETCore.Platforms/1.1.0": {},
"NETStandard.Library/2.0.3": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
"Newtonsoft.Json/12.0.3": {
"runtime": {
"lib/netstandard2.0/Newtonsoft.Json.dll": {
"assemblyVersion": "12.0.0.0",
"fileVersion": "12.0.3.23909"
}
}
}
}
},
"libraries": {
"RDNET/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"path": "microsoft.netcore.platforms/1.1.0",
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
},
"NETStandard.Library/2.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"path": "netstandard.library/2.0.3",
"hashPath": "netstandard.library.2.0.3.nupkg.sha512"
},
"Newtonsoft.Json/12.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==",
"path": "newtonsoft.json/12.0.3",
"hashPath": "newtonsoft.json.12.0.3.nupkg.sha512"
}
}
}

Binary file not shown.

Binary file not shown.