March 8Mar 8 What it doesThis script recursively scans your Media library and renames subtitle files from the deprecated gre ISO 639-2 language code to the correct ell code for Modern Greek. Most modern media players and tools (Jellyfin, Plex, Kodi, etc.) now expect ell and may not recognise gre.Example:Movie.2020.gre.srt → Movie.2020.ell.srt The script is designed to run safely on an Unraid server via the User Scripts plugin, with low I/O and CPU priority so it does not disturb normal operations.*Feel free to adapt for other language code migrations (e.g. heb→iw, iw→he, etc.)RequirementsUnraid with the User Scripts plugin installed (Community Applications)Media stored under a consistent path (e.g. /mnt/user/Media/ or /mnt/user0/Media/)SetupOpen Settings → User Scripts → Add New ScriptPaste the full script into the editorEdit the variables in the USER CONFIGURATION section at the top (see below)Save and optionally set a schedule (e.g. monthly)Run once with --dry-run first to verify what would be renamed⚙️ User Configuration — Variables to customiseThese are the only variables you should need to change. Everything else can be left as-is.ROOTS — Your media pathsROOTS=( "$BASE/Media/Movies" "$BASE/Media/TV Shows" ) Change these to match your actual folder names. Add or remove lines as needed.# Examples: ROOTS=( "$BASE/Media/Films" "$BASE/Media/Series" "$BASE/Media/Documentaries" ) $BASE is automatically set to /mnt/user or /mnt/user0 depending on your array layout — you typically don't need to change it. If your media is on a specific share or disk, you can override it manually:BASE="/mnt/user" LOG — Log file locationLOG="/mnt/user/appdata/srt_rename_gre_to_ell.log" Change this if you prefer logs in a different location, e.g. inside a dedicated appdata subfolder.LOG_MAX_LINES — Log rotation sizeLOG_MAX_LINES=5000 The log file is trimmed to this many lines at the start of each run. Increase if you have a very large library and want more history, or decrease to keep things lean.HEARTBEAT_SECS — Progress logging intervalHEARTBEAT_SECS=60 How often (in seconds) a progress line is written to the log while scanning. Useful for monitoring long runs. Set to 300 for less noise, or 15 for very frequent updates.🚀 Run flags (optional)You can pass these as Script Arguments in the User Scripts UI, or append them to the script call:FlagDescription--dry-runPreview only — no files are renamed. Always run this first!--forceRename even if a .ell.srt file already exists at the destination--incIncremental mode — only scan files newer than the last successful run--fullFull scan (default) — scan all files every time--wakeWake all array disks before scanning (default: enabled)Recommended first run: Set Script Arguments to --dry-run and check the log at the path defined in LOG.📋 What gets loggedEach run produces entries like:[2025-03-08 14:00:01] ---- RUN START ---- [2025-03-08 14:00:01] Mode: FULL=1 | DRY_RUN=0 | FORCE=0 | Wake=1 | Heartbeat=60s [2025-03-08 14:01:03] Renamed: /mnt/user/Media/Movies/Foo (2020)/Foo.gre.srt -> Foo.ell.srt [2025-03-08 14:02:00] HEARTBEAT | elapsed=00:02:00 | found=145 renamed=12 skipped=3 errors=0 [2025-03-08 14:05:22] === DONE === [2025-03-08 14:05:22] TOTAL: Found=892 | Renamed=47 | Skipped=5 | Errors=0 | Duration=00:05:21 📝 Full Script#!/bin/bash set -euo pipefail IFS=$'\n\t' renice -n 15 -p $$ >/dev/null 2>&1 || true ionice -c2 -n7 -p $$ >/dev/null 2>&1 || true echo "=== STARTING SRT RENAME: .gre.srt -> .ell.srt ===" # ============================================================ # USER CONFIGURATION — edit these to match your setup # ============================================================ BASE="/mnt/user" [[ -d "/mnt/user0/Media" ]] && BASE="/mnt/user0" ROOTS=( "$BASE/Media/Movies" "$BASE/Media/TV Shows" ) LOG="/mnt/user/appdata/srt_rename_gre_to_ell.log" STATE="/mnt/user/appdata/srt_rename_gre_to_ell.last_run" SUCCESS_MARK="/mnt/user/appdata/srt_rename_gre_to_ell.last_success" LOG_MAX_LINES=5000 HEARTBEAT_SECS=60 # ============================================================ # END OF USER CONFIGURATION # ============================================================ DRY_RUN=0 FORCE=0 FULL=1 WAKE_DISKS=1 while [[ $# -gt 0 ]]; do arg="${1//$'\r'/}" case "$arg" in "") shift ;; --dry-run) DRY_RUN=1; shift ;; --force) FORCE=1; shift ;; --full) FULL=1; shift ;; --inc) FULL=0; shift ;; --wake) WAKE_DISKS=1; shift ;; *) echo "Unknown option: [$(printf '%q' "$arg")]"; exit 1 ;; esac done mkdir -p "$(dirname "$LOG")" if [[ -f "$LOG" ]]; then tmp_log="$(mktemp)" tail -n "$LOG_MAX_LINES" "$LOG" > "$tmp_log" && mv "$tmp_log" "$LOG" fi fmt_secs() { local s="$1" printf "%02d:%02d:%02d" $((s/3600)) $(((s%3600)/60)) $((s%60)) } log() { local msg="$1" local ts ts="$(date '+%F %T')" echo "[$ts] $msg" | tee -a "$LOG" logger -t srt-rename "$msg" 2>/dev/null || true } missing=0 log "Using BASE: $BASE" for r in "${ROOTS[@]}"; do if [[ ! -d "$r" ]]; then log "ERROR: root not found: $r" missing=1 else log "OK: root exists: $r" fi done if [[ $missing -eq 1 ]]; then log "ABORT: Fix ROOTS and rerun." exit 1 fi START_TS="$(date +%s)" CURRENT_ROOT="(none)" LAST_FILE="(none)" LAST_BEAT_TS="$START_TS" found=0 renamed=0 skipped=0 errors=0 beat() { local now elapsed now="$(date +%s)" elapsed=$((now - START_TS)) log "HEARTBEAT | elapsed=$(fmt_secs "$elapsed") | root=\"$CURRENT_ROOT\" | found=$found renamed=$renamed skipped=$skipped errors=$errors | last=\"$LAST_FILE\"" LAST_BEAT_TS="$now" } wake_all_disks() { log "Waking ALL array disks (/mnt/disk*) ..." shopt -s nullglob for d in /mnt/disk*; do [[ -d "$d" ]] || continue ls -1 "$d" >/dev/null 2>&1 || true done shopt -u nullglob log "Disk wake pass done." } process_one() { local file="$1" found=$(( found + 1 )) LAST_FILE="$file" local new_file new_file="$(printf '%s' "$file" | sed -E 's/\.gre\.srt$/.ell.srt/')" if [[ "$new_file" == "$file" ]]; then skipped=$(( skipped + 1 )) return fi if [[ -e "$new_file" && $FORCE -eq 0 ]]; then skipped=$(( skipped + 1 )) log "SKIP (target exists): $file -> $new_file" return fi if [[ $DRY_RUN -eq 1 ]]; then renamed=$(( renamed + 1 )) log "[DRY-RUN] Would rename: $file -> $new_file" return fi if mv -- "$file" "$new_file"; then renamed=$(( renamed + 1 )) log "Renamed: $file -> $new_file" else errors=$(( errors + 1 )) log "ERROR renaming: $file" fi } log "---- RUN START ----" log "Roots: ${ROOTS[*]}" log "Mode: FULL=$FULL | DRY_RUN=$DRY_RUN | FORCE=$FORCE | Wake=$WAKE_DISKS | Heartbeat=${HEARTBEAT_SECS}s" if [[ $WAKE_DISKS -eq 1 ]]; then wake_all_disks fi if [[ $FULL -eq 0 ]]; then if [[ -f "$STATE" ]]; then log "Incremental since: $(date -r "$STATE")" else log "Incremental since: (none) -> first run will behave like full for matches" fi else log "FULL scan mode" fi beat for root in "${ROOTS[@]}"; do CURRENT_ROOT="$root" log "Scanning: $root" if [[ $FULL -eq 0 && -f "$STATE" ]]; then while IFS= read -r -d '' file; do now="$(date +%s)" if (( now - LAST_BEAT_TS >= HEARTBEAT_SECS )); then beat fi process_one "$file" done < <(find "$root" \( -type d -name ".Recycle.Bin" -prune \) -o \ \( -type f -iname "*.gre.srt" -newer "$STATE" -print0 \)) else while IFS= read -r -d '' file; do now="$(date +%s)" if (( now - LAST_BEAT_TS >= HEARTBEAT_SECS )); then beat fi process_one "$file" done < <(find "$root" \( -type d -name ".Recycle.Bin" -prune \) -o \ \( -type f -iname "*.gre.srt" -print0 \)) fi beat log "Finished root: $root" done END_TS="$(date +%s)" TOTAL_SECS=$((END_TS - START_TS)) log "=== DONE ===" log "TOTAL: Found=$found | Renamed=$renamed | Skipped=$skipped | Errors=$errors | Duration=$(fmt_secs "$TOTAL_SECS")" if [[ $DRY_RUN -eq 0 && $errors -eq 0 ]]; then touch "$STATE" touch "$SUCCESS_MARK" log "State updated: $STATE" log "Success marker updated: $SUCCESS_MARK" else log "State NOT updated (dry-run or errors present)" fi exit 0 NotesThe script skips .Recycle.Bin folders automaticallyIf a .ell.srt file already exists at the destination, the original is skipped (use --force to override)State files are only updated if the run completes with zero errorsLogs go to both the file and Unraid's syslog (logger)Tested on Unraid 7.2.+📁 GitHub Repository: Link Edited March 8Mar 8 by Lazaros Chalkidis
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.