Files
alembic/pipeline/lib/sync-bandcamp.sh
T
andrew 68cb007e4c Stage 0: migrate pipeline scripts from /opt/sldl, scaffold repo
Moves all ~30 pipeline scripts, configs, and the vendored sldl binary into
this repo (source /opt/sldl left untouched). Removes all docker exec/docker
compose dependencies now that beets and sldl run in-process/as a subprocess
of this container instead of via soulbeet/on-demand sldl containers.
Replaces hardcoded host paths, Navidrome credentials, and Spotify credential
sourcing with env-var-driven paths and shared credential loaders. Adds
Dockerfile, entrypoint.sh, requirements.txt, docker-compose.snippet.yml, and
the initial app DB schema.
2026-07-08 13:10:55 -06:00

123 lines
4.9 KiB
Bash
Executable File

#!/bin/bash
# Wrapper: sync new Bandcamp purchases, then push them through the standard
# import-track.sh pipeline so they end up in the Navidrome library tagged
# the same way as everything else.
#
# 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
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
CONFIG="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/bandcamp/config.env"
LOG=${ALEMBIC_CONFIG_DIR:-/config}/logs/bandcamp-$(date +%Y%m%d).log
SYNC_PY=${PIPELINE_DIR:-/app/pipeline}/lib/sync-bandcamp.py
IMPORT_ME=${MUSIC_DATA_DIR:-/data/music}/import-me
mkdir -p "$(dirname "$LOG")"
log() { echo "[$(date -Iseconds)] $*" | tee -a "$LOG"; }
if [[ ! -f "$CONFIG" ]]; then
log "ERROR: $CONFIG not found"
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG"
if [[ ! -f "$BANDCAMP_COOKIES" ]]; then
log "ERROR: cookies file missing at $BANDCAMP_COOKIES"
log " Export Bandcamp cookies in Netscape format (Get cookies.txt LOCALLY"
log " extension, while logged into bandcamp.com), save to that path."
exit 1
fi
# Refuse to run against a dead/empty share: mkdir -p below would otherwise
# silently create shadow directories inside the container, downloads would
# land there invisibly, and state.json would mark those purchases as done —
# permanently skipping them after the mount comes back. `mountpoint -q` (the
# original host-side check) doesn't apply to a bind-mounted container path;
# a canary file + minimum artist-dir count is the container-compatible
# equivalent (same pattern as navidrome-scan.sh's share-health gate).
LIB="${MUSIC_DATA_DIR:-/data/music}/Library"
CANARY="$LIB/.navidrome-canary"
MIN_ARTIST_DIRS=500
if [[ ! -r "$CANARY" ]]; then
log "ERROR: canary $CANARY missing/unreadable — share looks unhealthy — aborting sync"
exit 1
fi
n=$(find "$LIB" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
if [[ "$n" -lt "$MIN_ARTIST_DIRS" ]]; then
log "ERROR: only $n artist dirs under $LIB (< $MIN_ARTIST_DIRS) — library looks truncated — aborting sync"
exit 1
fi
mkdir -p "$BANDCAMP_STAGING" "$IMPORT_ME"
log "=== Bandcamp sync starting ==="
log "user=$BANDCAMP_USERNAME staging=$BANDCAMP_STAGING format=$BANDCAMP_FORMAT_PREF"
if ! python3 "$SYNC_PY" \
"$BANDCAMP_USERNAME" \
"$BANDCAMP_COOKIES" \
"$BANDCAMP_STATE" \
"$BANDCAMP_STAGING" \
"$BANDCAMP_FORMAT_PREF" >> "$LOG" 2>&1; then
log "WARN: sync-bandcamp.py exited non-zero (likely auth issue or network)"
fi
# Anything new? If staging dir has audio, hand it off to import-track.sh.
NEW_COUNT=$(find "$BANDCAMP_STAGING" -type f \( -iname "*.flac" -o -iname "*.mp3" -o -iname "*.wav" -o -iname "*.m4a" \) 2>/dev/null | wc -l)
if [[ "$NEW_COUNT" -eq 0 ]]; then
log "No new files to import"
log "=== Bandcamp sync done (no-op) ==="
exit 0
fi
log "$NEW_COUNT new audio file(s) staged; moving to $IMPORT_ME for import"
# Move everything from staging into import-me. Preserve the band/album folder
# structure since import-track.sh handles directories fine.
shopt -s dotglob nullglob
for entry in "$BANDCAMP_STAGING"/*; do
[[ -e "$entry" ]] || continue
mv "$entry" "$IMPORT_ME/" 2>>"$LOG"
done
shopt -u dotglob nullglob
# Hand off to the standard manual-import pipeline. No playlist tag — Bandcamp
# purchases aren't part of any Spotify playlist. The import-track.sh guard,
# albumartist fallback, beets import, and Navidrome scan all kick in normally.
log "Calling import-track.sh"
if ${PIPELINE_DIR:-/app/pipeline}/bin/import-track.sh >> "$LOG" 2>&1; then
log "import-track.sh OK"
else
log "WARN: import-track.sh exited non-zero (exit $?)"
fi
# Safety net: if anything is stranded in ${MUSIC_DATA_DIR:-/data/music}/downloads (rare with
# duplicate_action=keep set in beets config, but possible for files beets
# couldn't process), find each one's library counterpart and replace if the
# new file is higher quality, OR import as new if there's no counterpart.
log "Running replace-with-better safety pass on /downloads stragglers"
if ${PIPELINE_DIR:-/app/pipeline}/lib/replace-with-better.sh --apply >> "$LOG" 2>&1; then
log "replace-with-better OK"
else
log "WARN: replace-with-better.sh exited non-zero (exit $?)"
fi
# Now dedupe across the library. With duplicate_action=keep beets imported
# every Bandcamp file even when it conflicts with an existing track at the
# same path (it creates `.1.ext` siblings). dedup-library.sh's policy is
# "FLAC > MP3, then largest file" — Bandcamp version wins, soulseek version
# is removed from both the beets DB and disk.
log "Running dedup pass (Bandcamp FLAC wins over older Soulseek copies)"
if ${PIPELINE_DIR:-/app/pipeline}/lib/dedup-library.sh --apply >> "$LOG" 2>&1; then
log "dedup OK"
else
log "WARN: dedup-library.sh exited non-zero (exit $?)"
fi
log "=== Bandcamp sync done ==="
exit 0