feat: add Goldfish Recap YouTube summary PWA
YouTube subtitles via OpenRouter become persistent, shareable recaps with timestamp jump links, PIN-protected creation, and Traefik deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from typing import Any
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from server.settings import settings
|
||||
|
||||
SCHEMA_PATH = str(pathlib.Path(__file__).resolve().parents[1] / "tools" / "schema.sql")
|
||||
|
||||
|
||||
async def get_db() -> aiosqlite.Connection:
|
||||
pathlib.Path(settings.db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
db = await aiosqlite.connect(settings.db_path)
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("PRAGMA foreign_keys = ON;")
|
||||
return db
|
||||
|
||||
|
||||
async def ensure_schema(db: aiosqlite.Connection) -> None:
|
||||
schema = pathlib.Path(SCHEMA_PATH).read_text(encoding="utf-8")
|
||||
await db.executescript(schema)
|
||||
await db.commit()
|
||||
|
||||
|
||||
def row_to_recap(row: aiosqlite.Row) -> dict[str, Any]:
|
||||
data = dict(row)
|
||||
data["sections"] = json.loads(data.pop("sections_json"))
|
||||
return data
|
||||
|
||||
|
||||
async def get_recap(db: aiosqlite.Connection, video_id: str) -> dict[str, Any] | None:
|
||||
cur = await db.execute("SELECT * FROM recaps WHERE video_id = ?", (video_id,))
|
||||
row = await cur.fetchone()
|
||||
await cur.close()
|
||||
return row_to_recap(row) if row else None
|
||||
|
||||
|
||||
async def list_recaps(db: aiosqlite.Connection, *, limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
|
||||
cur = await db.execute(
|
||||
"""
|
||||
SELECT video_id, title, channel, thumbnail_url, duration_sec, language,
|
||||
tldr, vibe_check, watch_verdict, goldfish_note, created_at, updated_at
|
||||
FROM recaps
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(limit, offset),
|
||||
)
|
||||
rows = await cur.fetchall()
|
||||
await cur.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def count_recaps(db: aiosqlite.Connection) -> int:
|
||||
cur = await db.execute("SELECT COUNT(*) FROM recaps")
|
||||
row = await cur.fetchone()
|
||||
await cur.close()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
|
||||
async def upsert_recap(db: aiosqlite.Connection, recap: dict[str, Any]) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO recaps (
|
||||
video_id, title, channel, thumbnail_url, duration_sec, language,
|
||||
tldr, vibe_check, watch_verdict, goldfish_note, sections_json,
|
||||
transcript_hash, model, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(video_id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
channel = excluded.channel,
|
||||
thumbnail_url = excluded.thumbnail_url,
|
||||
duration_sec = excluded.duration_sec,
|
||||
language = excluded.language,
|
||||
tldr = excluded.tldr,
|
||||
vibe_check = excluded.vibe_check,
|
||||
watch_verdict = excluded.watch_verdict,
|
||||
goldfish_note = excluded.goldfish_note,
|
||||
sections_json = excluded.sections_json,
|
||||
transcript_hash = excluded.transcript_hash,
|
||||
model = excluded.model,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
recap["video_id"],
|
||||
recap["title"],
|
||||
recap.get("channel"),
|
||||
recap.get("thumbnail_url"),
|
||||
recap.get("duration_sec"),
|
||||
recap.get("language"),
|
||||
recap["tldr"],
|
||||
recap.get("vibe_check"),
|
||||
recap["watch_verdict"],
|
||||
recap.get("goldfish_note"),
|
||||
json.dumps(recap["sections"], ensure_ascii=False),
|
||||
recap.get("transcript_hash"),
|
||||
recap.get("model"),
|
||||
recap["created_at"],
|
||||
recap["updated_at"],
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user