feat(plex): S5a HU-overlay backend — schema, guid, overlay sync

Adds the storage + sync for the Hungarian-metadata overlay (the browsable EN sections gain HU
twin fields written from a paired HU section, matched by guid; no browsable HU rows).

Schema (2 migrations, ADD-only; run before the first HU sync while *_hu is NULL so the generated
column build is trivial):
- 0063: plex_libraries.metadata_lang ('en' default) + self-FK overlay_of.
- 0064: plex_items/plex_shows.guid (indexed) + title_hu/summary_hu/genres_hu (+ GIN) + a STORED
  generated search_vector_hu mirroring the EN vector (title_hu A / people_text B / summary_hu C) + GIN.

Sync:
- includeGuids=1 on section_items so the canonical guid rides the cheap listing.
- _canonical_guid(meta) (tmdb > imdb > tvdb) captured onto every EN row in _apply_item/_sync_shows.
- Two-pass sync(): pass 1 mirrors browsable (non-overlay) sections; pass 2 reads plex_overlay_map
  ('huKey:enKey', new sysconfig), designates each HU section (metadata_lang='hu' + overlay_of), and
  _sync_overlay writes title_hu/summary_hu/genres_hu onto the guid-matched EN rows.

Browse: _content_libs now excludes overlay libraries (overlay_of is None); the unified_library and
facets inline library partitions fold into it, and list_libraries/collections exclude overlays too.

Tests: DB-lane coverage of the overlay write (matched vs unmatched rows, no browsable HU rows, the
generated search_vector_hu populates), the guid parse, and the browse exclusion. Full suite 272
passed; migrations verified reversible (upgrade→downgrade→upgrade). S5b (persist the live detail
fields + serve item/show detail from DB) is the follow-up.
This commit is contained in:
2026-08-16 22:16:54 +02:00
parent 072d8fc88f
commit c959859d29
9 changed files with 378 additions and 27 deletions
+18 -23
View File
@@ -82,9 +82,12 @@ def _field_recheck(model, field: str, tsq):
def list_libraries(
user: User = Depends(current_user), db: Session = Depends(get_db)
) -> dict:
"""The mirrored libraries the user can browse (drives the Plex scope selector)."""
"""The mirrored libraries the user can browse (drives the Plex scope selector). HU overlay
libraries are excluded — they're not browsable, they only feed the EN twin's Hungarian metadata."""
libs = db.execute(
select(PlexLibrary).order_by(PlexLibrary.kind.desc(), PlexLibrary.title)
select(PlexLibrary)
.where(PlexLibrary.overlay_of.is_(None))
.order_by(PlexLibrary.kind.desc(), PlexLibrary.title)
).scalars().all()
out = []
for lib in libs:
@@ -291,11 +294,7 @@ def unified_library(
episodes come back in a separate `episodes` list (grouped result — the 'Episodes' section)."""
offset = max(0, offset)
p = f.as_dict()
libs = db.execute(
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
).scalars().all()
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]
movie_lib_ids, show_lib_ids = _content_libs(db)
want_movies = scope in ("movie", "both") and bool(movie_lib_ids)
want_shows = scope in ("show", "both") and bool(show_lib_ids)
@@ -531,17 +530,9 @@ def facets(
"duration_min": None,
"duration_max": None,
}
libs = db.execute(
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
).scalars().all()
movie_ids = (
[lb.id for lb in libs if lb.kind == "movie"]
if scope in ("movie", "both")
else []
)
show_ids = (
[lb.id for lb in libs if lb.kind == "show"] if scope in ("show", "both") else []
)
all_movie_ids, all_show_ids = _content_libs(db)
movie_ids = all_movie_ids if scope in ("movie", "both") else []
show_ids = all_show_ids if scope in ("show", "both") else []
if not movie_ids and not show_ids:
return empty
p = f.as_dict()
@@ -758,7 +749,9 @@ def collections(
else:
enabled_ids = list(
db.execute(
select(PlexLibrary.id).where(PlexLibrary.enabled.is_(True))
select(PlexLibrary.id).where(
PlexLibrary.enabled.is_(True), PlexLibrary.overlay_of.is_(None)
)
).scalars()
)
if not enabled_ids:
@@ -1545,11 +1538,13 @@ def _rich_meta(meta: dict) -> dict:
def _content_libs(db: Session) -> tuple[list[int], list[int]]:
"""The enabled content libraries, partitioned into (movie_lib_ids, show_lib_ids) — the single
source of the browsable-library scope shared by people_cards and the person filmography endpoint.
(S5 will extend this to exclude HU overlay libraries and fold in the browse/facets partition sites.)"""
"""The BROWSABLE content libraries, partitioned into (movie_lib_ids, show_lib_ids) — the single
source of the browse scope. Excludes HU overlay libraries (`overlay_of` set): those are never
browsed on their own, they only lend their Hungarian metadata to the EN twin's rows (E5 S5)."""
libs = db.execute(
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
select(PlexLibrary).where(
PlexLibrary.enabled.is_(True), PlexLibrary.overlay_of.is_(None)
)
).scalars().all()
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]