"""Stable Jellyfin identities for private, user-scoped integrations.""" import hashlib from contextlib import closing from .. import db def source_key(base_url: str | None) -> str: return hashlib.sha256(str(base_url or "").strip().rstrip("/").encode()).hexdigest() def linked_user_id(username: str, base_url: str | None) -> str | None: user = db.get_user_by_username(username) if not user or not base_url: return None with closing(db._connect()) as conn, conn: row = conn.execute( "SELECT jellyfin_user_id FROM jellyfin_user_links WHERE source = ? AND local_user_id = ?", (source_key(base_url), user["id"]), ).fetchone() return row[0] if row else None def link_user(username: str, jellyfin_user_id: str, base_url: str | None) -> None: """Use only verified login or canonical Jellyfin user sync, never playback names.""" user = db.get_user_by_username(username) if not user or not jellyfin_user_id or not base_url: return with closing(db._connect()) as conn, conn: if conn.execute("SELECT 1 FROM user_identity_confirmations WHERE local_user_id = ?", (user["id"],)).fetchone(): # Reviewed identities are updated only through the admin confirmation workflow. return # A renamed or re-created account must not silently take over an existing identity. conn.execute( "INSERT OR IGNORE INTO jellyfin_user_links (source, local_user_id, jellyfin_user_id) VALUES (?, ?, ?)", (source_key(base_url), user["id"], str(jellyfin_user_id)), ) def user_for_identity(jellyfin_user_id: str, base_url: str | None): """Resolve a verified upstream login to its existing local account.""" if not jellyfin_user_id or not base_url: return None with closing(db._connect()) as conn: rows = conn.execute("SELECT local_user_id FROM jellyfin_user_links WHERE source=? AND lower(replace(jellyfin_user_id,'-',''))=?", (source_key(base_url), str(jellyfin_user_id).replace('-', '').lower())).fetchall() if len(rows) > 1: from fastapi import HTTPException raise HTTPException(409, 'Multiple accounts claim this Jellyfin ID. Ask an administrator to repair the links.') return db.get_user_by_id(rows[0][0]) if rows else None