Add watch folder monitor.

This commit is contained in:
Roger Far 2022-04-30 12:18:30 -06:00
parent cd6002b5d6
commit 3fe9680e35
14 changed files with 253 additions and 64 deletions

View file

@ -12,6 +12,9 @@
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2"> <li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
<a>Radarr/Sonarr</a> <a>Radarr/Sonarr</a>
</li> </li>
<li [ngClass]="{ 'is-active': activeTab === 5 }" (click)="activeTab = 5">
<a>Watch</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 3 }" (click)="activeTab = 3"> <li [ngClass]="{ 'is-active': activeTab === 3 }" (click)="activeTab = 3">
<a>Tests</a> <a>Tests</a>
</li> </li>
@ -70,7 +73,7 @@
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Automatically import category.</label> <label class="label">Automatically import category</label>
<div class="control"> <div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingProviderAutoImportCategory" /> <input class="input" type="text" maxlength="100" [(ngModel)]="settingProviderAutoImportCategory" />
</div> </div>
@ -87,6 +90,27 @@
</div> </div>
</div> </div>
</div> </div>
<div class="field">
<label class="label">Connection Timeout</label>
<div class="control">
<input class="input" type="number" min="1" max="9999" [(ngModel)]="settingProviderTimeout" />
</div>
<p class="help">
The timeout to use to connect to the provider in seconds, with a minimum of 5 seconds. Increase if you experience
timeout errors in the log. This setting is only used when there is a user connected to the web interface or if
AutoImport/AutoDelete has been enabled.
</p>
</div>
<div class="field">
<label class="label">Check Interval</label>
<div class="control">
<input class="input" type="number" min="10" max="9999" [(ngModel)]="settingProviderCheckInterval" />
</div>
<p class="help">
This interval is used to check Real-Debrid or AllDebrid for updates. This setting is only used when there are
active downloads. If there are no active downloads it will use this setting * 3, with a minimum of 30 seconds.
</p>
</div>
</div> </div>
<div *ngIf="activeTab === 0"> <div *ngIf="activeTab === 0">
@ -417,6 +441,50 @@
</div> </div>
</div> </div>
<div *ngIf="activeTab === 5">
<div class="field">
<label class="label">Watch Path</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingWatchPath" />
</div>
<p class="help">
Watch this path for .torrent or .magnet files. When a file is found it will be automatically imported.
</p>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="settingProviderAutoDelete" />
Automatically delete downloads removed from provider.
</label>
<div class="help">
When selected, cancel and delete downloads that have been removed from Real-Debrid or AllDebrid.
</div>
</div>
</div>
<div class="field">
<label class="label">Connection Timeout</label>
<div class="control">
<input class="input" type="number" min="1" max="9999" [(ngModel)]="settingProviderTimeout" />
</div>
<p class="help">
The timeout to use to connect to the provider in seconds, with a minimum of 5 seconds. Increase if you experience
timeout errors in the log. This setting is only used when there is a user connected to the web interface or if
AutoImport/AutoDelete has been enabled.
</p>
</div>
<div class="field">
<label class="label">Check Interval</label>
<div class="control">
<input class="input" type="number" min="10" max="9999" [(ngModel)]="settingProviderCheckInterval" />
</div>
<p class="help">
This interval is used to check Real-Debrid or AllDebrid for updates. This setting is only used when there are
active downloads. If there are no active downloads it will use this setting * 3, with a minimum of 30 seconds.
</p>
</div>
</div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div> <div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>

View file

@ -30,6 +30,8 @@ export class SettingsComponent implements OnInit {
public settingProviderAutoImport: boolean; public settingProviderAutoImport: boolean;
public settingProviderAutoImportCategory: string; public settingProviderAutoImportCategory: string;
public settingProviderAutoDelete: boolean; public settingProviderAutoDelete: boolean;
public settingProviderTimeout: number;
public settingProviderCheckInterval: number;
public settingRealDebridApiKey: string; public settingRealDebridApiKey: string;
public settingDownloadPath: string; public settingDownloadPath: string;
public settingMappedPath: string; public settingMappedPath: string;
@ -67,6 +69,8 @@ export class SettingsComponent implements OnInit {
this.settingProviderAutoImport = this.getSetting(results, 'ProviderAutoImport') === '1'; this.settingProviderAutoImport = this.getSetting(results, 'ProviderAutoImport') === '1';
this.settingProviderAutoImportCategory = this.getSetting(results, 'ProviderAutoImportCategory'); this.settingProviderAutoImportCategory = this.getSetting(results, 'ProviderAutoImportCategory');
this.settingProviderAutoDelete = this.getSetting(results, 'ProviderAutoDelete') === '1'; this.settingProviderAutoDelete = this.getSetting(results, 'ProviderAutoDelete') === '1';
this.settingProviderTimeout = parseInt(this.getSetting(results, 'ProviderTimeout'), 10);
this.settingProviderCheckInterval = parseInt(this.getSetting(results, 'ProviderCheckInterval'), 10);
this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey'); this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey');
this.settingLogLevel = this.getSetting(results, 'LogLevel'); this.settingLogLevel = this.getSetting(results, 'LogLevel');
this.settingDownloadPath = this.getSetting(results, 'DownloadPath'); this.settingDownloadPath = this.getSetting(results, 'DownloadPath');
@ -116,6 +120,14 @@ export class SettingsComponent implements OnInit {
settingId: 'ProviderAutoDelete', settingId: 'ProviderAutoDelete',
value: this.settingProviderAutoDelete ? '1' : '0', value: this.settingProviderAutoDelete ? '1' : '0',
}, },
{
settingId: 'ProviderTimeout',
value: (this.settingProviderTimeout ?? 10).toString(),
},
{
settingId: 'ProviderCheckInterval',
value: (this.settingProviderCheckInterval ?? 10).toString(),
},
{ {
settingId: 'RealDebridApiKey', settingId: 'RealDebridApiKey',
value: this.settingRealDebridApiKey, value: this.settingRealDebridApiKey,

View file

@ -62,6 +62,18 @@ public class DataContext : IdentityDbContext
Value = "0" Value = "0"
}, },
new Setting new Setting
{
SettingId = "ProviderTimeout",
Type = "Int32",
Value = "10"
},
new Setting
{
SettingId = "ProviderCheckInterval",
Type = "Int32",
Value = "10"
},
new Setting
{ {
SettingId = "DeleteOnError", SettingId = "DeleteOnError",
Type = "Int32", Type = "Int32",

View file

@ -47,6 +47,8 @@ public class SettingData
ProviderAutoImport = GetInt32("ProviderAutoImport"), ProviderAutoImport = GetInt32("ProviderAutoImport"),
ProviderAutoImportCategory = GetString("ProviderAutoImportCategory"), ProviderAutoImportCategory = GetString("ProviderAutoImportCategory"),
ProviderAutoDelete = GetInt32("ProviderAutoDelete"), ProviderAutoDelete = GetInt32("ProviderAutoDelete"),
ProviderTimeout = GetInt32("ProviderTimeout"),
ProviderCheckInterval = GetInt32("ProviderCheckInterval"),
RealDebridApiKey = GetString("RealDebridApiKey"), RealDebridApiKey = GetString("RealDebridApiKey"),
DownloadPath = GetString("DownloadPath"), DownloadPath = GetString("DownloadPath"),
DownloadClient = GetString("DownloadClient"), DownloadClient = GetString("DownloadClient"),

View file

@ -6,6 +6,8 @@ public class DbSettings
public Int32 ProviderAutoImport { get; set; } public Int32 ProviderAutoImport { get; set; }
public String ProviderAutoImportCategory { get; set; } public String ProviderAutoImportCategory { get; set; }
public Int32 ProviderAutoDelete { get; set; } public Int32 ProviderAutoDelete { get; set; }
public Int32 ProviderTimeout { get; set; }
public Int32 ProviderCheckInterval { get; set; }
public String RealDebridApiKey { get; set; } public String RealDebridApiKey { get; set; }
public String DownloadPath { get; set; } public String DownloadPath { get; set; }
public String DownloadClient { get; set; } public String DownloadClient { get; set; }

View file

@ -0,0 +1,74 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RdtClient.Service.Services;
namespace RdtClient.Service.BackgroundServices;
public class ProviderUpdater : BackgroundService
{
private readonly ILogger<TaskRunner> _logger;
private readonly IServiceProvider _serviceProvider;
private static DateTime _nextUpdate = DateTime.UtcNow;
public ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
using var scope = _serviceProvider.CreateScope();
var torrentService = scope.ServiceProvider.GetRequiredService<Torrents>();
_logger.LogInformation("ProviderUpdater started.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
var torrents = await torrentService.Get();
if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && Settings.Get.ProviderAutoImport == 0) || Settings.Get.ProviderAutoImport == 1))
{
_logger.LogDebug($"Updating torrent info from Real-Debrid");
var updateTime = Settings.Get.ProviderCheckInterval * 3;
if (updateTime < 30)
{
updateTime = 30;
}
if (RdtHub.HasConnections)
{
updateTime = Settings.Get.ProviderCheckInterval;
if (updateTime < 5)
{
updateTime = 5;
}
}
_nextUpdate = DateTime.UtcNow.AddSeconds(updateTime);
await torrentService.UpdateRdData();
_logger.LogDebug($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error occurred in ProviderUpdater: {ex.Message}");
}
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
_logger.LogInformation("ProviderUpdater stopped.");
}
}

View file

@ -3,10 +3,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Service.Services;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
namespace RdtClient.Service.Services; namespace RdtClient.Service.BackgroundServices;
public class Startup : IHostedService public class Startup : IHostedService
{ {

View file

@ -1,8 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RdtClient.Service.Services;
namespace RdtClient.Service.Services; namespace RdtClient.Service.BackgroundServices;
public class TaskRunner : BackgroundService public class TaskRunner : BackgroundService
{ {
@ -34,7 +35,7 @@ public class TaskRunner : BackgroundService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick"); _logger.LogError(ex, $"Unexpected error occurred in TorrentDownloadManager.Tick: {ex.Message}");
} }
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);

View file

@ -4,16 +4,16 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace RdtClient.Service.Services; namespace RdtClient.Service.BackgroundServices;
public class UpdateChecker : BackgroundService public class UpdateChecker : BackgroundService
{ {
public static String CurrentVersion { get; private set; } public static String CurrentVersion { get; private set; }
public static String LatestVersion { get; private set; } public static String LatestVersion { get; private set; }
private readonly ILogger<TaskRunner> _logger; private readonly ILogger<UpdateChecker> _logger;
public UpdateChecker(ILogger<TaskRunner> logger) public UpdateChecker(ILogger<UpdateChecker> logger)
{ {
_logger = logger; _logger = logger;
} }

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.TorrentClients;
@ -18,6 +19,7 @@ public static class DiConfig
services.AddScoped<Torrents>(); services.AddScoped<Torrents>();
services.AddScoped<TorrentRunner>(); services.AddScoped<TorrentRunner>();
services.AddHostedService<ProviderUpdater>();
services.AddHostedService<Startup>(); services.AddHostedService<Startup>();
services.AddHostedService<TaskRunner>(); services.AddHostedService<TaskRunner>();
services.AddHostedService<UpdateChecker>(); services.AddHostedService<UpdateChecker>();

View file

@ -24,19 +24,34 @@ public class AllDebridTorrentClient : ITorrentClient
private AllDebridNETClient GetClient() private AllDebridNETClient GetClient()
{ {
var apiKey = Settings.Get.RealDebridApiKey; try
if (String.IsNullOrWhiteSpace(apiKey))
{ {
throw new Exception("All-Debrid API Key not set in the settings"); var apiKey = Settings.Get.RealDebridApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new Exception("All-Debrid API Key not set in the settings");
}
var httpClient = _httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var allDebridNetClient = new AllDebridNETClient("RealDebridClient", apiKey);
return allDebridNetClient;
} }
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
var httpClient = _httpClientFactory.CreateClient(); throw;
httpClient.Timeout = TimeSpan.FromSeconds(10); }
catch (TaskCanceledException ex)
{
_logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
var allDebridNetClient = new AllDebridNETClient("RealDebridClient", apiKey); throw;
}
return allDebridNetClient;
} }
private static TorrentClientTorrent Map(Magnet torrent) private static TorrentClientTorrent Map(Magnet torrent)

View file

@ -11,7 +11,7 @@ public class RealDebridTorrentClient : ITorrentClient
{ {
private readonly ILogger<RealDebridTorrentClient> _logger; private readonly ILogger<RealDebridTorrentClient> _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private TimeSpan _offset; private TimeSpan? _offset = null;
public RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IHttpClientFactory httpClientFactory) public RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IHttpClientFactory httpClientFactory)
{ {
@ -21,24 +21,42 @@ public class RealDebridTorrentClient : ITorrentClient
private RdNetClient GetClient() private RdNetClient GetClient()
{ {
var apiKey = Settings.Get.RealDebridApiKey; try
if (String.IsNullOrWhiteSpace(apiKey))
{ {
throw new Exception("Real-Debrid API Key not set in the settings"); var apiKey = Settings.Get.RealDebridApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new Exception("Real-Debrid API Key not set in the settings");
}
var httpClient = _httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.ProviderTimeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
} }
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
var httpClient = _httpClientFactory.CreateClient(); throw;
httpClient.Timeout = TimeSpan.FromSeconds(10); }
catch (TaskCanceledException ex)
{
_logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
var rdtNetClient = new RdNetClient(null, httpClient, 5); throw;
rdtNetClient.UseApiAuthentication(apiKey); }
// Get the server time to fix up the timezones on results
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
return rdtNetClient;
} }
private TorrentClientTorrent Map(Torrent torrent) private TorrentClientTorrent Map(Torrent torrent)
@ -334,7 +352,12 @@ public class RealDebridTorrentClient : ITorrentClient
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
return dateTimeOffset?.Subtract(_offset).ToOffset(_offset); if (_offset == null)
{
return dateTimeOffset;
}
return dateTimeOffset?.Subtract(_offset.Value).ToOffset(_offset.Value);
} }
private void Log(String message, Data.Models.Data.Torrent torrent = null) private void Log(String message, Data.Models.Data.Torrent torrent = null)

View file

@ -14,8 +14,6 @@ namespace RdtClient.Service.Services;
public class TorrentRunner public class TorrentRunner
{ {
private static DateTime _nextUpdate = DateTime.UtcNow;
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new(); public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new(); public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
@ -297,35 +295,6 @@ public class TorrentRunner
torrents = torrents.Where(m => m.Completed == null).ToList(); torrents = torrents.Where(m => m.Completed == null).ToList();
// Only poll Real-Debrid every second when a hub is connected, otherwise every 30 seconds
if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && Settings.Get.ProviderAutoImport == 0) || Settings.Get.ProviderAutoImport == 1))
{
Log($"Updating torrent info from Real-Debrid");
#if DEBUG
var updateTime = 0;
_nextUpdate = DateTime.UtcNow;
#else
var updateTime = 30;
if (RdtHub.HasConnections)
{
updateTime = 10;
}
_nextUpdate = DateTime.UtcNow.AddSeconds(updateTime);
#endif
await _torrents.UpdateRdData();
// Re-get torrents to account for updated info
torrents = await _torrents.Get();
torrents = torrents.Where(m => m.Completed == null).ToList();
Log($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds");
}
if (torrents.Count > 0) if (torrents.Count > 0)
{ {
Log($"Processing {torrents.Count} torrents"); Log($"Processing {torrents.Count} torrents");
@ -569,7 +538,14 @@ public class TorrentRunner
break; break;
} }
await _torrents.RunTorrentComplete(torrent.TorrentId); try
{
await _torrents.RunTorrentComplete(torrent.TorrentId);
}
catch (Exception ex)
{
_logger.LogError(ex.Message, "Unable to run post process: {Message}", ex.Message);
}
} }
else else
{ {

View file

@ -9,6 +9,7 @@ using RdtClient.Data.Data;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.TorrentClients;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;