#!/bin/bash # ${PIPELINE_DIR:-/app/pipeline}/lib/tag-guard.sh # # Shared helper sourced by run-playlist.sh and import-track.sh. # Moves audio files with missing ARTIST or TITLE tags into a quarantine dir, # so they never reach beets and rot the library as "[Unknown Album]". # # Usage: quarantine_untagged # Returns: number of files moved (via stdout) # Separator pattern matching Navidrome's artist split list in navidrome.toml. # Used by set_albumartist_fallback to extract the primary artist from a multi- # artist string. Keep these two lists in sync. # # Only unambiguous separators here. ", " and " & " are NOT included because they # appear inside real band names ("Tyler, The Creator", "Simon & Garfunkel", # "piri & tommy"). For collab strings using those separators, ALBUMARTIST falls # back to the full string — Navidrome will show one combined artist for the album, # which is the lesser of two evils vs. corrupting real band names. _PRIMARY_ARTIST_SEP_PATTERN=' / | feat\. | feat | ft\. | ft |; ' # For each audio file under $scan_dir that has no ALBUMARTIST tag, set # ALBUMARTIST to the primary artist (everything before the first separator # in the ARTIST tag). Prevents Navidrome from coining ghost album-artists # like "Fred Again, The Blessed Madonna" when a track is a collab. # # Usage: set_albumartist_fallback # Returns: number of files updated (via stdout) set_albumartist_fallback() { local scan_dir="$1" local logfile="$2" local count=0 while IFS= read -r -d '' file; do case "${file##*.}" in flac|FLAC) local cur_aa artist primary cur_aa=$(metaflac --show-tag=ALBUMARTIST "$file" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1) [[ -n "$cur_aa" ]] && continue artist=$(metaflac --show-tag=ARTIST "$file" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1) [[ -z "$artist" ]] && continue primary=$(echo "$artist" | sed -E "s#(${_PRIMARY_ARTIST_SEP_PATTERN}).*##") metaflac --set-tag="ALBUMARTIST=${primary}" "$file" 2>>"$logfile" count=$((count + 1)) ;; mp3|MP3) local cur_aa artist primary cur_aa=$(id3v2 -l "$file" 2>/dev/null | sed -n 's/^TPE2[^:]*: //p' | head -1) [[ -n "$cur_aa" ]] && continue artist=$(id3v2 -l "$file" 2>/dev/null | sed -n 's/^TPE1[^:]*: //p' | head -1) [[ -z "$artist" ]] && continue primary=$(echo "$artist" | sed -E "s#(${_PRIMARY_ARTIST_SEP_PATTERN}).*##") id3v2 --TPE2 "$primary" "$file" 2>>"$logfile" count=$((count + 1)) ;; esac done < <(find "$scan_dir" -type f \( -iname "*.flac" -o -iname "*.mp3" \) -print0 2>/dev/null) echo "$count" } quarantine_untagged() { local scan_dir="$1" local quarantine_dir="$2" local logfile="$3" local moved=0 mkdir -p "$quarantine_dir" _has_flac_tags() { local artist title artist=$(metaflac --show-tag=ARTIST "$1" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1) title=$(metaflac --show-tag=TITLE "$1" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1) [[ -n "$artist" && -n "$title" ]] } _has_mp3_tags() { local artist title artist=$(id3v2 -l "$1" 2>/dev/null | sed -n 's/^TPE1[^:]*: //p' | head -1) title=$(id3v2 -l "$1" 2>/dev/null | sed -n 's/^TIT2[^:]*: //p' | head -1) [[ -n "$artist" && -n "$title" ]] } while IFS= read -r -d '' file; do local ok=1 case "${file##*.}" in flac|FLAC) _has_flac_tags "$file" || ok=0 ;; mp3|MP3) _has_mp3_tags "$file" || ok=0 ;; *) ok=0 ;; # unknown format — beets can't handle it anyway esac if [[ $ok -eq 0 ]]; then echo "[$(date -Iseconds)] QUARANTINE: missing artist/title — $(basename "$file")" >> "$logfile" mv -n "$file" "$quarantine_dir/" 2>>"$logfile" moved=$((moved + 1)) fi done < <(find "$scan_dir" -type f \( -iname "*.flac" -o -iname "*.mp3" -o -iname "*.m4a" \) -print0 2>/dev/null) echo "$moved" }