db13890fe1
- fix-genre.sh: set -euo pipefail; guard the tag-remove metaflac calls and the 'beet ls | head -5' verify (SIGPIPE) that would otherwise abort. - sync-bandcamp.sh: set -euo pipefail; the destructive chain already used 'if cmd; then..else WARN' (set -e exempt); guard the staging mv. - upgrade-mp3-to-flac.sh: set -euo pipefail; capture sldl/replace-with-better exit codes without aborting (was 'cmd; RC=$?', which set -e breaks); read Soulseek creds from the first available .conf instead of a hardcoded y2k.conf, guarded; guard the xargs purge and a grep -c. - pipeline-status.sh: deliberately kept at set -u (documented). It is read-only and assembles ~30 independent probes; set -e would abort the whole digest on one missing optional file, which is exactly how the 9am notification broke. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.0 KiB
Bash
Executable File
128 lines
4.0 KiB
Bash
Executable File
#!/bin/bash
|
|
# fix-genre.sh — quickly override the GENRE tag for every track by an artist.
|
|
#
|
|
# Use when spotify-genre.py picks something wrong (e.g. it tagged Yasuha as
|
|
# "Asian Music" when "Anime; Soundtrack" is what you actually want), or when
|
|
# you want to set a genre on an artist Spotify doesn't have data for.
|
|
#
|
|
# The new value is written to file tags AND beets's DB. The next nightly
|
|
# spotify-genre run will leave these alone (they already match the whitelist
|
|
# format), unless you pass --force.
|
|
#
|
|
# Usage:
|
|
# fix-genre.sh "<artist>" "<Genre1; Genre2; ...>" # apply
|
|
# fix-genre.sh --dry "<artist>" "<Genre1; ...>" # preview only
|
|
#
|
|
# Examples:
|
|
# sudo fix-genre.sh "Yasuha" "Anime; Soundtrack"
|
|
# sudo fix-genre.sh "Aphex Twin" "IDM; Electronic; Ambient"
|
|
# sudo fix-genre.sh --dry "Mac DeMarco" "Indie; Indie Rock"
|
|
|
|
set -euo pipefail
|
|
|
|
DRY=0
|
|
if [[ "${1:-}" == "--dry" ]]; then
|
|
DRY=1
|
|
shift
|
|
fi
|
|
|
|
ARTIST="${1:?Usage: $0 [--dry] <artist> <Genre1; Genre2; ...>}"
|
|
GENRE="${2:?Usage: $0 [--dry] <artist> <Genre1; Genre2; ...>}"
|
|
|
|
LIBRARY=${MUSIC_DATA_DIR:-/data/music}/Library
|
|
|
|
# Pull paths via beets (in-process, same container).
|
|
mapfile -t paths < <(beet ls -f '$path' "artist:$ARTIST" 2>/dev/null)
|
|
|
|
if [[ "${#paths[@]}" -eq 0 ]]; then
|
|
echo "[fix-genre] no tracks found in beets for artist:$ARTIST"
|
|
exit 1
|
|
fi
|
|
|
|
echo "[fix-genre] artist=\"$ARTIST\""
|
|
echo "[fix-genre] new GENRE=\"$GENRE\""
|
|
echo "[fix-genre] ${#paths[@]} track(s) match"
|
|
[[ $DRY -eq 1 ]] && echo "[fix-genre] DRY RUN — no tags will be written" && exit 0
|
|
|
|
ok=0
|
|
fail=0
|
|
for cp in "${paths[@]}"; do
|
|
hp="${LIBRARY}${cp#/music}"
|
|
if [[ ! -f "$hp" ]]; then
|
|
echo " MISS $hp"
|
|
fail=$((fail + 1))
|
|
continue
|
|
fi
|
|
case "${hp,,}" in
|
|
*.flac)
|
|
# remove-tag returns non-zero when the tag isn't present; `|| true` so
|
|
# that expected case doesn't abort under set -e.
|
|
metaflac --remove-tag=GENRE "$hp" 2>/dev/null || true
|
|
metaflac --set-tag="GENRE=$GENRE" "$hp" 2>/dev/null && ok=$((ok + 1)) || fail=$((fail + 1))
|
|
metaflac --remove-tag=GENRE_LOCK "$hp" 2>/dev/null || true
|
|
metaflac --set-tag="GENRE_LOCK=1" "$hp" 2>/dev/null || true
|
|
;;
|
|
*.mp3)
|
|
# mid3v2 / id3v2 don't have a clean way to set TCON via plain id3v2 here,
|
|
# so use mutagen via a one-liner.
|
|
python3 - "$hp" "$GENRE" <<'PY' && ok=$((ok + 1)) || fail=$((fail + 1))
|
|
import sys
|
|
from mutagen.id3 import ID3, TCON, TXXX, ID3NoHeaderError
|
|
path, g = sys.argv[1], sys.argv[2]
|
|
try:
|
|
t = ID3(path)
|
|
except ID3NoHeaderError:
|
|
t = ID3()
|
|
t.delall("TCON")
|
|
t.add(TCON(encoding=3, text=g))
|
|
t.delall("TXXX:GENRE_LOCK")
|
|
t.add(TXXX(encoding=3, desc="GENRE_LOCK", text="1"))
|
|
t.save(path)
|
|
PY
|
|
;;
|
|
*.m4a|*.mp4)
|
|
python3 - "$hp" "$GENRE" <<'PY' && ok=$((ok + 1)) || fail=$((fail + 1))
|
|
import sys
|
|
from mutagen.mp4 import MP4
|
|
path, g = sys.argv[1], sys.argv[2]
|
|
m = MP4(path)
|
|
m["\xa9gen"] = g
|
|
m.save()
|
|
PY
|
|
;;
|
|
*.opus|*.ogg)
|
|
python3 - "$hp" "$GENRE" <<'PY' && ok=$((ok + 1)) || fail=$((fail + 1))
|
|
import sys
|
|
from mutagen import File as MFile
|
|
path, g = sys.argv[1], sys.argv[2]
|
|
m = MFile(path)
|
|
m.tags["genre"] = [g]
|
|
m.save()
|
|
PY
|
|
;;
|
|
*)
|
|
echo " SKIP unsupported format: $hp"
|
|
fail=$((fail + 1))
|
|
;;
|
|
esac
|
|
done
|
|
|
|
echo "[fix-genre] tagged ok=$ok, failed=$fail"
|
|
|
|
# Sync beets DB so its $genres column matches the file tags.
|
|
# `-q` is NOT a flag for `beet update` (only `beet ls`); leaving it off
|
|
# lets beets actually apply the diff non-interactively.
|
|
echo "[fix-genre] syncing beets DB"
|
|
beet update "artist:$ARTIST" 2>&1 | grep -E '^\s+[+-]' || true
|
|
|
|
# Verify
|
|
echo "[fix-genre] new state:"
|
|
# `|| true`: head closes the pipe after 5 lines, so beet exits with SIGPIPE for
|
|
# any artist with more than 5 tracks; that's expected, not a failure.
|
|
beet ls -f ' $title — $genres' "artist:$ARTIST" | head -5 || true
|
|
remaining=$(beet ls "artist:$ARTIST" | wc -l)
|
|
shown=5
|
|
[[ "$remaining" -gt "$shown" ]] && echo " ...and $((remaining - shown)) more"
|
|
|
|
echo "[fix-genre] done. Trigger a Navidrome scan to surface the change in the UI."
|