Skip to content

Fix/premiumize progress status - #1014

Open
jvvilar wants to merge 2 commits into
rogerfar:mainfrom
jvvilar:fix/premiumize-progress-status
Open

Fix/premiumize progress status#1014
jvvilar wants to merge 2 commits into
rogerfar:mainfrom
jvvilar:fix/premiumize-progress-status

Conversation

@jvvilar

@jvvilar jvvilar commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes false stalled reporting for Premiumize torrents during the provider download phase.

Premiumize's /api/transfer/list only returns status, progress (0.0–1.0), and message — no seeders, speed, or byte counts. rdt-client previously mapped Seeders = 0, which triggered seeder-based stall logic shared with Real-Debrid and other providers. Every active Premiumize download appeared stalled in the web UI (Torrent stalled) and in the fake qBittorrent API (state: stalledDL), causing tools like Cleanuparr to treat healthy downloads as stuck and remove them after repeated stall strikes.

This PR tracks Premiumize progress over time instead, uses that for stall detection (20-minute threshold), exposes synthetic byte counts for the qBittorrent API, and shows Premiumize-specific status text (~/min) in the web UI. A follow-up fix persists ClientKind to the database so provider-specific logic actually applies after torrents are reloaded from SQLite.

How I found this

I use rdt-client's qBittorrent API with Cleanuparr. Premiumize torrents in the provider download phase were consistently reported as stalledDL, accumulating stall strikes and getting removed even while Premiumize showed them actively downloading.

Investigating the code path:

  • PremiumizeDebridClient.Map() hardcoded Seeders = 0, Speed = 0, Bytes = 0
  • QBittorrent.cs and TorrentDtoMapper.cs treat RdSeeders < 1 as stalled for non-NZB torrents
  • Cleanuparr only checks qBittorrent state, so every Premiumize provider-phase download looked stuck

Premiumize's API docs confirm there is no per-transfer detail endpoint with bytes or peers — only progress as a float and status. There is also no way to derive real total size from progress alone during an in-flight transfer.

Ruling out other causes

  • Not a Premiumize outage — progress % was moving in the Premiumize web UI while rdt-client reported stalled
  • Not Cleanuparr misconfiguration — it was correctly reacting to stalledDL from rdt-client
  • Not a frontend bug — the backend statusText and qBittorrent state both used the same seeder heuristic

Testing the fix locally

After deploying a local build, provider-phase torrents showed as actively downloading instead of stalled. Initially the UI still displayed synthetic KB/s (derived speed formatted like a real transfer rate). That led to a second discovery: ClientKind was set in memory during provider polls but never persisted in UpdateRdData, so torrents reloaded from the database had ClientKind = null and fell back to the generic status format. Persisting ClientKind fixed the UI to show the intended Torrent downloading (60% - ~0.4%/min) format.

Root cause

Two related problems:

  1. Wrong stall heuristic for Premiumize — stall detection assumed all providers expose seeder health. Premiumize does not; Seeders = 0 meant "unknown", not "dead".

  2. ClientKind not savedPremiumizeDebridClient.UpdateData set torrent.ClientKind = Provider.Premiumize in memory, but TorrentData.UpdateRdData did not write it to the database. Any code path that reloads torrents from SQLite (torrents.Get() for websockets, API list, qBittorrent) saw ClientKind = null, missing Premiumize-specific branches for status text, synthetic qBittorrent bytes, and tracker-based stall detection.

Solution

Track Premiumize progress over time in an in-memory singleton (PremiumizeProgressTracker):

  • Not stalled when progress increases between polls
  • Stalled when progress is unchanged for ≥ 20 minutes
  • Derive RdSpeed (bytes/s) from progress deltas using a 1 GB synthetic base (for qBittorrent/Cleanuparr compatibility only — not real bytes)
  • Derive ~/min for web UI status text (honest estimate from the only data Premiumize provides)

Real-Debrid and other providers are unchanged — they continue to use seeder-based stall logic.

Why a synthetic 1 GB total for qBittorrent

Premiumize provides no byte counts during running. Cleanuparr and other qBittorrent consumers expect size and downloaded to move with progress. A fixed 1 GB synthetic total lets downloaded = size × progress% increment as Premiumize progress advances, without pretending we know the real release size.

Why ~/min instead of KB/s in the UI

The derived KB/s is computed from the same synthetic base (deltaProgress × 1 GB / elapsed) and looks like a real transfer speed. Showing ~/min makes it clear we are estimating from Premiumize's done-% over time, not reporting actual bandwidth.

What this PR changes

Premiumize progress tracker

  • IPremiumizeProgressTracker / PremiumizeProgressTracker — singleton, in-memory per TorrentId
    • SyntheticSizeBytes = 1_000_000_000 (1 GB)
    • StallThreshold = 20 minutes
    • Update, IsStalled, GetSpeedBytesPerSec, GetProgressPerMin, Remove

Premiumize debrid client

  • Inject progress tracker
  • Map(): Seeders = null (unknown, not zero), store raw ProgressFraction
  • On running: update tracker, set RdSpeed from derived bytes/s
  • On finished / error: remove tracker entry
  • Explicit "error"TorrentStatus.Error

qBittorrent API (Cleanuparr / Sonarr / Radarr)

  • Premiumize torrents with no RdSize: synthetic 1 GB size / downloaded
  • Replace RdSeeders < 1 stall check with IsProviderStalled() — tracker for Premiumize, seeders for others

Web UI status text

  • TorrentDtoMapper accepts IPremiumizeProgressTracker (threaded through TorrentsController, RemoteService)
  • Premiumize provider-download phase:
    • Active with rate: Torrent downloading (60% - ~0.2%/min)
    • Active, first poll: Torrent downloading (60%)
    • Stalled: Torrent stalled (60%)
  • No frontend changes — API statusText is preferred over torrent-status.pipe.ts

ClientKind persistence (second commit)

  • TorrentData.UpdateRdData — persist ClientKind
  • Torrents.CaptureRdState — include ClientKind so first provider poll triggers a save
  • Regression test: UpdateRdData_ShouldPersistClientKind

No database migration — ClientKind column already exists; existing rows populate on the next provider poll.

Expected behaviour

Situation qBittorrent state Web UI status Cleanuparr
Premiumize running, progress moving downloading Torrent downloading (60% - ~0.2%/min) Left alone
Premiumize running, no progress 20+ min stalledDL Torrent stalled (60%) Stall rules apply
Real-Debrid / others Unchanged (seeder-based) Unchanged Unchanged

Out of scope

  • DB migration for progress history (in-memory is sufficient; restart resets stall timers ~20 min)
  • Premiumize message field in UI
  • Parsing torrent files or release sizes for real byte totals
  • ETA display (could be a follow-up; ~/min is the current honest metric)

Test plan

  • PremiumizeProgressTrackerTest — speed on progress increase, stall after 20 min, stall reset, remove
  • PremiumizeDebridClientTest — tracker integration on UpdateData
  • QBittorrentTest — Premiumize moving progress → downloading + synthetic size; stalled tracker → stalledDL
  • TorrentPayloadDataTests.UpdateRdData_ShouldPersistClientKind
  • dotnet test server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
  • Manual: Premiumize provider-phase torrents show ~/min (not fake KB/s) and are not falsely stalled
  • Manual: Cleanuparr leaves healthy Premiumize downloads alone
  • Manual: confirm genuinely stuck Premiumize transfer reports stalledDL after 20+ minutes without progress

jvvilar added 2 commits July 15, 2026 22:12
Premiumize reports no seeders or speed, so seeder-based stall logic falsely marked every provider download as stalled. Track progress over time instead, expose synthetic qBittorrent bytes for Cleanuparr, and show Premiumize-specific status text in the web UI.
ClientKind was set during provider polls but not written in UpdateRdData, so UI and qBittorrent logic could not detect Premiumize torrents after reloading from the database.
@jvvilar

jvvilar commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author
Screenshot 2026-07-15 231951

And this is how it looks in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant