9a116f3da4
Credentials page now shows a fact-checked one-line description per service and marks Spotify/Soulseek/Navidrome as Required (no toggle -- they're load-bearing for every playlist sync). The four optional integrations (Bandcamp, AzuraCast, Qobuz, Telegram) get a real on/off switch: Bandcamp pauses its scheduled job (same mechanism as the Jobs page), the other three just remove their rendered credential file, which the scripts that read them already treat as "not configured, skip" -- no script changes needed. Disabled optional scopes also drop off the dashboard's credential status row. New Settings > Artist Casing page to view/add/remove entries in artist-canonical.list without shelling in -- adding a name that already matches case-insensitively replaces its casing in place instead of duplicating. Playlist detail page's status section is now two side-by-side columns: the original Spotify tracklist on the left, and a tabbed Downloaded / Waiting-to-download view on the right (client-side tabs via Alpine), both with an internal scroll area so long playlists don't pull the columns out of sync. Uses the existing status_service reconciliation (sldl index + beets + quarantine) -- no backend changes needed there.
61 lines
2.3 KiB
Python
61 lines
2.3 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from app.db import get_db
|
|
from app.security.deps import require_auth
|
|
from app.services import credential_service
|
|
|
|
router = APIRouter(prefix="/settings/credentials", tags=["credentials"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
@router.get("")
|
|
async def credentials_index(request: Request, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
# Write-only by design: never send a previously-saved secret value back
|
|
# to the browser. Each scope just reports which fields are already set.
|
|
configured = {
|
|
scope: set(credential_service.get_scope(db, scope).keys())
|
|
for scope in credential_service.SCOPE_FIELDS
|
|
}
|
|
enabled = {
|
|
scope: credential_service.is_scope_enabled(db, scope)
|
|
for scope in credential_service.SCOPE_FIELDS
|
|
}
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"credentials/index.html",
|
|
{
|
|
"scope_fields": credential_service.SCOPE_FIELDS,
|
|
"scope_descriptions": credential_service.SCOPE_DESCRIPTIONS,
|
|
"core_scopes": credential_service.CORE_SCOPES,
|
|
"configured": configured,
|
|
"enabled": enabled,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/{scope}")
|
|
async def save_credentials(
|
|
scope: str, request: Request, user: dict = Depends(require_auth), db=Depends(get_db)
|
|
):
|
|
if scope not in credential_service.SCOPE_FIELDS:
|
|
raise HTTPException(404, "unknown credential scope")
|
|
form = await request.form()
|
|
values = {
|
|
field: value
|
|
for field, value in form.items()
|
|
if field in credential_service.SCOPE_FIELDS[scope] and value != ""
|
|
}
|
|
credential_service.set_credentials(db, scope, values)
|
|
return RedirectResponse(url="/settings/credentials", status_code=303)
|
|
|
|
|
|
@router.post("/{scope}/toggle")
|
|
async def toggle_scope(scope: str, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
if scope not in credential_service.OPTIONAL_SCOPES:
|
|
raise HTTPException(404, "unknown or non-toggleable credential scope")
|
|
currently_enabled = credential_service.is_scope_enabled(db, scope)
|
|
credential_service.set_scope_enabled(db, scope, not currently_enabled)
|
|
return RedirectResponse(url="/settings/credentials", status_code=303)
|