- 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.
165 lines
5.6 KiB
Python
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
|
|
}
|
|
|