59c369c9e2
Playlist detail page's status section (and the new two-column compare view) was never rendering: the router awaited status_service.playlist_status(), but that function is plain sync, so awaiting its dict return raised "object dict can't be used in 'await' expression" on every load, silently caught and shown as a generic fetch error. Removed the erroneous await. find-fuzzy-dupes.py's --apply and --apply-pairs both deleted by a `path::` regex query even though the beets id was already available in scope -- same mixed-path-storage issue (pre/post beets-2.11-upgrade items store absolute vs. library-relative paths) dedup-library.sh already worked around by deleting via id instead. This silently failed to match for most confirmed fuzzy-audio deletions (verified: 19 of 20 in one run). Added a delete_id column to dedup_candidates, threaded through the scan/apply pipeline, and switched both delete call sites to `id:`. Dedup scans also never checked whether a pair was already sitting in the pending list, so every re-scan (including the daily schedule) added a new row for the same unreviewed duplicate -- cleaned up 38 redundant rows already in production and added a check so future scans skip a pair that's already pending. Dashboard gets a Public IP stat card (cached 10 min, fetched via ipify) as a quick confidence check that outbound traffic is actually routed through gluetun's VPN and not the home connection.
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 = 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)
|