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

View file

@ -19,17 +19,21 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
NoOfSlots = activeTorrents.Count,
Slots = activeTorrents.Select((t, index) =>
{
Double downloadProgress = 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 bytesTotal = t.Downloads.Sum(m => m.BytesTotal);
downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
var bytesDone = dlStats.Sum(m => m.BytesDone);
var bytesTotal = dlStats.Sum(m => m.BytesTotal);
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 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,
NzoId = t.Hash,
Filename = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal - d.BytesDone)),
Size = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal - d.BytesDone)),
Percentage = (progress * 100.0).ToString("0"),
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 (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()
{
Log("Initializing TorrentRunner");
@ -665,8 +680,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
var completePerc = 0;
var totalDownloadBytes = torrent.Downloads.Sum(m => m.BytesTotal);
var totalDoneBytes = torrent.Downloads.Sum(m => m.BytesDone);
var totalDownloadBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesTotal);
var totalDoneBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesDone);
if (totalDownloadBytes > 0)
{

View file

@ -58,29 +58,15 @@ public class Torrents(
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()
{
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;
}
@ -663,8 +649,7 @@ public class Torrents(
{
Category = Settings.Get.Provider.Default.Category,
DownloadClient = Settings.Get.DownloadClient.Client,
DownloadAction =
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
FinishedAction = Settings.Get.Provider.Default.FinishedAction,
@ -881,22 +866,6 @@ public class Torrents(
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;
}
@ -1084,6 +1053,7 @@ public class Torrents(
logger.LogDebug(message);
}
private static String ComputeSha1Hash(String input)
{
using var sha1 = System.Security.Cryptography.SHA1.Create();