2a19f84575
genre_review_service.run() and dedup_review_service._run_scan() both unconditionally created a new run row even when the underlying job never actually executed (skipped_lock, or any other non-success status). For genres this was directly user-visible: the review page always shows the most recent run, so a skipped click created an empty run that displaced the real previous preview, making it look like every pending change had vanished. Both now return None (persisting nothing) when the job didn't succeed, and both routers surface a "didn't run, something else was using the pipeline" notice instead of silently redirecting. Cleaned up the one phantom empty genre_runs row already sitting in production, restoring the real 20-change preview. Also added a "Run now" button to each row on the Playlists list page (previously only on a playlist's own detail page), for the common case of adding new tracks and wanting to sync immediately.
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
from fastapi import APIRouter, Depends, Form, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from sqlalchemy import select
|
|
|
|
from app.db import get_db
|
|
from app.models import GenreCandidate, GenreRun
|
|
from app.security.deps import require_auth
|
|
from app.services import genre_review_service
|
|
|
|
router = APIRouter(prefix="/genres", tags=["genres"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
@router.get("")
|
|
async def genres_index(request: Request, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
latest_run = db.execute(select(GenreRun).order_by(GenreRun.id.desc())).scalars().first()
|
|
candidates = []
|
|
if latest_run is not None:
|
|
candidates = list(
|
|
db.execute(
|
|
select(GenreCandidate).where(GenreCandidate.genre_run_id == latest_run.id)
|
|
).scalars()
|
|
)
|
|
return templates.TemplateResponse(
|
|
request, "genres/index.html", {"latest_run": latest_run, "candidates": candidates}
|
|
)
|
|
|
|
|
|
@router.post("/scan")
|
|
async def scan(user: dict = Depends(require_auth)):
|
|
# Always force=True: matches the weekly scheduled run's own behavior
|
|
# (see genre_review_service.run's docstring), so a preview accurately
|
|
# shows what "Fix genres now" would actually do.
|
|
result = await genre_review_service.run(apply=False, force=True, triggered_by="manual")
|
|
if result is None:
|
|
return RedirectResponse(url="/genres?skipped=1", status_code=303)
|
|
return RedirectResponse(url="/genres", status_code=303)
|
|
|
|
|
|
@router.post("/apply")
|
|
async def apply(user: dict = Depends(require_auth)):
|
|
result = await genre_review_service.run(apply=True, force=True, triggered_by="manual")
|
|
if result is None:
|
|
return RedirectResponse(url="/genres?skipped=1", status_code=303)
|
|
return RedirectResponse(url="/genres", status_code=303)
|
|
|
|
|
|
@router.post("/lock")
|
|
async def lock(
|
|
artist: str = Form(...),
|
|
genre: str = Form(...),
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
changed_by = user.get("email") or user.get("sub", "unknown")
|
|
await genre_review_service.lock_artist_genre(artist, genre, changed_by)
|
|
return RedirectResponse(url="/genres", status_code=303)
|