#!/bin/bash # ============================================================================================== # ============================= Trailer Folder Migration ======================================= # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # One-off migration for TV show trailers downloaded before Trailarr's "Series # Trailers" profile was corrected. Emby ingests TV trailers sitting at the # series root using the movie-style " (Year)-trailer.ext" convention as # fake episodes instead of recognizing them as trailers — confirmed via Emby's # server log. The fix is a "trailers/" subfolder (lowercase) under the series # folder containing a plain "trailer.ext" file — same convention Trailarr now # uses going forward (customfilter_id=2, "Series Trailers" profile). # # This script only moves/renames existing misplaced files. It does not touch # Trailarr or Emby — a normal periodic Emby library scan picks up the moved # files on its own. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # Discovery # Scans SONARR_TV_ROOT one level deep for "*-trailer.*" files sitting # directly in a series folder (the movie-style convention). # # Single Trailer Per Show # / (Year)-trailer.mkv → /trailers/trailer.mkv # # Multiple Trailers Per Show # Sorted alphabetically for determinism. First file becomes trailer.ext, # subsequent files become trailer-2.ext, trailer-3.ext, etc. Emby's TV # trailer convention only requires the file to live in trailers/ — exact # filename beyond the first doesn't matter, but distinct names avoid # collisions and keep behavior deterministic on re-run. # # Collision Safety # If the destination filename already exists, the file is skipped and # logged as a conflict rather than overwritten. Lets the script be re-run # safely after a partial run or a manually resolved conflict. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Move, Never Copy or Delete # Files are relocated with mv and nothing is ever removed. A misjudged move is # reversible by hand; a delete is not. The script has no cleanup pass by design. # # Idempotent by Collision Skip # Re-running is safe because an existing destination is skipped rather than # overwritten. A partial run, an interrupted run, or a manually resolved conflict # can all be followed by a plain re-run. # # Deterministic Ordering # Multiple trailers per show are sorted before numbering, so the same input always # produces the same trailer.ext / trailer-2.ext assignment. Without the sort, the # numbering would depend on filesystem iteration order and a re-run could rename # files differently. # # Filesystem Only # Neither Trailarr nor Emby is touched. Emby's periodic library scan picks the moved # files up on its own — poking either service would add failure modes to what is # otherwise a pure file move. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Enforcement # Moves files owned by container users and chowns the created trailers/ directory. # # Lock Acquisition # acquire_lock prevents two runs numbering the same show's trailers concurrently. # # Host Detection # detect_hosts() aliases HOST*_SONARR_TV_ROOT → SONARR_TV_ROOT. # # TV Root Validation # Exits if SONARR_TV_ROOT is unset or is not a directory — the scan below is rooted # there, and an empty value would walk from the current directory. # # Bounded Scan Depth # find runs with -mindepth 2 -maxdepth 2, so only files sitting directly in a series # folder are considered. Trailers already correctly placed inside trailers/ are out # of range and cannot be picked up and re-moved. # # Collision Skip # An existing destination is never overwritten — the source is left in place and # counted as a conflict for review. # # Permission Matching on Created Directories # mkdir as root would leave trailers/ as root-owned. It is chowned to # PERMISSIONS_OWNER and chmodded to PERMISSIONS_DIR_MODE so it matches the rest of # the library and does not become an exception the permissions job has to correct. # # Dry Run Support # --dry-run reports every move and touches nothing. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # host*.conf # # HOST*_SONARR_TV_ROOT # Host filesystem path to the TV library. Aliased by detect_hosts() → # SONARR_TV_ROOT. # # master.conf # # PERMISSIONS_OWNER / PERMISSIONS_DIR_MODE # Applied to each created trailers/ directory so it matches library convention. # Shared with media_shares_permissions.sh. (defaults: nobody:users, 755) # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # trailer_folder_migration.sh # Move every misplaced trailer file into /trailers/. # # trailer_folder_migration.sh --dry-run # Show what would move, with no filesystem changes. # # trailer_folder_migration.sh --log # Verbose output — per-file source/destination. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== # Moves container-owned media files and chowns the trailers/ dirs it creates. if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi acquire_lock # detect_hosts() sets MY_ID and aliases HOST*_SONARR_TV_ROOT → SONARR_TV_ROOT detect_hosts if [[ -z "${SONARR_TV_ROOT:-}" ]]; then error "SONARR_TV_ROOT not set for $MY_ID — check HOST*_SONARR_TV_ROOT in host*.conf" exit 1 fi if [[ ! -d "$SONARR_TV_ROOT" ]]; then error "SONARR_TV_ROOT not found: $SONARR_TV_ROOT" exit 1 fi log "Identity: $MY_ID ($LOCAL_SERVER_NAME)" log "TV root: $SONARR_TV_ROOT" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be moved" # ============================================================================================== # ━━━ Discover misplaced trailer files, grouped by show ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_CLEAN Trailer Folder Migration — $MY_ID ━━━" START=$(date +%s) declare -A SHOW_FILES=() # show dir → newline-separated list of trailer file paths while IFS= read -r -d '' f; do show_dir="$(dirname "$f")" SHOW_FILES["$show_dir"]+="$f"$'\n' done < <(find "$SONARR_TV_ROOT" -mindepth 2 -maxdepth 2 -iname "*-trailer.*" -print0) TOTAL_SHOWS=${#SHOW_FILES[@]} if [[ "$TOTAL_SHOWS" -eq 0 ]]; then echo "$ICON_DONE No misplaced trailer files found — nothing to migrate" exit 0 fi warn "$TOTAL_SHOWS show(s) with misplaced trailer file(s)" MOVED=0 SKIPPED=0 FAILED=0 FAIL_LIST=() for show_dir in "${!SHOW_FILES[@]}"; do mapfile -t files < <(printf '%s' "${SHOW_FILES[$show_dir]}" | sort) trailers_dir="$show_dir/trailers" show_name="$(basename "$show_dir")" if [[ "$DRY_RUN" != true ]]; then mkdir -p "$trailers_dir" || { error "$show_name — could not create trailers/ dir" FAILED=$(( FAILED + ${#files[@]} )) FAIL_LIST+=("$show_name") continue } # mkdir as root defaults to root:root 777 — match the library's # nobody:users / 755 convention (PERMISSIONS_OWNER/PERMISSIONS_DIR_MODE) chown "${PERMISSIONS_OWNER:-nobody:users}" "$trailers_dir" 2>/dev/null chmod "${PERMISSIONS_DIR_MODE:-755}" "$trailers_dir" 2>/dev/null fi idx=0 for src in "${files[@]}"; do [[ -z "$src" ]] && continue idx=$(( idx + 1 )) ext="${src##*.}" if [[ "$idx" -eq 1 ]]; then dest="$trailers_dir/trailer.$ext" else dest="$trailers_dir/trailer-$idx.$ext" fi if [[ "$DRY_RUN" == true ]]; then log "$show_name — would move: $(basename "$src") → trailers/$(basename "$dest")" MOVED=$(( MOVED + 1 )) continue fi if [[ -e "$dest" ]]; then warn "$show_name — $(basename "$dest") already exists, skipping $(basename "$src")" SKIPPED=$(( SKIPPED + 1 )) continue fi if mv "$src" "$dest"; then log "$show_name — $(basename "$src") → trailers/$(basename "$dest")" MOVED=$(( MOVED + 1 )) else error "$show_name — failed to move $(basename "$src")" FAILED=$(( FAILED + 1 )) FAIL_LIST+=("$show_name") fi done done END=$(date +%s) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== echo "" echo "━━━━━ $ICON_SUMMARY TRAILER MIGRATION SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_CLEAN Shows: $TOTAL_SHOWS" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" echo "" echo "$ICON_DONE Moved: $MOVED" [[ "$SKIPPED" -gt 0 ]] && echo "$ICON_SKIP Skipped: $SKIPPED (destination already existed)" [[ "$FAILED" -gt 0 ]] && echo "$ICON_ERROR Failed: $FAILED" echo "" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no changes made" elif [[ "$FAILED" -gt 0 ]]; then echo "$ICON_ERROR Status: SOME MOVES FAILED — ${FAIL_LIST[*]}" notify "Trailer folder migration failed for: ${FAIL_LIST[*]}" \ "Trailer Migration" "warning" else echo "$ICON_DONE Status: done — $MOVED file(s) migrated across $TOTAL_SHOWS show(s)" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [[ "$FAILED" -gt 0 ]] && exit 1 exit 0