5eaae8e813
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.
100 lines
3.9 KiB
Bash
Executable File
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"
|
|
}
|