feat: slskd reconnect guard in downloaders_reset, mass v2 sync

- downloaders_reset: connection check block before slskd API sections;
  triggers PUT /api/v0/server reconnect if disconnected, polls 60s,
  gates Stuck Searches and Dead Transfer Records on SLSKD_CONNECTED
- Sync all modified/new/deleted files from v2 refactor across Docker_Essentials,
  Media, Monitors, Partnership, Rsync, Tools, Transcodes, unRAID_Essentials,
  common.sh, master confs, and new Manual/README docs
This commit is contained in:
Gmer4Lfe
2026-05-19 20:00:10 -04:00
parent 5cb16d4b18
commit e13f2fa14f
81 changed files with 12164 additions and 10656 deletions
+636
View File
@@ -0,0 +1,636 @@
# ━━━━━ MEDIA — Manual ━━━━━
Config reference, procedures, operational workflows.
For overview see README-Media.md. For per-script detail see script headers.
---
## ━━━ PERMISSIONS MODEL ━━━
```
Directories: 755 nobody:users
Owner (nobody) — rwx enter, list, create files
Group (users) — r-x enter and list
Others — r-x Samba guests can browse
No world-write — prevents accidental deletion by unauthenticated access
Files: 664 nobody:users
Owner (nobody) — rw read + write
Group (users) — rw arrs can import, rename, delete
Others — r Samba guests can read
No execute bit — media files are never executable
```
**Two separate passes — not a single recursive chmod.** Directories need the execute bit
to enter. Files must never have the execute bit. A single `chmod -R 664` would break
directory entry. The script runs `find -type d` and `find -type f` separately.
**If this script corrects many files on every run**, a container has wrong PUID/PGID.
Correct values on unRAID: `PUID=99 (nobody)` `PGID=100 (users)`. Add to each container's
environment in its Docker template. Common culprits: SABnzbd, qBittorrent, slskd.
Once fixed, this script corrects 0 files per run — it becomes a pure daily failsafe.
---
## ━━━ ARR CLEANUP — FILE CLASSIFICATION ━━━
Every file found on disk during an arr cleanup run falls into exactly one category:
```
TRACKED → arr API returned this exact path → leave it alone
PROTECTED → matches ARR_PROTECTED_PATTERNS → never delete
ORPHAN → media extension, not tracked, old enough → delete
JUNK → not a media extension, not protected → delete (any age)
RECENT → not tracked, under ARR_ORPHAN_AGE days → skip (may be mid-import)
```
**Why protected patterns are critical:** arrs generate artwork (`*.jpg`), metadata
(`*.nfo`), and subtitles/lyrics that do NOT appear in the tracked file API response.
Without protection, these would be classified as orphans and deleted — removing cover art
from every album, every movie poster, every TV show thumbnail. Requires a full rescan
to recover. Never remove artwork extensions from protected patterns.
---
## ━━━ ARR CLEANUP — SAFETY LAYERS ━━━
All 7 layers must pass before any file is touched. There is no way to push through a
failed safety check without the explicit override flag.
```
1. Container running + healthy — a stopped container has an empty API
2. API reachable — no API = no tracked file list = everything looks orphaned
3. API version matches — major version must match tested version in master.conf
4. Item count > 0 — no artists/series/movies = something is wrong with DB
5. Tracked file count > 0 — empty response = everything would be deleted
6. Tracked count >= MIN_TRACKED_PCT — dramatic drop from last run = abort and alert
7. Deletion size < MAX_DELETE_GB — last line of defense against misconfigured root path
```
Layer 7 is the catastrophic failure prevention. A misconfigured root path — pointing
cleanup at the wrong directory — means the API returns zero tracked files for a root
that actually contains thousands. Everything walks as an orphan. Everything gets deleted.
`LIDARR/SONARR/RADARR_MAX_DELETE_GB` requires `--i-know-what-im-doing` to proceed past it.
---
## ━━━ CONFIGURATION — master.conf ━━━
### Permissions
```bash
PERMISSIONS_DIR_MODE="755"
PERMISSIONS_FILE_MODE="664"
PERMISSIONS_OWNER="nobody:users"
```
---
### Media Cleaner — File Patterns
```bash
ANIME_FILE_PATTERNS=(
"*.sfv" # checksum verification — useless after download verified
"*.md5" "*.sha1" # other checksum formats
"*.nfo" # scene info file — not library metadata
"*.url" "*.lnk" # website shortcuts
"*.rar" "*.zip" # source archives kept by some clients after extraction
"*.info" # tool output files
"*.torrent" # torrent descriptor left by some clients
"*.sample*" # scene preview clip
"*.proof*" # screenshot proving encode quality
"*sync-conflict*" # Syncthing conflict copies
"*.scr" "*.exe" # executables — should never be in a media folder
"*.srr" # scene recovery record
"*.log" # tool/client logs
"*.json" # metadata or tool output
)
MEDIA_FILE_PATTERNS=(
"${ANIME_FILE_PATTERNS[@]}" # all anime patterns plus:
"*.iso" # disc images after ripping
"*.lrc" # lyric files in media folders
)
```
> **DO NOT add patterns that match media you want to keep:**
> `*.mkv *.mp4 *.avi *.m4v` — video files
> `*.flac *.mp3 *.m4a` — audio files
> `*.srt *.sub *.ass` — subtitle files (Bazarr managed)
> `*.jpg *.png` — artwork
> Always use `--dry-run` when adding new patterns.
---
### Lidarr Cleanup Thresholds
```bash
LIDARR_ORPHAN_AGE=7 # days — files newer than this are RECENT (mid-import window)
LIDARR_MIN_TRACKED_PCT=80 # abort if API returns < 80% of last known count
LIDARR_MAX_DELETE_GB=50 # require --i-know-what-im-doing above this
LIDARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan
LIDARR_VERSION_MAJOR=3 # expected Lidarr major version (API safety check)
LIDARR_EXTENSIONS=("flac" "mp3" "m4a" "wav" "aac" "ogg" "opus" "wma")
LIDARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.lrc")
LIDARR_TRACKED_COUNT_FILE=/boot/config/lidarr_tracked_count # persistent baseline
ARR_CLEANUP_STATS=/boot/config/arr_cleanup_stats.db # read by coffee report
```
---
### Sonarr Cleanup Thresholds
```bash
SONARR_ORPHAN_AGE=7
SONARR_MAX_DELETE_GB=50
SONARR_IMPORT_SCAN_TIMEOUT=600
SONARR_VERSION_MAJOR=4
SONARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "ts" "wmv" "mov")
SONARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.srt" "*.sub" "*.ass" "*.ssa")
```
Note: `*.ts` IS in extensions — transport stream is used for Live TV recordings tracked
by Sonarr. Orphaned `.ts` recordings should be cleaned like any other orphaned episode.
---
### Radarr Cleanup Thresholds
```bash
RADARR_ORPHAN_AGE=7
RADARR_MAX_DELETE_GB=50
RADARR_IMPORT_SCAN_TIMEOUT=600
RADARR_VERSION_MAJOR=6
RADARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "wmv" "mov")
RADARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.srt" "*.sub" "*.ass" "*.ssa")
```
---
### Arr Sync
```bash
ARR_SYNC_ENABLED=true
ARR_SYNC_BLOCKLIST=/boot/config/arr_sync_blocklist.tsv # tombstone file
ARR_SYNC_CONNECT_TIMEOUT=10 # SSH connect timeout in seconds
ARR_SYNC_API_TIMEOUT=60 # curl API call timeout in seconds
DOCKER_APPDATA_BASE=/mnt/user/appdata
ARR_SYNC_LIDARR_PORT=8686
ARR_SYNC_SONARR_PORT=8989
ARR_SYNC_RADARR_PORT=7878
```
---
### Arr Recovery
```bash
ARR_IMPORT_RECOVERY_AGE=6 # hours — items newer than this are skipped
SONARR_VERSION_MAJOR=4
RADARR_VERSION_MAJOR=6
LIDARR_VERSION_MAJOR=3
ARR_RECOVERY_STATS=/boot/config/arr_recovery_stats.db # read by coffee report
```
---
### TMDb / TVDB Removed
```bash
RADARR_DROPPED_ADD_EXCLUSION=true # add removed movies to Radarr import exclusion
SONARR_DROPPED_ADD_EXCLUSION=true # add removed series to Sonarr import exclusion
```
---
### Lidarr Missing Art
```bash
FANART_API_KEY="your-fanart-tv-api-key"
LASTFM_API_KEY="your-lastfm-api-key"
LIDARR_ART_MIN_SIZE=5000 # minimum valid download size in bytes
LIDARR_ART_MAX_PARALLEL=4 # concurrent background download jobs
LIDARR_ART_RETRIES=2 # download retry attempts per image
LIDARR_ART_SLEEP_BETWEEN=1 # seconds between fanart.tv API calls (rate limit)
```
---
### Orchestrator Job Order
```bash
MEDIA_MAINTENANCE_JOBS=(
"Media/media_shares_permissions.sh" # 1. permissions — always first
"Media/media_cleaner.sh anime" # 2. junk removal — before orphan scan
"Media/media_cleaner.sh media" # 3.
"Media/lidarr_cleanup.sh" # 4. arr cleanup — after permissions + clean
"Media/sonarr_cleanup.sh" # 5.
"Media/radarr_cleanup.sh" # 6.
)
```
---
## ━━━ CONFIGURATION — master_host*.conf ━━━
### master_host1.conf
```bash
# Shares this server applies permissions to
HOST1_MEDIA_PERMISSION_SHARES=(
"/mnt/user/Movies"
"/mnt/user/Tv_Shows"
"/mnt/user/Music"
"/mnt/user/Kids_Movies"
"/mnt/user/Kids_Tv_Shows"
"/mnt/user/Sports"
"/mnt/user/stand-up_comedy"
)
# Folders cleaned by each profile
HOST1_ANIME_CLEAN_FOLDERS=(
"/mnt/user/Anime_Movies"
"/mnt/user/Anime_Movies-Old"
"/mnt/user/Anime_Shows"
"/mnt/user/Anime_Shows-Old"
)
HOST1_MEDIA_CLEAN_FOLDERS=(
"/mnt/user/Kids_Movies"
"/mnt/user/Kids_Tv_Shows"
"/mnt/user/Movies"
"/mnt/user/Music"
"/mnt/user/Sports"
"/mnt/user/stand-up_comedy"
"/mnt/user/Tv_Shows"
)
# Arr connection details — must match arr settings exactly
HOST1_LIDARR_URL="http://192.168.50.2:8686"
HOST1_LIDARR_API_KEY="..."
HOST1_LIDARR_MUSIC_ROOT="/mnt/user/Music"
HOST1_LIDARR_PATH_MAP="" # container→host path translation if needed
HOST1_SONARR_URL="http://192.168.50.2:8989"
HOST1_SONARR_API_KEY="..."
HOST1_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
HOST1_SONARR_PATH_MAP=""
HOST1_RADARR_URL="http://192.168.50.2:7878"
HOST1_RADARR_API_KEY="..."
HOST1_RADARR_MOVIES_ROOT="/mnt/user/Movies"
HOST1_RADARR_PATH_MAP=""
HOST1_EMBY_URL="http://192.168.50.2:8096"
HOST1_EMBY_API_KEY="..."
```
> **LIDARR/SONARR/RADARR_MUSIC/TV/MOVIES_ROOT must exactly match the Root Folder path in
> the arr's own settings.** Arr UI → Settings → Media Management → Root Folders.
> A mismatch means every file on disk looks untracked — all appear as orphans.
> MAX_DELETE_GB is the only thing standing between a path mismatch and losing your library.
---
## ━━━ SAFE TESTING PROCEDURE ━━━
> **The arr cleanup scripts permanently delete files.** There is no recycle bin, no undo.
> Follow this procedure on first use, after any root path change, after any API key change,
> and after any significant arr library change.
### Step 1 — Dry Run With Full Logging
```bash
lidarr_cleanup.sh --dry-run --log
sonarr_cleanup.sh --dry-run --log
radarr_cleanup.sh --dry-run --log
```
### Step 2 — Review the Output
```
Are TRACKED files the ones you expect?
→ Known arr-managed files should show as TRACKED
→ If they show as ORPHAN, the root path is wrong — STOP
Is the ORPHAN count reasonable?
→ Healthy cleanup removes dozens to hundreds, not tens of thousands
→ Large count = stop, investigate root path before proceeding
Are PROTECTED patterns working?
→ Artwork (*.jpg) and subtitles (*.srt) must show as PROTECTED
→ If they show as ORPHAN, check PROTECTED_PATTERNS config
Are RECENT files being correctly skipped?
→ Files downloaded in the last 7 days should show as RECENT, not ORPHAN
```
### Step 3 — Check Numbers if Something Looks Wrong
```bash
# Root path mismatch? Compare these:
# Lidarr UI: Settings → Media Management → Root Folders
# Sonarr UI: Settings → Media Management → Root Folders
# Radarr UI: Settings → Media Management → Root Folders
# Must exactly match LIDARR_MUSIC_ROOT / SONARR_TV_ROOT / RADARR_MOVIES_ROOT
# Is the arr running?
docker ps | grep -E "Lidarr|Sonarr|Radarr"
# Library scan not complete?
# Trigger manual scan in arr UI and wait for completion
```
### Step 4 — Run Live
```bash
# Only after dry run review passes.
lidarr_cleanup.sh
sonarr_cleanup.sh
radarr_cleanup.sh
```
### Step 5 — Verify in Arr UI
```
Library count — should not have dropped significantly
healthy cleanup removes a small number, not a large percentage
Missing files — check if any monitored content shows as missing
Emby library — should show no ghost entries (notify_emby_scan handles this automatically)
```
---
## ━━━ PROCEDURES ━━━
### Adding a New Arr
```bash
# 1. Copy radarr_cleanup.sh as template
cp radarr_cleanup.sh readarr_cleanup.sh
# 2. Replace RADARR_ prefix with READARR_ throughout
# Update API endpoint, tracked file API path, extension list, protected patterns
# 3. Add to master_host*.conf
HOST1_READARR_URL="http://192.168.50.2:8787"
HOST1_READARR_API_KEY="your-api-key"
HOST1_READARR_BOOKS_ROOT="/mnt/user/Books"
# 4. Add thresholds to master.conf
READARR_ORPHAN_AGE=7
READARR_MAX_DELETE_GB=50
READARR_EXTENSIONS=("epub" "pdf" "mobi" "azw3" "cbz" "cbr")
READARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo")
# 5. Add to MEDIA_MAINTENANCE_JOBS in master.conf
MEDIA_MAINTENANCE_JOBS=(
...existing jobs...
"Media/readarr_cleanup.sh"
)
```
media_management.sh picks it up automatically. No orchestrator changes needed.
Run `--dry-run --log` before scheduling.
---
### Managing the Arr Sync Blocklist
```bash
# Add item to blocklist (removes from all arrs + tombstones the ID)
arr_sync.sh --blocklist-add lidarr <musicbrainz-artist-id> "reason"
arr_sync.sh --blocklist-add sonarr <tvdb-series-id> "reason"
arr_sync.sh --blocklist-add radarr <tmdb-movie-id> "reason"
# Remove from blocklist (un-tombstones the ID — does NOT re-add to arrs)
arr_sync.sh --blocklist-remove lidarr <id>
# View all blocklisted IDs
arr_sync.sh --blocklist-list
```
`--blocklist-add` is the only destructive operation — it simultaneously:
1. Writes the tombstone entry to the blocklist TSV file
2. Deletes the item from the local arr API (no file deletion)
3. SSHes each remote node and deletes from their arr API
Files become orphans on all nodes — arr_cleanup removes them on the next run.
---
## ━━━ TROUBLESHOOTING ━━━
### Arr Cleanup Deleting Files It Shouldn't
```
1. Check the protected patterns — artwork and subtitles must be listed
LIDARR_PROTECTED_PATTERNS / SONARR_PROTECTED_PATTERNS / RADARR_PROTECTED_PATTERNS
2. Check the root path matches arr settings exactly
Run: lidarr_cleanup.sh --status (shows configured root path)
Compare: Lidarr UI → Settings → Media Management → Root Folders
3. Check if files are truly orphaned
Run: lidarr_cleanup.sh --dry-run --log
Look for the specific file — verify it shows ORPHAN, not PROTECTED or TRACKED
```
### Arr Cleanup Aborting at Safety Layer 6 (Tracked Count Drop)
```
API returned far fewer tracked files than last run.
Possible causes:
- Arr database was recently rebuilt from scratch
- Large manual library removal
- Path map mismatch after arr migration
If intentional (library intentionally reduced):
Delete LIDARR_TRACKED_COUNT_FILE to reset the baseline
Run cleanup once — it will establish a new baseline
If unintentional:
Investigate before proceeding — the arr may have a problem
```
### Arr Sync Not Picking Up New Content
```
Is ARR_SYNC_ENABLED=true in master.conf?
Can this host SSH to the remote without password?
→ ssh -i [SSH_KEY] root@[remote-tailscale-ip] "hostname"
Is the arr accessible on the remote?
→ arr_sync.sh --status (shows each node's arr reachability)
→ arr_sync.sh --log (verbose output per-node, per-arr)
Is the item in the blocklist?
→ arr_sync.sh --blocklist-list
```
### Emby Still Showing Ghost Entries After Cleanup
```
notify_emby_scan() is called automatically after every arr cleanup deletion.
If ghosts persist:
1. Is Emby's API responding?
curl -s "http://[emby-ip]:8096/System/Info/Public"
2. Is EMBY_URL / EMBY_API_KEY correct in master_host*.conf?
Run: sonarr_cleanup.sh --status (shows Emby config)
3. Trigger manually in Emby:
Library → Manage Library → Clean Missing Files
```
---
## ━━━ FLAG REFERENCE ━━━
### media_shares_permissions.sh
`media_shares_permissions.sh`
Apply correct ownership and permissions to all configured media shares. Safe to run
manually at any time — idempotent, only changes what's wrong.
`media_shares_permissions.sh --dry-run`
Show how many files and directories would be corrected per share. If unexpectedly large,
check container PUID/PGID settings first (PUID=99 PGID=100).
`media_shares_permissions.sh --status`
Show configured share list and ownership of the share roots.
`media_shares_permissions.sh --log`
Show ownership correction count per share and per file (verbose).
> On large libraries this runs 20-30 minutes. This is expected — millions of files with
> recursive walk takes time. Designed to run overnight in the maintenance window.
---
### media_cleaner.sh
`media_cleaner.sh anime`
Remove junk files from anime share folders using ANIME_FILE_PATTERNS.
`media_cleaner.sh media`
Remove junk files from media share folders using MEDIA_FILE_PATTERNS.
`media_cleaner.sh [profile] --dry-run`
Show what would be deleted without removing anything. Always run first when adding new
patterns or folders.
`media_cleaner.sh [profile] --status`
Show folder list and file patterns for the profile.
`media_cleaner.sh [profile] --log`
Show every file examined, not just those removed.
---
### lidarr_cleanup.sh / sonarr_cleanup.sh / radarr_cleanup.sh
`[script] --dry-run --log`
Preview every classification decision. **Always run this first.** See Safe Testing Procedure.
`[script]`
Live run — deletes confirmed orphans and junk, triggers Emby clean.
`[script] --log`
Live run with verbose per-file output.
`[script] --status`
Show configuration, API status, tracked file count, and last run stats.
`[script] --i-know-what-im-doing`
Bypass the MAX_DELETE_GB size threshold. Required when deletion exceeds the configured
limit. Long flag name is intentional — cannot be added accidentally.
`[script] --skip-strike-list`
Bypass the ORPHAN_AGE age check. Deletes RECENT files too — files that are under the
age threshold. Use when you know recent downloads are actually orphans.
`[script] --i-know-what-im-doing --skip-strike-list`
**NUCLEAR MODE** — age check and size threshold both bypassed. Deletes on first pass.
Use when you want a clean one-pass wipe of everything the arr doesn't track.
No recovery possible after deletion.
---
### arrs_failed_stalled_recovery.sh
`arrs_failed_stalled_recovery.sh`
Check all configured arrs for failed imports and stalled downloads. Blocklist + remove +
re-search for each problem item.
`arrs_failed_stalled_recovery.sh --dry-run`
Show what would be actioned per arr without making any changes.
`arrs_failed_stalled_recovery.sh --status`
Show configuration, arr reachability, and last recovery stats.
`arrs_failed_stalled_recovery.sh --log`
Verbose output per item per arr.
---
### arr_sync.sh
`arr_sync.sh`
Sync all arr types across all configured nodes.
`arr_sync.sh --dry-run`
Show what would be added/removed on each node without making changes.
`arr_sync.sh --status`
Show node configuration, arr reachability, and blocklist count.
`arr_sync.sh --log`
Verbose per-node, per-arr output.
`arr_sync.sh --blocklist-add [arr] [id] "[reason]"`
Remove item from all arrs and tombstone the ID. See Procedures above.
`arr_sync.sh --blocklist-remove [arr] [id]`
Remove tombstone — does NOT re-add item to arrs.
`arr_sync.sh --blocklist-list`
Show all tombstoned IDs.
---
### radarr_tmdb_removed.sh / sonarr_tvdb_removed.sh
`[script]`
Remove records for entries with status="deleted" (dropped from upstream database).
Files are kept. Import exclusion is added.
`[script] --delete-files`
Also delete associated files from disk. Most dropped entries have no files — they were
announced movies/series that were never downloaded.
`[script] --dry-run`
Preview what would be removed without making changes.
`[script] --status`
Show arr connection status and current count of dropped entries.
`[script] --log`
Verbose per-entry output.
---
### lidarr_missing_art.sh
`lidarr_missing_art.sh`
Fetch all missing album and artist artwork from fanart.tv and fallback sources.
Never overwrites existing files.
`lidarr_missing_art.sh --dry-run`
Show what would be downloaded without writing any files.
`lidarr_missing_art.sh --status`
Show configuration and API key status.
`lidarr_missing_art.sh --log`
Verbose per-album, per-artist output.
+168 -925
View File
File diff suppressed because it is too large Load Diff
+92 -54
View File
@@ -1,78 +1,116 @@
#!/bin/bash
# ==============================================================================================
# ================================= ARR SYNC ===================================================
# ================================= ARR Sync ===================================================
# ==============================================================================================
# Bidirectional arr library sync across all nodes in the ecosystem.
# Syncs Lidarr, Sonarr, and Radarr libraries so every node tracks the same content.
# Run before rsync — once arrs agree on library, rsync spreads the files.
#
# ── DESIGN ────────────────────────────────────────────────────────────────────────────────────
# Full mesh: every node syncs with every other node — no primary, no hierarchy.
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Full-mesh arr library sync across all nodes — Lidarr, Sonarr, and Radarr.
# Every node syncs with every other, union model, no hierarchy. Run before
# rsync in the weekly sync window: once arrs agree on what to track, rsync
# spreads the actual files.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Full mesh: every node syncs with every other — no primary, no hierarchy.
# Union model: if any node tracks an item, all nodes get it (unless blocklisted).
# Convergence: any node can add content; after one full cycle all nodes agree.
# Upgrade-aware: server1 upgrades a file → arr tracks new path → rsync spreads it →
# arr_cleanup removes old file on all nodes because arr no longer tracks it.
# Upgrade-aware: a file upgrade on one node → arr tracks new path → rsync spreads
# it → arr_cleanup removes old path on all nodes (arr no longer tracks it).
#
# ── NODE DISCOVERY ────────────────────────────────────────────────────────────────────────────
# Reads HOST* vars from master.conf. Add HOST3= and it joins the sync automatically.
# No scripts change when adding a new node.
# Node discovery: reads HOST* vars from master.conf. Add HOST3= and it joins the
# sync automatically — no script changes needed for a new node.
#
# ── REMOTE API KEY ACCESS ─────────────────────────────────────────────────────────────────────
# Remote API keys are not stored anywhere. Script SSHes to each remote node and reads
# the key directly from that arr's config.xml in its appdata directory.
# Only the API response (JSON) is returned — key never leaves the remote node.
# Self-maintaining: remote key regeneration is picked up automatically.
# Graceful skip: arr not configured locally → skip cleanly. Arr not reachable on
# a remote → skip that node for that arr type, continue with others.
#
# ── BLOCKLIST ─────────────────────────────────────────────────────────────────────────────────
# ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added anywhere.
# Read from ALL nodes via SSH at start of each run — immediate effect across all nodes.
# (Each node reads every other node's blocklist file via SSH — no rsync delay.)
# Manage via --blocklist-add / --blocklist-remove / --blocklist-list.
#
# --blocklist-add does three things atomically:
# 1. Writes the TSV tombstone entry (prevents future re-adds by arr_sync)
# 2. Deletes the item from the local arr API (deleteFiles=false)
# 3. SSHes each remote node and deletes from their arr API (deleteFiles=false)
# Files become orphans on all nodes — arr_cleanup.sh removes them on next run.
#
# ── GRACEFUL SKIP ─────────────────────────────────────────────────────────────────────────────
# Arr not configured locally → skip cleanly, no error.
# Arr not reachable on a remote → skip that node for that arr type, continue with others.
# Partial mesh works — nodes that share an arr type sync with each other.
#
# ── WHAT GETS SYNCED ──────────────────────────────────────────────────────────────────────────
# Library items (tracked artists/series/movies) keyed on stable IDs:
# What gets synced — library items keyed on stable external IDs:
# Lidarr — MusicBrainz artist ID (foreignArtistId)
# Sonarr — TVDB series ID (tvdbId)
# Radarr — TMDB movie ID (tmdbId)
# When adding to a remote node, that node's own quality profile, metadata profile,
# and root folder path are used — settings are never copied from the source node.
# When adding to a remote, that node's own quality profile, metadata profile,
# and root folder path are used — settings are never copied from source.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Remote API Keys Never Stored
# SSHes to each remote node and reads the key directly from that arr's
# config.xml in its appdata directory. Only the API response (JSON) is
# returned — the key never leaves the remote node. Self-maintaining: key
# regeneration on the remote is picked up automatically next run.
#
# Blocklist TSV
# ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added
# anywhere. Read from ALL nodes via SSH at run start — immediate effect with
# no rsync delay.
#
# --blocklist-add does three things atomically:
# 1. Writes the TSV tombstone entry (prevents future re-adds by arr_sync)
# 2. Deletes the item from the local arr API (deleteFiles=false)
# 3. SSHes each remote node and deletes from their arr API (deleteFiles=false)
# Files become orphans on all nodes — arr_cleanup removes them on next run.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# acquire_lock — prevents two sync instances running simultaneously
# ARR_SYNC_ENABLED — global gate, exits cleanly when false
# SSH connect timeout — ARR_SYNC_CONNECT_TIMEOUT — does not hang on unreachable node
# API call timeout — ARR_SYNC_API_TIMEOUT — does not hang on slow arr
# Graceful skip — unreachable node/arr → skip and continue, never abort
# Blocklist gate — item in blocklist → never added to any node
# Silent by default — only additions produce output, clean runs stay silent
#
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
# ARR_SYNC_BLOCKLIST — TSV file in DATA_DIR (default: DATA_DIR/arr_sync_blocklist.tsv)
# Columns: arr_type, id, reason, date_added
# Read from all nodes via SSH at the start of each run.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — local Lidarr (aliased by detect_hosts)
# HOST*_SONARR_URL / HOST*_SONARR_API_KEY — local Sonarr
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY — local Radarr
#
# master.conf
#
# ARR_SYNC_ENABLED — global on/off toggle (default: true)
# ARR_SYNC_BLOCKLIST — path to TSV blocklist file
# ARR_SYNC_CONNECT_TIMEOUT — SSH connect timeout in seconds (default: 10)
# ARR_SYNC_API_TIMEOUT — curl API call timeout in seconds (default: 60)
# DOCKER_APPDATA_BASE — base path for arr appdata dirs (default: /mnt/user/appdata)
# ARR_SYNC_LIDARR_PORT — Lidarr port on all nodes (default: 8686)
# ARR_SYNC_SONARR_PORT — Sonarr port on all nodes (default: 8989)
# ARR_SYNC_RADARR_PORT — Radarr port on all nodes (default: 7878)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# arr_sync.sh — sync all arr types, all nodes
# arr_sync.sh --dry-run — preview only, no changes
# arr_sync.sh --log — verbose output
# arr_sync.sh --status — show config and exit
# arr_sync.sh --blocklist-add lidarr <mbid> "reason" — remove from all arrs + tombstone
#
# Blocklist management:
# arr_sync.sh --blocklist-add lidarr <mbid> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-add sonarr <tvdbId> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-add radarr <tmdbId> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-remove lidarr <id> — un-tombstone (does NOT re-add)
# arr_sync.sh --blocklist-list — show all blocklisted IDs
# arr_sync.sh --blocklist-list — show all blocklisted IDs
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# ARR_SYNC_ENABLED — global on/off toggle (default true)
# ARR_SYNC_BLOCKLIST — path to TSV blocklist (default: DATA_DIR/arr_sync_blocklist.tsv)
# ARR_SYNC_CONNECT_TIMEOUT — SSH connect timeout in seconds (default 10)
# ARR_SYNC_API_TIMEOUT — curl API call timeout in seconds (default 60)
# DOCKER_APPDATA_BASE — base path for arr appdata dirs (default /mnt/user/appdata)
# ARR_SYNC_LIDARR_PORT — Lidarr port on all nodes (default 8686)
# ARR_SYNC_SONARR_PORT — Sonarr port on all nodes (default 8989)
# ARR_SYNC_RADARR_PORT — Radarr port on all nodes (default 7878)
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — local Lidarr (aliased by detect_hosts)
# HOST*_SONARR_URL / HOST*_SONARR_API_KEY — local Sonarr
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY — local Radarr
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+50 -34
View File
@@ -2,13 +2,19 @@
# ==============================================================================================
# ========================= Arrs Failed / Stalled Recovery =====================================
# ==============================================================================================
# Automatically detects and recovers from failed imports and stalled downloads
# across Sonarr, Radarr, and Lidarr. Blocklists the bad release and triggers
# a new search — hands-free recovery while you sleep.
#
# ── WHAT IT CHECKS ────────────────────────────────────────────────────────────────────────────
# Four problem types from the arr queue API:
# importFailed — downloaded successfully but arr couldn't import the file
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Detect and recover failed imports and stalled downloads across Sonarr, Radarr,
# and Lidarr. Blocklists the bad release and triggers a re-search — hands-free
# overnight recovery.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Four problem types detected from the arr queue API:
# importFailed — downloaded but arr couldn't import the file
# importPending — downloaded, stuck waiting to import (will not self-resolve)
# error status — serious failure not covered by the above two states
# stalled — download stuck with no connections or no progress
@@ -16,55 +22,65 @@
# Never touches items with state "downloading" or "imported" — safe to run anytime.
# Items newer than ARR_IMPORT_RECOVERY_AGE are skipped — gives arr time to retry first.
#
# ── WHAT IT DOES PER PROBLEM ITEM ─────────────────────────────────────────────────────────────
# Per problem item (3-step response):
# 1. Blocklist the release — prevents re-grabbing the same bad release
# 2. Remove from queue — cleans up the failed item
# 3. Trigger new search — finds a different release automatically
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# detect_hosts() sets MY_ID and aliases all arr vars:
# SONARR_URL / SONARR_API_KEY / SONARR_RECOVERY
# RADARR_URL / RADARR_API_KEY / RADARR_RECOVERY
# LIDARR_URL / LIDARR_API_KEY / LIDARR_RECOVERY (HOST1 only — exits cleanly on HOST2)
# No manual HOST1/HOST2 comparisons needed — MY_ID routes automatically.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── API VERSION SAFETY ────────────────────────────────────────────────────────────────────────
# check_arr_version() verifies the running arr matches the tested major version in master.conf.
# If the API structure changed after an upgrade — exits rather than silently misoperating.
# acquire_lock — prevents concurrent runs overlapping
# jq validation — exits if jq not installed (required for JSON parsing)
# API pre-flight — checks each arr is reachable before querying queue
# Version check — check_arr_version() verifies running arr matches master.conf major
# version; exits rather than silently misoperating after upgrade
# Age threshold — skips items newer than ARR_IMPORT_RECOVERY_AGE (default 6hr)
# Silent by default — only problems produce output, clean arrs stay silent
#
# API version mapping (endpoint paths differ from major version labels):
# Sonarr v4 → /api/v3/ (v3 endpoint retained in v4)
# Radarr v6 → /api/v3/ (v3 endpoint retained in v6)
# Lidarr v3 → /api/v1/ (different from Sonarr/Radarr)
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock — prevents concurrent runs overlapping
# jq validation — exits if jq not installed (required for JSON parsing)
# API pre-flight — checks each arr is reachable before querying queue
# Version check — verifies arr major version matches tested version in master.conf
# Age threshold — skips items newer than ARR_IMPORT_RECOVERY_AGE (default 6hr)
# Silent by default — only problems produce output, clean arrs stay silent
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
# ARR_RECOVERY_STATS — stats file written after each run (read by coffee report)
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# HOST*_SONARR_URL / HOST*_SONARR_API_KEY / HOST*_SONARR_RECOVERY
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY / HOST*_RADARR_RECOVERY
# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY / HOST1_LIDARR_RECOVERY
# All aliased by detect_hosts() — script uses unprefixed names
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# ARR_IMPORT_RECOVERY_AGE — hours before item is eligible (default 6)
# SONARR_VERSION_MAJOR — expected Sonarr major version (e.g. 4)
# RADARR_VERSION_MAJOR — expected Radarr major version (e.g. 6)
# LIDARR_VERSION_MAJOR — expected Lidarr major version (e.g. 3)
# ARR_RECOVERY_STATS — stats file path (read by coffee report)
# master.conf
#
# ARR_IMPORT_RECOVERY_AGE — hours before item is eligible for recovery (default: 6)
# SONARR_VERSION_MAJOR — expected Sonarr major version (e.g. 4)
# RADARR_VERSION_MAJOR — expected Radarr major version (e.g. 6)
# LIDARR_VERSION_MAJOR — expected Lidarr major version (e.g. 3)
# ARR_RECOVERY_STATS — stats file path (read by coffee report)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# arrs_failed_stalled_recovery.sh — normal run
# arrs_failed_stalled_recovery.sh --dry-run — show what would be actioned, no changes
# arrs_failed_stalled_recovery.sh --log — verbose output
# arrs_failed_stalled_recovery.sh --status — show config and exit
#
# ── SCHEDULE ──────────────────────────────────────────────────────────────────────────────────
# Recommended: 0 5 * * * (5am daily)
# Or every 6hr: 0 */6 * * * (matches ARR_IMPORT_RECOVERY_AGE default)
# Recommended schedule: 0 5 * * * (5am daily)
# Or every 6hr: 0 */6 * * * (matches ARR_IMPORT_RECOVERY_AGE default)
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+71 -58
View File
@@ -2,83 +2,96 @@
# ==============================================================================================
# ================================= Lidarr Cleanup =============================================
# ==============================================================================================
# Removes orphaned music files from the library that Lidarr no longer tracks.
# Uses the Lidarr API to build a complete list of tracked file paths then compares
# against what exists on disk — anything untracked and older than LIDARR_ORPHAN_AGE
# days is considered an orphan and deleted.
#
# ── FILE CLASSIFICATION ───────────────────────────────────────────────────────────────────────
# TRACKED — Lidarr API knows about this exact file path → leave it alone
# PROTECTED — matches LIDARR_PROTECTED_PATTERNS → never delete (cover art, .nfo, .lrc etc.)
# ORPHAN — music file, not tracked, older than LIDARR_ORPHAN_AGE days → delete
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Delete orphaned music files not tracked by Lidarr. Queries the API for all
# tracked file paths, walks the library on disk, and removes anything untracked
# that is old enough to be past the import window. Triggers an Emby library
# clean after each deletion run so ghost entries disappear immediately.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Every file encountered on disk is classified into one of five categories:
#
# TRACKED — Lidarr API knows this exact path → leave it alone
# PROTECTED — matches LIDARR_PROTECTED_PATTERNS → never delete
# ORPHAN — music file, not tracked, older than LIDARR_ORPHAN_AGE → delete
# JUNK — not a music extension, not protected → delete regardless of age
# RECENT — not tracked, under LIDARR_ORPHAN_AGE days old → skip (may be mid-import)
# RECENT — not tracked, under LIDARR_ORPHAN_AGE → skip (may be mid-import)
#
# ── WHY PROTECTED PATTERNS MATTER ─────────────────────────────────────────────────────────────
# Lidarr generates cover art (*.jpg), metadata (*.nfo) and lyrics (*.lrc) but does NOT
# include these in its tracked file API response. Without protection these would be
# classified as orphans and deleted — breaking Lidarr and Emby metadata display.
# Lidarr generates cover art (*.jpg), metadata (*.nfo), and lyrics (*.lrc) but
# does NOT include these in its tracked file API response. Without PROTECTED
# classification these would be deleted — breaking Lidarr and Emby display.
#
# ── SAFETY LAYERS — ALL MUST PASS BEFORE ANY FILE IS TOUCHED ─────────────────────────────────
# 1. Container must be running and not starting/unhealthy
# 2. API must be reachable
# 3. API version must match tested major version in master.conf
# 4. Artist count must be > 0
# 5. Tracked file count must be > 0
# 6. Tracked count must be >= LIDARR_MIN_TRACKED_PCT % of last known count
# 7. Deletion size must be < LIDARR_MAX_DELETE_GB — or --i-know-what-im-doing required
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── OVERRIDE FLAGS ────────────────────────────────────────────────────────────────────────────
# --i-know-what-im-doing
# Required when deletion would exceed LIDARR_MAX_DELETE_GB.
# Long and annoying by design — cannot be added accidentally.
# Seven gates — ALL must pass before any file is touched:
# 1. Container running and not starting/unhealthy
# 2. API reachable
# 3. API version matches LIDARR_VERSION_MAJOR in master.conf
# 4. Artist count > 0
# 5. Tracked file count > 0
# 6. Tracked count >= LIDARR_MIN_TRACKED_PCT % of last known count
# 7. Deletion size < LIDARR_MAX_DELETE_GB — or --i-know-what-im-doing required
#
# --skip-strike-list
# Bypasses the LIDARR_ORPHAN_AGE age check — deletes recent files too.
#
# NUCLEAR MODE — both flags active together:
# Age check bypassed, size threshold bypassed, deletes on first pass.
# Use when Soularr has filled the gaps and you want a clean one-pass wipe.
# ⚠️ Script author takes NO responsibility for data loss with both flags active.
# The user accepts full responsibility — this is 100% intentional by design.
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# Runs on any node where Lidarr is configured — skips cleanly if LIDARR_URL/API_KEY not set.
# detect_hosts() aliases LIDARR_URL, LIDARR_API_KEY, LIDARR_MUSIC_ROOT from HOST*_LIDARR_*.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — large scans take time, wait for previous run to finish
# jq + curl validation — exits if either tool missing
# DOCKER_TIMEOUT — container checks protected against daemon hangs
# 7 safety layers — all must pass before any file is touched
# Duplicate detection — temp file of tracked paths, grep before delete
# validate_unraid_cmd — notify script validated before use
# Silent by default — orphans/junk warn(), clean library logs silently
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
# LIDARR_TRACKED_COUNT_FILE — persistent baseline for the tracked % safety check (gate 6)
# Updated after each successful run. Protects against misconfigured root path
# returning an empty API response and deleting the entire library.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY / HOST1_LIDARR_MUSIC_ROOT
# HOST1_LIDARR_PATH_MAP — container path → host path translation
# All aliased by detect_hosts() — script uses unprefixed names
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# LIDARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# LIDARR_LOCK_WARN_AGE — override default lock warning age (large libraries)
# LIDARR_ORPHAN_AGE — days before untracked file is eligible for deletion
# LIDARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# LIDARR_MIN_TRACKED_PCT — abort if tracked count drops below this % of last run
# LIDARR_TRACKED_COUNT_FILE — persistent baseline file path
# LIDARR_EXTENSIONS — music file extensions to consider for orphan classification
# LIDARR_PROTECTED_PATTERNS — file patterns that are never deleted
# LIDARR_VERSION_MAJOR — expected Lidarr major version for API safety check
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
# master.conf
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# lidarr_cleanup.sh — normal run
# lidarr_cleanup.sh --dry-run — preview, no deletions
# lidarr_cleanup.sh --log verbose output
# lidarr_cleanup.sh --status — show config and exit
# lidarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# LIDARR_ORPHAN_AGE — days before untracked file is eligible for deletion
# LIDARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# LIDARR_MIN_TRACKED_PCT — abort if tracked count drops below this % of last run
# LIDARR_TRACKED_COUNT_FILEpersistent baseline file path
# LIDARR_EXTENSIONS — music file extensions for orphan classification
# LIDARR_PROTECTED_PATTERNS — file patterns that are never deleted
# LIDARR_VERSION_MAJOR — expected Lidarr major version for API safety check
# LIDARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# LIDARR_LOCK_WARN_AGE — override default lock warning age (large libraries)
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# lidarr_cleanup.sh — normal run
# lidarr_cleanup.sh --dry-run — preview, no deletions
# lidarr_cleanup.sh --log — verbose output
# lidarr_cleanup.sh --status — show config and exit
# lidarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# lidarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
#
# NUCLEAR MODE: both flags bypass age check AND size threshold. Use when Soularr
# has filled the gaps and you want a clean one-pass wipe. User accepts full
# responsibility — the flag name is long and annoying by design.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+47 -38
View File
@@ -2,60 +2,69 @@
# ==============================================================================================
# ================================= Lidarr Missing Art =========================================
# ==============================================================================================
# Fetches missing album and artist artwork for the Lidarr music library.
# Reads from Lidarr API to discover album/artist paths, then downloads only
# what is missing — never overwrites existing files.
#
# ── SAFE DESIGN ───────────────────────────────────────────────────────────────────────────────
# READS from Lidarr only — no writes back to Lidarr
# NEVER modifies audio tags or renames media files
# NEVER overwrites existing artwork
# ONLY writes missing artwork files to existing album/artist directories
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Fetch missing album and artist artwork for the Lidarr music library. Downloads
# only what is absent — never overwrites existing files. Idempotent re-runs are
# safe.
#
# ── ARTWORK TARGETS ───────────────────────────────────────────────────────────────────────────
# Album folder: cover.jpg cdart.png back.jpg
# Artist folder: folder.jpg fanart.jpg logo.png banner.jpg
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# ── SOURCES ───────────────────────────────────────────────────────────────────────────────────
# Artwork targets per album folder: cover.jpg cdart.png back.jpg
# Artwork targets per artist folder: folder.jpg fanart.jpg logo.png banner.jpg
#
# Sources (tried in order, first success wins):
# Album covers: fanart.tv → iTunes fallback
# Artist art: fanart.tv → Deezer fallback → Last.fm fallback
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# Lidarr runs on HOST1 only. detect_hosts() sets LIDARR_URL — if empty (HOST2) the
# script exits cleanly with no action rather than failing.
# Reads from Lidarr API only — no writes back to Lidarr. Never modifies audio
# tags or renames media files. Only writes missing artwork files to existing
# album/artist directories.
#
# ── OUTPUT ────────────────────────────────────────────────────────────────────────────────────
# Minimal by default — section headers + per-section summary always visible.
# --log shows per-item detail (each album, each artist, each file fetched).
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock — prevents concurrent runs during large library scans
# curl + jq check — fail fast if tools missing
# API reachability — verified before processing begins
# Skip existing — never overwrites, idempotent re-runs are safe
# Min file size — rejects corrupt/placeholder downloads (LIDARR_ART_MIN_SIZE)
# Parallel jobs capped at LIDARR_ART_MAX_PARALLEL to avoid hammering APIs
# Download retries — LIDARR_ART_RETRIES attempts per image before giving up
# Dry-run mode — logs what would be downloaded without writing anything
# acquire_lock — prevents concurrent runs during large library scans
# curl + jq check — fail fast if tools missing
# API reachability — verified before processing begins
# detect_hosts() — exits cleanly if LIDARR_URL empty (HOST2, no Lidarr)
# Skip existing — never overwrites, idempotent re-runs are safe
# Min file size check — rejects corrupt/placeholder downloads (LIDARR_ART_MIN_SIZE)
# Parallel job cap — LIDARR_ART_MAX_PARALLEL avoids hammering external APIs
# Download retries — LIDARR_ART_RETRIES attempts per image before giving up
# Rate limiting — LIDARR_ART_SLEEP_BETWEEN between fanart.tv API calls
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# FANART_API_KEY — fanart.tv API key
# LASTFM_API_KEY — last.fm API key
# LIDARR_ART_MIN_SIZE — minimum valid download size in bytes
# LIDARR_ART_MAX_PARALLEL — concurrent background download jobs
# LIDARR_ART_RETRIES — download retry attempts per image
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY
# Aliased by detect_hosts() — script uses LIDARR_URL / LIDARR_API_KEY
#
# master.conf
#
# FANART_API_KEY — fanart.tv API key
# LASTFM_API_KEY — last.fm API key
# LIDARR_ART_MIN_SIZE — minimum valid download size in bytes
# LIDARR_ART_MAX_PARALLEL — concurrent background download jobs
# LIDARR_ART_RETRIES — download retry attempts per image
# LIDARR_ART_SLEEP_BETWEEN — seconds between fanart.tv API calls
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# HOST1_LIDARR_URL — Lidarr base URL
# HOST1_LIDARR_API_KEY — Lidarr API key
# All aliased by detect_hosts() — script uses unprefixed LIDARR_URL / LIDARR_API_KEY
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# lidarr_missing_art.sh — fetch all missing artwork
# lidarr_missing_art.sh --dry-run — preview without downloading
# lidarr_missing_art.sh --log — verbose per-item output
# lidarr_missing_art.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+31 -19
View File
@@ -2,20 +2,24 @@
# ==============================================================================================
# ================================= Media Cleaner ==============================================
# ==============================================================================================
# Removes unwanted junk files from media share folders using configurable file patterns.
# Two profiles — anime and media — each with their own folder list and file patterns.
# Runs daily via DAILY_MAINTENANCE_SCRIPTS after media_shares_permissions.sh.
#
# ── PROFILES ──────────────────────────────────────────────────────────────────────────────────
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Remove junk files from media shares using configurable file patterns. Two
# profiles — anime and media — each with their own folder list and patterns.
# Runs second in the daily maintenance window, after permissions and before
# arr cleanup, so orphan detection only encounters actual media files.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Profiles:
# anime — cleans ANIME_CLEAN_FOLDERS using ANIME_FILE_PATTERNS
# typical targets: *.sfv *.nfo *.url *.rar *.zip *.sample* etc.
# media — cleans MEDIA_CLEAN_FOLDERS using MEDIA_FILE_PATTERNS (adds *.iso *.lrc)
#
# media — cleans MEDIA_CLEAN_FOLDERS using MEDIA_FILE_PATTERNS
# same patterns plus *.iso *.lrc (media-specific extras)
#
# ── WHAT IT REMOVES ───────────────────────────────────────────────────────────────────────────
# Junk files left behind by download clients, scene releases, and various tools:
# *.sfv *.md5 *.sha1 — checksum verification files — useless post-download
# Removes junk left by download clients, scene releases, and tools:
# *.sfv *.md5 *.sha1 — checksum files — useless post-download
# *.nfo *.url *.lnk — scene info files — not needed in media library
# *.rar *.zip — archives — source files not needed after extraction
# *.sample* *.proof* — scene samples — never needed
@@ -24,12 +28,10 @@
# *.torrent — torrent files left by download clients
# *.log *.json — tool output files
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# detect_hosts() sets MY_ID and aliases HOST*_ANIME_CLEAN_FOLDERS → ANIME_CLEAN_FOLDERS
# and HOST*_MEDIA_CLEAN_FOLDERS → MEDIA_CLEAN_FOLDERS.
# Each server only cleans the shares it owns — correct folders per host automatically.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — wait if previous run still active
# detect_hosts() — correct folder lists per host via MY_ID aliases
# Empty array guards — warns and exits cleanly if no folders or patterns configured
@@ -37,22 +39,32 @@
# validate_unraid_cmd — notify script validated before use
# Silent by default — only problems and removals produce output
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST*_ANIME_CLEAN_FOLDERS — folders cleaned by the anime profile on this host
# HOST*_MEDIA_CLEAN_FOLDERS — folders cleaned by the media profile on this host
# Aliased by detect_hosts() — script uses ANIME_CLEAN_FOLDERS / MEDIA_CLEAN_FOLDERS
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# master.conf
#
# ANIME_FILE_PATTERNS — file patterns removed by the anime profile
# MEDIA_FILE_PATTERNS — file patterns removed by the media profile
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# media_cleaner.sh anime — clean anime shares
# media_cleaner.sh media — clean media shares
# media_cleaner.sh anime --dry-run — preview anime clean, no deletions
# media_cleaner.sh media --dry-run — preview media clean, no deletions
# media_cleaner.sh anime --log — verbose output
# media_cleaner.sh anime --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+30 -38
View File
@@ -2,45 +2,22 @@
# ==============================================================================================
# ============================= Media Shares Permissions =======================================
# ==============================================================================================
# Applies correct ownership and permissions to all configured media shares.
# Runs daily via DAILY_MAINTENANCE_SCRIPTS — first job before arr cleanup scripts.
# Arr cleanup depends on correct ownership to rename and delete files safely.
#
# ── WHY THIS EXISTS ───────────────────────────────────────────────────────────────────────────
# Originally a band-aid for 777 permissions caused by containers running as root.
# Now a proper daily failsafe — even with correct container config, files can arrive
# with wrong ownership from:
# - rsync without --chown (brings source server's ownership)
# - Manual admin copies (creates root:root files)
# - New containers not yet configured with correct PUID/PGID
# - unRAID updates that reset container environments
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Apply nobody:users ownership and correct permissions to all media shares.
# Runs daily as the first job in the maintenance window — arr cleanup depends
# on correct ownership to rename and delete files.
#
# ── PERMISSIONS MODEL ─────────────────────────────────────────────────────────────────────────
# Directories: 755 nobody:users
# Owner (nobody) — rwx enter, list, create files ✅
# Group (users) — r-x enter and list ✅
# Others — r-x Samba guests can browse ✅
# No world-write — prevents accidental deletion by unauthenticated access
# Now a proper daily failsafe: files arrive with wrong ownership from rsync
# without --chown, manual admin copies, containers with unconfigured PUID/PGID,
# or unRAID environment resets after updates.
#
# Files: 664 nobody:users
# Owner (nobody) — rw read + write ✅
# Group (users) — rw arrs can import/rename ✅
# Others — r Samba guests can read ✅
# No execute bit — media files are never executable ✅
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── DIAGNOSTIC — HIGH CORRECTED COUNT ─────────────────────────────────────────────────────────
# If this script corrects many files every run, a container has wrong PUID/PGID:
# Correct values on unRAID: PUID=99 (nobody) PGID=100 (users)
# Add to each container's environment in its Docker template
# Common culprits: SABnzbd, qBittorrent, slskd — check these first
# Once fixed, this script should correct 0 files per run (pure failsafe)
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# detect_hosts() sets MY_ID and aliases HOST*_MEDIA_PERMISSION_SHARES → MEDIA_PERMISSION_SHARES
# Each server only applies permissions to the shares it owns.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — wait if previous run still active (large share scans take time)
# acquire_lock "wait" — wait if previous run still active (large share scans)
# detect_hosts() — correct share list per host via MY_ID aliases
# Empty array guard — warns and exits cleanly if no shares configured
# Folder existence — skips missing shares with warning, continues others
@@ -48,20 +25,35 @@
# validate_unraid_cmd — notify script validated before use
# Silent by default — only failures produce output, success is silent
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# Diagnostic — high corrected count on every run means a container has wrong PUID/PGID:
# Correct values on unRAID: PUID=99 (nobody) PGID=100 (users)
# Common culprits: SABnzbd, qBittorrent, slskd — check these first
# Once fixed, this script should correct 0 files per run (pure failsafe)
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST*_MEDIA_PERMISSION_SHARES — shares this host applies permissions to
# Aliased by detect_hosts() — script uses MEDIA_PERMISSION_SHARES
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# master.conf
#
# PERMISSIONS_DIR_MODE — directory permissions (default 755)
# PERMISSIONS_FILE_MODE — file permissions (default 664)
# PERMISSIONS_OWNER — ownership applied to all files (default nobody:users)
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# media_shares_permissions.sh — normal run
# media_shares_permissions.sh --dry-run — preview without making changes
# media_shares_permissions.sh --log — verbose output
# media_shares_permissions.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+9 -16
View File
@@ -1,24 +1,17 @@
#!/bin/bash
# ==============================================================================================
# =============================== LIDARR DISCOVERY =============================================
# =========================== Playback-Aware Lidarr Discovery ==================================
# ==============================================================================================
# Lidarr discovery orchestrator.
#
# Consumes:
# Kernel/decision_engine.sh
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Behavior-driven music discovery scoring prototype. Consumes
# Kernel/decision_engine.sh to score artist/album candidates for acquisition
# using family-aware and playback-weighted logic.
#
# Purpose:
# Discover high-quality artists/albums for acquisition using
# family-aware and behavior-driven scoring logic.
#
# This script is intentionally selective.
#
# Music discovery is treated differently than TV/movies:
#
# Higher strictness
# Stronger quality bias
# Lower tolerance for trend chasing
# Longer behavioral memory
# WIP — not yet connected to a live data source or scheduled. Discovery logic
# is intentionally selective: higher strictness, stronger quality bias, lower
# tolerance for trend-chasing, longer behavioral memory than TV/movies.
#
# ==============================================================================================
+61 -51
View File
@@ -2,77 +2,87 @@
# ==============================================================================================
# ================================= Radarr Cleanup =============================================
# ==============================================================================================
# Removes orphaned movie files from the library that Radarr no longer tracks.
# Uses the Radarr API to build a complete list of tracked movie file paths then compares
# against what exists on disk — anything untracked and older than RADARR_ORPHAN_AGE
# days is considered an orphan and deleted.
#
# ── FILE CLASSIFICATION ───────────────────────────────────────────────────────────────────────
# TRACKED — Radarr API knows about this exact file path → leave it alone
# PROTECTED — matches RADARR_PROTECTED_PATTERNS → never delete (artwork, subtitles, .nfo)
# ORPHAN — video file, not tracked, older than RADARR_ORPHAN_AGE days → delete
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Delete orphaned movie files not tracked by Radarr. Queries the API for all
# tracked movie file paths, walks the library on disk, and removes anything
# untracked that is old enough to be past the import window. Triggers an Emby
# library clean after each deletion run so ghost entries disappear immediately.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Every file encountered on disk is classified into one of five categories:
#
# TRACKED — Radarr API knows this exact path → leave it alone
# PROTECTED — matches RADARR_PROTECTED_PATTERNS → never delete
# ORPHAN — video file, not tracked, older than RADARR_ORPHAN_AGE → delete
# JUNK — not a video extension, not protected → delete regardless of age
# RECENT — not tracked, under RADARR_ORPHAN_AGE days old → skip (may be mid-import)
# RECENT — not tracked, under RADARR_ORPHAN_AGE → skip (may be mid-import)
#
# ── WHY PROTECTED PATTERNS MATTER ─────────────────────────────────────────────────────────────
# Radarr generates movie artwork (*.jpg), metadata (*.nfo) and manages subtitles (*.srt,
# *.sub, *.ass) but does NOT include these in its tracked file API response.
# Without protection these would be classified as orphans and deleted — breaking
# Radarr and Emby metadata display.
# Radarr generates movie artwork (*.jpg), metadata (*.nfo), and manages subtitles
# (*.srt, *.sub, *.ass) but does NOT include these in its tracked file API response.
# Without PROTECTED classification these would be deleted — breaking Radarr and
# Emby metadata display.
#
# ── SAFETY LAYERS — ALL MUST PASS BEFORE ANY FILE IS TOUCHED ─────────────────────────────────
# 1. Container must be running and not starting/unhealthy
# 2. API must be reachable
# 3. API version must match tested major version in master.conf
# 4. Movie count must be > 0
# 5. Tracked file count must be > 0
# 6. Deletion size must be < RADARR_MAX_DELETE_GB — or --i-know-what-im-doing required
# After deletions: notify_emby_scan() triggers Emby "Clean Missing Files" task.
# Emby removes ghost entries immediately — no user-facing file-not-found errors.
#
# ── POST-DELETION ─────────────────────────────────────────────────────────────────────────────
# After files are deleted notify_emby_scan() triggers Emby "Clean Missing Files" task.
# Emby immediately removes ghost entries — no user-facing file-not-found errors.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── OVERRIDE FLAGS ────────────────────────────────────────────────────────────────────────────
# --i-know-what-im-doing required when deletion exceeds RADARR_MAX_DELETE_GB
# --skip-strike-list bypasses RADARR_ORPHAN_AGE age check
# NUCLEAR MODE — both active: age + size bypass, deletes on first pass
# ⚠️ User accepts full responsibility — no recovery possible after deletion
# Six gates — ALL must pass before any file is touched:
# 1. Container running and not starting/unhealthy
# 2. API reachable
# 3. API version matches RADARR_VERSION_MAJOR in master.conf
# 4. Movie count > 0
# 5. Tracked file count > 0
# 6. Deletion size < RADARR_MAX_DELETE_GB — or --i-know-what-im-doing required
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# HOST1 Radarr manages Movies. HOST2 Radarr manages Anime_Movies.
# detect_hosts() sets MY_ID and aliases RADARR_URL, RADARR_API_KEY, RADARR_MOVIES_ROOT.
# No manual HOST1/HOST2 comparisons — MY_ID routes correctly on any server.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — large scans take time, wait for previous run to finish
# jq + curl validation — exits if either tool missing
# DOCKER_TIMEOUT — container checks protected against daemon hangs
# 6 safety layers — all must pass before any file is touched
# notify_emby_scan() — triggers Emby clean after deletion
# validate_unraid_cmd — notify script validated before use
# Silent by default — orphans/junk warn(), clean library logs silently
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY / HOST*_RADARR_MOVIES_ROOT
# HOST*_RADARR_PATH_MAP — container path → host path translation
# All aliased by detect_hosts() — script uses unprefixed names
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# RADARR_ORPHAN_AGE — days before untracked file eligible for deletion
# RADARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# RADARR_EXTENSIONS — video file extensions considered for orphan classification
# RADARR_PROTECTED_PATTERNS — file patterns never deleted
# RADARR_VERSION_MAJOR — expected Radarr major version for API safety check
# RADARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
# master.conf
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# radarr_cleanup.sh — normal run
# radarr_cleanup.sh --dry-run — preview, no deletions
# radarr_cleanup.sh --log — verbose output
# radarr_cleanup.sh --status — show config and exit
# radarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# RADARR_ORPHAN_AGE — days before untracked file eligible for deletion
# RADARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# RADARR_EXTENSIONS — video file extensions for orphan classification
# RADARR_PROTECTED_PATTERNS — file patterns never deleted
# RADARR_VERSION_MAJOR — expected Radarr major version for API safety check
# RADARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# radarr_cleanup.sh — normal run
# radarr_cleanup.sh --dry-run — preview, no deletions
# radarr_cleanup.sh --log — verbose output
# radarr_cleanup.sh --status — show config and exit
# radarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# radarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
#
# NUCLEAR MODE: both flags bypass age check AND size threshold. User accepts full
# responsibility — the flag name is long and annoying by design.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+39 -17
View File
@@ -2,36 +2,58 @@
# ==============================================================================================
# ============================= Radarr — TMDb Removed ==========================================
# ==============================================================================================
# Removes movies from Radarr that have been dropped from TMDb.
# Radarr marks these with status="deleted" — they generate system health errors and
# can never be monitored or downloaded. 99% are future/announced movies that were
# delisted before release.
#
# ── WHAT IT DOES ──────────────────────────────────────────────────────────────────────────────
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Remove movies from Radarr that TMDb has dropped. Radarr marks these with
# status="deleted" — they generate health errors and can never be monitored
# or downloaded. Most are announced-but-never-released films delisted before
# release.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Queries Radarr API for movies with status="deleted" (TMDb removal marker)
# Reports each found entry with file status and size
# Reports each entry with file status and size
# Removes the movie record from Radarr
# Optionally deletes associated files (disabled by default — most have none)
# Optionally adds to Radarr's import exclusion list (default: true)
# Optionally adds to Radarr's import exclusion list (default: true — prevents re-add)
#
# ── SAFE DEFAULTS ─────────────────────────────────────────────────────────────────────────────
# Files are NOT deleted by default — use --delete-files to also remove from disk
# Import exclusion added by default — prevents Radarr re-adding dropped movies
# Per-deletion output always visible — deletions are never silently swallowed
# Files are NOT deleted by default — use --delete-files to also remove from disk.
# Per-deletion output is always visible — deletions are never silently swallowed.
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# Radarr runs on HOST1 only. detect_hosts() sets RADARR_URL — if empty (HOST2)
# the script exits cleanly.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# RADARR_DROPPED_ADD_EXCLUSION — add removed movies to import exclusion (default: true)
# detect_hosts() — exits cleanly if RADARR_URL empty (Radarr not on this host)
# API pre-flight — verifies Radarr reachable before querying
# Dry-run mode — full preview without removing anything
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# RADARR_DROPPED_ADD_EXCLUSION — add removed movies to import exclusion (default: true)
#
# master_host*.conf
#
# HOST1_RADARR_URL / HOST1_RADARR_API_KEY
# Aliased by detect_hosts() — script uses RADARR_URL / RADARR_API_KEY
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# radarr_tmdb_removed.sh — remove records, keep files, add exclusion
# radarr_tmdb_removed.sh --delete-files — also delete files from disk
# radarr_tmdb_removed.sh --dry-run — preview without removing anything
# radarr_tmdb_removed.sh --log — verbose output
# radarr_tmdb_removed.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+61 -51
View File
@@ -2,77 +2,87 @@
# ==============================================================================================
# ================================= Sonarr Cleanup =============================================
# ==============================================================================================
# Removes orphaned TV episode files from the library that Sonarr no longer tracks.
# Uses the Sonarr API to build a complete list of tracked episode file paths then compares
# against what exists on disk — anything untracked and older than SONARR_ORPHAN_AGE
# days is considered an orphan and deleted.
#
# ── FILE CLASSIFICATION ───────────────────────────────────────────────────────────────────────
# TRACKED — Sonarr API knows about this exact file path → leave it alone
# PROTECTED — matches SONARR_PROTECTED_PATTERNS → never delete (artwork, subtitles, .nfo)
# ORPHAN — video file, not tracked, older than SONARR_ORPHAN_AGE days → delete
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Delete orphaned TV episode files not tracked by Sonarr. Queries the API for
# all tracked episode file paths, walks the library on disk, and removes anything
# untracked that is old enough to be past the import window. Triggers an Emby
# library clean after each deletion run so ghost entries disappear immediately.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Every file encountered on disk is classified into one of five categories:
#
# TRACKED — Sonarr API knows this exact path → leave it alone
# PROTECTED — matches SONARR_PROTECTED_PATTERNS → never delete
# ORPHAN — video file, not tracked, older than SONARR_ORPHAN_AGE → delete
# JUNK — not a video extension, not protected → delete regardless of age
# RECENT — not tracked, under SONARR_ORPHAN_AGE days old → skip (may be mid-import)
# RECENT — not tracked, under SONARR_ORPHAN_AGE → skip (may be mid-import)
#
# ── WHY PROTECTED PATTERNS MATTER ─────────────────────────────────────────────────────────────
# Sonarr generates show artwork (*.jpg), metadata (*.nfo) and manages subtitles (*.srt,
# *.sub, *.ass) but does NOT include these in its tracked file API response.
# Without protection these would be classified as orphans and deleted — breaking
# Sonarr and Emby metadata display.
# Sonarr generates show artwork (*.jpg), metadata (*.nfo), and manages subtitles
# (*.srt, *.sub, *.ass) but does NOT include these in its tracked file API response.
# Without PROTECTED classification these would be deleted — breaking Sonarr and
# Emby metadata display.
#
# ── SAFETY LAYERS — ALL MUST PASS BEFORE ANY FILE IS TOUCHED ─────────────────────────────────
# 1. Container must be running and not starting/unhealthy
# 2. API must be reachable
# 3. API version must match tested major version in master.conf
# 4. Series count must be > 0
# 5. Tracked file count must be > 0
# 6. Deletion size must be < SONARR_MAX_DELETE_GB — or --i-know-what-im-doing required
# After deletions: notify_emby_scan() triggers Emby "Clean Missing Files" task.
# Emby removes ghost entries immediately — no user-facing file-not-found errors.
#
# ── POST-DELETION ─────────────────────────────────────────────────────────────────────────────
# After files are deleted notify_emby_scan() triggers Emby "Clean Missing Files" task.
# Emby immediately removes ghost entries — no user-facing file-not-found errors.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── OVERRIDE FLAGS ────────────────────────────────────────────────────────────────────────────
# --i-know-what-im-doing required when deletion exceeds SONARR_MAX_DELETE_GB
# --skip-strike-list bypasses SONARR_ORPHAN_AGE age check
# NUCLEAR MODE — both active: age + size bypass, deletes on first pass
# ⚠️ User accepts full responsibility — no recovery possible after deletion
# Six gates — ALL must pass before any file is touched:
# 1. Container running and not starting/unhealthy
# 2. API reachable
# 3. API version matches SONARR_VERSION_MAJOR in master.conf
# 4. Series count > 0
# 5. Tracked file count > 0
# 6. Deletion size < SONARR_MAX_DELETE_GB — or --i-know-what-im-doing required
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# HOST1 Sonarr manages Tv_Shows. HOST2 Sonarr manages Anime_Shows.
# detect_hosts() sets MY_ID and aliases SONARR_URL, SONARR_API_KEY, SONARR_TV_ROOT.
# No manual HOST1/HOST2 comparisons — MY_ID routes correctly on any server.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — large scans take time, wait for previous run to finish
# jq + curl validation — exits if either tool missing
# DOCKER_TIMEOUT — container checks protected against daemon hangs
# 6 safety layers — all must pass before any file is touched
# notify_emby_scan() — triggers Emby clean after deletion
# validate_unraid_cmd — notify script validated before use
# Silent by default — orphans/junk warn(), clean library logs silently
#
# ── CONFIGURATION (master_host*.conf) ─────────────────────────────────────────────────────────
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master_host*.conf
#
# HOST*_SONARR_URL / HOST*_SONARR_API_KEY / HOST*_SONARR_TV_ROOT
# HOST*_SONARR_PATH_MAP — container path → host path translation
# All aliased by detect_hosts() — script uses unprefixed names
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# SONARR_ORPHAN_AGE — days before untracked file eligible for deletion
# SONARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# SONARR_EXTENSIONS — video file extensions considered for orphan classification
# SONARR_PROTECTED_PATTERNS — file patterns never deleted
# SONARR_VERSION_MAJOR — expected Sonarr major version for API safety check
# SONARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
# master.conf
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# sonarr_cleanup.sh — normal run
# sonarr_cleanup.sh --dry-run — preview, no deletions
# sonarr_cleanup.sh --log — verbose output
# sonarr_cleanup.sh --status — show config and exit
# sonarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# SONARR_ORPHAN_AGE — days before untracked file eligible for deletion
# SONARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
# SONARR_EXTENSIONS — video file extensions for orphan classification
# SONARR_PROTECTED_PATTERNS — file patterns never deleted
# SONARR_VERSION_MAJOR — expected Sonarr major version for API safety check
# SONARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# sonarr_cleanup.sh — normal run
# sonarr_cleanup.sh --dry-run — preview, no deletions
# sonarr_cleanup.sh --log — verbose output
# sonarr_cleanup.sh --status — show config and exit
# sonarr_cleanup.sh --i-know-what-im-doing — bypass size threshold
# sonarr_cleanup.sh --i-know-what-im-doing --skip-strike-list — NUCLEAR MODE
#
# NUCLEAR MODE: both flags bypass age check AND size threshold. User accepts full
# responsibility — the flag name is long and annoying by design.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+38 -16
View File
@@ -2,35 +2,57 @@
# ==============================================================================================
# ============================= Sonarr — TVDB Removed ==========================================
# ==============================================================================================
# Removes series from Sonarr that have been dropped from TVDB.
# Sonarr marks these with status="deleted" — they generate system health errors and
# can never be monitored or downloaded.
#
# ── WHAT IT DOES ──────────────────────────────────────────────────────────────────────────────
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Remove series from Sonarr that TVDB has dropped. Sonarr marks these with
# status="deleted" — they generate health errors and can never be monitored
# or downloaded.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Queries Sonarr API for series with status="deleted" (TVDB removal marker)
# Reports each found entry with file count and total size
# Reports each entry with file count and total size
# Removes the series record from Sonarr
# Optionally deletes associated files (disabled by default)
# Optionally adds to Sonarr's import exclusion list (default: true)
# Optionally adds to Sonarr's import exclusion list (default: true — prevents re-add)
#
# ── SAFE DEFAULTS ─────────────────────────────────────────────────────────────────────────────
# Files are NOT deleted by default — use --delete-files to also remove from disk
# Import exclusion added by default — prevents Sonarr re-adding dropped series
# Per-deletion output always visible — deletions are never silently swallowed
# Files are NOT deleted by default — use --delete-files to also remove from disk.
# Per-deletion output is always visible — deletions are never silently swallowed.
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# Sonarr runs on HOST1 only. detect_hosts() sets SONARR_URL — if empty (HOST2)
# the script exits cleanly.
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# SONARR_DROPPED_ADD_EXCLUSION — add removed series to import exclusion (default: true)
# detect_hosts() — exits cleanly if SONARR_URL empty (Sonarr not on this host)
# API pre-flight — verifies Sonarr reachable before querying
# Dry-run mode — full preview without removing anything
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# SONARR_DROPPED_ADD_EXCLUSION — add removed series to import exclusion (default: true)
#
# master_host*.conf
#
# HOST1_SONARR_URL / HOST1_SONARR_API_KEY
# Aliased by detect_hosts() — script uses SONARR_URL / SONARR_API_KEY
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# sonarr_tvdb_removed.sh — remove records, keep files, add exclusion
# sonarr_tvdb_removed.sh --delete-files — also delete files from disk
# sonarr_tvdb_removed.sh --dry-run — preview without removing anything
# sonarr_tvdb_removed.sh --log — verbose output
# sonarr_tvdb_removed.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"