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
|
### 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).
|
- **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).
|
- **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 response = await this.makeRequest(`/api/v2/sync/maindata?rid=${this.lastRid}`);
|
||||||
const data = response.data;
|
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) {
|
if (data.full_update) {
|
||||||
// Full refresh: rebuild the entire map
|
// Full refresh: rebuild the entire map
|
||||||
this.torrentMap.clear();
|
this.torrentMap.clear();
|
||||||
@@ -249,6 +254,14 @@ class QBittorrentClient extends DownloadClient {
|
|||||||
downloaded: downloadedSize,
|
downloaded: downloadedSize,
|
||||||
speed: torrent.dlspeed,
|
speed: torrent.dlspeed,
|
||||||
eta: torrent.eta < 0 || torrent.eta === 8640000 ? null : torrent.eta,
|
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,
|
category: torrent.category || undefined,
|
||||||
tags: torrent.tags ? torrent.tags.split(',').filter(tag => tag.trim()) : [],
|
tags: torrent.tags ? torrent.tags.split(',').filter(tag => tag.trim()) : [],
|
||||||
savePath: torrent.content_path || torrent.save_path || undefined,
|
savePath: torrent.content_path || torrent.save_path || undefined,
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ describe('QBittorrentClient', () => {
|
|||||||
downloaded: 750000000,
|
downloaded: 750000000,
|
||||||
speed: 1048576,
|
speed: 1048576,
|
||||||
eta: 3600,
|
eta: 3600,
|
||||||
|
seeds: 0,
|
||||||
|
peers: 0,
|
||||||
category: 'movies',
|
category: 'movies',
|
||||||
tags: ['movie', 'hd'],
|
tags: ['movie', 'hd'],
|
||||||
savePath: '/downloads/test',
|
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', () => {
|
it('should handle unknown torrent states', () => {
|
||||||
const torrent = {
|
const torrent = {
|
||||||
hash: 'abc123',
|
hash: 'abc123',
|
||||||
|
|||||||
Reference in New Issue
Block a user