Refactor download progress calculation to use centralized stats and simplify logic across services.

- Fixes an issue where progress goes back to 50% as soon as downloads are done
- Prevents copy and update of torrent/nzb list by not updating download stats on every use, most callers don't require this data, notable perf/memory improvement.
This commit is contained in:
omgbeez 2026-02-13 17:06:12 -05:00 committed by Clifford Roche
parent a5f9cd353b
commit 40be170677
5 changed files with 58 additions and 68 deletions

View file

@ -40,6 +40,7 @@ public class SabnzbdTest
}; };
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
_torrentsMock.Setup(t => t.GetDownloadStats(It.IsAny<Guid>())).Returns((0, 1000, 500));
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);

View file

@ -223,29 +223,28 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var speed = torrent.RdSpeed ?? 0; var speed = torrent.RdSpeed ?? 0;
var bytesDone = (Int64)(bytesTotal * rdProgress); var bytesDone = (Int64)(bytesTotal * rdProgress);
Double downloadProgress = 0; Double progress;
if (torrent.Downloads is { Count: > 0 }) if (torrent.Downloads is { Count: > 0 })
{ {
var dlBytesDone = torrent.Downloads.Sum(m => m.BytesDone); var dlStats = torrent.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
var dlBytesTotal = torrent.Downloads.Sum(m => m.BytesTotal); var dlBytesDone = dlStats.Sum(m => m.BytesDone);
speed = (Int32)torrent.Downloads.Average(m => m.Speed); var dlBytesTotal = dlStats.Sum(m => m.BytesTotal);
downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)dlBytesDone / dlBytesTotal, 0.0, 1.0) : 0; speed = (Int32)(dlStats.Any() ? dlStats.Average(m => m.Speed) : 0);
var downloadProgress = dlBytesTotal > 0 ? Math.Clamp((Double)dlBytesDone / dlBytesTotal, 0.0, 1.0) : 0;
progress = (rdProgress + downloadProgress) / 2.0;
}
else
{
progress = rdProgress;
} }
var progress = (rdProgress + downloadProgress) / 2.0;
var remaining = TimeSpan.Zero; var remaining = TimeSpan.Zero;
var bytesRemaining = bytesTotal - bytesDone; if (progress > 0 && progress < 1.0)
if (speed > 0 && bytesRemaining > 0)
{ {
remaining = TimeSpan.FromSeconds(bytesRemaining / (Double)speed); var startTime = torrent.Retry > torrent.Added ? torrent.Retry.Value : torrent.Added;
var elapsed = DateTimeOffset.UtcNow - startTime;
// In case there is clock skew var totalEstimatedTime = TimeSpan.FromTicks((Int64)(elapsed.Ticks / progress));
if (remaining < TimeSpan.Zero) remaining = totalEstimatedTime - elapsed;
{
remaining = TimeSpan.Zero;
}
} }
var result = new TorrentInfo var result = new TorrentInfo
@ -361,9 +360,10 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
if (torrent.Downloads.Count > 0) if (torrent.Downloads.Count > 0)
{ {
bytesDone = torrent.Downloads.Sum(m => m.BytesDone); var dlStats = torrent.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal); bytesDone = dlStats.Sum(m => m.BytesDone);
speed = (Int32)torrent.Downloads.Average(m => m.Speed); bytesTotal = dlStats.Sum(m => m.BytesTotal);
speed = (Int32)(dlStats.Any() ? dlStats.Average(m => m.Speed) : 0);
} }
var result = new TorrentProperties var result = new TorrentProperties

View file

@ -19,17 +19,21 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
NoOfSlots = activeTorrents.Count, NoOfSlots = activeTorrents.Count,
Slots = activeTorrents.Select((t, index) => Slots = activeTorrents.Select((t, index) =>
{ {
Double downloadProgress = 0;
var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0; var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0;
Double progress;
if (t.Downloads is { Count: > 0 }) var dlStats = t.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
if (dlStats.Count > 0)
{ {
var bytesDone = t.Downloads.Sum(m => m.BytesDone); var bytesDone = dlStats.Sum(m => m.BytesDone);
var bytesTotal = t.Downloads.Sum(m => m.BytesTotal); var bytesTotal = dlStats.Sum(m => m.BytesTotal);
downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0; var downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
progress = (rdProgress + downloadProgress) / 2.0;
}
else
{
progress = rdProgress;
} }
var progress = (rdProgress + downloadProgress) / 2.0;
var timeLeft = "0:00:00"; var timeLeft = "0:00:00";
var startTime = t.Retry > t.Added ? t.Retry.Value : t.Added; var startTime = t.Retry > t.Added ? t.Retry.Value : t.Added;
@ -50,8 +54,8 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
Index = index, Index = index,
NzoId = t.Hash, NzoId = t.Hash,
Filename = t.RdName ?? t.Hash, Filename = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)), Size = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal - d.BytesDone)), SizeLeft = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal - d.BytesDone)),
Percentage = (progress * 100.0).ToString("0"), Percentage = (progress * 100.0).ToString("0"),
Status = t.RdStatus switch Status = t.RdStatus switch

View file

@ -23,6 +23,21 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
public static Boolean IsPausedForLowDiskSpace { get; set; } public static Boolean IsPausedForLowDiskSpace { get; set; }
public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
{
if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient))
{
return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone);
}
if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient))
{
return (0, 100, unpackClient.Progess);
}
return (0, 0, 0);
}
public async Task Initialize() public async Task Initialize()
{ {
Log("Initializing TorrentRunner"); Log("Initializing TorrentRunner");
@ -665,8 +680,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
var completePerc = 0; var completePerc = 0;
var totalDownloadBytes = torrent.Downloads.Sum(m => m.BytesTotal); var totalDownloadBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesTotal);
var totalDoneBytes = torrent.Downloads.Sum(m => m.BytesDone); var totalDoneBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesDone);
if (totalDownloadBytes > 0) if (totalDownloadBytes > 0)
{ {

View file

@ -58,29 +58,15 @@ public class Torrents(
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1); private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
public virtual (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetDownloadStats(Guid downloadId)
{
return TorrentRunner.GetStats(downloadId);
}
public virtual async Task<IList<Torrent>> Get() public virtual async Task<IList<Torrent>> Get()
{ {
var torrents = await torrentData.Get(); var torrents = await torrentData.Get();
foreach (var torrent in torrents)
{
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = downloadClient.Speed;
download.BytesTotal = downloadClient.BytesTotal;
download.BytesDone = downloadClient.BytesDone;
}
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
download.BytesTotal = 100;
download.BytesDone = unpackClient.Progess;
}
}
}
return torrents; return torrents;
} }
@ -663,8 +649,7 @@ public class Torrents(
{ {
Category = Settings.Get.Provider.Default.Category, Category = Settings.Get.Provider.Default.Category,
DownloadClient = Settings.Get.DownloadClient.Client, DownloadClient = Settings.Get.DownloadClient.Client,
DownloadAction = DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction, HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay, FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
FinishedAction = Settings.Get.Provider.Default.FinishedAction, FinishedAction = Settings.Get.Provider.Default.FinishedAction,
@ -881,22 +866,6 @@ public class Torrents(
await UpdateTorrentClientData(torrent); await UpdateTorrentClientData(torrent);
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = downloadClient.Speed;
download.BytesTotal = downloadClient.BytesTotal;
download.BytesDone = downloadClient.BytesDone;
}
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
download.BytesTotal = 100;
download.BytesDone = unpackClient.Progess;
}
}
return torrent; return torrent;
} }
@ -1084,6 +1053,7 @@ public class Torrents(
logger.LogDebug(message); logger.LogDebug(message);
} }
private static String ComputeSha1Hash(String input) private static String ComputeSha1Hash(String input)
{ {
using var sha1 = System.Security.Cryptography.SHA1.Create(); using var sha1 = System.Security.Cryptography.SHA1.Create();