diff --git a/pipeline/lib/tag-guard.sh b/pipeline/lib/tag-guard.sh index d8e2568..361938a 100755 --- a/pipeline/lib/tag-guard.sh +++ b/pipeline/lib/tag-guard.sh @@ -19,6 +19,42 @@ # which is the lesser of two evils vs. corrupting real band names. _PRIMARY_ARTIST_SEP_PATTERN=' / | feat\. | feat | ft\. | ft |; ' +# Read one ID3 text frame (e.g. TPE1, TIT2, TPE2) via mutagen instead of the +# `id3v2` CLI. id3v2 is id3lib-3.8.3 (last released 2013) and cannot parse +# ID3v2.4 tags at all -- it reports "No ID3 tag" on a file that has complete, +# valid tags, just written in the modern version most taggers (Mp3tag, +# yt-dlp, foobar2000) default to. That false negative used to send fully- +# tagged mp3s to quarantine. Usage: _mp3_tag +_mp3_tag() { + python3 - "$1" "$2" 2>/dev/null <<'PY' +import sys +from mutagen.id3 import ID3 +try: + tags = ID3(sys.argv[1]) + frame = tags.get(sys.argv[2]) + print(str(frame) if frame else "") +except Exception: + print("") +PY +} + +# Set one ID3 text frame via mutagen, preserving every other frame on the +# file. The `id3v2` CLI cannot be used for this: writing even a single frame +# with it to a file that already has an ID3v2.4 tag silently drops every +# other frame (title, album, genre, date, artwork -- all of it), because +# id3lib doesn't understand v2.4 and rewrites the whole tag as v2.3 with only +# the frame(s) it was told to set. Usage: _mp3_set_tag +_mp3_set_tag() { + python3 - "$1" "$2" "$3" <<'PY' +import sys +from mutagen.id3 import ID3, Frames +tags = ID3(sys.argv[1]) +frame_cls = Frames[sys.argv[2]] +tags.setall(sys.argv[2], [frame_cls(encoding=3, text=[sys.argv[3]])]) +tags.save(sys.argv[1]) +PY +} + # 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 @@ -45,12 +81,12 @@ set_albumartist_fallback() { ;; mp3|MP3) local cur_aa artist primary - cur_aa=$(id3v2 -l "$file" 2>/dev/null | sed -n 's/^TPE2[^:]*: //p' | head -1) + cur_aa=$(_mp3_tag "$file" TPE2) [[ -n "$cur_aa" ]] && continue - artist=$(id3v2 -l "$file" 2>/dev/null | sed -n 's/^TPE1[^:]*: //p' | head -1) + artist=$(_mp3_tag "$file" TPE1) [[ -z "$artist" ]] && continue primary=$(echo "$artist" | sed -E "s#(${_PRIMARY_ARTIST_SEP_PATTERN}).*##") - id3v2 --TPE2 "$primary" "$file" 2>>"$logfile" + _mp3_set_tag "$file" TPE2 "$primary" 2>>"$logfile" count=$((count + 1)) ;; esac @@ -76,8 +112,8 @@ quarantine_untagged() { _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) + artist=$(_mp3_tag "$1" TPE1) + title=$(_mp3_tag "$1" TIT2) [[ -n "$artist" && -n "$title" ]] }