Files
media-map/backend/app/api/admin.py
Danilo Reyes 335a53ee62 Add country metadata extraction and assignment features
- Updated `README.md` to include instructions for setting up the TMDB API key and new admin endpoints for managing country metadata.
- Implemented `/admin/missing-countries` endpoint to list media items without country metadata, with filtering options for source and media type.
- Added `/admin/assign-country` endpoint to manually assign a country code to a media item.
- Enhanced country extraction logic in `sync.py` to utilize TMDB and MusicBrainz APIs for automatic country retrieval based on available metadata.
- Updated configuration in `config.py` to include optional TMDB API key setting.
- Improved error handling and logging for country extraction failures.
- Ensured that country data is stored and utilized during media item synchronization across Radarr, Sonarr, and Lidarr.
2025-12-28 21:47:03 -06:00

165 lines
5.6 KiB
Python

"""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
}