270c9c0ed1
library_edit.update_track_fields: single-track tag edits via `beet modify -y -m` (subprocess with an argument list, not beets.library.Library in-process -- an arg-list subprocess has no shell-injection surface to avoid in the first place, and correctly reuses beets' own configured path-format/move logic rather than reimplementing it against a hand-built Library object with guessed config). Every changed field gets a manual_fix_audit row with old/new values; triggers a Navidrome rescan afterward if anything changed. Uses "genres" (plural) as the field name, matching this deployment's actual beets column and every existing script's convention, not the generic beets docs' singular "genre". genre_fix.set_artist_genre and retag.retag_from_url wrap the existing fix-genre.sh and fix-track-metadata.py as callable services rather than reimplementing their per-format mutagen tag-writing logic. manual_import: ties pipeline_runner.run_import_track to the manual_imports table, mirrors import-track.sh's own argument-count-based disambiguation (0/1/2 args), plus import-me/ listing and upload-with-path-traversal-guard. routers/library.py + routers/import_.py: browsable library list, a track detail/edit page (tag edit, artist-wide genre override, retag-from-URL forms), and the import page (upload + trigger). Minimal templates for now, Task 9 covers full UI polish. Verified update_track_fields end-to-end against a REAL beets library (not mocked): generated a tagged FLAC with ffmpeg/metaflac, imported it via a real `beet import`, edited artist+title through the service, and confirmed the file was physically moved to the new path-template location, tags were rewritten on disk, the DB updated, and both changes landed correctly in manual_fix_audit. Also verified the full app boots with the new routers registered and all four new routes correctly redirect to login when unauthenticated.
76 lines
2.6 KiB
Python
76 lines
2.6 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 beets_service, genre_fix, library_edit, retag
|
|
|
|
router = APIRouter(prefix="/library", tags=["library"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
@router.get("")
|
|
async def library_index(request: Request, grouping: str | None = None, user: dict = Depends(require_auth)):
|
|
items = beets_service.query_items(grouping=grouping)
|
|
return templates.TemplateResponse(
|
|
request, "library/index.html", {"items": items, "grouping": grouping}
|
|
)
|
|
|
|
|
|
@router.get("/track/{item_id}")
|
|
async def track_detail(request: Request, item_id: int, user: dict = Depends(require_auth)):
|
|
item = beets_service.get_item(item_id)
|
|
if item is None:
|
|
raise HTTPException(404, "no such track")
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"library/track_detail.html",
|
|
{"item": item, "editable_fields": library_edit.EDITABLE_FIELDS},
|
|
)
|
|
|
|
|
|
@router.post("/track/{item_id}")
|
|
async def update_track(
|
|
request: Request,
|
|
item_id: int,
|
|
user: dict = Depends(require_auth),
|
|
db=Depends(get_db),
|
|
):
|
|
form = await request.form()
|
|
changes = {
|
|
field: value
|
|
for field, value in form.items()
|
|
if field in library_edit.EDITABLE_FIELDS and value != ""
|
|
}
|
|
changed_by = user.get("email") or user.get("sub", "unknown")
|
|
changed = await library_edit.update_track_fields(db, item_id, changes, changed_by)
|
|
return RedirectResponse(url=f"/library/track/{item_id}?changed={len(changed)}", status_code=303)
|
|
|
|
|
|
@router.post("/track/{item_id}/genre")
|
|
async def set_genre(
|
|
item_id: int,
|
|
artist: str = Form(...),
|
|
genre: str = Form(...),
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
changed_by = user.get("email") or user.get("sub", "unknown")
|
|
await genre_fix.set_artist_genre(artist, genre, changed_by)
|
|
return RedirectResponse(url=f"/library/track/{item_id}", status_code=303)
|
|
|
|
|
|
@router.post("/track/{item_id}/retag")
|
|
async def retag_track(
|
|
item_id: int,
|
|
url: str = Form(...),
|
|
keep_genre: bool = Form(False),
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
item = beets_service.get_item(item_id)
|
|
if item is None:
|
|
raise HTTPException(404, "no such track")
|
|
changed_by = user.get("email") or user.get("sub", "unknown")
|
|
await retag.retag_from_url(item["path"], url, changed_by, keep_genre=keep_genre)
|
|
return RedirectResponse(url=f"/library/track/{item_id}", status_code=303)
|