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.
8.2 KiB
━━━━━ MEDIA — Manual ━━━━━
Config reference, procedures, operational workflows for Media/ scripts. For overview see README-Media.md. For per-script detail see script headers.
For arr stack config and procedures (cleanup, release fixer, sync, discovery) see
Arrs_Stack/Manual-Arrs_Stack.md.
━━━ 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.
━━━ CONFIGURATION — master.conf ━━━
Permissions
PERMISSIONS_DIR_MODE="755"
PERMISSIONS_FILE_MODE="664"
PERMISSIONS_OWNER="nobody:users"
Media Cleaner — File Patterns
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-runwhen adding new patterns.
Play State Sync
PLAY_SYNC_ENABLED=true # toggle entire sync
PLAY_SYNC_REMOTE=true # sync across hosts via Tailscale
# false = local servers only (this host's Emby + Jellyfin)
PLAY_SYNC_TYPES="Movie,Episode" # item types to sync — Audio excluded, music library too large
PLAY_SYNC_FAV_TYPES="MusicArtist,MusicAlbum,Movie,Series" # favourites, union sync, never unmarks
PLAY_SYNC_PROBE=true # skip per-item work when nothing changed since last run
PLAY_SYNC_PROBE_MAX_AGE_HOURS=24 # force a full comparison when the fingerprint is older
PLAY_SYNC_HANDBACK_RETRIES=5 # fallback handback: attempts before DNS cutover proceeds
PLAY_SYNC_HANDBACK_RETRY_DELAY=60 # seconds between those attempts
There is no date/lookback filter, and one must not be re-added. An earlier version gated on played-date; it was removed once the real cost was measured — the 30-minute runtime was fork overhead per item, not API volume or item count. The fix was jq epoch parsing plus the response-hash probe below. Re-introducing a date window would reduce correctness (older items silently stop syncing) without meaningfully reducing runtime.
Emby and Jellyfin servers configured per-host:
# host*.conf
HOST1_EMBY_URL="http://192.168.50.2:8096"
HOST1_EMBY_API_KEY="..."
HOST1_JELLYFIN_URL="" # empty = skip Jellyfin on this host
HOST1_JELLYFIN_API_KEY=""
━━━ CONFIGURATION — host*.conf ━━━
# 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"
)
━━━ OUTPUT TIERS ━━━
All scripts have two output levels controlled by --log.
Without --log, each script processes silently and concludes with a summary block.
Warnings and errors are always visible.
With --log, per-item detail appears — individual shares being processed, file counts,
per-server sync results.
━━━ 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.
play_state_sync.sh
play_state_sync.sh
Sync played/unplayed state, resume positions and favourites across every configured Emby
and Jellyfin server — not just local→remote. Servers are discovered from every
HOST*_TRANSCODE_SERVERS entry, with remote hosts' localhost URLs rewritten to their
Tailscale IP. Newest LastPlayedDate wins; state only ever moves forward, never clears.
No age filter — every matched item is considered on every run. The change probe is what keeps that cheap: the raw API responses are hashed and compared against the fingerprint from the last successful run, and per-item processing is skipped entirely when nothing moved. Fetches still happen every run, so nothing can be missed by the probe.
play_state_sync.sh --full
Bypass the change probe and force the full per-item comparison even when the fingerprint
matches. Use after a new Emby install or database restore, or when debugging a sync that
appears to be skipping work it should be doing.
play_state_sync.sh --wait
Wait for an in-progress run instead of exiting. For manual runs that would otherwise be
skipped by the scheduled every-30-minute pass. Used by fallback.sh during handback.
play_state_sync.sh --dry-run
Show what would be synced without writing any state.
play_state_sync.sh --status
Show configured servers, reachability, and user counts.
play_state_sync.sh --log
Verbose output — show each item comparison.