1c7cf83a1b
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.
136 lines
5.1 KiB
Bash
Executable File
136 lines
5.1 KiB
Bash
Executable File
#!/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 <scan_dir> <quarantine_dir> <log_file>
|
|
# 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 |; '
|
|
|
|
# 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
|
|
# like "Fred Again, The Blessed Madonna" when a track is a collab.
|
|
#
|
|
# Usage: set_albumartist_fallback <scan_dir> <log_file>
|
|
# 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=$(_mp3_tag "$file" TPE2)
|
|
[[ -n "$cur_aa" ]] && continue
|
|
artist=$(_mp3_tag "$file" TPE1)
|
|
[[ -z "$artist" ]] && continue
|
|
primary=$(echo "$artist" | sed -E "s#(${_PRIMARY_ARTIST_SEP_PATTERN}).*##")
|
|
_mp3_set_tag "$file" TPE2 "$primary" 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=$(_mp3_tag "$1" TPE1)
|
|
title=$(_mp3_tag "$1" TIT2)
|
|
[[ -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"
|
|
}
|