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
|
# 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user