"""Admin management routes.""" import json import os from fastapi import APIRouter, HTTPException, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, or_ from app.database import get_db from app.models import Video, DownloadLog, AppSetting from app.schemas import ( VideoInfo, VideoListResponse, StorageStats, DownloadLogInfo, DownloadLogListResponse, CleanupConfig, CleanupStatus, DiskStats, ) from app.auth import get_current_user from app.services.cleanup import get_setting, set_setting, disk_stats, run_cleanup from app.services.downloader import get_progress router = APIRouter(prefix="/api/admin", tags=["admin"]) def human_size(size_bytes: int) -> str: for unit in ["B", "KB", "MB", "GB", "TB"]: if size_bytes < 1024: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024 return f"{size_bytes:.1f} PB" @router.get("/videos", response_model=VideoListResponse) async def list_videos( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), search: str = Query("", max_length=200), status: str = Query("", max_length=20), user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): query = select(Video).order_by(Video.created_at.desc()) count_query = select(func.count(Video.id)) if search: flt = or_(Video.title.contains(search), Video.url.contains(search)) query = query.where(flt) count_query = count_query.where(flt) if status: query = query.where(Video.status == status) count_query = count_query.where(Video.status == status) total = (await db.execute(count_query)).scalar() or 0 videos = (await db.execute(query.offset((page - 1) * page_size).limit(page_size))).scalars().all() items = [] for v in videos: info = VideoInfo.model_validate(v) if v.status == "downloading": info.progress = get_progress(v.task_id) items.append(info) return VideoListResponse(videos=items, total=total, page=page, page_size=page_size) @router.delete("/videos/{video_id}") async def delete_video(video_id: int, user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)): video = (await db.execute(select(Video).where(Video.id == video_id))).scalar_one_or_none() if not video: raise HTTPException(status_code=404, detail="Video not found") # Delete file from disk if video.file_path and os.path.exists(video.file_path): os.remove(video.file_path) await db.delete(video) await db.commit() return {"ok": True} @router.get("/stats", response_model=StorageStats) async def storage_stats(user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)): total = (await db.execute(select(func.count(Video.id)).where(Video.status == "done"))).scalar() or 0 total_size = (await db.execute(select(func.sum(Video.file_size)).where(Video.status == "done"))).scalar() or 0 return StorageStats(total_videos=total, total_size=total_size, total_size_human=human_size(total_size)) @router.get("/download-logs", response_model=DownloadLogListResponse) async def download_logs( page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), video_id: int = Query(None), user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): from sqlalchemy.orm import joinedload query = ( select(DownloadLog) .options(joinedload(DownloadLog.video)) .order_by(DownloadLog.downloaded_at.desc()) ) count_query = select(func.count(DownloadLog.id)) if video_id is not None: query = query.where(DownloadLog.video_id == video_id) count_query = count_query.where(DownloadLog.video_id == video_id) total = (await db.execute(count_query)).scalar() or 0 logs = (await db.execute(query.offset((page - 1) * page_size).limit(page_size))).scalars().all() items = [] for l in logs: d = DownloadLogInfo.model_validate(l) if l.video: d.video_title = l.video.title or "" d.video_platform = l.video.platform or "" items.append(d) return DownloadLogListResponse(logs=items, total=total, page=page, page_size=page_size) @router.get("/settings/cleanup", response_model=CleanupStatus) async def get_cleanup_settings(user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)): video_base = os.getenv("VIDEO_BASE_PATH", "/home/xdl/xdl_videos") last_result_raw = await get_setting(db, "cleanup_last_result", "{}") try: last_result = json.loads(last_result_raw) if last_result_raw else {} except Exception: last_result = {} return CleanupStatus( config=CleanupConfig( enabled=(await get_setting(db, "cleanup_enabled", "true")) == "true", retention_minutes=int(await get_setting(db, "cleanup_retention_minutes", "10080")), storage_limit_pct=int(await get_setting(db, "cleanup_storage_limit_pct", "80")), ), disk=DiskStats(**disk_stats(video_base)), last_run=await get_setting(db, "cleanup_last_run", ""), last_result=last_result, ) @router.put("/settings/cleanup", response_model=CleanupStatus) async def update_cleanup_settings(cfg: CleanupConfig, user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)): await set_setting(db, "cleanup_enabled", "true" if cfg.enabled else "false") await set_setting(db, "cleanup_retention_minutes", str(cfg.retention_minutes)) await set_setting(db, "cleanup_storage_limit_pct", str(cfg.storage_limit_pct)) return await get_cleanup_settings(user=user, db=db) @router.post("/cleanup/run") async def trigger_cleanup(user: dict = Depends(get_current_user)): result = await run_cleanup() return result