d3ce89f228
Docker / docker (push) Successful in 1m58s
If DB has any concerts data (even expired), return it immediately and refresh in background. Start pre-warming at container startup so the scrape runs before the first user request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
4.4 KiB
Python
162 lines
4.4 KiB
Python
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from pydantic import BaseModel
|
|
|
|
from arte_api import fetch_concerts, get_all_concerts, get_concerts_by_category, invalidate_cache, CATEGORIES
|
|
from downloader import DownloadManager
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
AUTO_DL_INTERVAL = int(os.getenv("AUTO_DL_INTERVAL", "3600"))
|
|
|
|
dm = DownloadManager()
|
|
|
|
|
|
async def _run_auto_dl_check() -> int:
|
|
cats = dm.get_watched_categories()
|
|
if not cats:
|
|
return 0
|
|
total = 0
|
|
for cat in cats:
|
|
concerts = await get_concerts_by_category(cat)
|
|
for c in concerts:
|
|
if not dm.already_enqueued(c["url"]):
|
|
m = re.search(r"\b(20\d{2})\b", c.get("subtitle", ""))
|
|
year = int(m.group(1)) if m else c.get("tmdb_year")
|
|
await dm.enqueue(c["url"], c["title"], c.get("subtitle", ""), year, cat)
|
|
total += 1
|
|
return total
|
|
|
|
|
|
async def _auto_dl_loop():
|
|
await asyncio.sleep(60)
|
|
while True:
|
|
try:
|
|
n = await _run_auto_dl_check()
|
|
if n:
|
|
logger.info("Auto-DL: enqueued %d new concert(s)", n)
|
|
except Exception as e:
|
|
logger.warning("Auto-DL check failed: %s", e)
|
|
await asyncio.sleep(AUTO_DL_INTERVAL)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await dm.resume_pending()
|
|
tasks = [
|
|
asyncio.create_task(dm.start_worker()),
|
|
asyncio.create_task(_auto_dl_loop()),
|
|
asyncio.create_task(get_all_concerts()), # pre-warm cache at startup
|
|
]
|
|
yield
|
|
for t in tasks:
|
|
t.cancel()
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
|
app = FastAPI(title="Arte-dl", lifespan=lifespan)
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
# ------------------------------------------------------------------ pages
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def index(request: Request):
|
|
return templates.TemplateResponse("index.html", {"request": request})
|
|
|
|
|
|
# ------------------------------------------------------------------ API: concerts
|
|
|
|
|
|
@app.get("/api/categories")
|
|
async def api_categories():
|
|
return CATEGORIES
|
|
|
|
|
|
@app.get("/api/concerts")
|
|
async def api_concerts(page: int = 1, search: str = "", page_size: int = 24, category: str = ""):
|
|
return await fetch_concerts(page=page, search=search, page_size=page_size, category=category)
|
|
|
|
|
|
@app.post("/api/refresh")
|
|
async def api_refresh():
|
|
count = await invalidate_cache()
|
|
return {"count": count}
|
|
|
|
|
|
# ------------------------------------------------------------------ API: auto-download
|
|
|
|
|
|
@app.get("/api/auto-dl")
|
|
async def api_auto_dl_list():
|
|
return dm.get_watched_categories()
|
|
|
|
|
|
@app.post("/api/auto-dl/check")
|
|
async def api_auto_dl_check():
|
|
n = await _run_auto_dl_check()
|
|
return {"enqueued": n}
|
|
|
|
|
|
@app.post("/api/auto-dl/{category}")
|
|
async def api_auto_dl_watch(category: str):
|
|
if category not in CATEGORIES:
|
|
raise HTTPException(status_code=404, detail="Unknown category")
|
|
dm.watch_category(category)
|
|
return {"watched": True}
|
|
|
|
|
|
@app.delete("/api/auto-dl/{category}")
|
|
async def api_auto_dl_unwatch(category: str):
|
|
dm.unwatch_category(category)
|
|
return {"watched": False}
|
|
|
|
|
|
# ------------------------------------------------------------------ API: downloads
|
|
|
|
|
|
class DownloadRequest(BaseModel):
|
|
url: str
|
|
title: str
|
|
subtitle: str = ""
|
|
year: int | None = None
|
|
category: str = ""
|
|
|
|
|
|
@app.post("/api/download")
|
|
async def api_download(req: DownloadRequest):
|
|
if not req.url:
|
|
raise HTTPException(status_code=400, detail="url required")
|
|
dl_id = await dm.enqueue(req.url, req.title, req.subtitle, req.year, req.category)
|
|
return {"id": dl_id}
|
|
|
|
|
|
@app.get("/api/downloads")
|
|
async def api_downloads():
|
|
return dm.history()
|
|
|
|
|
|
@app.get("/api/progress/{dl_id}")
|
|
async def api_progress(dl_id: str):
|
|
async def stream():
|
|
while True:
|
|
s = dm.status(dl_id)
|
|
yield f"data: {json.dumps(s)}\n\n"
|
|
if s.get("state") in ("done", "error", "unknown"):
|
|
break
|
|
await asyncio.sleep(0.8)
|
|
|
|
return StreamingResponse(stream(), media_type="text/event-stream")
|