c87ddc2649
job descriptions Dashboard: format breakdown (FLAC/MP3/etc. counts), approximate library size (bitrate*length/8, same approximation `beet stats` itself uses -- verified to match its "100.5 GiB" output exactly against the real library), and a credential auth-state summary per scope (configured y/n plus, for Bandcamp, cookie expiry parsed locally from the stored cookie jar -- deliberately not a live network probe like pipeline-status.sh's Qobuz check, since this renders on every dashboard load). Library: was one unfiltered page dumping all 4072 tracks. Added search (artist/title/album), playlist and format filter dropdowns, and real SQL-level pagination (LIMIT/OFFSET, not fetch-everything-then-slice). beets_service gained count_items()/distinct_formats() to support this. Playlists: cron_expr is still the stored/scheduled representation, but the UI now shows and edits a plain daily time picker instead of raw cron syntax -- every playlist schedule today is a simple daily HH:MM anyway. playlist_service.cron_to_time()/time_to_cron() convert at the router boundary; verified round-trip against all real playlist cron values. Jobs: each maintenance job now shows a short one-line description of what it actually does (MAINTENANCE_JOB_DESCRIPTIONS), and "next run" is formatted consistently with playlists' time style (HH:MM, with a day qualifier for non-today runs -- maintenance jobs can be weekly/monthly, unlike playlists' plain daily schedule). Verified end-to-end against the real production data (4072 tracks, 100.5 GiB, real credentials): stats/format-breakdown/search/pagination all correct, all 7 credential scopes report configured with Bandcamp's real cookie expiry (325 days), and a full authenticated page-render sweep (dashboard, library with every filter combination, playlist detail, jobs) all returned 200 with no template errors.
93 lines
2.8 KiB
Python
93 lines
2.8 KiB
Python
from fastapi import APIRouter, Depends, Form, 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 playlist_service, status_service
|
|
|
|
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
@router.get("")
|
|
async def playlists_index(request: Request, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
playlists = playlist_service.list_all(db)
|
|
sync_times = {p.id: playlist_service.cron_to_time(p.cron_expr) for p in playlists}
|
|
return templates.TemplateResponse(
|
|
request, "playlists/index.html", {"playlists": playlists, "sync_times": sync_times}
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def create_playlist(
|
|
name: str = Form(...),
|
|
spotify_url: str = Form(...),
|
|
sync_time: str = Form(""),
|
|
no_m3u: bool = Form(False),
|
|
user: dict = Depends(require_auth),
|
|
db=Depends(get_db),
|
|
):
|
|
playlist_service.create(
|
|
db,
|
|
name=name,
|
|
spotify_url=spotify_url,
|
|
cron_expr=playlist_service.time_to_cron(sync_time),
|
|
no_m3u=no_m3u,
|
|
)
|
|
return RedirectResponse(url="/playlists", status_code=303)
|
|
|
|
|
|
@router.get("/{playlist_id}")
|
|
async def playlist_detail(
|
|
request: Request, playlist_id: int, user: dict = Depends(require_auth), db=Depends(get_db)
|
|
):
|
|
playlist = playlist_service.get(db, playlist_id)
|
|
if playlist is None:
|
|
raise HTTPException(404, "no such playlist")
|
|
|
|
status = None
|
|
error = None
|
|
try:
|
|
status = await status_service.playlist_status(db, playlist.name, playlist.spotify_url)
|
|
except Exception as exc:
|
|
error = str(exc)
|
|
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"playlists/detail.html",
|
|
{
|
|
"playlist": playlist,
|
|
"sync_time": playlist_service.cron_to_time(playlist.cron_expr),
|
|
"status": status,
|
|
"error": error,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/{playlist_id}")
|
|
async def update_playlist(
|
|
playlist_id: int,
|
|
active: bool = Form(False),
|
|
sync_time: str = Form(""),
|
|
no_m3u: bool = Form(False),
|
|
notes: str = Form(""),
|
|
user: dict = Depends(require_auth),
|
|
db=Depends(get_db),
|
|
):
|
|
playlist_service.update(
|
|
db,
|
|
playlist_id,
|
|
active=active,
|
|
cron_expr=playlist_service.time_to_cron(sync_time),
|
|
no_m3u=no_m3u,
|
|
notes=notes or None,
|
|
)
|
|
return RedirectResponse(url=f"/playlists/{playlist_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{playlist_id}/delete")
|
|
async def delete_playlist(playlist_id: int, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
playlist_service.delete(db, playlist_id)
|
|
return RedirectResponse(url="/playlists", status_code=303)
|