feat(plex): prune orphaned people after each sync (S4)
The plex_people headshot cache can outlive its subjects — when a title leaves the Plex library, cast/crew who appear in nothing else are orphaned. prune_orphan_people deletes rows whose name no longer appears in ANY movie/show cast_names/directors, run at the end of every plex_sync (best-effort; a jsonb_typeof='array' guard skips scalar/null JSONB values). The filterable cast_names self-clean with their row; this cleans the only per-person cache. Verified: an injected orphan is dropped, real people kept. (Plex cast-explore S4)
This commit is contained in:
@@ -12,6 +12,7 @@ Cast + intro/credit markers are fetched lazily on the item-detail/player path (P
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import sysconfig
|
from app import sysconfig
|
||||||
@@ -135,10 +136,36 @@ def sync(db: Session) -> dict:
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
log.warning("Plex sync failed: %s", e)
|
log.warning("Plex sync failed: %s", e)
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
# Prune the headshot cache: a reconcile may have removed titles, orphaning people who no longer
|
||||||
|
# appear in ANY item's cast/directors — drop those rows, mirroring how Plex drops orphaned metadata.
|
||||||
|
try:
|
||||||
|
stats["people_pruned"] = prune_orphan_people(db)
|
||||||
|
except Exception as e: # best-effort housekeeping — never fail the sync over it
|
||||||
|
log.warning("Plex people prune failed: %s", e)
|
||||||
log.info("Plex sync done: %s", stats)
|
log.info("Plex sync done: %s", stats)
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# The person names still referenced by ANY movie/show (cast OR directors). plex_people rows outside
|
||||||
|
# this set are orphans left by a library removal.
|
||||||
|
_REFERENCED_PEOPLE_SQL = """
|
||||||
|
SELECT jsonb_array_elements_text(cast_names) FROM plex_items WHERE jsonb_typeof(cast_names) = 'array'
|
||||||
|
UNION SELECT jsonb_array_elements_text(directors) FROM plex_items WHERE jsonb_typeof(directors) = 'array'
|
||||||
|
UNION SELECT jsonb_array_elements_text(cast_names) FROM plex_shows WHERE jsonb_typeof(cast_names) = 'array'
|
||||||
|
UNION SELECT jsonb_array_elements_text(directors) FROM plex_shows WHERE jsonb_typeof(directors) = 'array'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def prune_orphan_people(db: Session) -> int:
|
||||||
|
"""Delete plex_people (headshot cache) rows whose name no longer appears in any movie/show cast or
|
||||||
|
directors — orphaned by a library removal. Returns the number pruned. Commits its own work."""
|
||||||
|
res = db.execute(
|
||||||
|
text(f"DELETE FROM plex_people WHERE name NOT IN ({_REFERENCED_PEOPLE_SQL})")
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return res.rowcount or 0
|
||||||
|
|
||||||
|
|
||||||
def _upsert_library(db: Session, key: str, title: str, kind: str) -> PlexLibrary:
|
def _upsert_library(db: Session, key: str, title: str, kind: str) -> PlexLibrary:
|
||||||
lib = db.query(PlexLibrary).filter_by(plex_key=key).first()
|
lib = db.query(PlexLibrary).filter_by(plex_key=key).first()
|
||||||
if lib is None:
|
if lib is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user