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
This commit is contained in:
2026-09-09 03:16:58 +00:00
parent 6e93ca6c39
commit a54f11ccbc
+60 -36
View File
@@ -5,19 +5,17 @@ description: Use Sonarr to add TV shows and manually import existing raw-named f
# Sonarr library migration/import # Sonarr library migration/import
**⚠️ `qbt-relink.sh` is currently UNSAFE — do not use it until this warning is **`qbt-relink.sh` was paused after a real data-loss incident (2026-09-08,
removed.** It caused real, apparently unrecoverable data loss (Babylon 5 Babylon 5 S03/S04, 44 episodes — see "Incident 3" below) and has since been
seasons 3 and 4, 44 episodes) on 2026-09-08: qBittorrent's own automatic rewritten with a real safety model and re-verified end-to-end against a live
incomplete-file management fought the script's manual `setLocation` calls 73GB torrent.** Root cause: repeated manual re-triggering (stop/setLocation/
across repeated resume/stop cycles and physically moved/destroyed the real recheck called more than once across separate debugging attempts on the same
files, even after every safety check the script performs (stopped state, hash) raced against qBittorrent's own automatic incomplete-file management.
confirmed `save_path`, confirmed `content_path`) reported success. See The fix isn't a patch on top of the old script - it's a different operating
"Incident 3" below for the full detail. **The Sonarr import workflow itself discipline: **the whole relink is one blocking pass with no manual
(`sonarr.sh scan`/`import`) is unaffected and still safe** — it's specifically re-intervention, ever**. See "Current safety model" below before touching
the qBittorrent-side relink step that's paused. Until this is fixed, leave this again if you're tempted to run raw curl calls against a hash mid-flow -
qBittorrent's stale torrent entries alone after a Sonarr import rather than that impulse is exactly what caused the incident.
touching them — a `missingFiles` error in the qBittorrent UI is a much
smaller problem than what happened here.
This repo's Sonarr manages TV show organization for both Jellyfin and Plex 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 (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 "<show name>" | python3 -c "import sys,json; [print(t['hash'], t['name']) for t in json.load(sys.stdin)]" | grep -i "<show name>"
``` ```
It **stops the torrent first** (qBittorrent 5.x renamed pause/resume to ## Current safety model (post-rewrite)
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:
```bash The script runs the **entire operation as one blocking pass** — it does not
bash ~/.claude/plugins/cache/claude-homelab/homelab-core/*/skills/qbittorrent/scripts/qbit-api.sh info <hash> 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 **The hard rule this exists to enforce: never call `stop`/`setLocation`/
means the recheck found missing or mismatched pieces), start it again from `recheck` against the same torrent hash a second time while a previous
the WebUI. **Never assume a relink succeeded without checking** — see the invocation (of this script, or a raw curl call) might still be settling.**
incident below. 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. 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 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 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 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 **Known limitation**: if Sonarr split a single torrent's files across two
destination folders (e.g. a season pack that included specials, which Sonarr 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 `content_path` vs `save_path` directly — a mismatch means the recheck is
running against the wrong location and will "succeed" at finding nothing. 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 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, fixes in place (`api_call` status checking, torrent stopped first,