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:
@@ -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 "<show name>"
|
||||
```
|
||||
|
||||
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 <hash>
|
||||
```
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user