Fix regen.sh PATH bug; add legacy credential import script

playlist_service.regenerate_confs() was passing a bare PATH=/usr/bin:/bin
to the regen.sh subprocess, but python3 (which regen.sh shells out to for
parsing playlists.json) lives at /opt/venv/bin/python3 in this image.
Every call silently generated zero .conf files -- no error surfaced, since
the failure was inside a `python3 -c ... | while read` pipeline whose
input was just empty. Found this deploying to the real host: seed_legacy()
reported "17 playlists" successfully, but zero .conf files existed. Fixed
to match pipeline_runner's already-correct PATH.

scripts/import_legacy_credentials.py: the Stage 1/2 one-time migration
script -- reads spotify/soulseek creds from a sample playlist .conf,
Navidrome's password (extracted from navidrome-scan.sh, never lived in a
file), and bandcamp/azuracast/qobuz/telegram from their legacy scattered
locations, then populates credential_service. Read-only against the
legacy files, idempotent. Run as a one-off `docker run --entrypoint python`
with the legacy paths bind-mounted read-only (documented in the script's
own docstring) -- entrypoint.sh always execs uvicorn regardless of CMD, so
--entrypoint must override it for one-off invocations like this.

Verified against the real production host: seeded all 17 legacy playlists
with their historical cron schedules, confirmed all 17 .conf files render
correctly after the PATH fix, ran the credential import for all 7 scopes,
and byte-diffed the rendered spotify/soulseek values in a sample .conf
against the original (identical, verified via diff -q without displaying
the actual secret values in any tool output).

Also had to chown 4 root:root mode-600 legacy credential files
(bandcampconfig/cookies.txt, azuracastconfig/api_key, qobuzconfig/token,
telegramconfig/notify.env) to 1000:1000 so the container could read them
for the import -- root retains read access regardless, so this doesn't
affect the still-running host-cron pipeline.
This commit is contained in:
andrew
2026-07-08 14:47:39 -06:00
parent 5601de3d44
commit a1f5bbb121
2 changed files with 187 additions and 4 deletions
+8 -4
View File
@@ -142,9 +142,8 @@ def regenerate_confs(db: Session) -> subprocess.CompletedProcess:
"""Write playlists.json, then run regen.sh to render/refresh every
playlist's .conf file. Newly-created .conf files still have unpatched
SOULSEEK_USER/SOULSEEK_PASS/SPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRET
placeholders at this point — the caller (routers/playlists.py) is
responsible for re-rendering credentials afterward via
credential_service.render_scope(db, "soulseek"/"spotify")."""
placeholders at this point -- _sync_to_disk() (this function's caller)
re-renders credentials into them immediately afterward."""
_write_playlists_json(db)
regen_script = settings.pipeline_dir / "configs" / "regen.sh"
return subprocess.run(
@@ -155,7 +154,12 @@ def regenerate_confs(db: Session) -> subprocess.CompletedProcess:
"ALEMBIC_CONFIG_DIR": str(settings.alembic_config_dir),
"MUSIC_DATA_DIR": str(settings.music_data_dir),
"PIPELINE_DIR": str(settings.pipeline_dir),
"PATH": "/usr/bin:/bin",
# Must include /opt/venv/bin: regen.sh shells out to `python3`
# to parse playlists.json, and that's where this image's Python
# actually lives (confirmed against the real container -- a
# bare /usr/bin:/bin PATH made every regen.sh call silently
# generate zero .conf files, no error surfaced to the caller).
"PATH": "/opt/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
},
)