Initial commit: Add T YouTube text-based reader web app

- FastAPI backend running on port 8000
- Svelte 5 runes frontend dashboard supporting Read/Skip state toggles
- YouTube subscription feed scraper using yt-dlp & Netscape cookies.txt
- Transcript downloader and Claude 3.5 Haiku summarizer via bytecatcode proxy
- Multi-stage Dockerfile and docker-compose.yml configuration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gan, Jimmy
2026-06-03 11:00:34 +08:00
commit dbd3dc0237
17 changed files with 967 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import os
import logging
from fastapi import FastAPI, BackgroundTasks, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import Optional, List
import aiosqlite
from db import init_db, get_db
from fetcher import fetch_subscriptions
from summarizer import summarize_video, summarize_all_pending
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__)
app = FastAPI(title="T YouTube", description="Text-based YouTube Summarizer")
# Enable CORS for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class StatusUpdate(BaseModel):
status: str
# Database Startup Hook
@app.on_event("startup")
async def startup():
await init_db()
@app.get("/api/health")
async def health():
return {"status": "ok"}
@app.get("/api/videos")
async def get_videos(
status: Optional[str] = "pending",
limit: int = 50,
db: aiosqlite.Connection = Depends(get_db)
):
query = "SELECT * FROM videos WHERE status = ? ORDER BY publish_date DESC, created_at DESC LIMIT ?"
cursor = await db.execute(query, (status, limit))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def bg_fetch_and_summarize():
log.info("Starting background subscription fetch and summarization loop...")
try:
new_count = await fetch_subscriptions()
if new_count > 0:
await summarize_all_pending()
except Exception as e:
log.error(f"Error in background fetch and summarize task: {e}")
@app.post("/api/videos/fetch")
async def trigger_fetch(background_tasks: BackgroundTasks):
background_tasks.add_task(bg_fetch_and_summarize)
return {"message": "Subscription sync and summarization running in background."}
@app.post("/api/videos/{video_id}/summarize")
async def trigger_summarize(video_id: str, background_tasks: BackgroundTasks, db: aiosqlite.Connection = Depends(get_db)):
cursor = await db.execute("SELECT 1 FROM videos WHERE video_id = ?", (video_id,))
exists = await cursor.fetchone()
if not exists:
raise HTTPException(status_code=404, detail="Video not found")
background_tasks.add_task(summarize_video, video_id)
return {"message": f"Summarization for video {video_id} queued."}
@app.post("/api/videos/{video_id}/status")
async def update_status(
video_id: str,
update: StatusUpdate,
db: aiosqlite.Connection = Depends(get_db)
):
if update.status not in ["pending", "read", "skipped"]:
raise HTTPException(status_code=400, detail="Invalid status value")
cursor = await db.execute("SELECT 1 FROM videos WHERE video_id = ?", (video_id,))
exists = await cursor.fetchone()
if not exists:
raise HTTPException(status_code=404, detail="Video not found")
await db.execute("UPDATE videos SET status = ? WHERE video_id = ?", (update.status, video_id))
await db.commit()
return {"message": f"Video {video_id} status updated to {update.status}."}
# Mount frontend built files for production if they exist
frontend_build_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend/dist")
if os.path.exists(frontend_build_path):
app.mount("/", StaticFiles(directory=frontend_build_path, html=True), name="frontend")
log.info("Mounted static frontend build successfully.")
else:
log.warning("Frontend build directory 'frontend/dist' not found. API only mode is active.")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)