feat: notes + agent queue, frontend rebuild, pip mirror, gitignore sync_rotation.json

- Add video_notes table and agent_queue table for persistent notes and AI agent tasks
- Add API endpoints: POST/GET /api/notes/{id}, POST /api/notes/migrate, agent queue endpoints
- Rebuild frontend: fix duplicate loadVideos(), non-blocking localStorage migration, notes UI
- Switch pip install to Tsinghua mirror for faster downloads in China
- Add backend/sync_rotation.json to .gitignore (runtime state)
This commit is contained in:
Gan, Jimmy
2026-07-08 02:23:00 +08:00
parent 77c6a9d963
commit a154bc7978
6 changed files with 434 additions and 117 deletions
+144 -7
View File
@@ -1,4 +1,5 @@
import os
import json
import logging
from fastapi import FastAPI, BackgroundTasks, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
@@ -16,7 +17,6 @@ log = logging.getLogger(__name__)
app = FastAPI(title="T YouTube", description="Text-based YouTube Summarizer")
# Enable CORS for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -28,7 +28,20 @@ app.add_middleware(
class StatusUpdate(BaseModel):
status: str
# Database Startup Hook
class NoteUpdate(BaseModel):
note_text: str
queue_to_agent: bool = False
class NoteMigrate(BaseModel):
notes: dict
class AgentQueueItem(BaseModel):
video_id: str
user_prompt: str
class AgentQueueUpdate(BaseModel):
status: str
@app.on_event("startup")
async def startup():
await init_db()
@@ -39,7 +52,6 @@ async def health():
@app.get("/auto_categories.json")
async def auto_categories():
import json, os
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "src", "auto_categories.json")
if os.path.exists(path):
with open(path) as f:
@@ -93,7 +105,6 @@ async def trigger_summarize(video_id: str, background_tasks: BackgroundTasks, db
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."}
@@ -105,17 +116,143 @@ async def update_status(
):
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
# ---- Notes (server-side, replaces localStorage) ----
@app.get("/api/notes")
async def list_notes(db: aiosqlite.Connection = Depends(get_db)):
cursor = await db.execute("SELECT video_id, note_text, updated_at FROM video_notes ORDER BY updated_at DESC")
rows = await cursor.fetchall()
return [dict(row) for row in rows]
@app.get("/api/notes/{video_id}")
async def get_note(video_id: str, db: aiosqlite.Connection = Depends(get_db)):
cursor = await db.execute("SELECT note_text, updated_at FROM video_notes WHERE video_id = ?", (video_id,))
row = await cursor.fetchone()
if not row:
return {"note_text": "", "updated_at": None}
return dict(row)
@app.post("/api/notes/{video_id}")
async def save_note(video_id: str, update: NoteUpdate, db: aiosqlite.Connection = Depends(get_db)):
await db.execute("""
INSERT INTO video_notes (video_id, note_text, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(video_id) DO UPDATE SET
note_text = excluded.note_text,
updated_at = CURRENT_TIMESTAMP
""", (video_id, update.note_text))
await db.commit()
if update.queue_to_agent and update.note_text.strip():
await db.execute("""
INSERT INTO agent_queue (video_id, user_prompt, status)
VALUES (?, ?, 'pending')
""", (video_id, update.note_text.strip()))
await db.commit()
log.info(f"Queued video {video_id} for agent: {update.note_text[:80]}")
return {"message": "Note saved"}
@app.post("/api/notes/migrate")
async def migrate_notes(migration: NoteMigrate, db: aiosqlite.Connection = Depends(get_db)):
count = 0
for video_id, note_text in migration.notes.items():
if not note_text.strip():
continue
await db.execute("""
INSERT INTO video_notes (video_id, note_text, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(video_id) DO UPDATE SET
note_text = excluded.note_text,
updated_at = CURRENT_TIMESTAMP
""", (video_id, note_text))
count += 1
await db.commit()
return {"message": f"{count} notes migrated"}
# ---- Agent Queue ----
@app.get("/api/agent-queue")
async def list_agent_queue(
status: Optional[str] = None,
limit: int = 50,
db: aiosqlite.Connection = Depends(get_db)
):
if status:
query = """
SELECT q.id, q.video_id, q.user_prompt, q.status, q.created_at, q.processed_at,
v.title, v.channel_name, v.url, v.summary
FROM agent_queue q
LEFT JOIN videos v ON q.video_id = v.video_id
WHERE q.status = ?
ORDER BY q.created_at DESC LIMIT ?
"""
cursor = await db.execute(query, (status, limit))
else:
query = """
SELECT q.id, q.video_id, q.user_prompt, q.status, q.created_at, q.processed_at,
v.title, v.channel_name, v.url, v.summary
FROM agent_queue q
LEFT JOIN videos v ON q.video_id = v.video_id
ORDER BY q.created_at DESC LIMIT ?
"""
cursor = await db.execute(query, (limit,))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
@app.post("/api/agent-queue")
async def add_to_agent_queue(item: AgentQueueItem, db: aiosqlite.Connection = Depends(get_db)):
cursor = await db.execute("SELECT 1 FROM videos WHERE video_id = ?", (item.video_id,))
exists = await cursor.fetchone()
if not exists:
raise HTTPException(status_code=404, detail="Video not found")
await db.execute("""
INSERT INTO agent_queue (video_id, user_prompt, status)
VALUES (?, ?, 'pending')
""", (item.video_id, item.user_prompt))
await db.commit()
return {"message": "Item queued for agent"}
@app.patch("/api/agent-queue/{item_id}")
async def update_agent_queue(item_id: int, update: AgentQueueUpdate, db: aiosqlite.Connection = Depends(get_db)):
valid_statuses = ["pending", "processing", "done", "failed"]
if update.status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {valid_statuses}")
cursor = await db.execute("SELECT 1 FROM agent_queue WHERE id = ?", (item_id,))
exists = await cursor.fetchone()
if not exists:
raise HTTPException(status_code=404, detail="Queue item not found")
if update.status in ("done", "failed"):
await db.execute("""
UPDATE agent_queue SET status = ?, processed_at = CURRENT_TIMESTAMP WHERE id = ?
""", (update.status, item_id))
else:
await db.execute("UPDATE agent_queue SET status = ? WHERE id = ?", (update.status, item_id))
await db.commit()
return {"message": f"Queue item {item_id} updated to {update.status}"}
@app.delete("/api/agent-queue/{item_id}")
async def remove_from_agent_queue(item_id: int, db: aiosqlite.Connection = Depends(get_db)):
cursor = await db.execute("SELECT 1 FROM agent_queue WHERE id = ?", (item_id,))
exists = await cursor.fetchone()
if not exists:
raise HTTPException(status_code=404, detail="Queue item not found")
await db.execute("DELETE FROM agent_queue WHERE id = ?", (item_id,))
await db.commit()
return {"message": f"Queue item {item_id} removed"}
# ---- Static frontend ----
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")