#!/usr/bin/env bash # fix-artist-separators.sh # Splits artist tags on "/" into ";" — with hardcoded exceptions for artist # names that legitimately contain "/" (Au/Ra, HUNTR/X). # # Requires: ffprobe, ffmpeg, python3 # # Usage: # bash fix-artist-separators.sh [music_dir] -- dry run (no changes) # bash fix-artist-separators.sh [music_dir] --apply -- actually modify files MUSIC_DIR="${1:-/opt/navidrome/music}" APPLY=false [[ "${2:-}" == "--apply" ]] && APPLY=true if [[ ! -d "$MUSIC_DIR" ]]; then echo "Error: directory not found: $MUSIC_DIR" exit 1 fi if $APPLY; then echo "🔧 APPLY mode — files will be modified" else echo "🔍 DRY RUN — no files will be changed (add --apply to apply)" fi echo "📂 Scanning: $MUSIC_DIR" echo "🚫 Exceptions: Au/Ra, HUNTR/X" echo "" count_found=0 count_ok=0 count_fail=0 # Splits artist string on "/" while preserving exception names. # Example: "HUNTR/X/EJAE/AUDREY NUNA" -> "HUNTR/X;EJAE;AUDREY NUNA" # "-Prey/zecki" -> "-Prey;zecki" # "Au/Ra" -> "Au/Ra" (exception, unchanged) SPLIT_PY=' import sys EXCEPTIONS = ["Au/Ra", "HUNTR/X"] exc_lookup = [(exc, exc.lower(), exc.split("/")) for exc in EXCEPTIONS] def smart_split(artist): parts = artist.split("/") result = [] i = 0 while i < len(parts): matched = False for exc, exc_lower, exc_parts in exc_lookup: n = len(exc_parts) candidate = parts[i:i+n] if len(candidate) == n and "/".join(candidate).lower() == exc_lower: result.append(exc) i += n matched = True break if not matched: p = parts[i].strip() if p: result.append(p) i += 1 return ";".join(result) artist = sys.stdin.read().strip() if "/" not in artist: print(artist) else: print(smart_split(artist)) ' get_artist() { ffprobe -v quiet -print_format json -show_format "$1" 2>/dev/null \ | python3 -c " import sys, json data = json.load(sys.stdin) tags = data.get('format', {}).get('tags', {}) print(next((v for k, v in tags.items() if k.lower() == 'artist'), '')) " 2>/dev/null } while IFS= read -r -d '' file; do artist=$(get_artist "$file") || continue [[ "$artist" != *"/"* ]] && continue new_artist=$(echo "$artist" | python3 -c "$SPLIT_PY" 2>/dev/null) || continue [[ "$artist" == "$new_artist" ]] && continue count_found=$((count_found + 1)) echo "📄 $file" echo " Before : $artist" echo " After : $new_artist" if $APPLY; then ext="${file##*.}" tmp="${file%.*}.artist_fix_tmp.$ext" if ffmpeg -i "$file" -map_metadata 0 -metadata "artist=$new_artist" \ -c copy "$tmp" -y -loglevel quiet 2>/dev/null; then mv "$tmp" "$file" echo " ✅ Updated" count_ok=$((count_ok + 1)) else rm -f "$tmp" echo " ❌ Failed" count_fail=$((count_fail + 1)) fi fi echo "" done < <(find "$MUSIC_DIR" -type f \( \ -name "*.mp3" -o -name "*.flac" -o \ -name "*.m4a" -o -name "*.ogg" -o \ -name "*.opus" \) -print0) echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Files to change: $count_found" if $APPLY; then echo "Updated: $count_ok | Failed: $count_fail" [[ $count_ok -gt 0 ]] && echo "" && echo "💡 Trigger a Navidrome rescan to pick up the changes." else echo "" echo "Run with --apply to fix them:" echo " bash fix-artist-separators.sh \"$MUSIC_DIR\" --apply" fi