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.
This commit is contained in:
andrew
2026-07-08 13:10:55 -06:00
commit 68cb007e4c
48 changed files with 9261 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
#!/bin/bash
# upgrade-mp3-to-flac.sh — for every MP3 currently in the library, ask sldl
# to find a FLAC version of the same track. If found, replace the MP3.
# If not found, the MP3 stays (we don't lose anything).
#
# Strategy:
# 1. Build a CSV from beets with one row per MP3 track.
# 2. Run sldl with --format flac (HARD format requirement, not preferred —
# sldl will skip any track where no FLAC is available rather than
# falling back to MP3).
# 3. For each FLAC that arrives in the staging dropbox, run
# replace-with-better.sh which already knows the FLAC > MP3 rule.
#
# Conservative — if Soulseek peers are slow that day or the FLAC isn't
# available, we leave the MP3 alone. Re-runs are cheap; sldl's m3u skip
# logic means it won't re-search tracks it has already FLAC-found.
#
# Usage:
# 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
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
# shellcheck 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
# shellcheck 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
LOG=${ALEMBIC_CONFIG_DIR:-/config}/logs/upgrade-mp3-$(date +%Y%m%d-%H%M%S).log
STAGING_HOST=${MUSIC_DATA_DIR:-/data/music}/sldl-dropbox/_upgrade
QUARANTINE_DIR=${MUSIC_DATA_DIR:-/data/music}/Songs/untagged
CSV_HOST=${ALEMBIC_CONFIG_DIR:-/config}/pipeline/_upgrade.csv
SLDL_BIN=${PIPELINE_DIR:-/app/pipeline}/sldl
mkdir -p "$(dirname "$LOG")" "$STAGING_HOST"
log() { echo "[$(date -Iseconds)] $*" | tee -a "$LOG"; }
log "=== MP3 upgrade scan starting ==="
# Build CSV from beets. sldl's CSV format: comma-separated with header
# Artist, Title, Album, Length (sec), URI (optional). We omit URI; sldl
# falls back to a Soulseek text search when no URI is given.
log "Querying beets for MP3 tracks..."
beet ls -f '$artist|$title|$album|$length' 'format:mp3' 2>/dev/null > /tmp/mp3-rows.txt
COUNT=$(wc -l < /tmp/mp3-rows.txt)
log " $COUNT MP3 tracks to attempt"
# Convert | to , and quote fields that contain commas
{
echo "Artist,Title,Album,Length"
awk -F'|' 'BEGIN{OFS=""} {
for (i=1; i<=NF; i++) {
g = $i; gsub(/"/, "\"\"", g)
if (g ~ /[,"]/) g = "\"" g "\""
$i = g
}
print $1 "," $2 "," $3 "," $4
}' /tmp/mp3-rows.txt
} > "$CSV_HOST"
log " CSV written: $CSV_HOST ($(wc -l < "$CSV_HOST") lines incl. header)"
if [[ $CSV_ONLY -eq 1 ]]; then
log "--csv-only flag — exiting before sldl run"
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 ' ')
log "Running sldl with --format flac (HARD requirement, no MP3/WAV fallback)"
"$SLDL_BIN" \
"$CSV_HOST" \
--input-type csv \
--user "$SOULSEEK_USER" \
--pass "$SOULSEEK_PASS" \
--path "$STAGING_HOST" \
--format flac \
--pref-min-bitrate 320 \
--concurrent-downloads 8 \
--search-timeout 30000 \
--strict-title \
--strict-artist \
--no-browse-folder \
--no-skip-existing \
--verbose \
>> "$LOG" 2>&1
SLDL_EXIT=$?
log "sldl exit code: $SLDL_EXIT"
# Guard: this script is FLAC-upgrade-only. If sldl ever drops anything else
# in staging (WAV/MP3/etc.), purge it so the import-leftover fallback in
# replace-with-better.sh doesn't pull non-FLACs into the library.
NON_FLAC=$(find "$STAGING_HOST" -type f ! -iname "*.flac" ! -name "*.incomplete" 2>/dev/null)
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
fi
NEW_FLAC_COUNT=$(find "$STAGING_HOST" -type f -iname "*.flac" 2>/dev/null | wc -l)
log "Staging has $NEW_FLAC_COUNT FLAC file(s) ready for upgrade"
if [[ "$NEW_FLAC_COUNT" -eq 0 ]]; then
log "Nothing to upgrade. Done."
exit 0
fi
# === Treat staging files like new playlist downloads ===
# Same hygiene + prep as run-playlist.sh, applied to /sldl-dropbox/_upgrade
# before any of these files enter the library.
# Quarantine FLACs with missing ARTIST/TITLE — same rule as playlist runs.
# Prevents the __.wav-style "untagged files in /Library/ root" failure mode.
QUARANTINED=$(quarantine_untagged "$STAGING_HOST" "$QUARANTINE_DIR" "$LOG" || echo 0)
[[ "$QUARANTINED" -gt 0 ]] && log "Quarantined $QUARANTINED file(s) to $QUARANTINE_DIR"
# Recount post-quarantine
NEW_FLAC_COUNT=$(find "$STAGING_HOST" -type f -iname "*.flac" 2>/dev/null | wc -l)
[[ "$NEW_FLAC_COUNT" -eq 0 ]] && { log "Nothing left after quarantine. Done."; exit 0; }
# Strip MB IDs (prevents Navidrome album fragmentation). Scoped to staging:
# the library-wide strip-mb-tags.sh runs weekly anyway and wouldn't touch
# staging FLACs (they aren't in /Library/ yet).
MB_STRIPPED=$(strip_mb_tags "$STAGING_HOST" "$LOG" || echo 0)
log "Stripped MB tags from $MB_STRIPPED file(s) in staging"
# ALBUMARTIST fallback — safety net for no-match leftovers whose uploader
# didn't set ALBUMARTIST. For matched files this is overwritten by the
# tag-copy step inside replace-with-better.sh (canonical MP3 tags win).
AA_SET=$(set_albumartist_fallback "$STAGING_HOST" "$LOG" || echo 0)
[[ "$AA_SET" -gt 0 ]] && log "Set ALBUMARTIST fallback on $AA_SET file(s)"
# ReplayGain + autocue tagging — same as run-playlist.sh.
log "Pre-tagging with ReplayGain + autocue"
PREPPED=$(prep_audio "$STAGING_HOST" "$LOG" || echo 0)
log "Pre-tagged $PREPPED file(s)"
# Use replace-with-better.sh against the staging dir. --copy-tags-from-existing
# makes it copy canonical tags (incl. GROUPING) from each matched MP3 onto
# 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=$?
log "replace-with-better exit: $RBE_EXIT"
# Regenerate playlist M3Us. Upgraded tracks have new paths (FLAC vs MP3) but
# their GROUPING tags were carried over via --copy-tags-from-existing, so the
# beet-driven regen below resolves them to the new FLAC paths.
log "Regenerating playlist M3Us"
PLAYLISTS_DIR=${MUSIC_DATA_DIR:-/data/music}/playlists
mkdir -p "$PLAYLISTS_DIR"
M3U_COUNT=0
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" -gt 0 ]] || continue
{ echo "#EXTM3U"; echo "$BEET_OUTPUT"; } > "$M3U_OUT"
log " $playlist.m3u8 — $TRACK_COUNT tracks"
M3U_COUNT=$((M3U_COUNT + 1))
done < <(beet ls -f '$grouping' 2>/dev/null | sort -u | grep -v '^$')
log "Regenerated $M3U_COUNT M3U file(s)"
# Cleanup staging
log "Cleaning up empty staging dir"
find "$STAGING_HOST" -type f -delete 2>>"$LOG" || true
find "$STAGING_HOST" -mindepth 1 -type d -empty -delete 2>>"$LOG" || true
log "=== MP3 upgrade scan done ==="