import asyncio import json import logging from datetime import datetime, timezone from app.config import settings from app.database import get_db from app.services.jellyfin import get_all_movies, get_played_movie_ids, get_users logger = logging.getLogger("movie-night.sync") async def sync_movie_metadata(): """Full sync of movie metadata from Jellyfin.""" logger.info("Starting movie metadata sync...") users = await get_users() if not users: logger.warning("No Jellyfin users found, skipping sync") return # Use first user to fetch library (all users see the same movies) user_id = users[0]["id"] movies = await get_all_movies(user_id) logger.info(f"Fetched {len(movies)} movies from Jellyfin") db = await get_db() try: now = datetime.now(timezone.utc).isoformat() for movie in movies: await db.execute( """INSERT OR REPLACE INTO movies (jellyfin_id, title, sort_title, year, genres, overview, community_rating, critic_rating, runtime_minutes, content_rating, studios, people, tags, synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( movie["jellyfin_id"], movie["title"], movie["sort_title"], movie["year"], json.dumps(movie["genres"]), movie["overview"], movie["community_rating"], movie["critic_rating"], movie["runtime_minutes"], movie["content_rating"], json.dumps(movie["studios"]), json.dumps(movie["people"]), json.dumps(movie["tags"]), now, ), ) # Update sync timestamp await db.execute( "INSERT OR REPLACE INTO sync_status (key, value) VALUES ('last_metadata_sync', ?)", (now,), ) await db.commit() logger.info(f"Movie metadata sync complete: {len(movies)} movies") finally: await db.close() async def sync_watch_state(): """Sync watch state for all users.""" logger.info("Starting watch state sync...") users = await get_users() db = await get_db() try: now = datetime.now(timezone.utc).isoformat() for user in users: played_ids = await get_played_movie_ids(user["id"]) logger.info(f"User {user['name']}: {len(played_ids)} played movies") # Get all movie IDs cursor = await db.execute("SELECT jellyfin_id FROM movies") all_movie_ids = {row["jellyfin_id"] for row in await cursor.fetchall()} for movie_id in all_movie_ids: is_played = 1 if movie_id in played_ids else 0 await db.execute( """INSERT OR REPLACE INTO watch_state (jellyfin_id, user_id, is_played, synced_at) VALUES (?, ?, ?, ?)""", (movie_id, user["id"], is_played, now), ) await db.execute( "INSERT OR REPLACE INTO sync_status (key, value) VALUES ('last_watch_sync', ?)", (now,), ) await db.commit() logger.info("Watch state sync complete") finally: await db.close() async def needs_metadata_sync() -> bool: db = await get_db() try: cursor = await db.execute( "SELECT value FROM sync_status WHERE key = 'last_metadata_sync'" ) row = await cursor.fetchone() if not row: return True last_sync = datetime.fromisoformat(row["value"]) age_hours = (datetime.now(timezone.utc) - last_sync).total_seconds() / 3600 return age_hours > settings.sync_interval_hours finally: await db.close() async def needs_watch_sync() -> bool: db = await get_db() try: cursor = await db.execute( "SELECT value FROM sync_status WHERE key = 'last_watch_sync'" ) row = await cursor.fetchone() if not row: return True last_sync = datetime.fromisoformat(row["value"]) age_hours = (datetime.now(timezone.utc) - last_sync).total_seconds() / 3600 return age_hours > settings.watch_state_sync_hours finally: await db.close() async def run_periodic_sync(): """Background task that periodically syncs library data.""" # Initial sync on startup try: if await needs_metadata_sync(): await sync_movie_metadata() if await needs_watch_sync(): await sync_watch_state() except Exception as e: logger.error(f"Initial sync failed: {e}") # Periodic sync loop while True: try: await asyncio.sleep(3600) # Check every hour if await needs_metadata_sync(): await sync_movie_metadata() if await needs_watch_sync(): await sync_watch_state() except asyncio.CancelledError: break except Exception as e: logger.error(f"Periodic sync failed: {e}")