Swapped out the timer GET for a signalr connection.

This commit is contained in:
Roger Far 2021-01-10 20:21:49 -07:00
parent 8f35952f90
commit 21ad2ebfd6
14 changed files with 218 additions and 69 deletions

View file

@ -2,5 +2,10 @@
"/Api": {
"target": "http://localhost:6500/",
"secure": false
},
"/hub": {
"target": "http://localhost:6500/",
"secure": false,
"ws": true
}
}

View file

@ -1618,6 +1618,18 @@
"schema-utils": "^2.7.0"
}
},
"@microsoft/signalr": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-5.0.1.tgz",
"integrity": "sha512-qk5xMEpEqvxmj2/7ZRkHpWyt33gGXiNTkM+iIFr0QYEaynsbwGUxyV5WhuxcOP9LyXgUx6IXBer2HVAGOC4zpw==",
"requires": {
"abort-controller": "^3.0.0",
"eventsource": "^1.0.7",
"fetch-cookie": "^0.7.3",
"node-fetch": "^2.6.0",
"ws": "^6.0.0"
}
},
"@ngtools/webpack": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-11.0.5.tgz",
@ -2002,6 +2014,14 @@
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==",
"dev": true
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "^5.0.0"
}
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -2333,8 +2353,7 @@
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"asynckit": {
"version": "0.4.0",
@ -4628,6 +4647,11 @@
"next-tick": "~1.0.0"
}
},
"es6-denodeify": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-denodeify/-/es6-denodeify-0.1.5.tgz",
"integrity": "sha1-MdTV/pxVA+ElRgQ5MQ4WoqPznB8="
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
@ -4733,6 +4757,11 @@
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"dev": true
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@ -4749,7 +4778,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
"integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
"dev": true,
"requires": {
"original": "^1.0.0"
}
@ -5072,6 +5100,15 @@
"websocket-driver": ">=0.5.1"
}
},
"fetch-cookie": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.7.3.tgz",
"integrity": "sha512-rZPkLnI8x5V+zYAiz8QonAHsTb4BY+iFowFBI1RFn0zrO343AVp9X7/yUj/9wL6Ef/8fLls8b/vGtzUvmyAUGA==",
"requires": {
"es6-denodeify": "^0.1.1",
"tough-cookie": "^2.3.3"
}
},
"figgy-pudding": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@ -7801,6 +7838,11 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-fetch-npm": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz",
@ -8311,7 +8353,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
"integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
"dev": true,
"requires": {
"url-parse": "^1.4.3"
}
@ -9883,8 +9924,7 @@
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
"dev": true
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"public-encrypt": {
"version": "4.0.3",
@ -9944,8 +9984,7 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"q": {
"version": "1.5.1",
@ -9980,8 +10019,7 @@
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"randombytes": {
"version": "2.1.0",
@ -10281,8 +10319,7 @@
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"resolve": {
"version": "1.15.1",
@ -12004,7 +12041,6 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
@ -12354,7 +12390,6 @@
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
@ -13607,7 +13642,6 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
}

View file

@ -21,6 +21,7 @@
"@angular/platform-browser-dynamic": "~11.0.5",
"@angular/router": "~11.0.5",
"@fortawesome/fontawesome-free": "^5.15.1",
"@microsoft/signalr": "^5.0.1",
"bulma": "^0.9.1",
"curray": "^1.0.8",
"ngx-filesize": "^2.0.16",

View file

@ -3,6 +3,8 @@
{{ file.path }}
</td>
<td>{{ file.bytes | filesize }}</td>
<td></td>
<td></td>
<td>
{{ file | fileStatus }}
</td>

View file

@ -9,9 +9,9 @@
{{ torrent.downloads.length | number }}
</td>
<td class="auto">
<i class="fas fa-download" *ngIf="!torrent.autoDownload" title="Auto download"></i>
<i class="fas fa-box-open" *ngIf="!torrent.autoUnpack" title="Auto unpack"></i>
<i class="fas fa-trash" *ngIf="!torrent.autoDelete" title="Auto delete"></i>
<i class="fas fa-download" *ngIf="torrent.autoDownload" title="Auto download"></i>
<i class="fas fa-box-open" *ngIf="torrent.autoUnpack" title="Auto unpack"></i>
<i class="fas fa-trash" *ngIf="torrent.autoDelete" title="Auto delete"></i>
</td>
<td>
{{ torrent | status }}

View file

@ -5,12 +5,12 @@
<div class="table-container">
<table class="table is-fullwidth is-hoverable">
<colgroup>
<col style="width: calc(65% - 550px)" />
<col style="width: calc(40% - 550px)" />
<col style="width: 15%" />
<col style="width: 150px" />
<col style="width: 150px" />
<col style="width: 150px" />
<col style="width: 20%" />
<col style="width: 40%" />
<col style="width: 100px" />
</colgroup>
<thead>
@ -32,12 +32,7 @@
(click)="selectTorrent(torrent)"
(delete)="showDeleteModal($event)"
></tr>
<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 *ngIf="showFiles[torrent.torrentId]">
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
</ng-container>
</ng-container>

View file

@ -20,26 +20,27 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
private timer: any;
constructor(private torrentService: TorrentService) {}
ngOnInit(): void {
this.timer = setInterval(() => {
this.torrentService.getList().subscribe(
(result) => {
this.torrents = result;
},
(err) => {
this.error = err.error;
clearInterval(this.timer);
}
);
}, 1000);
this.torrentService.getList().subscribe(
(result) => {
this.torrents = result;
this.torrentService.connect();
this.torrentService.update$.subscribe((result2) => {
this.torrents = result2;
});
},
(err) => {
this.error = err.error;
}
);
}
ngOnDestroy(): void {
clearInterval(this.timer);
this.torrentService.disconnect();
}
public selectTorrent(torrent: Torrent): void {

View file

@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import * as signalR from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs';
import { Torrent } from './models/torrent.model';
@Injectable({
@ -9,6 +10,23 @@ import { Torrent } from './models/torrent.model';
export class TorrentService {
constructor(private http: HttpClient) {}
public update$: Subject<Torrent[]> = new Subject();
private connection: signalR.HubConnection;
public connect(): void {
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
this.connection.start().catch((err) => console.error(err));
this.connection.on('update', (torrents: Torrent[]) => {
this.update$.next(torrents);
});
}
public disconnect(): void {
this.connection?.stop();
}
public getList(): Observable<Torrent[]> {
return this.http.get<Torrent[]>(`/Api/Torrents`);
}

View file

@ -10,6 +10,7 @@ namespace RdtClient.Service
services.AddScoped<IAuthentication, Authentication>();
services.AddScoped<IDownloads, Downloads>();
services.AddScoped<IQBittorrent, QBittorrent>();
services.AddScoped<IRemoteService, RemoteService>();
services.AddScoped<ISettings, Settings>();
services.AddScoped<ITorrents, Torrents>();
services.AddScoped<ITorrentRunner, TorrentRunner>();

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace RdtClient.Service.Services
{
public class RdtHub : Hub
{
private static readonly ConcurrentDictionary<String, String> Users = new ConcurrentDictionary<String, String>();
public static Boolean HasConnections => Users.Any();
public override async Task OnConnectedAsync()
{
Users.TryAdd(Context.ConnectionId, Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
Users.TryRemove(Context.ConnectionId, out _);
await base.OnDisconnectedAsync(exception);
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace RdtClient.Service.Services
{
public interface IRemoteService
{
Task Update();
}
public class RemoteService : IRemoteService
{
private readonly IHubContext<RdtHub> _hub;
private readonly ITorrents _torrents;
public RemoteService(IHubContext<RdtHub> hub, ITorrents torrents)
{
_hub = hub;
_torrents = torrents;
}
public async Task Update()
{
var torrents = await _torrents.Get();
// Prevent infinite recursion when serializing
foreach (var file in torrents.SelectMany(torrent => torrent.Downloads))
{
file.Torrent = null;
}
await _hub.Clients.All.SendCoreAsync("update",
new Object[]
{
torrents
});
}
}
}

View file

@ -18,23 +18,25 @@ namespace RdtClient.Service.Services
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new ConcurrentDictionary<Guid, DownloadClient>();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new ConcurrentDictionary<Guid, UnpackClient>();
private readonly IDownloads _downloads;
private readonly IRemoteService _remoteService;
private readonly ISettings _settings;
private readonly ITorrents _torrents;
private readonly IDownloads _downloads;
public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads)
public TorrentRunner(ISettings settings, ITorrents torrents, IDownloads downloads, IRemoteService remoteService)
{
_settings = settings;
_torrents = torrents;
_downloads = downloads;
_remoteService = remoteService;
}
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);
@ -43,7 +45,7 @@ namespace RdtClient.Service.Services
{
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);
@ -53,7 +55,7 @@ namespace RdtClient.Service.Services
await _downloads.UpdateUnpackingStarted(download.DownloadId, null);
}
}
public async Task Tick()
{
var settingApiKey = await _settings.GetString("RealDebridApiKey");
@ -62,7 +64,7 @@ namespace RdtClient.Service.Services
{
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();
@ -78,14 +80,14 @@ namespace RdtClient.Service.Services
{
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)
@ -98,24 +100,30 @@ namespace RdtClient.Service.Services
{
await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow);
}
ActiveUnpackClients.TryRemove(downloadId, out _);
}
}
// Only poll RealDebrid every 30 seconds
// Only poll RealDebrid every 5 when a hub is connected, otherwise ever 30 seconds
if (_nextUpdate < DateTime.UtcNow)
{
_nextUpdate = DateTime.UtcNow.AddSeconds(5);
var updateTime = 30;
if (RdtHub.HasConnections)
{
updateTime = 5;
}
_nextUpdate = DateTime.UtcNow.AddSeconds(updateTime);
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)
@ -125,7 +133,7 @@ namespace RdtClient.Service.Services
{
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)
@ -167,22 +175,23 @@ namespace RdtClient.Service.Services
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 &&
.Where(m => m.DownloadStarted == null &&
m.DownloadFinished == null)
.ToList();
if (downloadsPending.Count > 0)
{
foreach (var download in downloadsPending)
{
await _torrents.Download(download.DownloadId);
}
continue;
}
}
@ -191,22 +200,22 @@ namespace RdtClient.Service.Services
{
// If all files are finished downloading, move to the unpacking step.
var unpackingPending = torrent.Downloads
.Where(m => m.DownloadFinished != null &&
m.UnpackingStarted == null &&
.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)
{
@ -224,6 +233,8 @@ namespace RdtClient.Service.Services
}
}
}
await _remoteService.Update();
}
}
}

View file

@ -169,7 +169,10 @@ namespace RdtClient.Service.Services
var torrentPath = Path.Combine(settingDownloadFolder, torrent.RdName);
Directory.Delete(torrentPath, true);
if (Directory.Exists(torrentPath))
{
Directory.Delete(torrentPath, true);
}
}
if (deleteData)

View file

@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
namespace RdtClient.Web
{
@ -39,6 +40,11 @@ namespace RdtClient.Web
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "wwwroot"; });
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
services.AddHttpContextAccessor();
services.AddCors(options =>
@ -112,7 +118,11 @@ namespace RdtClient.Web
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<RdtHub>("/hub");
endpoints.MapControllers();
});
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{