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.
///