Merge pull request #901 from Chesyre/main

Add automatic disk space monitoring for Bezzad downloader
This commit is contained in:
Roger Far 2025-11-15 17:15:01 -07:00 committed by GitHub
commit 371a13e060
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 282 additions and 5 deletions

View file

@ -0,0 +1,6 @@
export interface DiskSpaceStatus {
isPaused: boolean;
availableSpaceGB: number;
thresholdGB: number;
lastCheckTime: Date;
}

View file

@ -4,6 +4,15 @@
Please refresh the screen after fixing this error.
</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">
<table class="table is-fullwidth is-hoverable">
<thead>

View file

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model';
import { DiskSpaceStatus } from '../models/disk-space-status.model';
import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs';
import { FormsModule } from '@angular/forms';
@ -48,19 +49,31 @@ export class TorrentTableComponent implements OnInit {
public updateSettingsDeleteOnError: number;
public updateSettingsTorrentLifetime: number;
public diskSpaceStatus: DiskSpaceStatus | null = null;
constructor(
private router: Router,
private torrentService: TorrentService,
) {}
ngOnInit(): void {
this.torrentService.getDiskSpaceStatus().subscribe({
next: (status) => {
this.diskSpaceStatus = status;
},
});
this.torrentService.diskSpaceStatus$.subscribe((status) => {
this.diskSpaceStatus = status;
});
this.torrentService.update$.subscribe((result) => {
this.torrents = result;
});
this.torrentService.getList().subscribe({
next: (result) => {
this.torrents = result;
this.torrentService.update$.subscribe((result2) => {
this.torrents = result2;
});
},
error: (err) => {
this.error = err.error;

View file

@ -3,6 +3,7 @@ import { Inject, Injectable } from '@angular/core';
import * as signalR from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs';
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
import { DiskSpaceStatus } from './models/disk-space-status.model';
import { APP_BASE_HREF } from '@angular/common';
@Injectable({
@ -10,6 +11,7 @@ import { APP_BASE_HREF } from '@angular/common';
})
export class TorrentService {
public update$: Subject<Torrent[]> = new Subject();
public diskSpaceStatus$: Subject<DiskSpaceStatus> = new Subject();
private connection: signalR.HubConnection;
@ -29,11 +31,26 @@ export class TorrentService {
.withUrl(`${this.baseHref}hub`)
.withAutomaticReconnect()
.build();
this.connection.start().catch((err) => console.error(err));
this.connection.on('update', (torrents: Torrent[]) => {
this.update$.next(torrents);
});
this.connection.on('diskSpaceStatus', (status: any) => {
this.diskSpaceStatus$.next(status);
});
this.connection.onreconnected(() => {
this.getDiskSpaceStatus().subscribe({
next: (status) => {
if (status) {
this.diskSpaceStatus$.next(status);
}
},
});
});
this.connection.start().catch((err) => console.error(err));
}
public getList(): Observable<Torrent[]> {
@ -44,6 +61,10 @@ export class TorrentService {
return this.http.get<Torrent>(`${this.baseHref}Api/Torrents/Get/${torrentId}`);
}
public getDiskSpaceStatus(): Observable<DiskSpaceStatus | null> {
return this.http.get<DiskSpaceStatus | null>(`${this.baseHref}Api/Torrents/DiskSpaceStatus`);
}
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
magnetLink,

View file

@ -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;

View 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; }
}

View file

@ -0,0 +1,157 @@
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;
}
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 (shouldPause && _isPausedForLowDiskSpace)
{
logger.LogDebug($"Still paused: {availableSpaceGB} GB available (need {minimumFreeSpaceGB * 2} GB to resume)");
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);
}
await Task.Delay(TimeSpan.FromMinutes(intervalMinutes), stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, $"Unexpected error in DiskSpaceMonitor: {ex.Message}");
}
}
logger.LogInformation("DiskSpaceMonitor stopped.");
}
}

View file

@ -47,6 +47,7 @@ public static class DiConfig
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
services.AddHostedService<DiskSpaceMonitor>();
services.AddHostedService<ProviderUpdater>();
services.AddHostedService<Startup>();
services.AddHostedService<TaskRunner>();

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -19,4 +19,9 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
allTorrents
]);
}
public async Task UpdateDiskSpaceStatus(Object status)
{
await hub.Clients.All.SendCoreAsync("diskSpaceStatus", [status]);
}
}

View file

@ -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, UnpackClient> 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<TorrentRunner> 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<TorrentRunner> 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)
{

View file

@ -45,6 +45,14 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
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>
/// Used for debugging only. Force a tick.
/// </summary>