diff --git a/backend/app/routes/plex/collections.py b/backend/app/routes/plex/collections.py index 45064772..e7850720 100644 --- a/backend/app/routes/plex/collections.py +++ b/backend/app/routes/plex/collections.py @@ -11,6 +11,7 @@ from app.models import ( PlexCollection, PlexItem, PlexLibrary, + PlexShow, User, ) from app.plex import sync as plex_sync @@ -95,17 +96,25 @@ def collections( func.lower(PlexCollection.title), ) ).scalars().all() - # A montage of the top films per tile + the collection's total runtime (color hash + no-runtime is - # the client fallback for show-only / empty collections). Two grouped queries over the movie libs. - movie_ids, _show_ids = _content_libs(db) + # A montage of the top titles per tile + the collection's total runtime. Art unions MOVIE and SHOW + # 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) keys = [c.rating_key for c in rows] - art = top_art_keys(db, PlexItem, movie_ids, PlexItem.collection_keys, keys, per=2) + 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) runtime = runtime_by_key(db, PlexItem, movie_ids, PlexItem.collection_keys, keys) + + def _art(c: PlexCollection) -> list[str]: + member = art_m.get(c.rating_key) or art_s.get(c.rating_key) + return member or ([c.rating_key] if c.thumb_key else []) + return { "collections": [ { **_collection_card(c), - "art": art.get(c.rating_key, []), + "art": _art(c), "total_runtime_s": runtime.get(c.rating_key, 0), } for c in rows diff --git a/backend/tests/test_plex_collections_filter.py b/backend/tests/test_plex_collections_filter.py index 8e5d4d89..de4e5496 100644 --- a/backend/tests/test_plex_collections_filter.py +++ b/backend/tests/test_plex_collections_filter.py @@ -10,7 +10,7 @@ import app.state as state from app.auth import current_user from app.db import get_db from app.main import app -from app.models import PlexCollection, PlexItem, PlexLibrary, User +from app.models import PlexCollection, PlexItem, PlexLibrary, PlexShow, User def _seed(db): @@ -92,3 +92,25 @@ def test_collection_cards_carry_montage_art_and_total_runtime(db, monkeypatch): if c["id"] == "A") assert card["art"] == ["m2", "m1"] # best-rated first (m2 9.0 > m1 8.0) assert card["total_runtime_s"] == 5400 # 3600 + 1800 + + +def test_show_collection_gets_montage_art_from_shows(db, monkeypatch): + # A show-library collection must get its montage from PlexShow members (else it renders blank) — the + # A3 fix that unions movie + show art. Runtime stays 0 (shows have no single duration). + lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True) + db.add(lib) + db.flush() + db.add(PlexCollection(rating_key="S", library_id=lib.id, title="Show Coll", child_count=2)) + db.add_all([ + PlexShow(rating_key="s1", library_id=lib.id, title="S1", collection_keys=["S"], rating=7.0, + added_at=datetime(2024, 1, 1, tzinfo=timezone.utc)), + PlexShow(rating_key="s2", library_id=lib.id, title="S2", collection_keys=["S"], rating=9.0, + added_at=datetime(2024, 1, 2, tzinfo=timezone.utc)), + ]) + user = User(email="showcoll@example.com") + db.add(user) + db.commit() + card = next(c for c in _get(db, user, monkeypatch, "/api/plex/collections").json()["collections"] + if c["id"] == "S") + assert card["art"] == ["s2", "s1"] # show members, best-rated first + assert card["total_runtime_s"] == 0