Files
media-map/backend/app/api/watched.py
Danilo Reyes c0371d85ce Refactor database connection handling in API endpoints
- Removed direct pool checks and replaced them with a centralized database initialization method in `init_db`.
- Updated API endpoints in `admin.py`, `collection.py`, `pins.py`, and `watched.py` to ensure the database connection pool is initialized before usage.
- Enhanced error handling to raise HTTP exceptions if the database is unavailable.
- Improved the `init_db` function in `database.py` to prevent multiple simultaneous initializations using an asyncio lock.
2025-12-28 21:37:31 -06:00

204 lines
6.7 KiB
Python

"""Watched items API endpoints"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
from uuid import UUID
from app.core.database import init_db, pool as db_pool
import json
router = APIRouter()
class WatchedItemCreate(BaseModel):
media_type: str # "movie" or "show"
title: str
year: Optional[int] = None
country_code: str
watched_at: Optional[datetime] = None
notes: Optional[str] = None
class WatchedItemUpdate(BaseModel):
title: Optional[str] = None
year: Optional[int] = None
country_code: Optional[str] = None
watched_at: Optional[datetime] = None
notes: Optional[str] = None
@router.get("/summary")
async def get_watched_summary():
"""Get watched items summary by country"""
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:
query = """
SELECT
country_code,
media_type,
COUNT(*) as count
FROM moviemap.watched_item
WHERE watched_at IS NOT NULL
GROUP BY country_code, media_type
ORDER BY country_code, media_type
"""
await cur.execute(query)
rows = await cur.fetchall()
result = {}
for row in rows:
country_code, media_type, count = row
if country_code not in result:
result[country_code] = {}
result[country_code][media_type] = count
return result
@router.get("")
async def list_watched_items():
"""List all watched items"""
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:
query = """
SELECT
id, media_type, title, year, country_code,
watched_at, notes, created_at, updated_at
FROM moviemap.watched_item
ORDER BY created_at DESC
"""
await cur.execute(query)
rows = await cur.fetchall()
items = []
for row in rows:
items.append({
"id": str(row[0]),
"media_type": row[1],
"title": row[2],
"year": row[3],
"country_code": row[4],
"watched_at": row[5].isoformat() if row[5] else None,
"notes": row[6],
"created_at": row[7].isoformat() if row[7] else None,
"updated_at": row[8].isoformat() if row[8] else None,
})
return items
@router.post("")
async def create_watched_item(item: WatchedItemCreate):
"""Create a new watched item"""
await init_db()
if db_pool is None:
raise HTTPException(status_code=503, detail="Database not available")
if item.media_type not in ["movie", "show"]:
raise HTTPException(status_code=400, detail="media_type must be 'movie' or 'show'")
async with db_pool.connection() as conn:
async with conn.cursor() as cur:
query = """
INSERT INTO moviemap.watched_item
(media_type, title, year, country_code, watched_at, notes)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
"""
await cur.execute(
query,
(
item.media_type,
item.title,
item.year,
item.country_code,
item.watched_at,
item.notes,
)
)
result = await cur.fetchone()
await conn.commit()
return {"id": str(result[0]), "status": "created"}
@router.patch("/{item_id}")
async def update_watched_item(item_id: UUID, item: WatchedItemUpdate):
"""Update a watched item"""
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:
# Build dynamic update query
updates = []
params = []
if item.title is not None:
updates.append("title = %s")
params.append(item.title)
if item.year is not None:
updates.append("year = %s")
params.append(item.year)
if item.country_code is not None:
updates.append("country_code = %s")
params.append(item.country_code)
if item.watched_at is not None:
updates.append("watched_at = %s")
params.append(item.watched_at)
if item.notes is not None:
updates.append("notes = %s")
params.append(item.notes)
if not updates:
raise HTTPException(status_code=400, detail="No fields to update")
updates.append("updated_at = NOW()")
params.append(str(item_id))
query = f"""
UPDATE moviemap.watched_item
SET {', '.join(updates)}
WHERE id = %s
RETURNING id
"""
await cur.execute(query, params)
result = await cur.fetchone()
await conn.commit()
if not result:
raise HTTPException(status_code=404, detail="Watched item not found")
return {"id": str(result[0]), "status": "updated"}
@router.delete("/{item_id}")
async def delete_watched_item(item_id: UUID):
"""Delete a watched item"""
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:
query = "DELETE FROM moviemap.watched_item WHERE id = %s RETURNING id"
await cur.execute(query, (str(item_id),))
result = await cur.fetchone()
await conn.commit()
if not result:
raise HTTPException(status_code=404, detail="Watched item not found")
return {"id": str(result[0]), "status": "deleted"}