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:
@@ -23,6 +23,25 @@ async def init_db():
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS video_notes (
|
||||
video_id TEXT PRIMARY KEY,
|
||||
note_text TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (video_id) REFERENCES videos(video_id)
|
||||
)
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS agent_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
video_id TEXT NOT NULL,
|
||||
user_prompt TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TEXT,
|
||||
FOREIGN KEY (video_id) REFERENCES videos(video_id)
|
||||
)
|
||||
""")
|
||||
await db.commit()
|
||||
|
||||
async def get_db():
|
||||
|
||||
+144
-7
@@ -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")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.20.0
|
||||
yt-dlp>=2023.0.0
|
||||
youtube-transcript-api>=0.6.0
|
||||
aiosqlite>=0.18.0
|
||||
httpx>=0.24.0
|
||||
|
||||
Reference in New Issue
Block a user