import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException, BackgroundTasks, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel from app import downloader from app.config import settings STATIC_DIR = Path(__file__).parent / "static" AUDIO_TMP_DIR = Path("/tmp/apoena-audio") @asynccontextmanager async def lifespan(app: FastAPI): AUDIO_TMP_DIR.mkdir(parents=True, exist_ok=True) yield app = FastAPI(title="apoena-transcript", lifespan=lifespan) class ExtractAudioRequest(BaseModel): url: str @app.get("/health") async def health(): return {"status": "ok"} @app.get("/worker.js") async def worker_js(): return FileResponse(STATIC_DIR / "worker.js", media_type="application/javascript") @app.get("/sw.js") async def sw_js(): return FileResponse(STATIC_DIR / "sw.js", media_type="application/javascript") @app.get("/manifest.json") async def manifest(): return FileResponse(STATIC_DIR / "manifest.json", media_type="application/manifest+json") @app.get("/icon.svg") async def icon(): return FileResponse(STATIC_DIR / "icon.svg", media_type="image/svg+xml") @app.get("/icon-maskable.svg") async def icon_maskable(): return FileResponse(STATIC_DIR / "icon-maskable.svg", media_type="image/svg+xml") @app.post("/extract-audio") async def extract_audio(body: ExtractAudioRequest, background_tasks: BackgroundTasks): try: audio_path = await downloader.extract_audio(body.url) except RuntimeError as e: raise HTTPException(status_code=422, detail=str(e)) background_tasks.add_task(_delete_file, audio_path) return FileResponse(audio_path, media_type="audio/mpeg", filename="audio.mp3") def _delete_file(path): try: os.unlink(path) except OSError: pass @app.post("/admin/cookies") async def upload_cookies(file: UploadFile): if not settings.yt_dlp_cookies_file: raise HTTPException(status_code=500, detail="YT_DLP_COOKIES_FILE not configured") cookies_path = Path(settings.yt_dlp_cookies_file) cookies_path.parent.mkdir(parents=True, exist_ok=True) content = await file.read() cookies_path.write_bytes(content) return {"status": "ok", "path": str(cookies_path)} @app.get("/admin/cookies/status") async def cookies_status(): if not settings.yt_dlp_cookies_file: return {"present": False} path = Path(settings.yt_dlp_cookies_file) return {"present": path.exists(), "size": path.stat().st_size if path.exists() else 0} @app.get("/") async def index(): return FileResponse(STATIC_DIR / "index.html")