R7: extract the M3U regeneration into a shared sourced helper

run-playlist.sh and import-track.sh had the same beets->#EXTM3U block, but
import-track.sh wrote the file non-atomically. Move it to pipeline/lib/m3u.sh
as regen_m3u <name> <out>, sourced by both. The helper always writes atomically
(temp + rename), so a reader like Navidrome never sees a partial file and a
stray wrong-owner leftover .m3u8 is replaced without a chown. Returns 0 always
(empty result is a warning), so it's safe as a standalone call under set -e.

Verified: tests/test_m3u.py (atomic write with tracks, no file when empty; 44
tests green) plus a live regen against the real library (techno -> 229 tracks,
no temp leftover).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-10 16:16:09 -06:00
parent 1b775e42f0
commit 543f10e662
4 changed files with 83 additions and 52 deletions
+4 -30
View File
@@ -36,6 +36,8 @@ SLDL_BIN=${PIPELINE_DIR:-/app/pipeline}/sldl
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
# shellcheck source=${PIPELINE_DIR:-/app/pipeline}/lib/m3u.sh
source ${PIPELINE_DIR:-/app/pipeline}/lib/m3u.sh
# ==== Setup ====
mkdir -p "$LOG_DIR" "$DROPBOX" "$PLAYLISTS_DIR"
@@ -159,36 +161,8 @@ log "Beets has $TAGGED_COUNT tracks tagged with grouping:$PLAYLIST_NAME"
if [[ "$NO_M3U" == true ]]; then
log "Skipping M3U generation (--no-m3u)"
else
M3U_OUT="${PLAYLISTS_DIR}/${PLAYLIST_NAME}.m3u8"
log "Regenerating M3U at $M3U_OUT from beets library"
# Capture beet output to a variable first, then write to file.
BEET_OUTPUT=$(beet ls -f '$path' "grouping:${PLAYLIST_NAME}" 2>>"$LOG" || true)
# grep -c '^' exits 1 on empty input, which would trip set -e and kill the
# whole run silently right here. Count lines a way that's safe on no-match.
if [[ -z "$BEET_OUTPUT" ]]; then
TRACK_COUNT=0
else
TRACK_COUNT=$(printf '%s\n' "$BEET_OUTPUT" | wc -l)
fi
if [[ "$TRACK_COUNT" -gt 0 ]]; then
# Write to a temp file in the same dir, then rename over the target.
# rename(2) only needs write access on the directory, not on the file
# being replaced -- so this also survives a stray wrong-owner leftover
# M3U (e.g. one written by a previous non-containerized run) without
# needing a host-side chown, and it's atomic for anything (Navidrome)
# reading the file mid-write.
M3U_TMP="${M3U_OUT}.tmp.$$"
{
echo "#EXTM3U"
echo "$BEET_OUTPUT"
} > "$M3U_TMP"
mv "$M3U_TMP" "$M3U_OUT"
log "M3U updated with $TRACK_COUNT tracks"
else
log "WARNING: beet list returned no tracks for grouping:$PLAYLIST_NAME — not updating M3U"
fi
log "Regenerating M3U for $PLAYLIST_NAME from beets library"
regen_m3u "$PLAYLIST_NAME" "${PLAYLISTS_DIR}/${PLAYLIST_NAME}.m3u8"
fi
log "=== Finished playlist run: $PLAYLIST_NAME ==="