# ━━━━━ 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.
