#!/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