Two bugs conspired to fill the library with .1.flac duplicates on the
night of 2026-07-15/16:
1. sldl derives its skip-index folder from the input: the Spotify
playlist display name before 0.6.2, the CSV filename after. The
switch stranded every playlist's download history in the old
emoji-named subfolder, so sldl started from an empty index and
re-downloaded every playlist in full. Fix: pin index-path in
_template.conf, seed the pinned file from all stranded indexes
(merge-sldl-indexes.py, called from run-playlist.sh whenever the
pinned index is missing or older than a stranded one). Recovered
rows keep the CSV-era identity fields but are marked downloaded, so
tracks that failed last night yet exist in the library since months
ago are not fetched again.
2. The safety net that should have flagged the flood, dedup-library.sh,
has silently reported 0 groups since the 2026-07-08 path rewrite:
host_path() still assumed /music/... display paths and prepended the
library dir to already-correct /data/music/Library/... paths, so
every candidate failed the -f check. Fix: pass real paths through
unchanged, keep the /music prefix swap only for legacy entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sldl's vendored Spotify client still calls GET /playlists/{id}/tracks, which
Spotify removed in its February 2026 API changes. Grandfathered apps still get
a pass; apps created after the change get a hard 403 there no matter how they
authenticate (client-credentials or a correctly-scoped OAuth user token), so
sldl can never load a playlist for a new app regardless of what we hand it.
Instead of depending on sldl's Spotify client at all, run-playlist.sh now reads
the playlist itself via the still-working /items endpoint (new
spotify-playlist-csv.py, creds sourced from _spotify.env) and invokes sldl with
the CSV + --input-type csv, which override the conf's input lines while -c still
supplies Soulseek login, paths, and quality settings (verified live). The same
CSV format upgrade-mp3-to-flac.sh already feeds sldl. All artists are
comma-joined in the CSV so multi-artist tracks search no worse than before, and
a 403/404 on the fetch prints the account-visibility hint instead of a bare
traceback.
Since sldl no longer talks to Spotify, playlist .confs no longer carry
spotify-id/spotify-secret/spotify-refresh: _template.conf drops them,
render_playlist_confs stops injecting them, and a Spotify credential save no
longer re-renders confs (only _spotify.env). The retag step in run-playlist.sh
reuses the sourced _spotify.env creds instead of scraping the conf lines that
no longer exist. Confs are also re-rendered once at app startup so
already-deployed confs converge on upgrade (and shed the stale secret lines)
without waiting for a playlist or credential change. Playlist delete now also
removes the generated .csv.
Tests updated: conf rendering must NOT contain Spotify creds but must keep the
input URL line; new coverage for _spotify.env rendering (quoting, refresh-token
presence/blank, 0600).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add "Connect Spotify" OAuth flow (/connect/spotify) so newly created Spotify
apps can read playlists: Spotify now requires a user token for playlist
reads, which client-credentials alone can no longer provide. The stored
refresh token is rendered as spotify-refresh into each playlist's sldl
.conf -- sldl's own docs confirm supplying it skips its interactive login
flow, which is what was causing multi-hour hangs on a stuck sync.
Grandfathered apps keep working unchanged if no account is connected.
- Fix the connect flow's redirect_uri: request.url_for() reflected the raw
connection scheme (http) rather than what the reverse proxy actually
served over, which Spotify's exact-match redirect_uri check rejected.
Force https rather than trust the unproxied scheme.
- Add an AzuraCast base URL credential field alongside the API key, with a
keyfile fallback so the scheduled enrich-buy-url.py job can actually reach
AzuraCast (previously it had no way to receive a base URL at all when run
from the scheduler, so the reprocess call was silently always skipped).
- Fix build-fingerprint-index.py exiting non-zero on any single fingerprint
failure instead of only when nothing was written.
- Guard enrich-buy-url.py's AzuraCast reprocess against an empty
--azuracast-base (raised ValueError since PD2 removed the hardcoded base).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The app owns every input to a playlist's sldl .conf (playlists in its DB,
credentials in its encrypted store), so the old
DB -> playlists.json -> regen.sh subprocess -> regex credential patch-back
chain was three serialization hops for no reason. Collapse it:
- credential_service.render_playlist_confs(db) renders each <playlist>.conf
from _template.conf in one pass: path placeholders substituted as literal
text, then the four credential lines set, written 0600. This is now the
single source of .conf rendering.
- playlist_service loses _write_playlists_json and regenerate_confs; _sync_to_disk
just calls render_playlist_confs and syncs the scheduler. No subprocess, no
intermediate JSON file.
- render_scope for spotify/soulseek re-renders confs via the same function
(spotify still also writes _spotify.env for the python scripts). The dead
_patch_conf_field / _every_playlist_conf helpers are removed.
- Delete pipeline/configs/regen.sh and drop it from the Dockerfile chmod;
update _template.conf's comments.
Nothing outside playlist_service consumed regen.sh or playlists.json (verified).
Also closes the last remnants of the S2/S3 injection surface: names are
re-validated and path/credential values are substituted as literals, never
through sed or a shell.
Verified: end-to-end render in a throwaway DB (paths, creds incl. a password
with shell metacharacters, 0600, bad-name rejection) and a live re-render of
all 17 playlist configs with credentials preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.