feat: integrated audio player + animated transcript follower

- Video detail (done videos): added 'Download audio' button; removed the
  manual 'Thumbnail' button (thumbnails auto-download now, so it was redundant).
- New 'GET /api/videos/{id}/audio' endpoint (also serves HEAD for probing),
  serving data/audio/<video_id>.mp3. Audio job outtmpl switched to %(id)s so
  files are addressable per video.
- Integrated <audio> player appears in the detail view once the MP3 is present
  (probed via HEAD on openVideo; polled after a download job until ready).
- Synced transcript follower: as audio plays, the matching segment is
  highlighted (seg-active) and auto-scrolled into view like a karaoke/lyrics
  tracker. Clicking any segment timestamp or chapter seeks the audio to that
  point (falls back to scroll when no audio). Playback indicator pulses while
  playing.
This commit is contained in:
urieljareth
2026-07-27 00:00:28 -06:00
parent 0866c92650
commit 06497299ab
51 changed files with 16013 additions and 17 deletions
+16
View File
@@ -575,6 +575,22 @@ class Store:
cur.execute("SELECT * FROM scrape_jobs ORDER BY started_at DESC LIMIT ?", (limit,))
return [_row_to_jobrow(r) for r in cur.fetchall()]
TERMINAL_STATUSES = ("done", "error", "cancelled")
def delete_job(self, job_id: str) -> bool:
with self._cursor() as cur:
cur.execute("DELETE FROM scrape_jobs WHERE id = ?", (job_id,))
return cur.rowcount > 0
def delete_terminal_jobs(self) -> int:
placeholders = ",".join("?" for _ in self.TERMINAL_STATUSES)
with self._cursor() as cur:
cur.execute(
f"DELETE FROM scrape_jobs WHERE status IN ({placeholders})",
list(self.TERMINAL_STATUSES),
)
return cur.rowcount
# ------------------------------------------------------------------ aggregates
def stats(self, channel_id: str | None = None) -> dict[str, int]: