diff --git a/backend/app/plex/query.py b/backend/app/plex/query.py index 19f21885..c3fd6f7e 100644 --- a/backend/app/plex/query.py +++ b/backend/app/plex/query.py @@ -8,8 +8,9 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import and_, case, func, or_, select, type_coerce from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Session -from app.models import PlexItem +from app.models import PlexItem, PlexLibrary TS_CONFIG = "public.unaccent_simple" @@ -342,3 +343,26 @@ def apply_show_state_filter(query, tot, wat, inp, show: str): if show == "unwatched": return query.where(wat == 0, inp == 0) return query + + +def content_libs(db: Session) -> tuple[list[int], list[int]]: + """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). + + Lives here, in the query layer, rather than in `routes/plex/_shared.py` where it started: E24's + Home surface needs the same scope, and a domain package importing from `routes/` would invert the + one-way dependency CLAUDE.md draws. It never touched request state, so nothing was lost by the + move.""" + libs = ( + db.execute( + 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"] + return movie_lib_ids, show_lib_ids diff --git a/backend/app/routes/plex/_shared.py b/backend/app/routes/plex/_shared.py index 0e50040b..2a193eab 100644 --- a/backend/app/routes/plex/_shared.py +++ b/backend/app/routes/plex/_shared.py @@ -3,13 +3,9 @@ import logging from fastapi import HTTPException -from sqlalchemy import select from sqlalchemy.orm import Session -from app.models import ( - PlexItem, - PlexLibrary, -) +from app.models import PlexItem from app.plex.lookups import by_rating_key log = logging.getLogger("siftlode.plex") @@ -25,21 +21,3 @@ def item_or_404(db: Session, rating_key: str) -> PlexItem: def _norm_lang(lang: str | None) -> str: """The overlay has an EN base + an optional HU twin; anything other than `hu` serves EN.""" return "hu" if lang == "hu" else "en" - - -def _content_libs(db: Session) -> tuple[list[int], list[int]]: - """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), 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"] - return movie_lib_ids, show_lib_ids diff --git a/backend/app/routes/plex/browse.py b/backend/app/routes/plex/browse.py index 0ff9dff8..5cdda59a 100644 --- a/backend/app/routes/plex/browse.py +++ b/backend/app/routes/plex/browse.py @@ -34,6 +34,7 @@ from app.plex.query import ( apply_meta_filters, apply_movie_state_filter, apply_show_state_filter, + content_libs, field_recheck, genres_col, meta_filter_conds, @@ -47,7 +48,7 @@ from app.plex.query import ( vector_col, vector_match, ) -from app.routes.plex._shared import _content_libs, _norm_lang +from app.routes.plex._shared import _norm_lang from app.utils import to_tsquery_str router = APIRouter(prefix="/api/plex", tags=["plex"]) @@ -109,7 +110,7 @@ def unified_library( offset = max(0, offset) lang = _norm_lang(lang) p = f.as_dict() - movie_lib_ids, show_lib_ids = _content_libs(db) + 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) @@ -328,7 +329,7 @@ def facets( "duration_min": None, "duration_max": None, } - all_movie_ids, all_show_ids = _content_libs(db) + 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: diff --git a/backend/app/routes/plex/collections.py b/backend/app/routes/plex/collections.py index 308ccb68..862f5f9a 100644 --- a/backend/app/routes/plex/collections.py +++ b/backend/app/routes/plex/collections.py @@ -19,8 +19,7 @@ from app.plex import sync as plex_sync from app.plex.cards import movie_card, show_card from app.plex.client import PlexClient, PlexError, PlexNotConfigured from app.plex.lookups import by_rating_key -from app.plex.query import like_escape, runtime_by_key, top_art_keys -from app.routes.plex._shared import _content_libs +from app.plex.query import content_libs, like_escape, runtime_by_key, top_art_keys # The most-restrictive → least ordering used to pick a collection's effective content rating from its # members (Plex shows one cert on the collection; we don't store it, so derive it). @@ -128,7 +127,7 @@ def collections( # members (a collection lives in one library, so exactly one side has art) — a show collection would # otherwise render blank; the collection's own poster is the next fallback, then the colour hash. # Runtime is movie-only (shows have no single duration). - movie_ids, show_ids = _content_libs(db) + movie_ids, show_ids = content_libs(db) keys = [c.rating_key for c in rows] art_m = top_art_keys(db, PlexItem, movie_ids, PlexItem.collection_keys, keys, per=2) art_s = top_art_keys(db, PlexShow, show_ids, PlexShow.collection_keys, keys, per=2) diff --git a/backend/app/routes/plex/people.py b/backend/app/routes/plex/people.py index 9c1e3db8..ccef7c92 100644 --- a/backend/app/routes/plex/people.py +++ b/backend/app/routes/plex/people.py @@ -19,8 +19,7 @@ from app.models import ( from app.plex import rollup from app.plex import tmdb as plex_tmdb from app.plex.cards import movie_card, show_card -from app.plex.query import split_csv -from app.routes.plex._shared import _content_libs +from app.plex.query import content_libs, split_csv router = APIRouter(prefix="/api/plex", tags=["plex"]) log = logging.getLogger("siftlode.plex") @@ -71,7 +70,7 @@ def people_cards( wanted = split_csv(names)[:16] if not wanted: return {"people": []} - movie_lib_ids, show_lib_ids = _content_libs(db) + 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) photos = { @@ -185,7 +184,7 @@ def person_detail( and _should_schedule_enrich(person.id) ): background_tasks.add_task(_enrich_person_bg, person.id) - movie_lib_ids, show_lib_ids = _content_libs(db) + movie_lib_ids, show_lib_ids = content_libs(db) movies: list[dict] = [] if movie_lib_ids: diff --git a/backend/tests/test_plex_overlay.py b/backend/tests/test_plex_overlay.py index 3fa4c0c6..5b30d00b 100644 --- a/backend/tests/test_plex_overlay.py +++ b/backend/tests/test_plex_overlay.py @@ -12,7 +12,7 @@ import app.plex.sync as syncmod from app import sysconfig from app.models import PlexItem, PlexLibrary from app.plex.sync import _apply_item, _canonical_guid, _sync_overlay -from app.routes.plex._shared import _content_libs +from app.plex.query import content_libs class _StubPlex: @@ -155,7 +155,7 @@ def test_sync_designates_overlay_and_writes_hu(db, monkeypatch): assert m.title_hu == "Öldöklés" assert m.genres_hu == ["Akció"] # The HU section is not browsable and mirrored no rows of its own. - movie_ids, _ = _content_libs(db) + movie_ids, _ = content_libs(db) assert en.id in movie_ids and hu.id not in movie_ids assert db.scalar(select(func.count(PlexItem.id)).where(PlexItem.library_id == hu.id)) == 0 assert stats["overlaid"] == 1 @@ -240,7 +240,7 @@ def test_content_libs_excludes_overlay_libraries(db): db.add(overlay) db.commit() - movie_ids, show_ids = _content_libs(db) + 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] diff --git a/backend/tests/test_plex_person.py b/backend/tests/test_plex_person.py index 5daa5809..760aac7c 100644 --- a/backend/tests/test_plex_person.py +++ b/backend/tests/test_plex_person.py @@ -1,6 +1,6 @@ """DB-backed regression (E5 S4): GET /api/plex/person/{name} returns a cast/crew member's filmography — movies + shows in the ENABLED libraries whose cast OR directors include the name, -each carrying the caller's watch-state, plus the cached headshot. Also covers `_content_libs` +each carrying the caller's watch-state, plus the cached headshot. Also covers `content_libs` scoping (disabled libraries excluded) and the kind filter (episodes are not movies). Uses the DB lane (`db` fixture) — skipped when no Postgres is reachable. @@ -20,7 +20,7 @@ from app.models import ( PlexState, User, ) -from app.routes.plex._shared import _content_libs +from app.plex.query import content_libs NAME = "Robert De Niro" @@ -151,5 +151,5 @@ def test_content_libs_partitions_only_enabled(db): ] ) db.commit() - movie_ids, show_ids = _content_libs(db) + movie_ids, show_ids = content_libs(db) assert len(movie_ids) == 1 and len(show_ids) == 1 # the disabled movie lib is excluded