import json from app.db import SessionLocal from app.models import GenreCandidate, GenreRun from app.services import genre_fix, pipeline_runner from app.settings import settings _SCRIPT = "spotify-genre.py" def _parse_json_lines(output: str) -> list[dict]: candidates = [] for line in output.splitlines(): line = line.strip() if not line.startswith("{"): continue try: candidates.append(json.loads(line)) except json.JSONDecodeError: continue return candidates async def run(apply: bool, force: bool = False, triggered_by: str = "schedule") -> GenreRun: """Run spotify-genre.py --json (dry-run or --apply), persisting the diff into a fresh genre_runs/genre_candidates pair either way. Unlike dedup, genre writes stay auto-apply on the weekly schedule (low-risk/reversible, GENRE_LOCK-protected) -- this wraps that same scheduled run so its diff becomes reviewable after the fact, rather than being a separate preview-only mode. scheduler_service's maintenance:spotify_genre job calls this with apply=True.""" args = ["--json"] if apply: args.append("--apply") if force: args.append("--force") script = str(settings.pipeline_dir / "lib" / _SCRIPT) job_run, output = await pipeline_runner.run_job_capture( "genre:run", [script] + args, triggered_by=triggered_by ) candidates = _parse_json_lines(output) db = SessionLocal() try: genre_run = GenreRun( started_at=job_run.started_at, finished_at=job_run.finished_at, mode="apply" if apply else "dry_run", written=len(candidates) if apply else 0, unchanged=None, no_match=None, no_meta=None, log_path=job_run.log_path, ) db.add(genre_run) db.commit() db.refresh(genre_run) for c in candidates: db.add( GenreCandidate( genre_run_id=genre_run.id, artist=c.get("artist"), title=c.get("title"), old_genre=c.get("old_genre"), new_genre=c.get("new_genre"), ) ) db.commit() db.refresh(genre_run) return genre_run finally: db.close() async def lock_artist_genre(artist: str, genre: str, changed_by: str): """One-click 'lock this artist's genre' action from the review UI, for when a scheduled run got something wrong -- wraps genre_fix.set_artist_genre (fix-genre.sh sets GENRE_LOCK=1 as part of applying, which is what makes future spotify-genre runs leave it alone).""" return await genre_fix.set_artist_genre(artist, genre, changed_by)