158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
|
|
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}")
|