Files
arte-dl/main.py
T

160 lines
4.3 KiB
Python
Raw Normal View History

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_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 None
await dm.enqueue_direct(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):
tasks = [
asyncio.create_task(dm.start_worker()),
asyncio.create_task(_auto_dl_loop()),
]
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
2026-04-26 13:03:52 +02:00
@app.get("/api/categories")
async def api_categories():
return CATEGORIES
@app.get("/api/concerts")
2026-04-26 13:03:52 +02:00
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")