fix(qbittorrent): add seeds/peers fields (num_seeds/num_leechs), guard empty response (closes #64)
Build and Push Docker Image / build (push) Successful in 50s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m9s
Docs Check / Markdown lint (push) Successful in 1m19s
CI / Security audit (push) Successful in 2m8s
Docs Check / Mermaid diagram parse check (push) Successful in 2m33s
CI / Swagger Validation & Coverage (push) Successful in 2m40s
CI / Tests & coverage (push) Successful in 2m54s
Build and Push Docker Image / build (push) Successful in 50s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m9s
Docs Check / Markdown lint (push) Successful in 1m19s
CI / Security audit (push) Successful in 2m8s
Docs Check / Mermaid diagram parse check (push) Successful in 2m33s
CI / Swagger Validation & Coverage (push) Successful in 2m40s
CI / Tests & coverage (push) Successful in 2m54s
This commit is contained in:
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
### Fixed
|
||||
|
||||
- **qBittorrentClient Peer Data & Response Safety (Issue #64)** — `QBittorrentClient.normalizeDownload()` now exposes two new fields on every torrent record: `seeds` (sourced from qBittorrent's `num_seeds`, the count of connected seed peers) and `peers` (sourced from `num_leechs`, the count of connected leecher peers). The connected counts were chosen deliberately over the swarm totals `num_complete`/`num_incomplete` so the values remain consistent with what other clients (Transmission via `peersConnected`/`peersSendingToUs`, rTorrent via `d.peers_connected`) report on the same normalised contract. `QBittorrentClient.getMainData()` now also defensively returns the existing in-memory torrent map (rather than dereferencing a null) when the qBittorrent server responds with an empty body to `/api/v2/sync/maindata`, eliminating a crash class observed against transiently-restarting qBittorrent instances. A regression test verifies the new fields are populated from `num_seeds`/`num_leechs` and not from the swarm-total fields. Resolves Gitea Issue [#64](https://git.i3omb.com/Gandalf/sofarr/issues/64).
|
||||
- **Season Pack Queue Handling & Crash Prevention (Issue #61)** — Extracted a shared `buildArrQueueCache(queues, instances, mediaKey)` helper at `server/utils/arrQueueHelpers.js` covering both Sonarr and Radarr, replacing four previously-divergent inline `flatMap` blocks across the background poller (`server/utils/poller.js`) and the webhook event processor (`server/routes/webhook.js`) that built the `poll:sonarr-queue` and `poll:radarr-queue` cache entries. Sonarr queue records that share a `downloadId` (the canonical fingerprint for a season-pack release) are now annotated with `isSeasonPack: true` and `episodeCount: <n>` so downstream consumers — including the active-downloads matching service — can identify and de-duplicate season packs without re-deriving the grouping. The helper is wrapped in per-record and per-instance `try`/`catch` guards: malformed records (`null`, missing `data`, unknown instance ids) are skipped with a warning rather than throwing, eliminating a class of crashes that previously bubbled out of the `flatMap` and tore down the entire poll cycle or webhook refresh. Movies (Radarr) skip season-pack annotation by design. A new unit test suite at `tests/unit/utils/arrQueueHelpers.test.js` covers tagging, season-pack grouping, null-safety, and unknown-instance fallback. Resolves Gitea Issue [#61](https://git.i3omb.com/Gandalf/sofarr/issues/61).
|
||||
- **Webhook Reliability (Issue #62)** — Hardened the webhook replay protection to prevent false-duplicate detection while preserving protection against genuine retries. The replay key for Sonarr and Radarr now incorporates a content identifier (`downloadId`, falling back to `series.id` or `movie.id`) alongside the existing `eventType:instanceName:eventDate` components, so that multiple distinct events sharing the same timestamp (for example, several `Grab` events fired in the same second for episodes in a season pack) no longer collide and get silently dropped. Events without a content identifier (such as `Test`) fall back gracefully to the previous key shape so existing behaviour is preserved. The Ombi handler — which already uses a distinct `requestId`-bearing key — is unchanged. Additionally, the Sonarr and Radarr handlers now log an explicit warning when the inbound `instanceName` fails to match any configured instance and processing falls back to the first instance, improving diagnosability of misconfigured webhook senders. Resolves Gitea Issue [#62](https://git.i3omb.com/Gandalf/sofarr/issues/62).
|
||||
|
||||
|
||||
@@ -104,6 +104,11 @@ class QBittorrentClient extends DownloadClient {
|
||||
const response = await this.makeRequest(`/api/v2/sync/maindata?rid=${this.lastRid}`);
|
||||
const data = response.data;
|
||||
|
||||
if (!data) {
|
||||
logToFile(`[qBittorrent:${this.name}] Empty response from sync/maindata`);
|
||||
return Array.from(this.torrentMap.values());
|
||||
}
|
||||
|
||||
if (data.full_update) {
|
||||
// Full refresh: rebuild the entire map
|
||||
this.torrentMap.clear();
|
||||
@@ -249,6 +254,14 @@ class QBittorrentClient extends DownloadClient {
|
||||
downloaded: downloadedSize,
|
||||
speed: torrent.dlspeed,
|
||||
eta: torrent.eta < 0 || torrent.eta === 8640000 ? null : torrent.eta,
|
||||
// Connected peer counts (Issue #64). qBittorrent exposes:
|
||||
// num_seeds — connected seeds (peers we have a connection to)
|
||||
// num_leechs — connected leechers (peers downloading from us)
|
||||
// num_complete / num_incomplete — *swarm* totals reported by tracker
|
||||
// We expose the connected counts to stay consistent with what other
|
||||
// clients (e.g. Transmission via peersConnected/peersSendingToUs) report.
|
||||
seeds: torrent.num_seeds ?? 0,
|
||||
peers: torrent.num_leechs ?? 0,
|
||||
category: torrent.category || undefined,
|
||||
tags: torrent.tags ? torrent.tags.split(',').filter(tag => tag.trim()) : [],
|
||||
savePath: torrent.content_path || torrent.save_path || undefined,
|
||||
|
||||
@@ -130,6 +130,8 @@ describe('QBittorrentClient', () => {
|
||||
downloaded: 750000000,
|
||||
speed: 1048576,
|
||||
eta: 3600,
|
||||
seeds: 0,
|
||||
peers: 0,
|
||||
category: 'movies',
|
||||
tags: ['movie', 'hd'],
|
||||
savePath: '/downloads/test',
|
||||
@@ -138,6 +140,28 @@ describe('QBittorrentClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose connected seeds/peers from num_seeds and num_leechs (Issue #64)', () => {
|
||||
const torrent = {
|
||||
hash: 'def456',
|
||||
name: 'Swarm Torrent',
|
||||
state: 'downloading',
|
||||
progress: 0.1,
|
||||
size: 1000,
|
||||
completed: 100,
|
||||
dlspeed: 0,
|
||||
eta: 0,
|
||||
num_seeds: 7,
|
||||
num_leechs: 3,
|
||||
// Swarm totals — must NOT be picked up as connected counts
|
||||
num_complete: 200,
|
||||
num_incomplete: 50
|
||||
};
|
||||
|
||||
const normalized = client.normalizeDownload(torrent);
|
||||
expect(normalized.seeds).toBe(7);
|
||||
expect(normalized.peers).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle unknown torrent states', () => {
|
||||
const torrent = {
|
||||
hash: 'abc123',
|
||||
|
||||
Reference in New Issue
Block a user