Files
alembic/pipeline/lib/tag-guard.sh
T
andrew 5eaae8e813 Add playlist_service and credential_service
playlist_service: CRUD for the playlists table, playlists.json export,
regen.sh invocation, and a one-time seed_legacy() importing the 17-entry
legacy array. Creating/updating a playlist automatically re-renders confs
and re-patches credentials into any newly-generated .conf file.

credential_service: encrypted credential storage (via security/crypto.py)
and per-scope rendering to the exact files the pipeline scripts read --
every <playlist>.conf (spotify/soulseek), navidrome/admin.env,
bandcamp/config.env+cookies.txt, azuracast/api_key, qobuz/{token,app_id,region},
telegram/notify.env. set_credentials() batches multi-field saves so a render
never sees a half-populated scope.

Also fixes a real bug found via integration testing: regen.sh's embedded
python3 -c snippet used backslash-escaped quotes inside a single-quoted
bash string, which is invalid Python syntax (backslash passed through
literally) -- switched to double-quoted bash wrapper + single-quoted Python
strings.

Verified end-to-end: seeded 17 playlists, regenerated all 17 .conf files,
saved spotify/soulseek/navidrome credentials and confirmed correct
patching into rendered files (including bash-sourcing admin.env with
special characters in the password), and confirmed delete/create both
correctly add/remove .conf files with credentials pre-patched on create.
2026-07-08 13:32:37 -06:00

100 lines
3.9 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 |; '
# 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=$(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"
}