Add automatic disk space monitoring for Bezzad downloader
This commit is contained in:
parent
c51f8cb503
commit
27398ba993
12 changed files with 245 additions and 0 deletions
|
|
@ -4,6 +4,15 @@
|
||||||
Please refresh the screen after fixing this error.
|
Please refresh the screen after fixing this error.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (diskSpaceStatus?.isPaused) {
|
||||||
|
<div class="notification is-warning">
|
||||||
|
<strong>Bezzad downloads paused due to low disk space</strong>
|
||||||
|
<br />
|
||||||
|
Available: {{ diskSpaceStatus.availableSpaceGB }} GB | Resume at: {{ diskSpaceStatus.thresholdGB * 2 }} GB
|
||||||
|
<br />
|
||||||
|
<small>Last check: {{ diskSpaceStatus.lastCheckTime | date: 'short' }}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-hoverable">
|
<table class="table is-fullwidth is-hoverable">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,21 @@ export class TorrentTableComponent implements OnInit {
|
||||||
public updateSettingsDeleteOnError: number;
|
public updateSettingsDeleteOnError: number;
|
||||||
public updateSettingsTorrentLifetime: number;
|
public updateSettingsTorrentLifetime: number;
|
||||||
|
|
||||||
|
public diskSpaceStatus: any = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private torrentService: TorrentService,
|
private torrentService: TorrentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Load initial disk space status
|
||||||
|
this.torrentService.getDiskSpaceStatus().subscribe({
|
||||||
|
next: (status) => {
|
||||||
|
this.diskSpaceStatus = status;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.torrentService.getList().subscribe({
|
this.torrentService.getList().subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.torrents = result;
|
this.torrents = result;
|
||||||
|
|
@ -61,6 +70,10 @@ export class TorrentTableComponent implements OnInit {
|
||||||
this.torrentService.update$.subscribe((result2) => {
|
this.torrentService.update$.subscribe((result2) => {
|
||||||
this.torrents = result2;
|
this.torrents = result2;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.torrentService.diskSpaceStatus$.subscribe((status) => {
|
||||||
|
this.diskSpaceStatus = status;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { APP_BASE_HREF } from '@angular/common';
|
||||||
})
|
})
|
||||||
export class TorrentService {
|
export class TorrentService {
|
||||||
public update$: Subject<Torrent[]> = new Subject();
|
public update$: Subject<Torrent[]> = new Subject();
|
||||||
|
public diskSpaceStatus$: Subject<any> = new Subject();
|
||||||
|
|
||||||
private connection: signalR.HubConnection;
|
private connection: signalR.HubConnection;
|
||||||
|
|
||||||
|
|
@ -34,6 +35,10 @@ export class TorrentService {
|
||||||
this.connection.on('update', (torrents: Torrent[]) => {
|
this.connection.on('update', (torrents: Torrent[]) => {
|
||||||
this.update$.next(torrents);
|
this.update$.next(torrents);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.connection.on('diskSpaceStatus', (status: any) => {
|
||||||
|
this.diskSpaceStatus$.next(status);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getList(): Observable<Torrent[]> {
|
public getList(): Observable<Torrent[]> {
|
||||||
|
|
@ -44,6 +49,10 @@ export class TorrentService {
|
||||||
return this.http.get<Torrent>(`${this.baseHref}Api/Torrents/Get/${torrentId}`);
|
return this.http.get<Torrent>(`${this.baseHref}Api/Torrents/Get/${torrentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDiskSpaceStatus(): Observable<any> {
|
||||||
|
return this.http.get<any>(`${this.baseHref}Api/Torrents/DiskSpaceStatus`);
|
||||||
|
}
|
||||||
|
|
||||||
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
|
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
|
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
|
||||||
magnetLink,
|
magnetLink,
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,14 @@ http://127.0.0.1:6800/jsonrpc.")]
|
||||||
[Description("The root path to doawnload the file on the Synology DownloadStation host, if empty use the default DownloadStation path.")]
|
[Description("The root path to doawnload the file on the Synology DownloadStation host, if empty use the default DownloadStation path.")]
|
||||||
public String? DownloadStationDownloadPath { get; set; } = null;
|
public String? DownloadStationDownloadPath { get; set; } = null;
|
||||||
|
|
||||||
|
[DisplayName("Minimum free disk space (GB) (Bezzad only)")]
|
||||||
|
[Description("Minimum free disk space in GB. Bezzad downloads will pause when space falls below this value and will resume only when double this space is available. Set to 0 to disable.")]
|
||||||
|
public Int32 MinimumFreeSpaceGB { get; set; } = 0;
|
||||||
|
|
||||||
|
[DisplayName("Disk space check interval (minutes)")]
|
||||||
|
[Description("How often to check available disk space, in minutes.")]
|
||||||
|
public Int32 DiskSpaceCheckIntervalMinutes { get; set; } = 5;
|
||||||
|
|
||||||
[DisplayName("Log level")]
|
[DisplayName("Log level")]
|
||||||
[Description("Only set when trying to debug a download client, can generate a lot of logs.")]
|
[Description("Only set when trying to debug a download client, can generate a lot of logs.")]
|
||||||
public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None;
|
public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None;
|
||||||
|
|
|
||||||
9
server/RdtClient.Data/Models/Internal/DiskSpaceStatus.cs
Normal file
9
server/RdtClient.Data/Models/Internal/DiskSpaceStatus.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace RdtClient.Data.Models.Internal;
|
||||||
|
|
||||||
|
public class DiskSpaceStatus
|
||||||
|
{
|
||||||
|
public Boolean IsPaused { get; set; }
|
||||||
|
public Int64 AvailableSpaceGB { get; set; }
|
||||||
|
public Int32 ThresholdGB { get; set; }
|
||||||
|
public DateTimeOffset LastCheckTime { get; set; }
|
||||||
|
}
|
||||||
143
server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs
Normal file
143
server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using RdtClient.Data.Models.Internal;
|
||||||
|
using RdtClient.Service.Helpers;
|
||||||
|
using RdtClient.Service.Services;
|
||||||
|
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.BackgroundServices;
|
||||||
|
|
||||||
|
public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider serviceProvider) : BackgroundService
|
||||||
|
{
|
||||||
|
private Boolean _isPausedForLowDiskSpace;
|
||||||
|
private static DiskSpaceStatus? _lastStatus;
|
||||||
|
|
||||||
|
public static DiskSpaceStatus? GetCurrentStatus()
|
||||||
|
{
|
||||||
|
return _lastStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!Startup.Ready)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var remoteService = scope.ServiceProvider.GetRequiredService<RemoteService>();
|
||||||
|
|
||||||
|
logger.LogInformation("DiskSpaceMonitor started.");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var minimumFreeSpaceGB = Settings.Get.DownloadClient.MinimumFreeSpaceGB;
|
||||||
|
|
||||||
|
if (minimumFreeSpaceGB <= 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervalMinutes = Settings.Get.DownloadClient.DiskSpaceCheckIntervalMinutes;
|
||||||
|
if (intervalMinutes < 1)
|
||||||
|
{
|
||||||
|
intervalMinutes = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(intervalMinutes), stoppingToken);
|
||||||
|
|
||||||
|
var downloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||||
|
logger.LogDebug($"Checking disk space for path: {downloadPath}");
|
||||||
|
|
||||||
|
if (!Directory.Exists(downloadPath))
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Download path does not exist: {downloadPath}");
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableSpaceGB = FileHelper.GetAvailableFreeSpaceGB(downloadPath);
|
||||||
|
logger.LogDebug($"Disk space check: {availableSpaceGB} GB available (threshold: {minimumFreeSpaceGB} GB, resume: {minimumFreeSpaceGB * 2} GB, isPaused: {_isPausedForLowDiskSpace})");
|
||||||
|
|
||||||
|
if (availableSpaceGB == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Failed to get disk space for path: {downloadPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldPause = availableSpaceGB > 0 && availableSpaceGB < minimumFreeSpaceGB;
|
||||||
|
var shouldResume = availableSpaceGB >= (minimumFreeSpaceGB * 2);
|
||||||
|
|
||||||
|
if (shouldPause && !_isPausedForLowDiskSpace)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Pausing Bezzad downloads: {availableSpaceGB} GB available, threshold is {minimumFreeSpaceGB} GB");
|
||||||
|
|
||||||
|
var pausedCount = 0;
|
||||||
|
foreach (var download in TorrentRunner.ActiveDownloadClients)
|
||||||
|
{
|
||||||
|
if (download.Value.Type == DownloadClient.Bezzad)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Pausing Bezzad download: {download.Key}");
|
||||||
|
await download.Value.Pause();
|
||||||
|
pausedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Paused {pausedCount} active Bezzad downloads");
|
||||||
|
|
||||||
|
TorrentRunner.IsPausedForLowDiskSpace = true;
|
||||||
|
_isPausedForLowDiskSpace = true;
|
||||||
|
|
||||||
|
var status = new DiskSpaceStatus
|
||||||
|
{
|
||||||
|
IsPaused = true,
|
||||||
|
AvailableSpaceGB = availableSpaceGB,
|
||||||
|
ThresholdGB = minimumFreeSpaceGB,
|
||||||
|
LastCheckTime = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
_lastStatus = status;
|
||||||
|
await remoteService.UpdateDiskSpaceStatus(status);
|
||||||
|
}
|
||||||
|
else if (shouldResume && _isPausedForLowDiskSpace)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Resuming Bezzad downloads: {availableSpaceGB} GB available, resume threshold is {minimumFreeSpaceGB * 2} GB");
|
||||||
|
|
||||||
|
var resumedCount = 0;
|
||||||
|
foreach (var download in TorrentRunner.ActiveDownloadClients)
|
||||||
|
{
|
||||||
|
if (download.Value.Type == DownloadClient.Bezzad)
|
||||||
|
{
|
||||||
|
logger.LogDebug($"Resuming Bezzad download: {download.Key}");
|
||||||
|
await download.Value.Resume();
|
||||||
|
resumedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Resumed {resumedCount} Bezzad downloads");
|
||||||
|
|
||||||
|
TorrentRunner.IsPausedForLowDiskSpace = false;
|
||||||
|
_isPausedForLowDiskSpace = false;
|
||||||
|
|
||||||
|
var status = new DiskSpaceStatus
|
||||||
|
{
|
||||||
|
IsPaused = false,
|
||||||
|
AvailableSpaceGB = availableSpaceGB,
|
||||||
|
ThresholdGB = minimumFreeSpaceGB,
|
||||||
|
LastCheckTime = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
_lastStatus = status;
|
||||||
|
await remoteService.UpdateDiskSpaceStatus(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, $"Unexpected error in DiskSpaceMonitor: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("DiskSpaceMonitor stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ public static class DiConfig
|
||||||
|
|
||||||
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
||||||
|
|
||||||
|
services.AddHostedService<DiskSpaceMonitor>();
|
||||||
services.AddHostedService<ProviderUpdater>();
|
services.AddHostedService<ProviderUpdater>();
|
||||||
services.AddHostedService<Startup>();
|
services.AddHostedService<Startup>();
|
||||||
services.AddHostedService<TaskRunner>();
|
services.AddHostedService<TaskRunner>();
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,25 @@ public static class FileHelper
|
||||||
stringBuilder.AppendLine($"{indent}{file.Name}");
|
stringBuilder.AppendLine($"{indent}{file.Name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Int64 GetAvailableFreeSpaceGB(String path)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var driveInfo = new DriveInfo(path);
|
||||||
|
return driveInfo.AvailableFreeSpace / (1024 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,11 +122,15 @@ public class BezzadDownloader : IDownloader
|
||||||
|
|
||||||
public Task Pause()
|
public Task Pause()
|
||||||
{
|
{
|
||||||
|
_logger.Debug($"Pausing download {_uri}");
|
||||||
|
_downloadService.Pause();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Resume()
|
public Task Resume()
|
||||||
{
|
{
|
||||||
|
_logger.Debug($"Resuming download {_uri}");
|
||||||
|
_downloadService.Resume();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,9 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
|
||||||
allTorrents
|
allTorrents
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDiskSpaceStatus(Object status)
|
||||||
|
{
|
||||||
|
await hub.Clients.All.SendCoreAsync("diskSpaceStatus", [status]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
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();
|
||||||
|
|
||||||
|
public static Boolean IsPausedForLowDiskSpace { get; set; }
|
||||||
|
|
||||||
private readonly HttpClient _httpClient = new()
|
private readonly HttpClient _httpClient = new()
|
||||||
{
|
{
|
||||||
Timeout = TimeSpan.FromSeconds(10)
|
Timeout = TimeSpan.FromSeconds(10)
|
||||||
|
|
@ -428,6 +430,13 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsPausedForLowDiskSpace && torrent.DownloadClient == Data.Enums.DownloadClient.Bezzad)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Not starting Bezzad download because of low disk space {download.ToLog()} {torrent.ToLog()}");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ActiveDownloadClients.ContainsKey(download.DownloadId))
|
if (ActiveDownloadClients.ContainsKey(download.DownloadId))
|
||||||
{
|
{
|
||||||
Log($"Not starting download because this download is already active", download, torrent);
|
Log($"Not starting download because this download is already active", download, torrent);
|
||||||
|
|
@ -499,6 +508,12 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
{
|
{
|
||||||
await downloads.UpdateRemoteId(download.DownloadId, remoteId);
|
await downloads.UpdateRemoteId(download.DownloadId, remoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsPausedForLowDiskSpace && downloadClient.Type == Data.Enums.DownloadClient.Bezzad)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Pausing new Bezzad download due to low disk space {download.ToLog()} {torrent.ToLog()}");
|
||||||
|
await downloadClient.Pause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
||||||
return Ok(torrent);
|
return Ok(torrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("DiskSpaceStatus")]
|
||||||
|
public ActionResult<RdtClient.Data.Models.Internal.DiskSpaceStatus?> GetDiskSpaceStatus()
|
||||||
|
{
|
||||||
|
var status = RdtClient.Service.BackgroundServices.DiskSpaceMonitor.GetCurrentStatus();
|
||||||
|
return Ok(status);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used for debugging only. Force a tick.
|
/// Used for debugging only. Force a tick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue