from datetime import datetime, timezone from typing import Any from slugify import slugify as _slugify from supabase import Client def slugify(title: str, artist: str, max_len: int = 80) -> str: raw = _slugify(f"{title} {artist}") return raw[:max_len].rstrip("-") _TERMINAL = {"ready", "failed_fetch", "failed_separation", "failed_other"} class SongRepo: def __init__(self, client: Client): self._c = client # songs --------------------------------------------------------------- def get_song(self, youtube_id: str) -> dict | None: res = ( self._c.table("songs").select("*") .eq("youtube_id", youtube_id).maybe_single().execute() ) return res.data if res else None def get_song_by_slug(self, slug: str) -> dict | None: res = ( self._c.table("songs").select("*") .eq("slug", slug).eq("status", "ready").maybe_single().execute() ) return res.data if res else None def upsert_song(self, row: dict[str, Any]) -> None: self._c.table("songs").upsert(row, on_conflict="youtube_id").execute() def increment_play_count(self, youtube_id: str) -> None: # Best-effort, ignore the row if missing. self._c.rpc("increment_play_count", {"yt_id": youtube_id}).execute() # jobs ---------------------------------------------------------------- def create_job(self, youtube_id: str) -> str: res = self._c.table("jobs").insert( {"youtube_id": youtube_id, "status": "queued"} ).execute() return res.data[0]["id"] def update_job(self, job_id: str, *, status: str, error: str | None = None) -> None: patch: dict[str, Any] = {"status": status, "error": error} if status in _TERMINAL: patch["finished_at"] = datetime.now(timezone.utc).isoformat() self._c.table("jobs").update(patch).eq("id", job_id).execute() def get_job(self, job_id: str) -> dict | None: res = ( self._c.table("jobs").select("*") .eq("id", job_id).maybe_single().execute() ) return res.data if res else None