Files
Varaverk/Media/README-Media.md
T
Gmer4Lfe 8a2707ee37 Correct and extend folder docs for Docker Essentials, Watchdogs, Media and Arrs Stack
The docs had drifted from the scripts — a script that no longer exists, three wrong variable
names, a reversed run order, and seven scheduled scripts that were never documented at all.
2026-08-01 22:59:07 -04:00

6.2 KiB

━━━━━ MEDIA ━━━━━

Foundation-level media library management — permissions, junk removal, and play state sync. These three scripts run before and independently of arr stack operations.

For arr stack scripts (orphan cleanup, release fixer, sync, discovery, webhooks) see Arrs_Stack/README-Arrs_Stack.md.


━━━ THE PROBLEMS THAT BUILT THIS ━━━

Files Owned by Root That Arrs Can't Touch Download clients without explicit PUID/PGID write files owned by root. Arrs running as nobody:users cannot rename, move, or delete them. Import fails. Upgrade attempts fail. The failure is subtle — arr shows the file as managed but can't touch it. You only discover this when an upgrade is requested and the old version refuses to delete. The fix: media_shares_permissions.sh applies correct ownership daily. Even if a container is misconfigured, the window is at most 24 hours.

Scene Junk Confusing Orphan Detection Scene releases include .sfv, .nfo, .rar, .sample files alongside the actual media. After extraction and import these are worthless — but they're not tracked by any arr. They look like orphans. Processing them as orphans means the cleanup output is full of noise, making it hard to spot actual orphaned media. The fix: media_cleaner.sh runs before any arr cleanup and removes all known junk patterns first. By the time arr cleanup runs, every untracked file is actual media.

Watch State Diverging Across Servers With two Emby servers, played status and resume positions diverge — a film marked watched on HOST1 shows as unwatched on HOST2. Two users on different servers get different continue-watching rows. The fix: play_state_sync.sh syncs watched/played state and resume positions every 30 minutes. Newest timestamp wins. Both servers always reflect the same play history.


━━━ WHAT THIS FOLDER DOES ━━━

Library Foundation media_shares_permissions.sh — normalize ownership and permissions daily. Runs first in every maintenance window because arr cleanup depends on correct ownership to delete files.

Junk Removal media_cleaner.sh — remove scene debris and tool artifacts before orphan scan. Runs before any arr cleanup so orphan detection only encounters actual media files.

Play State Sync play_state_sync.sh — syncs watched/played state and resume positions across all configured Emby and Jellyfin servers. Newest timestamp wins. Runs every 30 minutes via critical_sync_maintenance.sh.


━━━ EXECUTION ORDER ━━━

Daily via daily_sync_maintenance.sh (DAILY_MAINTENANCE_SCRIPTS — runs first):

1. media_shares_permissions.sh   — permissions first — arr cleanup depends on this
2. media_cleaner.sh anime        — junk before orphan scan
3. media_cleaner.sh media

These complete before any Arrs_Stack/ scripts run.

Every 30 min via critical_sync_maintenance.sh (CRITICAL_MAINTENANCE_SCRIPTS):

play_state_sync.sh   — sync watched/resume state across Emby + Jellyfin

Why permissions before everything else: arr cleanup needs nobody:users ownership to delete files. If a file is root:root, deletion fails silently — the file looks processed but stays on disk.

Why junk before arr cleanup: junk files are not tracked by any arr — they look like orphans. Removing them first means orphan detection only finds actual media. Cleaner output, more accurate detection.


━━━ HOST AWARENESS ━━━

Scripts run on both servers via detect_hosts(), which aliases all HOST*_ prefixed vars to their unprefixed names at runtime.

Permissions and cleaner scripts run locally against each server's own shares, defined in HOST*_MEDIA_PERMISSION_SHARES and HOST*_MEDIA_CLEAN_FOLDERS in host*.conf.

play_state_sync.sh reads both servers' Emby/Jellyfin endpoints from host*.conf and syncs between them.


━━━ SCRIPTS IN THIS FOLDER ━━━

Script Role When It Runs
media_shares_permissions.sh Apply nobody:users ownership + correct permissions to all media shares Daily — runs first
media_cleaner.sh Remove junk files (two profiles: anime + media) Daily — runs before arr cleanup
play_state_sync.sh Sync watched/played state + resume positions across Emby + Jellyfin Every 30 min

━━━ THE ctime INVARIANT — READ BEFORE CHANGING PERMISSIONS ━━━

media_shares_permissions.sh applies every pass conditionally — it touches only entries whose owner or mode is actually wrong. That is not an optimisation, and it must stay that way.

chown and chmod rewrite an inode's ctime even when the value does not change. A blanket pass would therefore restamp every file in the library every night.

The arr cleanup scripts (sonarr_cleanup.sh, radarr_cleanup.sh, lidarr_cleanup.sh) gate orphan deletion on ctime. mtime cannot substitute: an import preserves the release's original timestamp, so mtime says nothing about when a file arrived here. Measured 2026-07-27 — of 400 files imported that week, all 400 had mtimes over 7 days old, one of them 9613 days.

So:

  blanket chown/chmod  →  every ctime resets to today
                       →  no file ever appears older than *_ORPHAN_AGE
                       →  orphan collection silently stops
                       →  nothing errors, nothing warns, disk just fills

The failure is invisible. No script fails, no notification fires. The only symptom is orphans quietly accumulating until a pool fills — which is exactly how the 755 GB / 89%-full cache pool incident happened.

Two rules follow, and both are load-bearing:

  1. media_shares_permissions.sh passes stay conditional. Making any of them unconditional breaks orphan collection ecosystem-wide.
  2. Tools/bulk_permissions_repair.sh is unconditional by design — it exists to repair known-wrong paths where correctness beats preserving a clock. That is precisely why it is a manual, targeted tool and not scheduled. Pointing it at a whole media root pauses orphan collection there for *_ORPHAN_AGE days.

Both scripts' headers carry this warning too. If you are reading this because you are about to "simplify" the permissions job, this is the thing that breaks.