diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html index 01ce261..b4c0e79 100644 --- a/client/src/app/torrent-table/torrent-table.component.html +++ b/client/src/app/torrent-table/torrent-table.component.html @@ -4,6 +4,15 @@ Please refresh the screen after fixing this error. } +@if (diskSpaceStatus?.isPaused) { +
+ Bezzad downloads paused due to low disk space +
+ Available: {{ diskSpaceStatus.availableSpaceGB }} GB | Resume at: {{ diskSpaceStatus.thresholdGB * 2 }} GB +
+ Last check: {{ diskSpaceStatus.lastCheckTime | date: 'short' }} +
+}
diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts index 48454ba..4b15410 100644 --- a/client/src/app/torrent-table/torrent-table.component.ts +++ b/client/src/app/torrent-table/torrent-table.component.ts @@ -48,12 +48,21 @@ export class TorrentTableComponent implements OnInit { public updateSettingsDeleteOnError: number; public updateSettingsTorrentLifetime: number; + public diskSpaceStatus: any = null; + constructor( private router: Router, private torrentService: TorrentService, ) {} ngOnInit(): void { + // Load initial disk space status + this.torrentService.getDiskSpaceStatus().subscribe({ + next: (status) => { + this.diskSpaceStatus = status; + }, + }); + this.torrentService.getList().subscribe({ next: (result) => { this.torrents = result; @@ -61,6 +70,10 @@ export class TorrentTableComponent implements OnInit { this.torrentService.update$.subscribe((result2) => { this.torrents = result2; }); + + this.torrentService.diskSpaceStatus$.subscribe((status) => { + this.diskSpaceStatus = status; + }); }, error: (err) => { this.error = err.error; diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts index cc690d6..b7a063f 100644 --- a/client/src/app/torrent.service.ts +++ b/client/src/app/torrent.service.ts @@ -10,6 +10,7 @@ import { APP_BASE_HREF } from '@angular/common'; }) export class TorrentService { public update$: Subject = new Subject(); + public diskSpaceStatus$: Subject = new Subject(); private connection: signalR.HubConnection; @@ -34,6 +35,10 @@ export class TorrentService { this.connection.on('update', (torrents: Torrent[]) => { this.update$.next(torrents); }); + + this.connection.on('diskSpaceStatus', (status: any) => { + this.diskSpaceStatus$.next(status); + }); } public getList(): Observable { @@ -44,6 +49,10 @@ export class TorrentService { return this.http.get(`${this.baseHref}Api/Torrents/Get/${torrentId}`); } + public getDiskSpaceStatus(): Observable { + return this.http.get(`${this.baseHref}Api/Torrents/DiskSpaceStatus`); + } + public uploadMagnet(magnetLink: string, torrent: Torrent): Observable { return this.http.post(`${this.baseHref}Api/Torrents/UploadMagnet`, { magnetLink, diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 03bd0cb..d0076fc 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -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.")] 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")] [Description("Only set when trying to debug a download client, can generate a lot of logs.")] public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None; diff --git a/server/RdtClient.Data/Models/Internal/DiskSpaceStatus.cs b/server/RdtClient.Data/Models/Internal/DiskSpaceStatus.cs new file mode 100644 index 0000000..df63eff --- /dev/null +++ b/server/RdtClient.Data/Models/Internal/DiskSpaceStatus.cs @@ -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; } +} diff --git a/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs b/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs new file mode 100644 index 0000000..1261091 --- /dev/null +++ b/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs @@ -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 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(); + + 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."); + } +} diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 8a53391..c93f5e5 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -47,6 +47,7 @@ public static class DiConfig services.AddSingleton(); + services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); diff --git a/server/RdtClient.Service/Helpers/FileHelper.cs b/server/RdtClient.Service/Helpers/FileHelper.cs index b6926be..df03d16 100644 --- a/server/RdtClient.Service/Helpers/FileHelper.cs +++ b/server/RdtClient.Service/Helpers/FileHelper.cs @@ -105,4 +105,25 @@ public static class FileHelper 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; + } + } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs b/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs index cb2e112..be503e7 100644 --- a/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs @@ -122,11 +122,15 @@ public class BezzadDownloader : IDownloader public Task Pause() { + _logger.Debug($"Pausing download {_uri}"); + _downloadService.Pause(); return Task.CompletedTask; } public Task Resume() { + _logger.Debug($"Resuming download {_uri}"); + _downloadService.Resume(); return Task.CompletedTask; } diff --git a/server/RdtClient.Service/Services/RemoteService.cs b/server/RdtClient.Service/Services/RemoteService.cs index 032e3f8..8eea62c 100644 --- a/server/RdtClient.Service/Services/RemoteService.cs +++ b/server/RdtClient.Service/Services/RemoteService.cs @@ -19,4 +19,9 @@ public class RemoteService(IHubContext hub, Torrents torrents) allTorrents ]); } + + public async Task UpdateDiskSpaceStatus(Object status) + { + await hub.Clients.All.SendCoreAsync("diskSpaceStatus", [status]); + } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 1679c28..50e87ed 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -16,6 +16,8 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow public static readonly ConcurrentDictionary ActiveDownloadClients = new(); public static readonly ConcurrentDictionary ActiveUnpackClients = new(); + public static Boolean IsPausedForLowDiskSpace { get; set; } + private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) @@ -428,6 +430,13 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow 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)) { Log($"Not starting download because this download is already active", download, torrent); @@ -499,6 +508,12 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow { 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) { diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index f713985..fd3a0dc 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -45,6 +45,14 @@ public class TorrentsController(ILogger logger, Torrents tor return Ok(torrent); } + [HttpGet] + [Route("DiskSpaceStatus")] + public ActionResult GetDiskSpaceStatus() + { + var status = RdtClient.Service.BackgroundServices.DiskSpaceMonitor.GetCurrentStatus(); + return Ok(status); + } + /// /// Used for debugging only. Force a tick. ///