refactor: use qBittorrent Sync API (/api/v2/sync/maindata) with fallback
All checks were successful
Docs Check / Markdown lint (push) Successful in 51s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m16s
CI / Tests & coverage (push) Successful in 1m37s
CI / Security audit (push) Successful in 1m44s
Docs Check / Mermaid diagram parse check (push) Successful in 1m52s

- QBittorrentClient now uses the incremental Sync API instead of repeatedly
  fetching the full torrent list via /api/v2/torrents/info.
- Per-client state: lastRid, torrentMap, fallbackThisCycle.
- Handles full_update, delta updates, and torrents_removed.
- Falls back to legacy torrents/info at most once per poll cycle.
- getAllTorrents() resets fallback flags before each cycle.
- Added 9 new unit tests covering: first sync, delta merge, full_update,
  torrents_removed, fallback path, direct-legacy-after-fallback, 403 re-auth,
  completed-field computation, and fallback reset.
This commit is contained in:
2026-05-19 09:33:20 +01:00
parent 8c4cc20551
commit 0a54d0d302
3 changed files with 392 additions and 11 deletions

View File

@@ -226,7 +226,7 @@ sofarr/
**`poller.js`** — Background polling engine. Fetches data from all configured service instances in parallel with per-task timing. Stores results in the cache with a configurable TTL. Can be disabled entirely (`POLL_INTERVAL=0`), in which case data is fetched on-demand. After each successful poll it notifies all registered SSE subscriber callbacks so connected clients receive data immediately.
**`qbittorrent.js`** — `QBittorrentClient` class with cookie-based authentication, automatic re-auth on 403, and persistent client instances. Includes torrent-to-download mapping (`mapTorrentToDownload`) and formatting utilities (`formatBytes`, `formatSpeed`, `formatEta`).
**`qbittorrent.js`** — `QBittorrentClient` class with cookie-based authentication, automatic re-auth on 403, and persistent client instances. **Uses the qBittorrent Sync API (`/api/v2/sync/maindata`) for incremental updates**: the first call sends `rid=0` for a full list; subsequent calls send the last `rid` to receive delta updates only (changed fields + removed hashes). If the Sync API fails, it falls back once per poll cycle to the legacy `GET /api/v2/torrents/info`. Includes torrent-to-download mapping (`mapTorrentToDownload`) and formatting utilities (`formatBytes`, `formatSpeed`, `formatEta`).
**`tokenStore.js`** — JSON file-backed store (`DATA_DIR/tokens.json`) for Emby `AccessToken`s. Tokens are stored server-side and **never sent to the client**. Writes are atomic (write to `.tmp` then rename). Entries expire after 31 days (slightly longer than the maximum 30-day cookie). Pruning runs on startup and hourly.
@@ -254,10 +254,32 @@ Every `POLL_INTERVAL` ms (default 5000), the poller fetches from all services in
| Radarr Queue | `GET /api/v3/queue` | `includeMovie=true` |
| Radarr History | `GET /api/v3/history` | `pageSize=10` |
| Radarr Tags | `GET /api/v3/tag` | — |
| qBittorrent | `GET /api/v2/torrents/info` | — |
| qBittorrent | `GET /api/v2/sync/maindata?rid=N` | Fallback to `GET /api/v2/torrents/info` |
Results are stored in the cache under `poll:*` keys with a TTL of `POLL_INTERVAL × 3`.
#### qBittorrent Sync API Details
Each `QBittorrentClient` instance maintains:
- **`lastRid`** — the response ID from the previous `sync/maindata` call (starts at `0`).
- **`torrentMap`** — a `Map<hash, torrent>` holding the complete state for every known torrent on this qBittorrent instance.
- **`fallbackThisCycle`** — boolean tracking whether this poll cycle has already fallen back to the legacy endpoint.
**Flow per poll cycle:**
1. `getAllTorrents()` resets `fallbackThisCycle = false` on every client.
2. `client.getTorrents()` attempts `GET /api/v2/sync/maindata?rid={lastRid}`.
3. qBittorrent returns:
- `rid` — new response ID to use next time.
- `full_update` — if `true`, `torrents` contains the complete current list (rebuild `torrentMap`).
- `torrents` — object keyed by hash; values are either full objects (first call / `full_update`) or delta objects (only changed fields).
- `torrents_removed` — array of hashes to delete from `torrentMap`.
4. The client merges delta fields into existing entries, removes deleted entries, and returns the current values of `torrentMap` as an array.
5. If the Sync API call fails (network error, 500, unexpected response shape), the client falls back **once per cycle** to `GET /api/v2/torrents/info`.
6. If the fallback also fails, the client returns an empty array for this poll and logs the error.
**Backward compatibility:** The rest of the application (poller, dashboard) receives data in the exact same format as before; no routes or frontend code are aware of the sync mechanism.
### 5.2 SSE Stream
When a browser opens `GET /api/dashboard/stream` (after authentication):