"""Admin API endpoints""" from fastapi import APIRouter, HTTPException, Header, Query from typing import Optional, List from app.core.config import settings from app.core.database import init_db, pool as db_pool from app.services.sync import sync_all_arrs router = APIRouter() async def verify_admin_token(authorization: Optional[str] = Header(None)): """Verify admin token if configured""" if settings.admin_token: if not authorization or authorization != f"Bearer {settings.admin_token}": raise HTTPException(status_code=401, detail="Unauthorized") # If no admin token configured, allow (assuming localhost-only access) @router.post("/sync") async def trigger_sync(authorization: Optional[str] = Header(None)): """ Trigger sync from all *arr instances. Requires admin token if MOVIEMAP_ADMIN_TOKEN is set. """ await verify_admin_token(authorization) try: result = await sync_all_arrs() return { "status": "success", "synced": result } except Exception as e: raise HTTPException(status_code=500, detail=f"Sync failed: {str(e)}") @router.get("/missing-countries") async def get_missing_countries( authorization: Optional[str] = Header(None), source_kind: Optional[str] = Query(None, description="Filter by source: radarr, sonarr, lidarr"), media_type: Optional[str] = Query(None, description="Filter by media type: movie, show, music"), limit: int = Query(100, ge=1, le=1000) ): """ Get list of media items without country metadata. Requires admin token if MOVIEMAP_ADMIN_TOKEN is set. """ await verify_admin_token(authorization) await init_db() if db_pool is None: raise HTTPException(status_code=503, detail="Database not available") async with db_pool.connection() as conn: async with conn.cursor() as cur: # Get total count count_query = """ SELECT COUNT(DISTINCT mi.id) FROM moviemap.media_item mi LEFT JOIN moviemap.media_country mc ON mi.id = mc.media_item_id WHERE mc.media_item_id IS NULL """ count_params = [] if source_kind: count_query += " AND mi.source_kind = %s" count_params.append(source_kind) if media_type: count_query += " AND mi.media_type = %s" count_params.append(media_type) await cur.execute(count_query, count_params if count_params else None) total_count = (await cur.fetchone())[0] # Get items query = """ SELECT mi.id, mi.source_kind, mi.source_item_id, mi.title, mi.year, mi.media_type FROM moviemap.media_item mi LEFT JOIN moviemap.media_country mc ON mi.id = mc.media_item_id WHERE mc.media_item_id IS NULL """ params = [] if source_kind: query += " AND mi.source_kind = %s" params.append(source_kind) if media_type: query += " AND mi.media_type = %s" params.append(media_type) query += " ORDER BY mi.title LIMIT %s" params.append(limit) await cur.execute(query, params) rows = await cur.fetchall() items = [] for row in rows: items.append({ "id": str(row[0]), "source_kind": row[1], "source_item_id": row[2], "title": row[3], "year": row[4], "media_type": row[5], }) return { "total": total_count, "returned": len(items), "items": items } @router.post("/assign-country") async def assign_country_manually( item_id: str, country_code: str, authorization: Optional[str] = Header(None) ): """ Manually assign a country code to a media item. Requires admin token if MOVIEMAP_ADMIN_TOKEN is set. """ await verify_admin_token(authorization) await init_db() if db_pool is None: raise HTTPException(status_code=503, detail="Database not available") # Validate country code (should be 2 letters) if len(country_code) != 2 or not country_code.isalpha(): raise HTTPException(status_code=400, detail="Country code must be 2 letters (ISO 3166-1 alpha-2)") country_code = country_code.upper() async with db_pool.connection() as conn: async with conn.cursor() as cur: # Check if item exists await cur.execute("SELECT id FROM moviemap.media_item WHERE id = %s", (item_id,)) if not await cur.fetchone(): raise HTTPException(status_code=404, detail="Media item not found") # Insert or update country association await cur.execute( """ INSERT INTO moviemap.media_country (media_item_id, country_code) VALUES (%s, %s) ON CONFLICT (media_item_id, country_code) DO NOTHING """, (item_id, country_code) ) await conn.commit() return { "status": "success", "item_id": item_id, "country_code": country_code }