From a54f11ccbcfb1c372e9403444a543caae8d753bf Mon Sep 17 00:00:00 2001 From: poprhythm Date: Wed, 9 Sep 2026 03:16:58 +0000 Subject: [PATCH] Update sonarr skill doc: qbt-relink.sh re-enabled Reflects the rewritten script's blocking single-pass model and the hard rule against re-triggering stop/setLocation/recheck against the same hash more than once. Marks Incident 3 as fixed and verified. Claude-Session: https://claude.ai/code/session_01HZQK6jHmdTpFjFZM8FUnqA --- .claude/skills/sonarr/SKILL.md | 96 +++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/.claude/skills/sonarr/SKILL.md b/.claude/skills/sonarr/SKILL.md index ff59e91..ba9a8a9 100644 --- a/.claude/skills/sonarr/SKILL.md +++ b/.claude/skills/sonarr/SKILL.md @@ -5,19 +5,17 @@ description: Use Sonarr to add TV shows and manually import existing raw-named f # Sonarr library migration/import -**⚠️ `qbt-relink.sh` is currently UNSAFE — do not use it until this warning is -removed.** It caused real, apparently unrecoverable data loss (Babylon 5 -seasons 3 and 4, 44 episodes) on 2026-09-08: qBittorrent's own automatic -incomplete-file management fought the script's manual `setLocation` calls -across repeated resume/stop cycles and physically moved/destroyed the real -files, even after every safety check the script performs (stopped state, -confirmed `save_path`, confirmed `content_path`) reported success. See -"Incident 3" below for the full detail. **The Sonarr import workflow itself -(`sonarr.sh scan`/`import`) is unaffected and still safe** — it's specifically -the qBittorrent-side relink step that's paused. Until this is fixed, leave -qBittorrent's stale torrent entries alone after a Sonarr import rather than -touching them — a `missingFiles` error in the qBittorrent UI is a much -smaller problem than what happened here. +**`qbt-relink.sh` was paused after a real data-loss incident (2026-09-08, +Babylon 5 S03/S04, 44 episodes — see "Incident 3" below) and has since been +rewritten with a real safety model and re-verified end-to-end against a live +73GB torrent.** Root cause: repeated manual re-triggering (stop/setLocation/ +recheck called more than once across separate debugging attempts on the same +hash) raced against qBittorrent's own automatic incomplete-file management. +The fix isn't a patch on top of the old script - it's a different operating +discipline: **the whole relink is one blocking pass with no manual +re-intervention, ever**. See "Current safety model" below before touching +this again if you're tempted to run raw curl calls against a hash mid-flow - +that impulse is exactly what caused the incident. This repo's Sonarr manages TV show organization for both Jellyfin and Plex (they share the same `nas_media` library at `/data/video/tv`). The @@ -77,35 +75,53 @@ bash ~/.claude/plugins/cache/claude-homelab/homelab-core/*/skills/qbittorrent/sc | python3 -c "import sys,json; [print(t['hash'], t['name']) for t in json.load(sys.stdin)]" | grep -i "" ``` -It **stops the torrent first** (qBittorrent 5.x renamed pause/resume to -stop/start), sets its location *and* download-path override to the season -folder — verifying both actually took effect before continuing, not just -trusting a 200 response — pairs and renames each file to match Sonarr's -output (by parsed `SxxEyy` episode number, not sort order — see below), then -triggers a recheck, and leaves the torrent **stopped** afterward rather than -auto-resuming. Since it's the same underlying data (hardlink, same inode), -the hash check passes and the torrent is ready to seed normally again once -you manually start it — no re-download needed. **The recheck reads the whole -file over NFS and is slow** (minutes per multi-GB file, and this qBittorrent -instance seems to only actively process one or two full-file rechecks at a -time — others sit at `stoppedDL`/0% "queued" looking identical to a real -failure until their turn comes) — kick off several in parallel rather than -waiting on each one serially, and poll with: +## Current safety model (post-rewrite) -```bash -bash ~/.claude/plugins/cache/claude-homelab/homelab-core/*/skills/qbittorrent/scripts/qbit-api.sh info -``` +The script runs the **entire operation as one blocking pass** — it does not +return control partway through for you to poll and come back to. It takes +minutes per multi-GB file (recheck reads the whole file over NFS), so expect +it to sit there running; that's normal, not stuck. Run it via a background +shell / `run_in_background` and wait for it to actually exit rather than +interrupting it. -Once a torrent shows 100% progress / `stoppedUP` (not `stoppedDL` — that -means the recheck found missing or mismatched pieces), start it again from -the WebUI. **Never assume a relink succeeded without checking** — see the -incident below. +**The hard rule this exists to enforce: never call `stop`/`setLocation`/ +`recheck` against the same torrent hash a second time while a previous +invocation (of this script, or a raw curl call) might still be settling.** +That repeated-intervention pattern — re-triggering by hand while +investigating why something looked stuck — is what caused Incident 3's real +data loss, even though every individual safety check along the way reported +success. If a run times out (40 min) or fails partway, do not react by +re-running raw curl commands against that hash — either re-run this exact +script invocation (it's idempotent: it detects an already-correctly-relinked +torrent by checking that qBittorrent's tracked filenames actually match the +destination, not just progress, and does nothing further) or stop and +investigate read-only first (`torrents/info`, `torrents/files`, the +`/api/v2/log/main` log) before taking any write action. + +What one run does, in order: stop the torrent → set its location *and* +download-path override to the season folder, each verified with a short poll +(both are asynchronous — a single immediate check can read stale data) → +pair and rename each file to match Sonarr's output (by parsed `SxxEyy` +episode number, not sort order — see below) → trigger exactly one recheck → +block, polling every 15s, until it leaves a `checking*` state (up to 40 min) +→ compare the destination folder against a filesystem manifest taken +*before* any of this started → report pass/fail. The torrent is left +**stopped** regardless of outcome — start it yourself from the WebUI once +you're satisfied, never automatically. + +The pre/post filesystem manifest comparison is the real safety net — it does +not trust qBittorrent's self-reported state at all for the final verdict, +since Incident 3 demonstrated that state can look fine while real files are +gone. If the script reports a mismatch, it exits without attempting any +further remediation; investigate by hand from that known-bad state. For a multi-season show downloaded as separate per-season torrents (e.g. Babylon 5), run `qbt-relink.sh` once per season/torrent, pointing each at its own `Season NN` folder — don't try to relink multiple torrents to one shared show-root folder, since the tool matches file counts 1:1 between the torrent -and the destination folder. +and the destination folder. Kick off separate torrents' relinks as separate +background invocations if you want them running concurrently, but never two +invocations against the *same* hash at once. **Known limitation**: if Sonarr split a single torrent's files across two destination folders (e.g. a season pack that included specials, which Sonarr @@ -144,7 +160,15 @@ or its `state` is `checkingDL` instead of `checkingUP`**, check `content_path` vs `save_path` directly — a mismatch means the recheck is running against the wrong location and will "succeed" at finding nothing. -### Incident 3: qBittorrent's own automatic file management destroyed real data despite every check passing — data loss, `qbt-relink.sh` is now paused +### Incident 3: qBittorrent's own automatic file management destroyed real data despite every check passing + +**Status: fixed and re-verified (2026-09-09)** — see "Current safety model" +above. Root cause turned out to be repeated manual re-triggering (multiple +separate stop/setLocation/recheck calls against the same hash while +debugging), not a flaw in any single check. The rewritten script enforces a +single blocking pass with no re-intervention, and was re-verified end-to-end +against a live 73GB torrent, including catching a real stale-state bug in +its own idempotency check during that testing (see below). 2026-09-08, same overall session, later batch. Even with incidents 1 and 2's fixes in place (`api_call` status checking, torrent stopped first,