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.
100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
"""DB-backed regression (E5 S5a): the HU-overlay sync writes Hungarian metadata onto the matched EN
|
|
rows (paired by guid), never creating browsable HU rows, and overlay libraries are excluded from the
|
|
browse scope. Also covers the canonical-guid parse and that the generated search_vector_hu populates.
|
|
|
|
Uses the DB lane (`db` fixture) — skipped when no Postgres is reachable. Runs against the real schema
|
|
(migrations 0063/0064) so the JSONB + STORED generated columns are exercised.
|
|
"""
|
|
from sqlalchemy import func, select
|
|
|
|
from app.models import PlexItem, PlexLibrary
|
|
from app.plex.sync import _canonical_guid, _sync_overlay
|
|
from app.routes.plex.browse import _content_libs
|
|
|
|
|
|
class _StubPlex:
|
|
"""Minimal PlexClient stand-in: _sync_overlay only calls section_items via _paginate."""
|
|
|
|
def __init__(self, items: list[dict]):
|
|
self._items = items
|
|
|
|
def section_items(self, key, item_type, start, size):
|
|
return self._items[start : start + size], len(self._items)
|
|
|
|
|
|
def _hu_movie(rk: str, title: str, summary: str, genres: list[str], guid: str | None) -> dict:
|
|
meta = {"ratingKey": rk, "title": title, "summary": summary, "Genre": [{"tag": g} for g in genres]}
|
|
if guid:
|
|
meta["Guid"] = [{"id": guid}]
|
|
return meta
|
|
|
|
|
|
def test_canonical_guid_prefers_tmdb_then_imdb():
|
|
assert _canonical_guid({"Guid": [{"id": "imdb://tt1"}, {"id": "tmdb://55"}]}) == "tmdb:55"
|
|
assert _canonical_guid({"Guid": [{"id": "imdb://tt9"}, {"id": "tvdb://7"}]}) == "imdb:tt9"
|
|
assert _canonical_guid({"Guid": []}) is None
|
|
assert _canonical_guid({}) is None
|
|
|
|
|
|
def test_sync_overlay_writes_hu_onto_matched_en_rows(db):
|
|
en_lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
|
|
hu_lib = PlexLibrary(plex_key="9", title="Filmek", kind="movie", metadata_lang="hu")
|
|
db.add_all([en_lib, hu_lib])
|
|
db.flush()
|
|
hu_lib.overlay_of = en_lib.id
|
|
matched = PlexItem(
|
|
rating_key="m1", library_id=en_lib.id, kind="movie", title="Heat", guid="tmdb:1"
|
|
)
|
|
unmatched = PlexItem(
|
|
rating_key="m2", library_id=en_lib.id, kind="movie", title="Solo", guid="tmdb:2"
|
|
)
|
|
db.add_all([matched, unmatched])
|
|
db.commit()
|
|
|
|
stub = _StubPlex(
|
|
[
|
|
_hu_movie("h1", "Öldöklés", "Egy rablás terve.", ["Akció", "Krimi"], "tmdb://1"),
|
|
_hu_movie("h2", "Ismeretlen", "Nincs EN párja.", ["Dráma"], "tmdb://999"),
|
|
]
|
|
)
|
|
stats = {"overlaid": 0}
|
|
_sync_overlay(db, stub, hu_lib, en_lib.id, stats)
|
|
db.commit()
|
|
|
|
db.refresh(matched)
|
|
db.refresh(unmatched)
|
|
assert matched.title_hu == "Öldöklés"
|
|
assert matched.summary_hu == "Egy rablás terve."
|
|
assert matched.genres_hu == ["Akció", "Krimi"]
|
|
assert matched.search_vector_hu is not None # the STORED generated column populated from title_hu
|
|
# The unmatched EN row keeps NULL HU fields (serving falls back to EN).
|
|
assert unmatched.title_hu is None
|
|
assert stats["overlaid"] == 1
|
|
# No browsable rows were created — the HU items never became plex_items of the overlay library.
|
|
hu_row_count = db.scalar(
|
|
select(func.count(PlexItem.id)).where(PlexItem.library_id == hu_lib.id)
|
|
)
|
|
assert hu_row_count == 0
|
|
# The EN library still holds exactly its two movies (no dup from the overlay pass).
|
|
en_row_count = db.scalar(
|
|
select(func.count(PlexItem.id)).where(PlexItem.library_id == en_lib.id)
|
|
)
|
|
assert en_row_count == 2
|
|
|
|
|
|
def test_content_libs_excludes_overlay_libraries(db):
|
|
en_lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
|
|
en_shows = PlexLibrary(plex_key="1", title="TV Shows", kind="show")
|
|
db.add_all([en_lib, en_shows])
|
|
db.flush()
|
|
overlay = PlexLibrary(
|
|
plex_key="9", title="Filmek", kind="movie", metadata_lang="hu", overlay_of=en_lib.id
|
|
)
|
|
db.add(overlay)
|
|
db.commit()
|
|
|
|
movie_ids, show_ids = _content_libs(db)
|
|
assert en_lib.id in movie_ids
|
|
assert overlay.id not in movie_ids # the HU overlay is never browsable
|
|
assert show_ids == [en_shows.id]
|