0.6.15: Fix false-positive quarantine of ID3v2.4-tagged MP3s

tag-guard.sh checked MP3 tags with the id3v2 CLI (id3lib-3.8.3, last released
in 2013), which cannot parse ID3v2.4 tags at all. It reported "No ID3 tag" on
files that were completely and correctly tagged, just written in the modern
tag version most taggers default to now (Mp3tag, yt-dlp, foobar2000). Every
manual import of ID3v2.4 MP3s got its files quarantined as untagged and the
whole import failed once nothing survived the quarantine pass.

Replaced the id3v2-based reads in tag-guard.sh with mutagen, which handles
both tag versions correctly. Also replaced the ALBUMARTIST-fallback write
(previously done via `id3v2 --TPE2`) with a mutagen-based write, because
testing turned up something worse: writing a single frame with the id3v2 CLI
to a file that already has an ID3v2.4 tag silently destroys every other
frame on the file (title, album, genre, date, artwork, all of it), since
id3lib doesn't understand v2.4 and rewrites the whole tag as v2.3 containing
only the frame it was told to set. The mutagen write sets just the one frame
and leaves everything else untouched.

Other id3v2 CLI writes still exist elsewhere in the pipeline (playlist
grouping tags, genre fixes, buy-url tagging) and carry the same corruption
risk against ID3v2.4 files; not touched in this release.
This commit is contained in:
andrew
2026-07-23 15:40:04 -06:00
parent 55a059b6da
commit 15008f7f09
+41 -5
View File
@@ -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 <file> <frame>
_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 <file> <frame> <value>
_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" ]]
}