Harden fix-genre, sync-bandcamp, upgrade-mp3-to-flac under strict mode (R1)

- 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>
This commit is contained in:
andrew
2026-07-10 10:52:44 -06:00
parent 41ef26a40b
commit db13890fe1
4 changed files with 41 additions and 20 deletions
+9 -5
View File
@@ -18,7 +18,7 @@
# sudo fix-genre.sh "Aphex Twin" "IDM; Electronic; Ambient"
# sudo fix-genre.sh --dry "Mac DeMarco" "Indie; Indie Rock"
set -u
set -euo pipefail
DRY=0
if [[ "${1:-}" == "--dry" ]]; then
@@ -55,10 +55,12 @@ for cp in "${paths[@]}"; do
fi
case "${hp,,}" in
*.flac)
metaflac --remove-tag=GENRE "$hp" 2>/dev/null
# 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
metaflac --set-tag="GENRE_LOCK=1" "$hp" 2>/dev/null
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,
@@ -115,7 +117,9 @@ beet update "artist:$ARTIST" 2>&1 | grep -E '^\s+[+-]' || true
# Verify
echo "[fix-genre] new state:"
beet ls -f ' $title — $genres' "artist:$ARTIST" | head -5
# `|| 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"
+8
View File
@@ -18,6 +18,14 @@
# Only flags SCRIPT-level failures, not per-file Soulseek errors (those are
# normal — peers go offline, connections reset).
# set -u only, NOT -e/pipefail, on purpose. This script is read-only: it
# assembles a status digest from ~30 independent probes (grep over logs, beet
# queries, curl health checks, optional-credential file reads). Each probe is
# allowed to fail individually and report a warn/skip line; that's the whole
# design. Under set -e a single missing optional file (e.g. qobuz/app_id) or a
# no-match grep would abort the entire run and send NO digest at all, which is
# the opposite of what a health report should do. It writes no library state,
# so there is nothing to leave half-applied.
set -u
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+2 -2
View File
@@ -6,7 +6,7 @@
# Reads $ALEMBIC_CONFIG_DIR/pipeline/bandcamp/config.env for username, format pref, etc.
# Run as root (cron). Logs to ${ALEMBIC_CONFIG_DIR:-/config}/logs/bandcamp-YYYYMMDD.log.
set -u
set -euo pipefail
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
@@ -81,7 +81,7 @@ log "$NEW_COUNT new audio file(s) staged; moving to $IMPORT_ME for import"
shopt -s dotglob nullglob
for entry in "$BANDCAMP_STAGING"/*; do
[[ -e "$entry" ]] || continue
mv "$entry" "$IMPORT_ME/" 2>>"$LOG"
mv "$entry" "$IMPORT_ME/" 2>>"$LOG" || log "WARN: failed to move $entry into $IMPORT_ME"
done
shopt -u dotglob nullglob
+22 -13
View File
@@ -19,16 +19,18 @@
# upgrade-mp3-to-flac.sh # run the scanner end-to-end
# upgrade-mp3-to-flac.sh --csv-only # just write the CSV; don't run sldl
set -u
set -euo pipefail
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
# These helpers are the same ones import-track.sh sources and runs under its
# own set -euo pipefail on every manual import, so they are already strict-safe.
# shellcheck source=${PIPELINE_DIR:-/app/pipeline}/lib/tag-guard.sh
source ${PIPELINE_DIR:-/app/pipeline}/lib/tag-guard.sh
source "${PIPELINE_DIR:-/app/pipeline}/lib/tag-guard.sh"
# shellcheck source=${PIPELINE_DIR:-/app/pipeline}/lib/prep-audio.sh
source ${PIPELINE_DIR:-/app/pipeline}/lib/prep-audio.sh
source "${PIPELINE_DIR:-/app/pipeline}/lib/prep-audio.sh"
# shellcheck source=${PIPELINE_DIR:-/app/pipeline}/lib/mb-tags.sh
source ${PIPELINE_DIR:-/app/pipeline}/lib/mb-tags.sh
source "${PIPELINE_DIR:-/app/pipeline}/lib/mb-tags.sh"
CSV_ONLY=0
[[ "${1:-}" == "--csv-only" ]] && CSV_ONLY=1
@@ -72,11 +74,19 @@ if [[ $CSV_ONLY -eq 1 ]]; then
exit 0
fi
# Pull credentials from any playlist conf (they're all the same)
SOULSEEK_USER=$(sed -n 's/^user *= *//p' ${ALEMBIC_CONFIG_DIR:-/config}/pipeline/y2k.conf | tr -d ' ')
SOULSEEK_PASS=$(sed -n 's/^pass *= *//p' ${ALEMBIC_CONFIG_DIR:-/config}/pipeline/y2k.conf | tr -d ' ')
# Pull Soulseek credentials from any rendered playlist conf (they all carry the
# same user/pass). Don't hardcode a specific playlist name: pick the first
# non-underscore .conf that exists. Guarded so a missing conf yields empty
# creds (sldl then fails and is handled) rather than aborting under set -e.
_CRED_CONF=$(ls "${ALEMBIC_CONFIG_DIR:-/config}"/pipeline/*.conf 2>/dev/null | grep -v '/_' | head -1 || true)
SOULSEEK_USER=$(sed -n 's/^user *= *//p' "${_CRED_CONF:-/nonexistent}" 2>/dev/null | tr -d ' ' || true)
SOULSEEK_PASS=$(sed -n 's/^pass *= *//p' "${_CRED_CONF:-/nonexistent}" 2>/dev/null | tr -d ' ' || true)
log "Running sldl with --format flac (HARD requirement, no MP3/WAV fallback)"
# sldl routinely exits non-zero (skipped/partial searches), which is normal
# here. Capture the code without letting set -e abort: init to 0, override in
# the || branch. (`cmd; RC=$?` would abort at cmd before RC=$? ran.)
SLDL_EXIT=0
"$SLDL_BIN" \
"$CSV_HOST" \
--input-type csv \
@@ -92,8 +102,7 @@ log "Running sldl with --format flac (HARD requirement, no MP3/WAV fallback)"
--no-browse-folder \
--no-skip-existing \
--verbose \
>> "$LOG" 2>&1
SLDL_EXIT=$?
>> "$LOG" 2>&1 || SLDL_EXIT=$?
log "sldl exit code: $SLDL_EXIT"
# Guard: this script is FLAC-upgrade-only. If sldl ever drops anything else
@@ -103,7 +112,7 @@ NON_FLAC=$(find "$STAGING_HOST" -type f ! -iname "*.flac" ! -name "*.incomplete"
if [[ -n "$NON_FLAC" ]]; then
log "Purging non-FLAC files from staging:"
echo "$NON_FLAC" | tee -a "$LOG"
echo "$NON_FLAC" | xargs -r rm -f
echo "$NON_FLAC" | xargs -r rm -f || true
fi
NEW_FLAC_COUNT=$(find "$STAGING_HOST" -type f -iname "*.flac" 2>/dev/null | wc -l)
@@ -149,8 +158,8 @@ log "Pre-tagged $PREPPED file(s)"
# the replacing FLAC before the swap, so playlist M3Us still resolve and the
# FLAC inherits spotify-retag's canonical metadata.
log "Running replace-with-better.sh on staging (--copy-tags-from-existing)"
${PIPELINE_DIR:-/app/pipeline}/lib/replace-with-better.sh --apply --copy-tags-from-existing "$STAGING_HOST" >> "$LOG" 2>&1
RBE_EXIT=$?
RBE_EXIT=0
${PIPELINE_DIR:-/app/pipeline}/lib/replace-with-better.sh --apply --copy-tags-from-existing "$STAGING_HOST" >> "$LOG" 2>&1 || RBE_EXIT=$?
log "replace-with-better exit: $RBE_EXIT"
# Regenerate playlist M3Us. Upgraded tracks have new paths (FLAC vs MP3) but
@@ -164,7 +173,7 @@ while IFS= read -r playlist; do
[[ -z "$playlist" ]] && continue
M3U_OUT="${PLAYLISTS_DIR}/${playlist}.m3u8"
BEET_OUTPUT=$(beet ls -f '$path' "grouping:${playlist}" 2>>"$LOG" || true)
TRACK_COUNT=$(echo -n "$BEET_OUTPUT" | grep -c '^')
TRACK_COUNT=$(echo -n "$BEET_OUTPUT" | grep -c '^' || true)
[[ "$TRACK_COUNT" -gt 0 ]] || continue
{ echo "#EXTM3U"; echo "$BEET_OUTPUT"; } > "$M3U_OUT"
log " $playlist.m3u8 — $TRACK_COUNT tracks"