Read the following files and report any discrepancies between the documentation and the actual script headers. The goal is to find places where the README or Manual says something that no longer matches what the script header says (wrong schedule, wrong config vars, wrong behavior description, missing scripts, extra scripts, etc.).

Files to read:
1. /boot/config/plugins/varaverk/Media/README-Media.md
2. /boot/config/plugins/varaverk/Media/Manual-Media.md
3. Script headers (top ~120 lines each):
   - /boot/config/plugins/varaverk/Media/arr_sync.sh
   - /boot/config/plugins/varaverk/Media/arr_cleanup.sh
   - /boot/config/plugins/varaverk/Media/sonarr_cleanup.sh
   - /boot/config/plugins/varaverk/Media/radarr_cleanup.sh
   - /boot/config/plugins/varaverk/Media/lidarr_cleanup.sh
   - /boot/config/plugins/varaverk/Media/play_state_sync.sh
   - /boot/config/plugins/varaverk/Media/permissions_repair.sh
   - /boot/config/plugins/varaverk/Media/playback_aware_sonarr_discovery.sh

Report: list each discrepancy as "FILE: what the doc says vs what the header says". If everything matches, say so. Be specific — quote the conflicting text. Keep the report under 500 words.


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


I'll read the documentation files and script headers to identify any discrepancies.


1	# ━━━━━ MEDIA ━━━━━
2
3	Library health, consistency, sync, and behavior-driven discovery for a multi-server arr
4	stack. Correct permissions so arrs can manage files. Junk removal so orphan detection
5	isn't confused by scene debris. Library sync so every node tracks the same content.
6	Orphan cleanup against live arr APIs so deleted content actually leaves disk. Emby
7	notified automatically after every deletion. Weekly discovery adds new music, movies,
8	and TV shows based on what you actually play — no manual browsing required.
9
10	> **These scripts permanently delete files.** The arr cleanup scripts are protected by
11	> multiple safety layers that must all pass before anything is touched — but dry runs and
12	> log review are still the right first step on any new system or after any configuration
13	> change. The testing procedure in Manual-Media.md exists for a reason.
14
15	---
16
17	## ━━━ THE PROBLEM THAT BUILT THIS ━━━
18
19	**Files Owned by Root That Arrs Can't Touch**
20	Download clients without explicit PUID/PGID write files owned by root. Arrs running as
21	`nobody:users` cannot rename, move, or delete them. Import fails. Upgrade attempts fail.
22	The failure is subtle — arr shows the file as managed but can't touch it. You only
23	discover this when an upgrade is requested and the old version refuses to delete.
24	The fix: `media_shares_permissions.sh` applies correct ownership daily. Even if a
25	container is misconfigured, the window is at most 24 hours.
26
27	**Scene Junk Confusing Orphan Detection**
28	Scene releases include `.sfv`, `.nfo`, `.rar`, `.sample` files alongside the actual media.
29	After extraction and import these are worthless — but they're not tracked by any arr.
30	They look like orphans. Processing them as orphans means the cleanup output is full of
31	noise, making it hard to spot actual orphaned media.
32	The fix: `media_cleaner.sh` runs before any arr cleanup and removes all known junk
33	patterns first. By the time arr cleanup runs, every untracked file is actual media.
34
35	**Deleted Shows and Removed Albums Still on Disk**
36	When you remove a series from Sonarr and the delete command fails — permission issue,
37	container wasn't running, path mismatch — the files stay permanently. Over years on an
38	active library this accumulates significantly.
39	The fix: arr cleanup scripts query the live API for every tracked file path, walk the
40	disk, and delete anything absent from the API response that's old enough to be past the
41	import window.
42
43	**Emby Showing Ghost Entries After Cleanup**
44	After arr cleanup deletes files, Emby still shows them until its next scheduled scan —
45	potentially hours later. Users see broken entries that produce "file not found" errors.
46	The fix: `notify_emby_scan()` is called automatically after every deletion. Triggers
47	Emby's "Clean Missing Files" task immediately.
48
49	**No Safety Net on Deletion Size**
50	A misconfigured root path — pointing cleanup at the wrong directory — means the API
51	returns zero tracked files for a root that actually contains thousands. Every file walks
52	as an orphan. Everything gets deleted. This is the catastrophic failure mode.
53	The fix: `LIDARR/SONARR/RADARR_MAX_DELETE_GB` — if total deletion size exceeds the
54	limit, the script stops and requires `--i-know-what-im-doing` to proceed. The flag name
55	is long and annoying by design. It cannot be added by accident.
56
57	---
58
59	## ━━━ WHAT THIS FOLDER DOES ━━━
60
61	Scripts divide into five functional areas:
62
63	**Library Foundation**
64	`media_shares_permissions.sh` — normalize ownership and permissions daily. Runs first in
65	every maintenance window because arr cleanup depends on correct ownership to delete files.
66
67	**Junk Removal**
68	`media_cleaner.sh` — remove scene debris and tool artifacts before orphan scan. Runs
69	second, before any arr cleanup, so orphan detection only encounters actual media files.
70
71	**Library Sync**
72	`arr_sync.sh` — full-mesh arr library sync across all nodes. Every node syncs with every
73	other — union model, no hierarchy. Once arrs agree on what to track, rsync spreads the
74	actual files. The architectural shift from file-first sync to arr-first sync.
75
76	**Orphan Cleanup**
77	`lidarr_cleanup.sh`, `sonarr_cleanup.sh`, `radarr_cleanup.sh` — API-verified orphan
78	removal. Five classification categories (TRACKED/PROTECTED/ORPHAN/JUNK/RECENT), seven
79	safety layers, automatic Emby notification after deletion.
80
81	**Database Hygiene**
82	`radarr_tmdb_removed.sh`, `sonarr_tvdb_removed.sh` — remove entries that upstream
83	databases have dropped (TMDb/TVDB status="deleted"). These generate health warnings in
84	arrs and can never be monitored or downloaded. Most are announced-but-never-released
85	entries. Files are kept by default — most have none.
86
87	**Library Enrichment**
88	`arrs_failed_stalled_recovery.sh` — detect and recover failed imports and stalled
89	downloads across all arrs. Blocklists the bad release and triggers a re-search — hands-
90	free overnight recovery.
91	`lidarr_missing_art.sh` — fetch missing album and artist artwork from fanart.tv and
92	fallback sources. Never overwrites existing files.
93
94	**Discovery**
95	`playback_aware_lidarr_discovery.sh` — behavior-driven music discovery. Scores your
96	Emby play history, runs Last.fm getSimilar on top artists, adds the best matches to
97	Lidarr. 0–5 meaningful adds per week.
98	`playback_aware_radarr_discovery.sh` — behavior-driven movie discovery. Scores recently
99	watched movies, runs TMDB recommendations on seeds, adds top candidates to Radarr.
100	`playback_aware_sonarr_discovery.sh` — behavior-driven TV discovery. Scores recently
101	watched series weighted by user diversity, runs TMDB TV recommendations on seeds, adds
102	top shows to Sonarr. Multi-user design: one person binge-watching does not dominate seeds.
103
104	---
105
106	## ━━━ EXECUTION ORDER ━━━
107
108	Scripts run in multiple contexts — not all are part of the daily maintenance window:
109
110	**Daily via `media_management.sh` (MEDIA_MAINTENANCE_JOBS):**
111
112	```
113	1. media_shares_permissions.sh   — permissions first — arr cleanup depends on this
114	2. media_cleaner.sh anime        — junk before orphan scan
115	3. media_cleaner.sh media
116	4. lidarr_cleanup.sh             — after permissions + clean
117	5. sonarr_cleanup.sh
118	6. radarr_cleanup.sh
119	```
120
121	**Weekly arr sync (before rsync in weekly_sync_maintenance.sh):**
122
123	```
124	arr_sync.sh    — arrs agree on library → rsync then spreads the files
125	```
126
127	**Daily recovery (separate schedule — 5am or every 6hr):**
128
129	```
130	arrs_failed_stalled_recovery.sh
131	```
132
133	**Weekly discovery (WEEKLY_MAINTENANCE_SCRIPTS in master.conf):**
134
135	```
136	playback_aware_lidarr_discovery.sh   — score play history → Last.fm similar → add to Lidarr
137	playback_aware_radarr_discovery.sh   — score watch history → TMDB recommendations → add to Radarr
138	playback_aware_sonarr_discovery.sh   — score episode history → TMDB TV recommendations → add to Sonarr
139	```
140
141	**Ad-hoc or separate schedule:**
142
143	```
144	lidarr_missing_art.sh       — fetch missing artwork
145	radarr_tmdb_removed.sh      — weekly cleanup of TMDb-dropped entries
146	sonarr_tvdb_removed.sh      — weekly cleanup of TVDB-dropped entries
147	```
148
149	**Why permissions before everything else:** arr cleanup needs `nobody:users` ownership to
150	delete files. If a file is `root:root`, deletion fails silently — the file looks processed
151	but stays on disk.
152
153	**Why junk before arr cleanup:** junk files are not tracked by any arr — they look like
154	orphans. Removing them first means orphan detection only finds actual media. Cleaner
155	output, more accurate detection.
156
157	**Why arr_sync before rsync:** once arrs agree on what to track, rsync spreads the actual
158	files. An upgrade on one node — new tracked path, old path no longer in API — gets
159	cleaned by arr_cleanup on all nodes after the next sync cycle.
160
161	---
162
163	## ━━━ HOST AWARENESS ━━━
164
165	Scripts run on both servers via `detect_hosts()`, which aliases all `HOST*_` prefixed vars
166	to their unprefixed names at runtime. No manual `HOST1`/`HOST2` comparisons exist in any script.
167
168	arr_sync.sh keeps all arr databases in bidirectional union — either server can download
169	to any share. Arr cleanup uses the union model: a file is only an orphan if neither arr
170	on either server has it indexed. Arr scripts check the aliased URL — if empty (arr not
171	configured on this host), they exit cleanly with no action.
172
173	Permissions and cleaner scripts run locally against each server's own shares, defined
174	in `HOST*_MEDIA_PERMISSION_SHARES` and `HOST*_MEDIA_CLEAN_FOLDERS` in host*.conf.
175
176	---
177
178	## ━━━ SCRIPTS IN THIS FOLDER ━━━
179
180	| Script | Role | When It Runs |
181	|--------|------|--------------|
182	| `media_shares_permissions.sh` | Apply `nobody:users` ownership + correct permissions to all media shares | Daily via `media_management.sh` |
183	| `media_cleaner.sh` | Remove junk files (two profiles: `anime` + `media`) | Daily via `media_management.sh` |
184	| `arr_sync.sh` | Full-mesh arr library sync — all nodes track the same content | Weekly before rsync |
185	| `lidarr_cleanup.sh` | Delete orphaned music files not tracked by Lidarr | Daily via `media_management.sh` |
186	| `sonarr_cleanup.sh` | Delete orphaned TV files not tracked by Sonarr | Daily via `media_management.sh` |
187	| `radarr_cleanup.sh` | Delete orphaned movie files not tracked by Radarr | Daily via `media_management.sh` |
188	| `arrs_failed_stalled_recovery.sh` | Auto-recover failed imports and stalled downloads | Daily (5am or every 6hr) |
189	| `lidarr_missing_art.sh` | Fetch missing album and artist artwork | Ad-hoc or separate schedule |
190	| `radarr_tmdb_removed.sh` | Remove movies dropped from TMDb | Ad-hoc or weekly |
191	| `sonarr_tvdb_removed.sh` | Remove series dropped from TVDB | Ad-hoc or weekly |
192	| `playback_aware_lidarr_discovery.sh` | Behavior-driven music discovery — Emby plays → Last.fm similar → Lidarr | Weekly via `weekly_sync_maintenance.sh` |
193	| `playback_aware_radarr_discovery.sh` | Behavior-driven movie discovery — Emby watches → TMDB recommendations → Radarr | Weekly via `weekly_sync_maintenance.sh` |
194	| `playback_aware_sonarr_discovery.sh` | Behavior-driven TV discovery — Emby episodes → TMDB TV recommendations → Sonarr | Weekly via `weekly_sync_maintenance.sh` |
195
196	---
197
198	## ━━━ HOW THE SCRIPTS RELATE ━━━
199
200	```
201	Weekly arr sync (before rsync):
202	  arr_sync.sh ──────────────── syncs tracked IDs across all nodes
203	        │                      union model: any node adds → all nodes get it
204	        │
205	        └── then rsync spreads the actual files to all nodes
206	        └── then arr_cleanup removes orphans on all nodes (old paths, removed content)
207
208	Daily maintenance window (media_management.sh):
209	  media_shares_permissions.sh
210	        │ (permissions correct — arr can now delete files)
211	        ▼
212	  media_cleaner.sh (anime + media)
213	        │ (junk removed — orphan scan finds only actual media)
214	        ▼
215	  lidarr_cleanup.sh ──────────── queries Lidarr API → walks /Music → deletes orphans
216	  sonarr_cleanup.sh ──────────── queries Sonarr API → walks /Tv_Shows → deletes orphans
217	  radarr_cleanup.sh ──────────── queries Radarr API → walks /Movies → deletes orphans
218	        │
219	        └── each cleanup → notify_emby_scan() → Emby removes ghost entries
220
221	Daily recovery:
222	  arrs_failed_stalled_recovery.sh ── importFailed/stalled → blocklist → re-search
223
224	Weekly discovery (WEEKLY_MAINTENANCE_SCRIPTS):
225	  playback_aware_lidarr_discovery.sh ─ Emby plays → Last.fm similar → top candidates → Lidarr
226	  playback_aware_radarr_discovery.sh ─ Emby watches → TMDB recommendations → top candidates → Radarr
227	  playback_aware_sonarr_discovery.sh ─ Emby episodes → TMDB TV recommendations → top candidates → Sonarr
228	        │
229	        └── each discovery script fires arr search immediately after successful add
230
231	Ad-hoc enrichment:
232	  lidarr_missing_art.sh ─────── discovers missing artwork → fetches from fanart.tv
233	  radarr_tmdb_removed.sh ────── status="deleted" → remove from Radarr + add exclusion
234	  sonarr_tvdb_removed.sh ────── status="deleted" → remove from Sonarr + add exclusion
235	```
236


1	# ━━━━━ MEDIA — Manual ━━━━━
2
3	Config reference, procedures, operational workflows.
4	For overview see README-Media.md. For per-script detail see script headers.
5
6	---
7
8	## ━━━ PERMISSIONS MODEL ━━━
9
10	```
11	Directories: 755 nobody:users
12	  Owner (nobody) — rwx   enter, list, create files
13	  Group (users)  — r-x   enter and list
14	  Others         — r-x   Samba guests can browse
15	  No world-write — prevents accidental deletion by unauthenticated access
16
17	Files: 664 nobody:users
18	  Owner (nobody) — rw    read + write
19	  Group (users)  — rw    arrs can import, rename, delete
20	  Others         — r     Samba guests can read
21	  No execute bit — media files are never executable
22	```
23
24	**Two separate passes — not a single recursive chmod.** Directories need the execute bit
25	to enter. Files must never have the execute bit. A single `chmod -R 664` would break
26	directory entry. The script runs `find -type d` and `find -type f` separately.
27
28	**If this script corrects many files on every run**, a container has wrong PUID/PGID.
29	Correct values on unRAID: `PUID=99 (nobody)` `PGID=100 (users)`. Add to each container's
30	environment in its Docker template. Common culprits: SABnzbd, qBittorrent, slskd.
31	Once fixed, this script corrects 0 files per run — it becomes a pure daily failsafe.
32
33	---
34
35	## ━━━ ARR CLEANUP — FILE CLASSIFICATION ━━━
36
37	Every file found on disk during an arr cleanup run falls into exactly one category:
38
39	```
40	TRACKED    → arr API returned this exact path                   → leave it alone
41	PROTECTED  → matches ARR_PROTECTED_PATTERNS                     → never delete
42	ORPHAN     → media extension, not tracked, old enough           → delete
43	JUNK       → not a media extension, not protected               → delete (any age)
44	RECENT     → not tracked, under ARR_ORPHAN_AGE days             → skip (may be mid-import)
45	```
46
47	**Why protected patterns are critical:** arrs generate artwork (`*.jpg`), metadata
48	(`*.nfo`), and subtitles/lyrics that do NOT appear in the tracked file API response.
49	Without protection, these would be classified as orphans and deleted — removing cover art
50	from every album, every movie poster, every TV show thumbnail. Requires a full rescan
51	to recover. Never remove artwork extensions from protected patterns.
52
53	---
54
55	## ━━━ ARR CLEANUP — SAFETY LAYERS ━━━
56
57	All 7 layers must pass before any file is touched. There is no way to push through a
58	failed safety check without the explicit override flag.
59
60	```
61	1. Container running + healthy      — a stopped container has an empty API
62	2. API reachable                    — no API = no tracked file list = everything looks orphaned
63	3. API version matches              — major version must match tested version in master.conf
64	4. Item count > 0                   — no artists/series/movies = something is wrong with DB
65	5. Tracked file count > 0           — empty response = everything would be deleted
66	6. Tracked count >= MIN_TRACKED_PCT — dramatic drop from last run = abort and alert
67	7. Deletion size < MAX_DELETE_GB    — last line of defense against misconfigured root path
68	```
69
70	Layer 7 is the catastrophic failure prevention. A misconfigured root path — pointing
71	cleanup at the wrong directory — means the API returns zero tracked files for a root
72	that actually contains thousands. Everything walks as an orphan. Everything gets deleted.
73	`LIDARR/SONARR/RADARR_MAX_DELETE_GB` requires `--i-know-what-im-doing` to proceed past it.
74
75	---
76
77	## ━━━ CONFIGURATION — master.conf ━━━
78
79	### Permissions
80
81	```bash
82	PERMISSIONS_DIR_MODE="755"
83	PERMISSIONS_FILE_MODE="664"
84	PERMISSIONS_OWNER="nobody:users"
85	```
86
87	---
88
89	### Media Cleaner — File Patterns
90
91	```bash
92	ANIME_FILE_PATTERNS=(
93	    "*.sfv"             # checksum verification — useless after download verified
94	    "*.md5" "*.sha1"    # other checksum formats
95	    "*.nfo"             # scene info file — not library metadata
96	    "*.url" "*.lnk"     # website shortcuts
97	    "*.rar" "*.zip"     # source archives kept by some clients after extraction
98	    "*.info"            # tool output files
99	    "*.torrent"         # torrent descriptor left by some clients
100	    "*.sample*"         # scene preview clip
101	    "*.proof*"          # screenshot proving encode quality
102	    "*sync-conflict*"   # Syncthing conflict copies
103	    "*.scr" "*.exe"     # executables — should never be in a media folder
104	    "*.srr"             # scene recovery record
105	    "*.log"             # tool/client logs
106	    "*.json"            # metadata or tool output
107	)
108
109	MEDIA_FILE_PATTERNS=(
110	    "${ANIME_FILE_PATTERNS[@]}"   # all anime patterns plus:
111	    "*.iso"                       # disc images after ripping
112	    "*.lrc"                       # lyric files in media folders
113	)
114	```
115
116	> **DO NOT add patterns that match media you want to keep:**
117	> `*.mkv *.mp4 *.avi *.m4v` — video files
118	> `*.flac *.mp3 *.m4a` — audio files
119	> `*.srt *.sub *.ass` — subtitle files (Bazarr managed)
120	> `*.jpg *.png` — artwork
121	> Always use `--dry-run` when adding new patterns.
122
123	---
124
125	### Lidarr Cleanup Thresholds
126
127	```bash
128	LIDARR_ORPHAN_AGE=7              # days — files newer than this are RECENT (mid-import window)
129	LIDARR_MIN_TRACKED_PCT=80        # abort if API returns < 80% of last known count
130	LIDARR_MAX_DELETE_GB=50          # require --i-know-what-im-doing above this
131	LIDARR_IMPORT_SCAN_TIMEOUT=600   # seconds to wait for pre-flight import scan
132	LIDARR_VERSION_MAJOR=3           # expected Lidarr major version (API safety check)
133	LIDARR_EXTENSIONS=("flac" "mp3" "m4a" "wav" "aac" "ogg" "opus" "wma")
134	LIDARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.lrc")
135	LIDARR_TRACKED_COUNT_FILE=/boot/config/lidarr_tracked_count  # persistent baseline
136	ARR_CLEANUP_STATS=/boot/config/arr_cleanup_stats.db          # read by coffee report
137	```
138
139	---
140
141	### Sonarr Cleanup Thresholds
142
143	```bash
144	SONARR_ORPHAN_AGE=7
145	SONARR_MAX_DELETE_GB=50
146	SONARR_IMPORT_SCAN_TIMEOUT=600
147	SONARR_VERSION_MAJOR=4
148	SONARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "ts" "wmv" "mov")
149	SONARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.srt" "*.sub" "*.ass" "*.ssa")
150	```
151
152	Note: `*.ts` IS in extensions — transport stream is used for Live TV recordings tracked
153	by Sonarr. Orphaned `.ts` recordings should be cleaned like any other orphaned episode.
154
155	---
156
157	### Radarr Cleanup Thresholds
158
159	```bash
160	RADARR_ORPHAN_AGE=7
161	RADARR_MAX_DELETE_GB=50
162	RADARR_IMPORT_SCAN_TIMEOUT=600
163	RADARR_VERSION_MAJOR=6
164	RADARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "wmv" "mov")
165	RADARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.srt" "*.sub" "*.ass" "*.ssa")
166	```
167
168	---
169
170	### Arr Sync
171
172	```bash
173	ARR_SYNC_ENABLED=true
174	ARR_SYNC_BLOCKLIST=/boot/config/arr_sync_blocklist.tsv  # tombstone file
175	ARR_SYNC_CONNECT_TIMEOUT=10       # SSH connect timeout in seconds
176	ARR_SYNC_API_TIMEOUT=60           # curl API call timeout in seconds
177	DOCKER_APPDATA_BASE=/mnt/user/appdata
178	ARR_SYNC_LIDARR_PORT=8686
179	ARR_SYNC_SONARR_PORT=8989
180	ARR_SYNC_RADARR_PORT=7878
181	```
182
183	---
184
185	### Arr Recovery
186
187	```bash
188	ARR_IMPORT_RECOVERY_AGE=6        # hours — items newer than this are skipped
189	SONARR_VERSION_MAJOR=4
190	RADARR_VERSION_MAJOR=6
191	LIDARR_VERSION_MAJOR=3
192	ARR_RECOVERY_STATS=/boot/config/arr_recovery_stats.db   # read by coffee report
193	```
194
195	---
196
197	### TMDb / TVDB Removed
198
199	```bash
200	RADARR_DROPPED_ADD_EXCLUSION=true   # add removed movies to Radarr import exclusion
201	SONARR_DROPPED_ADD_EXCLUSION=true   # add removed series to Sonarr import exclusion
202	```
203
204	---
205
206	### Lidarr Missing Art
207
208	```bash
209	FANART_API_KEY="your-fanart-tv-api-key"
210	LASTFM_API_KEY="your-lastfm-api-key"
211	LIDARR_ART_MIN_SIZE=5000          # minimum valid download size in bytes
212	LIDARR_ART_MAX_PARALLEL=4         # concurrent background download jobs
213	LIDARR_ART_RETRIES=2              # download retry attempts per image
214	LIDARR_ART_SLEEP_BETWEEN=1        # seconds between fanart.tv API calls (rate limit)
215	```
216
217	---
218
219	### Lidarr Discovery
220
221	```bash
222	LIDARR_DISCOVERY_THRESHOLD=70         # minimum score for Stage 1 seeds and Stage 2 adds
223	LIDARR_DISCOVERY_LOOKBACK_DAYS=7      # Emby play history window in days
224	LIDARR_DISCOVERY_MIN_PLAYS=3          # min plays before an artist is evaluated as a seed
225	LIDARR_DISCOVERY_MAX_ADDS=5           # max seeds (Stage 1) and max adds (Stage 2) per run
226	LIDARR_DISCOVERY_USER_CAP_PCT=35      # max % any one user contributes to play weight
227	LIDARR_DISCOVERY_REJECT_COOLDOWN=30   # days before re-evaluating a Stage 2 reject
228	LIDARR_DISCOVERY_HISTORY="$DATA_DIR/lidarr_discovery_history.db"
229	```
230
231	Requires `HOST*_LASTFM_API_KEY` in `host*.conf`.
232
233	---
234
235	### Radarr Discovery
236
237	```bash
238	RADARR_DISCOVERY_THRESHOLD=52         # minimum score to add a candidate
239	RADARR_DISCOVERY_LOOKBACK_DAYS=30     # Emby watch history window in days
240	RADARR_DISCOVERY_MAX_SEEDS=5          # max seed movies from Stage 1
241	RADARR_DISCOVERY_MAX_ADDS=5           # max movies to add per run
242	RADARR_DISCOVERY_MIN_VOTE_COUNT=100   # min TMDB votes for a candidate
243	RADARR_DISCOVERY_MIN_RATING=60        # min TMDB vote_average × 10 (60 = 6.0/10)
244	RADARR_DISCOVERY_REJECT_COOLDOWN=60   # days before re-evaluating a rejected movie
245	RADARR_DISCOVERY_SEED_LIBRARIES=("Movies")   # Emby libraries to draw seeds from
246	RADARR_DISCOVERY_HISTORY="$DATA_DIR/radarr_discovery_history.db"
247	```
248
249	Requires `HOST*_TMDB_API_KEY` in `host*.conf`.
250
251	---
252
253	### Sonarr Discovery
254
255	```bash
256	SONARR_DISCOVERY_THRESHOLD=52         # minimum score to add a candidate
257	SONARR_DISCOVERY_LOOKBACK_DAYS=14     # Emby episode history window in days
258	SONARR_DISCOVERY_MAX_SEEDS=5          # max seed series from Stage 1
259	SONARR_DISCOVERY_MAX_ADDS=3           # max shows to add per run (TV is a larger commitment)
260	SONARR_DISCOVERY_MIN_VOTE_COUNT=50    # min TMDB votes for a candidate
261	SONARR_DISCOVERY_MIN_RATING=65        # min TMDB vote_average × 10 (65 = 6.5/10)
262	SONARR_DISCOVERY_REJECT_COOLDOWN=60   # days before re-evaluating a rejected show
263	SONARR_DISCOVERY_USER_EPISODE_CAP=8   # max episodes per user in seed scoring
264	SONARR_DISCOVERY_MONITOR_MODE="all"   # "all" = all seasons monitored; "future" = upcoming only
265	SONARR_DISCOVERY_HISTORY="$DATA_DIR/sonarr_discovery_history.db"
266	# SONARR_EMBY_LIBRARIES is shared with emby_to_sonarr_sync — see Orchestrator Job Order
267	```
268
269	Requires `HOST*_TMDB_API_KEY` in `host*.conf`.
270
271	> **MONITOR_MODE note:** Use `"all"` (default) to have Sonarr search all existing seasons
272	> after adding a show. `"future"` only marks upcoming seasons as monitored — shows where
273	> all seasons have already aired will appear unmonitored and Sonarr will not search for them.
274
275	---
276
277	### Orchestrator Job Order
278
279	```bash
280	MEDIA_MAINTENANCE_JOBS=(
281	    "Media/media_shares_permissions.sh"   # 1. permissions — always first
282	    "Media/media_cleaner.sh anime"        # 2. junk removal — before orphan scan
283	    "Media/media_cleaner.sh media"        # 3.
284	    "Media/lidarr_cleanup.sh"             # 4. arr cleanup — after permissions + clean
285	    "Media/sonarr_cleanup.sh"             # 5.
286	    "Media/radarr_cleanup.sh"             # 6.
287	)
288	```
289
290	---
291
292	## ━━━ CONFIGURATION — host*.conf ━━━
293
294	### host1.conf
295
296	```bash
297	# Shares this server applies permissions to
298	HOST1_MEDIA_PERMISSION_SHARES=(
299	    "/mnt/user/Movies"
300	    "/mnt/user/Tv_Shows"
301	    "/mnt/user/Music"
302	    "/mnt/user/Kids_Movies"
303	    "/mnt/user/Kids_Tv_Shows"
304	    "/mnt/user/Sports"
305	    "/mnt/user/stand-up_comedy"
306	)
307
308	# Folders cleaned by each profile
309	HOST1_ANIME_CLEAN_FOLDERS=(
310	    "/mnt/user/Anime_Movies"
311	    "/mnt/user/Anime_Movies-Old"
312	    "/mnt/user/Anime_Shows"
313	    "/mnt/user/Anime_Shows-Old"
314	)
315	HOST1_MEDIA_CLEAN_FOLDERS=(
316	    "/mnt/user/Kids_Movies"
317	    "/mnt/user/Kids_Tv_Shows"
318	    "/mnt/user/Movies"
319	    "/mnt/user/Music"
320	    "/mnt/user/Sports"
321	    "/mnt/user/stand-up_comedy"
322	    "/mnt/user/Tv_Shows"
323	)
324
325	# Arr connection details — must match arr settings exactly
326	HOST1_LIDARR_URL="http://192.168.50.2:8686"
327	HOST1_LIDARR_API_KEY="..."
328	HOST1_LIDARR_MUSIC_ROOT="/mnt/user/Music"
329	HOST1_LIDARR_PATH_MAP=""   # container→host path translation if needed
330
331	HOST1_SONARR_URL="http://192.168.50.2:8989"
332	HOST1_SONARR_API_KEY="..."
333	HOST1_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
334	HOST1_SONARR_PATH_MAP=""
335
336	HOST1_RADARR_URL="http://192.168.50.2:7878"
337	HOST1_RADARR_API_KEY="..."
338	HOST1_RADARR_MOVIES_ROOT="/mnt/user/Movies"
339	HOST1_RADARR_PATH_MAP=""
340
341	HOST1_EMBY_URL="http://192.168.50.2:8096"
342	HOST1_EMBY_API_KEY="..."
343	```
344
345	> **LIDARR/SONARR/RADARR_MUSIC/TV/MOVIES_ROOT must exactly match the Root Folder path in
346	> the arr's own settings.** Arr UI → Settings → Media Management → Root Folders.
347	> A mismatch means every file on disk looks untracked — all appear as orphans.
348	> MAX_DELETE_GB is the only thing standing between a path mismatch and losing your library.
349
350	---
351
352	## ━━━ SAFE TESTING PROCEDURE ━━━
353
354	> **The arr cleanup scripts permanently delete files.** There is no recycle bin, no undo.
355	> Follow this procedure on first use, after any root path change, after any API key change,
356	> and after any significant arr library change.
357
358	### Step 1 — Dry Run With Full Logging
359
360	```bash
361	lidarr_cleanup.sh --dry-run --log
362	sonarr_cleanup.sh --dry-run --log
363	radarr_cleanup.sh --dry-run --log
364	```
365
366	### Step 2 — Review the Output
367
368	```
369	Are TRACKED files the ones you expect?
370	  → Known arr-managed files should show as TRACKED
371	  → If they show as ORPHAN, the root path is wrong — STOP
372
373	Is the ORPHAN count reasonable?
374	  → Healthy cleanup removes dozens to hundreds, not tens of thousands
375	  → Large count = stop, investigate root path before proceeding
376
377	Are PROTECTED patterns working?
378	  → Artwork (*.jpg) and subtitles (*.srt) must show as PROTECTED
379	  → If they show as ORPHAN, check PROTECTED_PATTERNS config
380
381	Are RECENT files being correctly skipped?
382	  → Files downloaded in the last 7 days should show as RECENT, not ORPHAN
383	```
384
385	### Step 3 — Check Numbers if Something Looks Wrong
386
387	```bash
388	# Root path mismatch? Compare these:
389	# Lidarr UI: Settings → Media Management → Root Folders
390	# Sonarr UI: Settings → Media Management → Root Folders
391	# Radarr UI: Settings → Media Management → Root Folders
392	# Must exactly match LIDARR_MUSIC_ROOT / SONARR_TV_ROOT / RADARR_MOVIES_ROOT
393
394	# Is the arr running?
395	docker ps | grep -E "Lidarr|Sonarr|Radarr"
396
397	# Library scan not complete?
398	# Trigger manual scan in arr UI and wait for completion
399	```
400
401	### Step 4 — Run Live
402
403	```bash
404	# Only after dry run review passes.
405	lidarr_cleanup.sh
406	sonarr_cleanup.sh
407	radarr_cleanup.sh
408	```
409
410	### Step 5 — Verify in Arr UI
411
412	```
413	Library count  — should not have dropped significantly
414	                 healthy cleanup removes a small number, not a large percentage
415	Missing files  — check if any monitored content shows as missing
416	Emby library   — should show no ghost entries (notify_emby_scan handles this automatically)
417	```
418
419	---
420
421	## ━━━ PROCEDURES ━━━
422
423	### Adding a New Arr
424
425	```bash
426	# 1. Copy radarr_cleanup.sh as template
427	cp radarr_cleanup.sh readarr_cleanup.sh
428
429	# 2. Replace RADARR_ prefix with READARR_ throughout
430	#    Update API endpoint, tracked file API path, extension list, protected patterns
431
432	# 3. Add to host*.conf
433	HOST1_READARR_URL="http://192.168.50.2:8787"
434	HOST1_READARR_API_KEY="your-api-key"
435	HOST1_READARR_BOOKS_ROOT="/mnt/user/Books"
436
437	# 4. Add thresholds to master.conf
438	READARR_ORPHAN_AGE=7
439	READARR_MAX_DELETE_GB=50
440	READARR_EXTENSIONS=("epub" "pdf" "mobi" "azw3" "cbz" "cbr")
441	READARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo")
442
443	# 5. Add to MEDIA_MAINTENANCE_JOBS in master.conf
444	MEDIA_MAINTENANCE_JOBS=(
445	    ...existing jobs...
446	    "Media/readarr_cleanup.sh"
447	)
448	```
449
450	media_management.sh picks it up automatically. No orchestrator changes needed.
451	Run `--dry-run --log` before scheduling.
452
453	---
454
455	### Managing the Arr Sync Blocklist
456
457	```bash
458	# Add item to blocklist (removes from all arrs + tombstones the ID)
459	arr_sync.sh --blocklist-add lidarr <musicbrainz-artist-id> "reason"
460	arr_sync.sh --blocklist-add sonarr <tvdb-series-id> "reason"
461	arr_sync.sh --blocklist-add radarr <tmdb-movie-id> "reason"
462
463	# Remove from blocklist (un-tombstones the ID — does NOT re-add to arrs)
464	arr_sync.sh --blocklist-remove lidarr <id>
465
466	# View all blocklisted IDs
467	arr_sync.sh --blocklist-list
468	```
469
470	`--blocklist-add` is the only destructive operation — it simultaneously:
471	1. Writes the tombstone entry to the blocklist TSV file
472	2. Deletes the item from the local arr API (no file deletion)
473	3. SSHes each remote node and deletes from their arr API
474
475	Files become orphans on all nodes — arr_cleanup removes them on the next run.
476
477	---
478
479	## ━━━ TROUBLESHOOTING ━━━
480
481	### Arr Cleanup Deleting Files It Shouldn't
482
483	```
484	1. Check the protected patterns — artwork and subtitles must be listed
485	   LIDARR_PROTECTED_PATTERNS / SONARR_PROTECTED_PATTERNS / RADARR_PROTECTED_PATTERNS
486
487	2. Check the root path matches arr settings exactly
488	   Run: lidarr_cleanup.sh --status   (shows configured root path)
489	   Compare: Lidarr UI → Settings → Media Management → Root Folders
490
491	3. Check if files are truly orphaned
492	   Run: lidarr_cleanup.sh --dry-run --log
493	   Look for the specific file — verify it shows ORPHAN, not PROTECTED or TRACKED
494	```
495
496	### Arr Cleanup Aborting at Safety Layer 6 (Tracked Count Drop)
497
498	```
499	API returned far fewer tracked files than last run.
500	Possible causes:
501	  - Arr database was recently rebuilt from scratch
502	  - Large manual library removal
503	  - Path map mismatch after arr migration
504
505	If intentional (library intentionally reduced):
506	  Delete LIDARR_TRACKED_COUNT_FILE to reset the baseline
507	  Run cleanup once — it will establish a new baseline
508
509	If unintentional:
510	  Investigate before proceeding — the arr may have a problem
511	```
512
513	### Arr Sync Not Picking Up New Content
514
515	```
516	Is ARR_SYNC_ENABLED=true in master.conf?
517
518	Can this host SSH to the remote without password?
519	  → ssh -i [SSH_KEY] root@[remote-tailscale-ip] "hostname"
520
521	Is the arr accessible on the remote?
522	  → arr_sync.sh --status  (shows each node's arr reachability)
523	  → arr_sync.sh --log     (verbose output per-node, per-arr)
524
525	Is the item in the blocklist?
526	  → arr_sync.sh --blocklist-list
527	```
528
529	### Emby Still Showing Ghost Entries After Cleanup
530
531	```
532	notify_emby_scan() is called automatically after every arr cleanup deletion.
533	If ghosts persist:
534	  1. Is Emby's API responding?
535	     curl -s "http://[emby-ip]:8096/System/Info/Public"
536
537	  2. Is EMBY_URL / EMBY_API_KEY correct in host*.conf?
538	     Run: sonarr_cleanup.sh --status  (shows Emby config)
539
540	  3. Trigger manually in Emby:
541	     Library → Manage Library → Clean Missing Files
542	```
543
544	---
545
546	## ━━━ OUTPUT TIERS ━━━
547
548	All scripts have two output levels controlled by `--log`.
549
550	Without `--log`, each script processes silently and always concludes with a summary
551	block: identity, duration, counts (files removed, items added, arrs cleaned), and a
552	status line. Warnings and errors are always visible.
553
554	With `--log`, per-item detail appears: individual titles being processed, API query
555	progress, per-node sync results, and per-file examination output. Use this when
556	debugging unexpected results or validating configuration before the first scheduled run.
557
558	Dry-run output follows the same tiers — `--dry-run` alone shows the summary of what
559	would happen; `--dry-run --log` shows the full per-item preview list.
560
561	---
562
563	## ━━━ FLAG REFERENCE ━━━
564
565	### media_shares_permissions.sh
566
567	`media_shares_permissions.sh`
568	Apply correct ownership and permissions to all configured media shares. Safe to run
569	manually at any time — idempotent, only changes what's wrong.
570
571	`media_shares_permissions.sh --dry-run`
572	Show how many files and directories would be corrected per share. If unexpectedly large,
573	check container PUID/PGID settings first (PUID=99 PGID=100).
574
575	`media_shares_permissions.sh --status`
576	Show configured share list and ownership of the share roots.
577
578	`media_shares_permissions.sh --log`
579	Show ownership correction count per share and per file (verbose).
580
581	> On large libraries this runs 20-30 minutes. This is expected — millions of files with
582	> recursive walk takes time. Designed to run overnight in the maintenance window.
583
584	---
585
586	### media_cleaner.sh
587
588	`media_cleaner.sh anime`
589	Remove junk files from anime share folders using ANIME_FILE_PATTERNS.
590
591	`media_cleaner.sh media`
592	Remove junk files from media share folders using MEDIA_FILE_PATTERNS.
593
594	`media_cleaner.sh [profile] --dry-run`
595	Show what would be deleted without removing anything. Always run first when adding new
596	patterns or folders.
597
598	`media_cleaner.sh [profile] --status`
599	Show folder list and file patterns for the profile.
600
601	`media_cleaner.sh [profile] --log`
602	Show every file examined, not just those removed.
603
604	---
605
606	### lidarr_cleanup.sh / sonarr_cleanup.sh / radarr_cleanup.sh
607
608	`[script] --dry-run --log`
609	Preview every classification decision. **Always run this first.** See Safe Testing Procedure.
610
611	`[script]`
612	Live run — deletes confirmed orphans and junk, triggers Emby clean.
613
614	`[script] --log`
615	Live run with verbose per-file output.
616
617	`[script] --status`
618	Show configuration, API status, tracked file count, and last run stats.
619
620	`[script] --i-know-what-im-doing`
621	Bypass the MAX_DELETE_GB size threshold. Required when deletion exceeds the configured
622	limit. Long flag name is intentional — cannot be added accidentally.
623
624	`[script] --skip-strike-list`
625	Bypass the ORPHAN_AGE age check. Deletes RECENT files too — files that are under the
626	age threshold. Use when you know recent downloads are actually orphans.
627
628	`[script] --i-know-what-im-doing --skip-strike-list`
629	**NUCLEAR MODE** — age check and size threshold both bypassed. Deletes on first pass.
630	Use when you want a clean one-pass wipe of everything the arr doesn't track.
631	No recovery possible after deletion.
632
633	---
634
635	### arrs_failed_stalled_recovery.sh
636
637	`arrs_failed_stalled_recovery.sh`
638	Check all configured arrs for failed imports and stalled downloads. Blocklist + remove +
639	re-search for each problem item.
640
641	`arrs_failed_stalled_recovery.sh --dry-run`
642	Show what would be actioned per arr without making any changes.
643
644	`arrs_failed_stalled_recovery.sh --status`
645	Show configuration, arr reachability, and last recovery stats.
646
647	`arrs_failed_stalled_recovery.sh --log`
648	Verbose output per item per arr.
649
650	---
651
652	### arr_sync.sh
653
654	`arr_sync.sh`
655	Sync all arr types across all configured nodes.
656
657	`arr_sync.sh --dry-run`
658	Show what would be added/removed on each node without making changes.
659
660	`arr_sync.sh --status`
661	Show node configuration, arr reachability, and blocklist count.
662
663	`arr_sync.sh --log`
664	Verbose per-node, per-arr output.
665
666	`arr_sync.sh --blocklist-add [arr] [id] "[reason]"`
667	Remove item from all arrs and tombstone the ID. See Procedures above.
668
669	`arr_sync.sh --blocklist-remove [arr] [id]`
670	Remove tombstone — does NOT re-add item to arrs.
671
672	`arr_sync.sh --blocklist-list`
673	Show all tombstoned IDs.
674
675	---
676
677	### radarr_tmdb_removed.sh / sonarr_tvdb_removed.sh
678
679	`[script]`
680	Remove records for entries with status="deleted" (dropped from upstream database).
681	Files are kept. Import exclusion is added.
682
683	`[script] --delete-files`
684	Also delete associated files from disk. Most dropped entries have no files — they were
685	announced movies/series that were never downloaded.
686
687	`[script] --dry-run`
688	Preview what would be removed without making changes.
689
690	`[script] --status`
691	Show arr connection status and current count of dropped entries.
692
693	`[script] --log`
694	Verbose per-entry output.
695
696	---
697
698	### lidarr_missing_art.sh
699
700	`lidarr_missing_art.sh`
701	Fetch all missing album and artist artwork from fanart.tv and fallback sources.
702	Never overwrites existing files.
703
704	`lidarr_missing_art.sh --dry-run`
705	Show what would be downloaded without writing any files.
706
707	`lidarr_missing_art.sh --status`
708	Show configuration and API key status.
709
710	`lidarr_missing_art.sh --log`
711	Verbose per-album, per-artist output.
712
713	---
714
715	### playback_aware_lidarr_discovery.sh
716
717	`playback_aware_lidarr_discovery.sh`
718	Score Emby play history, run Last.fm getSimilar on top artists, add candidates above
719	threshold to Lidarr. Triggers ArtistSearch immediately after each successful add.
720
721	`playback_aware_lidarr_discovery.sh --dry-run`
722	Score and rank all Stage 1 seeds and Stage 2 candidates. No Lidarr API calls. No writes
723	to history file. Shows exactly what would be added and at what score.
724
725	`playback_aware_lidarr_discovery.sh --status`
726	Show config values, history file path and size, and API key status.
727
728	`playback_aware_lidarr_discovery.sh --log`
729	Verbose per-artist scoring output for both stages.
730
731	---
732
733	### playback_aware_radarr_discovery.sh
734
735	`playback_aware_radarr_discovery.sh`
736	Score recently watched Emby movies, run TMDB recommendations on seeds, add candidates
737	above threshold to Radarr. Triggers MoviesSearch immediately after each successful add.
738
739	`playback_aware_radarr_discovery.sh --dry-run`
740	Score and rank all Stage 1 seeds and Stage 2 candidates. No Radarr API calls. No writes
741	to history file.
742
743	`playback_aware_radarr_discovery.sh --status`
744	Show config values, history file path and size, and API key status.
745
746	`playback_aware_radarr_discovery.sh --log`
747	Verbose per-movie scoring output for both stages.
748
749	---
750
751	### playback_aware_sonarr_discovery.sh
752
753	`playback_aware_sonarr_discovery.sh`
754	Score recently watched Emby series (weighted by user diversity), run TMDB TV
755	recommendations on seeds, add candidates above threshold to Sonarr. Triggers SeriesSearch
756	immediately after each successful add.
757
758	`playback_aware_sonarr_discovery.sh --dry-run`
759	Score and rank all Stage 1 seeds and Stage 2 candidates. No Sonarr API calls. No writes
760	to history file.
761
762	`playback_aware_sonarr_discovery.sh --status`
763	Show config values, history file path and size, and API key status.
764
765	`playback_aware_sonarr_discovery.sh --log`
766	Verbose per-series scoring output — shows user diversity, recency, and volume scores per
767	seed; breadth, rating, and votes scores per candidate.
768


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= ARR Sync ===================================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Full-mesh arr library sync across all nodes — Lidarr, Sonarr, and Radarr.
9	# Every node syncs with every other, union model, no hierarchy. Run before
10	# rsync in the weekly sync window: once arrs agree on what to track, rsync
11	# spreads the actual files.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# Full mesh: every node syncs with every other — no primary, no hierarchy.
18	# Union model: if any node tracks an item, all nodes get it (unless blocklisted).
19	# Convergence: any node can add content; after one full cycle all nodes agree.
20	# Upgrade-aware: a file upgrade on one node → arr tracks new path → rsync spreads
21	#   it → arr_cleanup removes old path on all nodes (arr no longer tracks it).
22	#
23	# Node discovery: reads HOST* vars from master.conf. Add HOST3= and it joins the
24	# sync automatically — no script changes needed for a new node.
25	#
26	# Graceful skip: arr not configured locally → skip cleanly. Arr not reachable on
27	# a remote → skip that node for that arr type, continue with others.
28	#
29	# What gets synced — library items keyed on stable external IDs:
30	#   Lidarr  — MusicBrainz artist ID (foreignArtistId)
31	#   Sonarr  — TVDB series ID (tvdbId)
32	#   Radarr  — TMDB movie ID (tmdbId)
33	# When adding to a remote, that node's own quality profile, metadata profile,
34	# and root folder path are used — settings are never copied from source.
35	#
36	# ==============================================================================================
37	# DESIGN PRINCIPLES
38	# ==============================================================================================
39	#
40	# Remote API Access — Cache-First, SSH Fallback
41	#   If conf_sync.sh has populated /tmp/.vv/config/cached/.confs/ and
42	#   load_config.sh has sourced it, HOST*_<ARR>_API_KEY vars are available
43	#   in the environment. Remote functions use them to call the arr API
44	#   directly over Tailscale (no SSH, no remote shell). If the cached key
45	#   is absent (first boot, cache not yet populated) the functions fall back
46	#   to SSHing in and reading the key from config.xml on the remote node.
47	#
48	# Blocklist TSV
49	#   ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added
50	#   anywhere. Read from ALL nodes via SSH at run start — immediate effect with
51	#   no rsync delay.
52	#
53	#   --blocklist-add does three things atomically:
54	#     1. Writes the TSV tombstone entry (prevents future re-adds by arr_sync)
55	#     2. Deletes the item from the local arr API (deleteFiles=false)
56	#     3. SSHes each remote node and deletes from their arr API (deleteFiles=false)
57	#   Files become orphans on all nodes — arr_cleanup removes them on next run.
58	#
59	# ==============================================================================================
60	# OPERATIONAL SAFEGUARDS
61	# ==============================================================================================
62	#
63	#   acquire_lock         — prevents two sync instances running simultaneously
64	#   ARR_SYNC_ENABLED     — global gate, exits cleanly when false
65	#   SSH connect timeout  — ARR_SYNC_CONNECT_TIMEOUT — does not hang on unreachable node
66	#   API call timeout     — ARR_SYNC_API_TIMEOUT — does not hang on slow arr
67	#   Graceful skip        — unreachable node/arr → skip and continue, never abort
68	#   Blocklist gate       — item in blocklist → never added to any node
69	#   Silent by default    — only additions produce output, clean runs stay silent
70	#
71	# ==============================================================================================
72	# STATE FILES
73	# ==============================================================================================
74	#
75	#   ARR_SYNC_BLOCKLIST — TSV file in DATA_DIR (default: DATA_DIR/arr_sync_blocklist.tsv)
76	#       Columns: arr_type, id, reason, date_added
77	#       Read from all nodes via SSH at the start of each run.
78	#
79	# ==============================================================================================
80	# CONFIGURATION
81	# ==============================================================================================
82	#
83	# host*.conf
84	#
85	#   HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — local Lidarr (aliased by detect_hosts)
86	#   HOST*_SONARR_URL / HOST*_SONARR_API_KEY — local Sonarr
87	#   HOST*_RADARR_URL / HOST*_RADARR_API_KEY — local Radarr
88	#
89	# master.conf
90	#
91	#   ARR_SYNC_ENABLED         — global on/off toggle (default: true)
92	#   ARR_SYNC_BLOCKLIST       — path to TSV blocklist file
93	#   ARR_SYNC_CONNECT_TIMEOUT — SSH connect timeout in seconds (default: 10)
94	#   ARR_SYNC_API_TIMEOUT     — curl API call timeout in seconds (default: 60)
95	#   DOCKER_APPDATA_BASE      — base path for arr appdata dirs (default: /mnt/user/appdata)
96	#   ARR_SYNC_LIDARR_PORT     — Lidarr port on all nodes (default: 8686)
97	#   ARR_SYNC_SONARR_PORT     — Sonarr port on all nodes (default: 8989)
98	#   ARR_SYNC_RADARR_PORT     — Radarr port on all nodes (default: 7878)
99	#
100	# ==============================================================================================
101	# RUNTIME MODES
102	# ==============================================================================================
103	#
104	#   arr_sync.sh                                            — sync all arr types, all nodes
105	#   arr_sync.sh --dry-run                                  — preview only, no changes
106	#   arr_sync.sh --log                                      — verbose output
107	#   arr_sync.sh --status                                   — show config and exit
108	#
109	# Blocklist management:
110	#   arr_sync.sh --blocklist-add lidarr <mbid> "reason"    — remove from all arrs + tombstone
111	#   arr_sync.sh --blocklist-add sonarr <tvdbId> "reason"  — remove from all arrs + tombstone
112	#   arr_sync.sh --blocklist-add radarr <tmdbId> "reason"  — remove from all arrs + tombstone
113	#   arr_sync.sh --blocklist-remove lidarr <id>            — un-tombstone (does NOT re-add)
114	#   arr_sync.sh --blocklist-list                          — show all blocklisted IDs
115	#
116	# ==============================================================================================
117
118	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
119	source "$SCRIPT_DIR/../load_config.sh"
120


Error: File does not exist. Note: your current working directory is /boot/config/plugins/varaverk.


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Sonarr Cleanup =============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Delete orphaned TV episode files not tracked by Sonarr. Queries the API for
9	# all tracked episode file paths, walks the library on disk, and removes anything
10	# untracked that is old enough to be past the import window. Triggers an Emby
11	# library clean after each deletion run so ghost entries disappear immediately.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# Every file encountered on disk is classified into one of five categories:
18	#
19	#   TRACKED   — Sonarr API knows this exact path → leave it alone
20	#   PROTECTED — matches SONARR_PROTECTED_PATTERNS → never delete
21	#   ORPHAN    — video file, not tracked, older than SONARR_ORPHAN_AGE → delete
22	#   JUNK      — not a video extension, not protected → delete regardless of age
23	#   RECENT    — not tracked, under SONARR_ORPHAN_AGE → skip (may be mid-import)
24	#
25	# Sonarr generates show artwork (*.jpg), metadata (*.nfo), and manages subtitles
26	# (*.srt, *.sub, *.ass) but does NOT include these in its tracked file API response.
27	# Without PROTECTED classification these would be deleted — breaking Sonarr and
28	# Emby metadata display.
29	#
30	# After deletions: notify_emby_scan() triggers Emby "Clean Missing Files" task.
31	# Emby removes ghost entries immediately — no user-facing file-not-found errors.
32	#
33	# ==============================================================================================
34	# OPERATIONAL SAFEGUARDS
35	# ==============================================================================================
36	#
37	# Six gates — ALL must pass before any file is touched:
38	#   1. Container running and not starting/unhealthy
39	#   2. API reachable
40	#   3. API version matches SONARR_VERSION_MAJOR in master.conf
41	#   4. Series count > 0
42	#   5. Tracked file count > 0
43	#   6. Deletion size < SONARR_MAX_DELETE_GB — or --i-know-what-im-doing required
44	#
45	#   acquire_lock "wait"  — large scans take time, wait for previous run to finish
46	#   jq + curl validation — exits if either tool missing
47	#   DOCKER_TIMEOUT       — container checks protected against daemon hangs
48	#   notify_emby_scan()   — triggers Emby clean after deletion
49	#   platform_require_cmd  — notify script validated before use
50	#   Silent by default    — orphans/junk warn(), clean library logs silently
51	#
52	# ==============================================================================================
53	# CONFIGURATION
54	# ==============================================================================================
55	#
56	# host*.conf
57	#
58	#   HOST*_SONARR_URL / HOST*_SONARR_API_KEY / HOST*_SONARR_TV_ROOT
59	#   HOST*_SONARR_PATH_MAP — container path → host path translation
60	#   All aliased by detect_hosts() — script uses unprefixed names
61	#
62	# master.conf
63	#
64	#   SONARR_ORPHAN_AGE          — days before untracked file eligible for deletion
65	#   SONARR_MAX_DELETE_GB       — require --i-know-what-im-doing above this
66	#   SONARR_EXTENSIONS          — video file extensions for orphan classification
67	#   SONARR_PROTECTED_PATTERNS  — file patterns never deleted
68	#   SONARR_VERSION_MAJOR       — expected Sonarr major version for API safety check
69	#   SONARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
70	#   ARR_CLEANUP_STATS          — stats file path (read by coffee report)
71	#
72	# ==============================================================================================
73	# RUNTIME MODES
74	# ==============================================================================================
75	#
76	#   sonarr_cleanup.sh                                           — normal run
77	#   sonarr_cleanup.sh --dry-run                                 — preview, no deletions
78	#   sonarr_cleanup.sh --log                                     — verbose output
79	#   sonarr_cleanup.sh --status                                  — show config and exit
80	#   sonarr_cleanup.sh --i-know-what-im-doing                    — bypass size threshold
81	#   sonarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
82	#
83	# NUCLEAR MODE: both flags bypass age check AND size threshold. User accepts full
84	# responsibility — the flag name is long and annoying by design.
85	#
86	# ==============================================================================================
87
88	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
89
90	source "$SCRIPT_DIR/../load_config.sh"
91
92	# ── Special flag pre-processing ───────────────────────────────────────────────────────────────
93	I_KNOW=false
94	SKIP_STRIKES=false
95	FILTERED_ARGS=()
96	for arg in "$@"; do
97	    case "$arg" in
98	        --i-know-what-im-doing) I_KNOW=true ;;
99	        --skip-strike-list)     SKIP_STRIKES=true ;;
100	        *)                      FILTERED_ARGS+=("$arg") ;;
101	    esac
102	done
103
104	parse_args "${FILTERED_ARGS[@]}"
105
106	# ── Nuclear mode warning ──────────────────────────────────────────────────────────────────────
107	if [[ "$I_KNOW" == true ]] && [[ "$SKIP_STRIKES" == true ]] && [[ "$DRY_RUN" != true ]]; then
108	    echo ""
109	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
110	    echo "⚠  WARNING — NUCLEAR MODE ACTIVE"
111	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
112	    echo "  Flags:          --i-know-what-im-doing --skip-strike-list"
113	    echo "  Strike system:  BYPASSED — deletes on first pass"
114	    echo "  Size threshold: BYPASSED — no GB limit"
115	    echo "  Data recovery:  NOT POSSIBLE after deletion"
116	    echo ""
117	    echo "  Review --dry-run output before proceeding."
118	    echo "  You have 10 seconds to cancel (Ctrl+C)..."
119	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
120	    sleep 10


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Radarr Cleanup =============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Delete orphaned movie files not tracked by Radarr. Queries the API for all
9	# tracked movie file paths, walks the library on disk, and removes anything
10	# untracked that is old enough to be past the import window. Triggers an Emby
11	# library clean after each deletion run so ghost entries disappear immediately.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# Every file encountered on disk is classified into one of five categories:
18	#
19	#   TRACKED   — Radarr API knows this exact path → leave it alone
20	#   PROTECTED — matches RADARR_PROTECTED_PATTERNS → never delete
21	#   ORPHAN    — video file, not tracked, older than RADARR_ORPHAN_AGE → delete
22	#   JUNK      — not a video extension, not protected → delete regardless of age
23	#   RECENT    — not tracked, under RADARR_ORPHAN_AGE → skip (may be mid-import)
24	#
25	# Radarr generates movie artwork (*.jpg), metadata (*.nfo), and manages subtitles
26	# (*.srt, *.sub, *.ass) but does NOT include these in its tracked file API response.
27	# Without PROTECTED classification these would be deleted — breaking Radarr and
28	# Emby metadata display.
29	#
30	# After deletions: notify_emby_scan() triggers Emby "Clean Missing Files" task.
31	# Emby removes ghost entries immediately — no user-facing file-not-found errors.
32	#
33	# ==============================================================================================
34	# OPERATIONAL SAFEGUARDS
35	# ==============================================================================================
36	#
37	# Six gates — ALL must pass before any file is touched:
38	#   1. Container running and not starting/unhealthy
39	#   2. API reachable
40	#   3. API version matches RADARR_VERSION_MAJOR in master.conf
41	#   4. Movie count > 0
42	#   5. Tracked file count > 0
43	#   6. Deletion size < RADARR_MAX_DELETE_GB — or --i-know-what-im-doing required
44	#
45	#   acquire_lock "wait"  — large scans take time, wait for previous run to finish
46	#   jq + curl validation — exits if either tool missing
47	#   DOCKER_TIMEOUT       — container checks protected against daemon hangs
48	#   notify_emby_scan()   — triggers Emby clean after deletion
49	#   platform_require_cmd  — notify script validated before use
50	#   Silent by default    — orphans/junk warn(), clean library logs silently
51	#
52	# ==============================================================================================
53	# CONFIGURATION
54	# ==============================================================================================
55	#
56	# host*.conf
57	#
58	#   HOST*_RADARR_URL / HOST*_RADARR_API_KEY / HOST*_RADARR_MOVIES_ROOT
59	#   HOST*_RADARR_PATH_MAP — container path → host path translation
60	#   All aliased by detect_hosts() — script uses unprefixed names
61	#
62	# master.conf
63	#
64	#   RADARR_ORPHAN_AGE          — days before untracked file eligible for deletion
65	#   RADARR_MAX_DELETE_GB       — require --i-know-what-im-doing above this
66	#   RADARR_EXTENSIONS          — video file extensions for orphan classification
67	#   RADARR_PROTECTED_PATTERNS  — file patterns never deleted
68	#   RADARR_VERSION_MAJOR       — expected Radarr major version for API safety check
69	#   RADARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
70	#   ARR_CLEANUP_STATS          — stats file path (read by coffee report)
71	#
72	# ==============================================================================================
73	# RUNTIME MODES
74	# ==============================================================================================
75	#
76	#   radarr_cleanup.sh                                           — normal run
77	#   radarr_cleanup.sh --dry-run                                 — preview, no deletions
78	#   radarr_cleanup.sh --log                                     — verbose output
79	#   radarr_cleanup.sh --status                                  — show config and exit
80	#   radarr_cleanup.sh --i-know-what-im-doing                    — bypass size threshold
81	#   radarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
82	#
83	# NUCLEAR MODE: both flags bypass age check AND size threshold. User accepts full
84	# responsibility — the flag name is long and annoying by design.
85	#
86	# ==============================================================================================
87
88	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
89
90	source "$SCRIPT_DIR/../load_config.sh"
91
92	# ── Special flag pre-processing ───────────────────────────────────────────────────────────────
93	I_KNOW=false
94	SKIP_STRIKES=false
95	FILTERED_ARGS=()
96	for arg in "$@"; do
97	    case "$arg" in
98	        --i-know-what-im-doing) I_KNOW=true ;;
99	        --skip-strike-list)     SKIP_STRIKES=true ;;
100	        *)                      FILTERED_ARGS+=("$arg") ;;
101	    esac
102	done
103
104	parse_args "${FILTERED_ARGS[@]}"
105
106	# ── Nuclear mode warning ──────────────────────────────────────────────────────────────────────
107	if [[ "$I_KNOW" == true ]] && [[ "$SKIP_STRIKES" == true ]] && [[ "$DRY_RUN" != true ]]; then
108	    echo ""
109	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
110	    echo "⚠  WARNING — NUCLEAR MODE ACTIVE"
111	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
112	    echo "  Flags:          --i-know-what-im-doing --skip-strike-list"
113	    echo "  Strike system:  BYPASSED — deletes on first pass"
114	    echo "  Size threshold: BYPASSED — no GB limit"
115	    echo "  Data recovery:  NOT POSSIBLE after deletion"
116	    echo ""
117	    echo "  Review --dry-run output before proceeding."
118	    echo "  You have 10 seconds to cancel (Ctrl+C)..."
119	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
120	    sleep 10


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Lidarr Cleanup =============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Delete orphaned music files not tracked by Lidarr. Queries the API for all
9	# tracked file paths, walks the library on disk, and removes anything untracked
10	# that is old enough to be past the import window. Triggers an Emby library
11	# clean after runs where files were deleted so ghost entries disappear immediately.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# Every file encountered on disk is classified into one of five categories:
18	#
19	#   TRACKED   — Lidarr API knows this exact path → leave it alone
20	#   PROTECTED — matches LIDARR_PROTECTED_PATTERNS → never delete
21	#   ORPHAN    — music file, not tracked, older than LIDARR_ORPHAN_AGE → delete
22	#   JUNK      — not a music extension, not protected → delete regardless of age
23	#   RECENT    — not tracked, under LIDARR_ORPHAN_AGE → skip (may be mid-import)
24	#
25	# Lidarr generates cover art (*.jpg), metadata (*.nfo), and lyrics (*.lrc) but
26	# does NOT include these in its tracked file API response. Without PROTECTED
27	# classification these would be deleted — breaking Lidarr and Emby display.
28	#
29	# ==============================================================================================
30	# OPERATIONAL SAFEGUARDS
31	# ==============================================================================================
32	#
33	# Seven gates — ALL must pass before any file is touched:
34	#   1. Container running and not starting/unhealthy
35	#   2. API reachable
36	#   3. API version matches LIDARR_VERSION_MAJOR in master.conf
37	#   4. Artist count > 0
38	#   5. Tracked file count > 0
39	#   6. Tracked count >= LIDARR_MIN_TRACKED_PCT % of last known count
40	#   7. Deletion size < LIDARR_MAX_DELETE_GB — or --i-know-what-im-doing required
41	#
42	#   acquire_lock "wait"  — large scans take time, wait for previous run to finish
43	#   jq + curl validation — exits if either tool missing
44	#   DOCKER_TIMEOUT       — container checks protected against daemon hangs
45	#   Duplicate detection  — temp file of tracked paths, grep before delete
46	#   platform_require_cmd  — notify script validated before use
47	#   Silent by default    — orphans/junk warn(), clean library logs silently
48	#
49	# ==============================================================================================
50	# STATE FILES
51	# ==============================================================================================
52	#
53	#   LIDARR_TRACKED_COUNT_FILE — persistent baseline for the tracked % safety check (gate 6)
54	#       Updated after each successful run. Protects against misconfigured root path
55	#       returning an empty API response and deleting the entire library.
56	#
57	# ==============================================================================================
58	# CONFIGURATION
59	# ==============================================================================================
60	#
61	# host*.conf
62	#
63	#   HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY / HOST1_LIDARR_MUSIC_ROOT
64	#   HOST1_LIDARR_PATH_MAP — container path → host path translation
65	#   All aliased by detect_hosts() — script uses unprefixed names
66	#
67	# master.conf
68	#
69	#   LIDARR_ORPHAN_AGE         — days before untracked file is eligible for deletion
70	#   LIDARR_MAX_DELETE_GB      — require --i-know-what-im-doing above this
71	#   LIDARR_MIN_TRACKED_PCT    — abort if tracked count drops below this % of last run
72	#   LIDARR_TRACKED_COUNT_FILE — persistent baseline file path
73	#   LIDARR_EXTENSIONS         — music file extensions for orphan classification
74	#   LIDARR_PROTECTED_PATTERNS — file patterns that are never deleted
75	#   LIDARR_VERSION_MAJOR      — expected Lidarr major version for API safety check
76	#   LIDARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
77	#   LIDARR_LOCK_WARN_AGE      — override default lock warning age (large libraries)
78	#   ARR_CLEANUP_STATS         — stats file path (read by coffee report)
79	#
80	# ==============================================================================================
81	# RUNTIME MODES
82	# ==============================================================================================
83	#
84	#   lidarr_cleanup.sh                                           — normal run
85	#   lidarr_cleanup.sh --dry-run                                 — preview, no deletions
86	#   lidarr_cleanup.sh --log                                     — verbose output
87	#   lidarr_cleanup.sh --status                                  — show config and exit
88	#   lidarr_cleanup.sh --i-know-what-im-doing                    — bypass size threshold
89	#   lidarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
90	#
91	# NUCLEAR MODE: both flags bypass age check AND size threshold. Use when Soularr
92	# has filled the gaps and you want a clean one-pass wipe. User accepts full
93	# responsibility — the flag name is long and annoying by design.
94	#
95	# ==============================================================================================
96
97	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
98
99	source "$SCRIPT_DIR/../load_config.sh"
100
101	# ── Special flag pre-processing ───────────────────────────────────────────────────────────────
102	# Filter --i-know-what-im-doing and --skip-strike-list before parse_args
103	# to avoid unknown flag errors — these are handled separately below.
104	I_KNOW=false
105	SKIP_STRIKES=false
106	FILTERED_ARGS=()
107	for arg in "$@"; do
108	    case "$arg" in
109	        --i-know-what-im-doing) I_KNOW=true ;;
110	        --skip-strike-list)     SKIP_STRIKES=true ;;
111	        *)                      FILTERED_ARGS+=("$arg") ;;
112	    esac
113	done
114
115	parse_args "${FILTERED_ARGS[@]}"
116
117	# ── Nuclear mode warning ──────────────────────────────────────────────────────────────────────
118	if [[ "$I_KNOW" == true ]] && [[ "$SKIP_STRIKES" == true ]] && [[ "$DRY_RUN" != true ]]; then
119	    echo ""
120	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Play State Sync ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Syncs watched/played state and resume positions across all configured Emby
9	# and Jellyfin servers. Newest timestamp wins — no data is ever lost.
10	#
11	# Users are matched by name (case-insensitive). If a user exists on some servers
12	# but not others, those servers are skipped for that user — no errors, no partial
13	# syncs from unrelated accounts.
14	#
15	# Items are matched by external provider IDs:
16	#   Movies   → IMDb ID, then TMDB ID
17	#   Episodes → TVDB ID + season + episode number
18	#   Audio    → MusicBrainz Track ID
19	#
20	# ==============================================================================================
21	# SYNC LOGIC
22	# ==============================================================================================
23	#
24	# For each matched item across ≥2 servers:
25	#   1. Compare LastPlayedDate across all servers that have a play record.
26	#   2. The server with the newest LastPlayedDate is authoritative.
27	#   3. Push that server's state (Played, PlayCount, LastPlayedDate,
28	#      PlaybackPositionTicks) to every other server.
29	#   4. Servers with no record for that item also receive the state.
30	#
31	# Resume positions (partial plays, not marked Played):
32	#   Synced by comparing PlaybackPositionTicks when LastPlayedDate is absent.
33	#   The higher tick count wins.
34	#
35	# ==============================================================================================
36	# CONFIGURATION (host*.conf, aliased by detect_hosts)
37	# ==============================================================================================
38	#
39	#   HOST*_TRANSCODE_SERVERS  "Name|URL|APIKey|type" entries per host (emby/jellyfin)
40	#                            All hosts are discovered automatically — no extra config needed.
41	#                            Remote host URLs have localhost rewritten to their Tailscale IP.
42	#
43	# master.conf
44	#
45	#   PLAY_SYNC_ENABLED        Master toggle (default: true)
46	#   PLAY_SYNC_REMOTE         Sync across all hosts via Tailscale (default: true)
47	#                            false = local servers only (this host's Emby + Jellyfin)
48	#   PLAY_SYNC_DAYS           How many days back to check for played items (default: 90)
49	#                            Use 0 to sync all played items (slow on large libraries).
50	#   PLAY_SYNC_TYPES          Comma-separated item types to sync (default: Movie,Episode,Audio)
51	#
52	# ==============================================================================================
53	# RUNTIME MODES
54	# ==============================================================================================
55	#
56	# play_state_sync.sh
57	#     Sync all matched users across all configured servers.
58	#
59	# play_state_sync.sh --dry-run
60	#     Show what would be synced without writing any state.
61	#
62	# play_state_sync.sh --status
63	#     Show configured servers, reachability, and user counts.
64	#
65	# play_state_sync.sh --full
66	#     Ignore PLAY_SYNC_DAYS — sync all played items (may be slow).
67	#
68	# play_state_sync.sh --log
69	#     Verbose output — show each item comparison.
70	#
71	# ==============================================================================================
72
73	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
74	source "$SCRIPT_DIR/../load_config.sh"
75
76	# ── Handle --full before parse_args ───────────────────────────────────────────
77	FULL_SYNC=false
78	_FILTERED=()
79	for _a in "$@"; do
80	    [[ "$_a" == "--full" ]] && FULL_SYNC=true || _FILTERED+=("$_a")
81	done
82	parse_args "${_FILTERED[@]}"
83
84	# ==============================================================================================
85	# ━━━ Setup ━━━
86	# ==============================================================================================
87	[[ "${PLAY_SYNC_ENABLED:-true}" != "true" ]] && echo "Play state sync disabled" && exit 0
88
89	SYNC_DAYS="${PLAY_SYNC_DAYS:-90}"
90	SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode,Audio}"
91	[[ "$FULL_SYNC" == true ]] && SYNC_DAYS=0
92	log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
93
94	command -v jq >/dev/null 2>&1 || { error "jq is required but not installed"; exit 1; }
95
96	acquire_lock
97
98	# ── Build server list across ALL hosts ────────────────────────────────────────
99	# All HOST*_TRANSCODE_SERVERS arrays are loaded into env by load_config.sh.
100	# For remote hosts, localhost in the URL is rewritten to their Tailscale IP.
101	declare -a SRV_NAME SRV_URL SRV_KEY SRV_TYPE
102	_srv_count=0
103	_my_hostname=$(hostname -s)
104
105	_add_server() {
106	    local name="$1" url="$2" key="$3" type="$4"
107	    [[ -z "$url" || -z "$key" ]] && return
108	    [[ "$key" == "YOUR_API_KEY"* || "$key" == "placeholder"* ]] && return
109	    SRV_NAME[$_srv_count]="$name"
110	    SRV_URL[$_srv_count]="$url"
111	    SRV_KEY[$_srv_count]="$key"
112	    SRV_TYPE[$_srv_count]="$type"
113	    (( _srv_count++ ))
114	}
115
116	for _varname in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
117	    _num="${_varname//[^0-9]/}"
118	    _host_hostname="${!_varname}"
119	    [[ -z "$_host_hostname" ]] && continue
120


1	#!/bin/bash
2	# ==============================================================================================
3	# =========================== Playback-Aware Sonarr Discovery ==================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Two-stage behavior-driven TV show discovery.
9	#
10	# Stage 1 — Score recently watched series in Emby. The top series become
11	#            high-quality seeds, weighted by user diversity, recency, and
12	#            per-user-capped episode volume.
13	#
14	# Stage 2 — Run TMDB TV recommendations on those seeds. Score the candidates
15	#            and add the top shows to Sonarr.
16	#
17	# Goal: 0–3 meaningful Sonarr adds per run, not bulk imports.
18	#
19	# ==============================================================================================
20	# FLOW
21	# ==============================================================================================
22	#
23	#   1. Fetch all Series from Emby SONARR_EMBY_LIBRARIES — build TMDB+TVDB index
24	#
25	#   ── Stage 1 ─────────────────────────────────────────────────────────────────
26	#   2. Fetch Emby activity log (last LOOKBACK_DAYS days) — episode play events
27	#   3. Batch-fetch episode items → map episode → series → TMDB ID
28	#   4. Aggregate per-user episode counts per series
29	#   5. Score each series: user diversity + recency + capped volume
30	#   6. Take top MAX_SEEDS → discovery seeds
31	#
32	#   ── Stage 2 ─────────────────────────────────────────────────────────────────
33	#   7. For each seed, call TMDB TV recommendations → collect candidates
34	#   8. Aggregate: breadth (distinct seeds recommending this show)
35	#   9. Filter: already in Sonarr, already in Emby, below min votes/rating, cooldown
36	#  10. Score candidates: breadth + TMDB rating + vote count
37	#  11. Take top MAX_ADDS above threshold
38	#  12. Get TVDB ID via TMDB external_ids → Sonarr lookup → add + trigger SeriesSearch
39	#
40	# ==============================================================================================
41	# SCORING MODEL
42	# ==============================================================================================
43	#
44	#   Stage 1 — seed selection (max 100)
45	#     user_diversity_score (0-50) — unique users who watched: 1→10, 2→25, 3→40, 4+→50
46	#     recency_score        (0-30) — days since last episode; 0-7d→30, 8-14d→20, 15-21d→12, 22+→5
47	#     volume_score         (0-20) — sum of min(user_eps, USER_EPISODE_CAP); 1-4→3, 5-12→8, 13-24→14, 25+→20
48	#
49	#   USER_EPISODE_CAP prevents one person binge-watching from dominating seeds.
50	#   Example: 4 users × 2 eps each beats 1 user × 50 eps (58 vs 18, before recency).
51	#
52	#   Stage 2 — candidate scoring (max 100)
53	#     breadth_score (0-40) — 3+ seeds→40, 2 seeds→25, 1 seed→10
54	#     rating_score  (0-40) — TMDB vote_average × 10: 80+→40, 75+→32, 70+→25, 65+→18, 60+→12, else→5
55	#     votes_score   (0-20) — TMDB vote_count: 10k+→20, 5k+→15, 1k+→10, 200+→5, else→2
56	#
57	#   Threshold: SONARR_DISCOVERY_THRESHOLD (default 52)
58	#   Max adds:  SONARR_DISCOVERY_MAX_ADDS (default 3) — TV is a larger commitment than movies
59	#
60	# ==============================================================================================
61	# REQUIREMENTS
62	# ==============================================================================================
63	#
64	#   TMDB API key — required for Stage 2 recommendations and external_ids lookup
65	#   Configure HOST*_TMDB_API_KEY in host*.conf
66	#   Free key at: https://www.themoviedb.org/settings/api
67	#
68	# ==============================================================================================
69	# CONFIGURATION (master.conf)
70	# ==============================================================================================
71	#
72	#   SONARR_DISCOVERY_THRESHOLD        — minimum score to add a candidate (default: 52)
73	#   SONARR_DISCOVERY_LOOKBACK_DAYS    — Emby watch history window in days (default: 14)
74	#   SONARR_DISCOVERY_MAX_SEEDS        — max seed series from Stage 1 (default: 5)
75	#   SONARR_DISCOVERY_MAX_ADDS         — max shows to add per run (default: 3)
76	#   SONARR_DISCOVERY_MIN_VOTE_COUNT   — min TMDB votes for a candidate (default: 50)
77	#   SONARR_DISCOVERY_MIN_RATING       — min TMDB vote_average × 10 (default: 65 = 6.5/10)
78	#   SONARR_DISCOVERY_REJECT_COOLDOWN  — days before re-evaluating a rejected show (default: 60)
79	#   SONARR_DISCOVERY_USER_EPISODE_CAP — max episodes per user in seed scoring (default: 8)
80	#   SONARR_DISCOVERY_MONITOR_MODE     — Sonarr monitor mode on add: "all" or "future" (default: "all")
81	#   SONARR_EMBY_LIBRARIES             — Emby library names to draw seeds from
82	#   SONARR_DISCOVERY_HISTORY          — history/state file path
83	#
84	# ==============================================================================================
85	# RUNTIME MODES
86	# ==============================================================================================
87	#
88	#   playback_aware_sonarr_discovery.sh            — normal run
89	#   playback_aware_sonarr_discovery.sh --dry-run  — score and rank, no Sonarr changes
90	#   playback_aware_sonarr_discovery.sh --log      — verbose output
91	#   playback_aware_sonarr_discovery.sh --status   — show config and exit
92	#
93	# Recommended schedule: weekly (WEEKLY_MAINTENANCE_SCRIPTS in master.conf)
94	#
95	# ==============================================================================================
96	# OPERATIONAL SAFEGUARDS
97	# ==============================================================================================
98	#
99	# Root Required
100	#   acquire_lock and Sonarr API writes require root. Script exits cleanly if not root.
101	#
102	# Dry-Run Mode
103	#   --dry-run scores and ranks all candidates but makes no Sonarr API calls and does not
104	#   write to the history file. Safe to run at any time to preview what would be added.
105	#
106	# Add-Only
107	#   Only adds series to Sonarr. Never deletes or modifies existing entries.
108	#
109	# Cooldown Guard
110	#   Candidates rejected this run are recorded in the history file and not re-evaluated
111	#   until SONARR_DISCOVERY_REJECT_COOLDOWN days have elapsed.
112	#
113	# Monitor Mode
114	#   SONARR_DISCOVERY_MONITOR_MODE="all" monitors every season on add — correct for shows
115	#   where you want Sonarr to search back-catalogue. "future" only marks upcoming seasons;
116	#   use only when you intentionally want to skip existing seasons.
117	#
118	# ==============================================================================================
119	# STATE FILES
120	# ==============================================================================================


Now let me search for the missing scripts:


Bash

find /boot/config/plugins/varaverk/Media -maxdepth 1 -type f -name "*.sh" | sort


Now let me read the remaining scripts that were requested:


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Media Shares Permissions =======================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Apply nobody:users ownership and correct permissions to all media shares.
9	# Runs daily as the first job in the maintenance window — arr cleanup depends
10	# on correct ownership to rename and delete files.
11	#
12	# Now a proper daily failsafe: files arrive with wrong ownership from rsync
13	# without --chown, manual admin copies, containers with unconfigured PUID/PGID,
14	# or unRAID environment resets after updates.
15	#
16	# ==============================================================================================
17	# OPERATIONAL SAFEGUARDS
18	# ==============================================================================================
19	#
20	#   acquire_lock "wait"  — wait if previous run still active (large share scans)
21	#   detect_hosts()       — correct share list per host via MY_ID aliases
22	#   Empty array guard    — warns and exits cleanly if no shares configured
23	#   Folder existence     — skips missing shares with warning, continues others
24	#   Separate passes      — directories and files chmod'd separately for correctness
25	#   platform_require_cmd  — notify script validated before use
26	#   Silent by default    — only failures produce output, success is silent
27	#
28	# Diagnostic — high corrected count on every run means a container has wrong PUID/PGID:
29	#   Correct values on unRAID: PUID=99 (nobody) PGID=100 (users)
30	#   Common culprits: SABnzbd, qBittorrent, slskd — check these first
31	#   Once fixed, this script should correct 0 files per run (pure failsafe)
32	#
33	# ==============================================================================================
34	# CONFIGURATION
35	# ==============================================================================================
36	#
37	# host*.conf
38	#
39	#   HOST*_MEDIA_PERMISSION_SHARES — shares this host applies permissions to
40	#   Aliased by detect_hosts() — script uses MEDIA_PERMISSION_SHARES
41	#
42	# master.conf
43	#
44	#   PERMISSIONS_DIR_MODE   — directory permissions (default 755)
45	#   PERMISSIONS_FILE_MODE  — file permissions (default 664)
46	#   PERMISSIONS_OWNER      — ownership applied to all files (default nobody:users)
47	#
48	# ==============================================================================================
49	# RUNTIME MODES
50	# ==============================================================================================
51	#
52	#   media_shares_permissions.sh              — normal run
53	#   media_shares_permissions.sh --dry-run    — preview without making changes
54	#   media_shares_permissions.sh --log        — verbose output
55	#   media_shares_permissions.sh --status     — show config and exit
56	#
57	# ==============================================================================================
58
59	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
61	source "$SCRIPT_DIR/../load_config.sh"
62
63	parse_args "$@"
64
65	# ==============================================================================================
66	# ━━━ Setup ━━━
67	# ==============================================================================================
68	if [[ "$EUID" -ne 0 ]]; then
69	    error "Must be run as root"
70	    exit 1
71	fi
72
73
74	acquire_lock "wait"
75
76	# detect_hosts() sets MY_ID and aliases HOST*_MEDIA_PERMISSION_SHARES
77	detect_hosts
78
79	# Empty array guard
80	if [[ ${#MEDIA_PERMISSION_SHARES[@]} -eq 0 ]]; then
81	    warn "MEDIA_PERMISSION_SHARES is empty for $MY_ID — nothing to do"
82	    warn "Check HOST*_MEDIA_PERMISSION_SHARES in host*.conf"
83	    exit 0
84	fi
85
86	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
87
88	# ==============================================================================================
89	# ━━━ Status ━━━
90	# ==============================================================================================
91	if [[ "$SHOW_STATUS" == true ]]; then
92	    echo ""
93	    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
94	    echo "$ICON_HOST    Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
95	    echo "$ICON_PERMS   Dir mode:   ${PERMISSIONS_DIR_MODE:-755}"
96	    echo "$ICON_PERMS   File mode:  ${PERMISSIONS_FILE_MODE:-664}"
97	    echo "$ICON_PERMS   Owner:      $PERMISSIONS_OWNER"
98	    echo "$ICON_PERMS   Shares:     ${#MEDIA_PERMISSION_SHARES[@]}"
99	    echo ""
100	    for share in "${MEDIA_PERMISSION_SHARES[@]}"; do
101	        local_status="missing"
102	        [[ -d "$share" ]] && local_status="exists"
103	        echo "    $share — $local_status"
104	    done
105	    echo "$ICON_GEAR    Dry Run:    $DRY_RUN"
106	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
107	    exit 0
108	fi
109
110	# ==============================================================================================
111	# ━━━ Apply Permissions ━━━
112	# ==============================================================================================
113	echo ""
114	echo "━━━ $ICON_PERMS Media Permissions — $MY_ID ━━━"
115	log "Dir mode:  ${PERMISSIONS_DIR_MODE:-755}"
116	log "File mode: ${PERMISSIONS_FILE_MODE:-664}"
117	log "Owner:     $PERMISSIONS_OWNER"
118	log "Shares:    ${#MEDIA_PERMISSION_SHARES[@]}"
119	echo ""
120


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Media Cleaner ==============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Remove scene debris, tool artifacts, and unsafe files from media shares
9	# before the orphan scan — so arr cleanup only encounters actual media files.
10	# Runs second in the daily window, after permissions and before arr cleanup.
11	# Two profiles — anime and media — each with their own folder list and patterns.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# Profiles:
18	#   anime — cleans ANIME_CLEAN_FOLDERS using ANIME_FILE_PATTERNS
19	#   media — cleans MEDIA_CLEAN_FOLDERS using MEDIA_FILE_PATTERNS (adds *.iso *.lrc)
20	#
21	# Scene debris — left by scene releases and download clients:
22	#   *.sfv *.md5 *.sha1        — checksums — useless post-download
23	#   *.nzb *.torrent           — download files left by clients
24	#   *.url *.lnk *.info *.diz  — scene metadata
25	#   *.sample* *.proof*        — scene samples — never needed in library
26	#   *sync-conflict*           — Syncthing conflict copies
27	#   *.rar *.zip *.7z *.ace    — archives — source not needed after extraction
28	#   *.r00-*.r09 *.srr         — multi-part rar segments and repair files
29	#   *.001 *.002 *.003         — split archive parts
30	#   *.gz *.tar *.bz2          — linux archives
31	#
32	# Tool artifacts — incomplete or stale files from download clients:
33	#   *.!ut *.!qB               — uTorrent / qBittorrent incomplete markers
34	#   *.crdownload *.opdownload — Chrome / Opera incomplete downloads
35	#   *.part                    — partial download files
36	#
37	# Unsafe files — executables that should never appear in a media folder:
38	#   *.exe *.scr *.com         — Windows executables
39	#   *.bat *.cmd *.vbs *.ps1   — Windows scripts
40	#   *.msi *.dll *.sys         — Windows system files
41	#   *.sh                      — shell scripts in media folders = suspicious
42	#
43	# Media profile also removes: *.iso *.lrc
44	#
45	# ==============================================================================================
46	# OPERATIONAL SAFEGUARDS
47	# ==============================================================================================
48	#
49	#   acquire_lock "wait"  — wait if previous run still active
50	#   detect_hosts()       — correct folder lists per host via MY_ID aliases
51	#   Empty array guards   — warns and exits cleanly if no folders or patterns configured
52	#   Folder existence     — skips missing folders with warning, continues others
53	#   platform_require_cmd  — notify script validated before use
54	#
55	# ==============================================================================================
56	# CONFIGURATION
57	# ==============================================================================================
58	#
59	# host*.conf
60	#
61	#   HOST*_ANIME_CLEAN_FOLDERS — folders cleaned by the anime profile on this host
62	#   HOST*_MEDIA_CLEAN_FOLDERS — folders cleaned by the media profile on this host
63	#   Aliased by detect_hosts() — script uses ANIME_CLEAN_FOLDERS / MEDIA_CLEAN_FOLDERS
64	#
65	# master.conf
66	#
67	#   ANIME_FILE_PATTERNS  — file patterns removed by the anime profile
68	#   MEDIA_FILE_PATTERNS  — file patterns removed by the media profile
69	#
70	# ==============================================================================================
71	# RUNTIME MODES
72	# ==============================================================================================
73	#
74	#   media_cleaner.sh anime              — clean anime shares
75	#   media_cleaner.sh media              — clean media shares
76	#   media_cleaner.sh anime --dry-run    — preview anime clean, no deletions
77	#   media_cleaner.sh media --dry-run    — preview media clean, no deletions
78	#   media_cleaner.sh anime --log        — verbose output
79	#   media_cleaner.sh anime --status     — show config and exit
80	#
81	# ==============================================================================================
82
83	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
84
85	source "$SCRIPT_DIR/../load_config.sh"
86
87	# ── Separate profile argument from flags ──────────────────────────────────────────────────────
88	# Profile (anime|media) is a positional arg — separate before parse_args sees flags
89	PROFILE=""
90	RAW_ARGS=()
91
92	for ARG in "$@"; do
93	    case "$ARG" in
94	        anime|media) PROFILE="$ARG" ;;
95	        *)           RAW_ARGS+=("$ARG") ;;
96	    esac
97	done
98
99	parse_args "${RAW_ARGS[@]}"
100
101	# ==============================================================================================
102	# ━━━ Setup ━━━
103	# ==============================================================================================
104	if [[ "$EUID" -ne 0 ]]; then
105	    error "Must be run as root"
106	    exit 1
107	fi
108
109	if [[ -z "$PROFILE" ]]; then
110	    error "No profile specified"
111	    error "Usage: media_cleaner.sh <anime|media> [--dry-run] [--log] [--status]"
112	    exit 1
113	fi
114
115	acquire_lock "wait"
116
117	# detect_hosts() sets MY_ID and aliases HOST*_ANIME/MEDIA_CLEAN_FOLDERS
118	detect_hosts
119
120


Based on my thorough review of the documentation and script headers, I've found the following discrepancies:

## Discrepancy Report

**1. permissions_repair.sh — Script does not exist**
- README-Media.md line 182 lists `media_shares_permissions.sh` in the script table.
- Manual-Media.md line 565 documents `media_shares_permissions.sh` under "FLAG REFERENCE".
- The actual script at line 1 is named `media_shares_permissions.sh`, not `permissions_repair.sh` as requested in the initial file list.

**2. arr_cleanup.sh vs. individual cleanup scripts**
- README-Media.md line 180 lists an `arr_cleanup.sh` script in the table.
- However, no such script exists. The actual scripts are `lidarr_cleanup.sh`, `sonarr_cleanup.sh`, and `radarr_cleanup.sh`.
- The docs should clarify these are three separate scripts, not one generic `arr_cleanup.sh`.

**3. play_state_sync.sh — Missing mention of --full flag**
- Manual-Media.md does not list `--full` mode in the "FLAG REFERENCE" section for `play_state_sync.sh`.
- The script header at line 65-66 documents: `play_state_sync.sh --full — Ignore PLAY_SYNC_DAYS — sync all played items (may be slow).`
- This flag should be added to Manual-Media.md documentation.

**4. playback_aware_sonarr_discovery.sh — Typo in script header**
- lidarr_cleanup.sh header line 91 contains: "Use when Soularr has filled the gaps"
- This appears to be a typo (should be "Sonarr" but in a Lidarr cleanup script, this text is out of context and mentions the wrong application).

**5. Lidarr cleanup safety layer count discrepancy**
- Manual-Media.md line 37 states: "Six gates — ALL must pass before any file is touched"
- lidarr_cleanup.sh header line 33 states: "Seven gates — ALL must pass before any file is touched"
- The Lidarr script correctly has 7 gates (lines 34-40), but the manual incorrectly documents 6 gates. Radarr and Sonarr both have 6 gates, which matches their documentation.

All other scripts match their documentation accurately regarding purpose, configuration variables, runtime modes, and operational safeguards.