e5d9c7f043
- 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>
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.exception_handlers import http_exception_handler
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
from app.db import enable_beets_db_wal, init_db, mark_interrupted_runs
|
|
from app.routers import artist_casing, auth, connect, credentials, dashboard, dedup, genres, health, import_, jobs, library, playlists
|
|
from app.security.session import resolve_session_secret
|
|
from app.services import scheduler_service
|
|
from app.settings import settings
|
|
|
|
log = logging.getLogger("alembic")
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
async def not_found_or_default(request: Request, exc: StarletteHTTPException):
|
|
"""Render the branded 404 page for missing pages; leave every other
|
|
HTTP exception (including the 303 login redirect raised by require_auth)
|
|
to FastAPI's default handler so their status and headers are preserved."""
|
|
if exc.status_code == 404:
|
|
return templates.TemplateResponse(request, "404.html", {}, status_code=404)
|
|
return await http_exception_handler(request, exc)
|
|
|
|
|
|
def _warn_if_key_colocated_with_config() -> None:
|
|
"""The Fernet master key decrypts every stored credential. If it lives
|
|
inside the config volume, one leaked volume or backup exposes both the
|
|
key and the encrypted database together, which defeats the encryption.
|
|
Warn loudly rather than fail, since some users deliberately accept this."""
|
|
try:
|
|
key_path = settings.encryption_key_file.resolve()
|
|
config_dir = settings.alembic_config_dir.resolve()
|
|
except OSError:
|
|
return
|
|
if config_dir == key_path or config_dir in key_path.parents:
|
|
log.warning(
|
|
"Master key %s is inside the config directory %s. Anyone with a "
|
|
"copy of that volume or backup gets both the key and the encrypted "
|
|
"credentials. Mount the key from a separate location kept out of "
|
|
"the config backup. See docker-compose.snippet.yml.",
|
|
key_path,
|
|
config_dir,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI):
|
|
_warn_if_key_colocated_with_config()
|
|
init_db()
|
|
mark_interrupted_runs()
|
|
enable_beets_db_wal()
|
|
|
|
scheduler = scheduler_service.create_scheduler()
|
|
scheduler_service.register_all_jobs(scheduler)
|
|
scheduler.start()
|
|
|
|
yield
|
|
|
|
scheduler.shutdown(wait=False)
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
app = FastAPI(title="alembic", lifespan=lifespan)
|
|
|
|
app.add_exception_handler(StarletteHTTPException, not_found_or_default)
|
|
|
|
app.add_middleware(SessionMiddleware, secret_key=resolve_session_secret())
|
|
|
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
|
|
|
app.include_router(health.router)
|
|
app.include_router(auth.router)
|
|
app.include_router(dashboard.router)
|
|
app.include_router(library.router)
|
|
app.include_router(import_.router)
|
|
app.include_router(dedup.router)
|
|
app.include_router(genres.router)
|
|
app.include_router(playlists.router)
|
|
app.include_router(credentials.router)
|
|
app.include_router(connect.router)
|
|
app.include_router(jobs.router)
|
|
app.include_router(artist_casing.router)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|