b4dd2286a9
Settings is now one top-nav entry with Credentials and Jobs as sidebar sub-pages (jobs moved from /jobs to /settings/jobs). Manual import page replaces the bare file input and free-text filename field with a drag-drop dropzone and a proper file-picker table. Genres page drops the "force" checkbox for two explicit buttons (Preview / Fix genres now). Dedup's "Scan now" runs both the file-naming and acoustic passes together, and the table labels which pass caught each candidate instead of showing the raw pass name. .btn-ghost gets a visible border so it reads as a real button next to Delete/Danger actions instead of looking unaligned. Job names throughout the UI are now human-readable instead of raw job_key strings. Also includes the fpcalc exit-code fix from earlier (fingerprint index was discarding valid fingerprints on files with a benign decode warning).
54 lines
2.0 KiB
Python
54 lines
2.0 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.
|
|
await genre_review_service.run(apply=False, force=True, triggered_by="manual")
|
|
return RedirectResponse(url="/genres", status_code=303)
|
|
|
|
|
|
@router.post("/apply")
|
|
async def apply(user: dict = Depends(require_auth)):
|
|
await genre_review_service.run(apply=True, force=True, triggered_by="manual")
|
|
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)
|