Files
movie-night/app/services/library_sync.py
T

158 lines
5.2 KiB
Python
Raw Normal View History

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