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.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""Admin API endpoints"""
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
from typing import Optional
|
||||
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()
|
||||
@@ -32,3 +33,132 @@ async def trigger_sync(authorization: Optional[str] = Header(None)):
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user