feat(384): auto-tune DetailPanel content-source member read endpoint #397

Merged
timothy merged 2 commits from feat/384-smartcollection-members into main 2026-07-17 12:43:50 +02:00
Owner

Backend child of #383 (Auto-Tune DetailPanel milestone).

Adds GET /api/v1/channels/auto-tune/members?axis=&value=&pageNum=&pageSize= — lists the distinct content sources a proposed auto-tune channel's server-generated SmartCollection resolves to.

What

  • New MediatR query GetAutoTuneChannelMembers + handler; new ChannelController GET action.
  • Runs the server-owned AutoTuneAxisMap.GenerateQuery(axis,value) through ISearchIndex.Search (client never sends Lucene, per #69 PR1), then rolls matching leaf items up to distinct sources: parent shows for the TV axes (ItemCount = query-matching episodes, not total), movies for the movie-genre axis.
  • Reuses existing PagedLibraryBrowseItemsResponseModel / LibraryBrowseItemResponseModel DTOs (no new schema) + LibraryBrowseItemMapper.GetShows/GetMovies. Title-ordered, paged, clamped (pageNum>=0, pageSize default 100 / max 200).

Why search-index, not EF

The built channel's playout comes from the SmartCollection via ISearchIndex (GetSmartCollectionItems); the DetailPanel preview must run the same query through the same index to be faithful. #69's EF preview enumeration is a different granularity (candidate axis values). See docs/decisions.md 2026-07-17 (#384).

Tests / verification

  • 9 new tests (handler: query-gen, blank-value short-circuit, episode->distinct-show roll-up with matching-count, paging, movie axis, empty; controller: route literal, mapping, paging clamp). Local pass green (140 in touched namespaces).
  • OpenAPI + endpoint-index regenerated; npm run check:api green (d.ts unchanged — reuses existing schema).

Docs

api-conventions §5 (endpoint inventory), decisions.md 2026-07-17, domain-model.

Read-only, catalog-read tier -> cold independent review acceptable per the skip rubric.

fixes #384

Backend child of #383 (Auto-Tune DetailPanel milestone). Adds `GET /api/v1/channels/auto-tune/members?axis=&value=&pageNum=&pageSize=` — lists the distinct content sources a proposed auto-tune channel's server-generated SmartCollection resolves to. ## What - New MediatR query `GetAutoTuneChannelMembers` + handler; new `ChannelController` GET action. - Runs the server-owned `AutoTuneAxisMap.GenerateQuery(axis,value)` through `ISearchIndex.Search` (client never sends Lucene, per #69 PR1), then rolls matching leaf items up to distinct sources: **parent shows** for the TV axes (`ItemCount` = query-matching episodes, not total), **movies** for the movie-genre axis. - Reuses existing `PagedLibraryBrowseItemsResponseModel` / `LibraryBrowseItemResponseModel` DTOs (no new schema) + `LibraryBrowseItemMapper.GetShows/GetMovies`. Title-ordered, paged, clamped (pageNum>=0, pageSize default 100 / max 200). ## Why search-index, not EF The built channel's playout comes from the SmartCollection via `ISearchIndex` (`GetSmartCollectionItems`); the DetailPanel preview must run the same query through the same index to be faithful. #69's EF preview enumeration is a different granularity (candidate axis *values*). See `docs/decisions.md` 2026-07-17 (#384). ## Tests / verification - 9 new tests (handler: query-gen, blank-value short-circuit, episode->distinct-show roll-up with matching-count, paging, movie axis, empty; controller: route literal, mapping, paging clamp). Local pass green (140 in touched namespaces). - OpenAPI + endpoint-index regenerated; `npm run check:api` green (d.ts unchanged — reuses existing schema). ## Docs api-conventions §5 (endpoint inventory), decisions.md 2026-07-17, domain-model. Read-only, catalog-read tier -> cold independent review acceptable per the skip rubric. fixes #384
Author
Owner

Independent cold-context adversarial review (review-only, cross-context agent) — initial diff ab1982ef.

Verdict: MERGEABLE. Core logic verified correct: episode->distinct-parent-show roll-up with query-matching (not total) ItemCount; total derived from the metadata join (no counted-but-dropped index/DB drift); deterministic title+id ordering (no page overlap/gap); 10k-cap under-report documented and matching the GetSmartCollectionItems precedent; catalog-read auth tier correct; movie ItemCount=1 intended.

One Low found and fixed in follow-up commit 567486cf:

  • L1 — a crafted out-of-range numeric ?axis=5 binds past [ApiController]'s auto-400, then GenerateQuery's throw surfaced as a 500. Now short-circuited to an empty result in the handler (matching #69's EnumerateAxis _ => []), with a regression test asserting no search runs. Also tidied the redundant pageSize lower clamp (N4).

The fix commit is a pure defensive guard + test (no concurrency/auth/migration surface); self-verified green (42 tests in the touched namespaces). Nits N2/N3 (multi-metadata-row title edge, no-metadata omission) left as accepted staleness — practically unreachable.

Review-verdict: MERGEABLE @ 567486cf004f2114337f85e50681dc1343433543

**Independent cold-context adversarial review** (review-only, cross-context agent) — initial diff `ab1982ef`. **Verdict: MERGEABLE.** Core logic verified correct: episode->distinct-parent-show roll-up with query-matching (not total) ItemCount; `total` derived from the metadata join (no counted-but-dropped index/DB drift); deterministic title+id ordering (no page overlap/gap); 10k-cap under-report documented and matching the `GetSmartCollectionItems` precedent; catalog-read auth tier correct; movie ItemCount=1 intended. One **Low** found and **fixed** in follow-up commit `567486cf`: - **L1** — a crafted out-of-range numeric `?axis=5` binds past `[ApiController]`'s auto-400, then `GenerateQuery`'s `throw` surfaced as a 500. Now short-circuited to an empty result in the handler (matching #69's `EnumerateAxis _ => []`), with a regression test asserting no search runs. Also tidied the redundant pageSize lower clamp (N4). The fix commit is a pure defensive guard + test (no concurrency/auth/migration surface); self-verified green (42 tests in the touched namespaces). Nits N2/N3 (multi-metadata-row title edge, no-metadata omission) left as accepted staleness — practically unreachable. Review-verdict: MERGEABLE @ 567486cf004f2114337f85e50681dc1343433543
timothy force-pushed feat/384-smartcollection-members from 567486cf00 to 1be45fbe88 2026-07-17 12:31:43 +02:00 Compare
Author
Owner

Rebased onto d38181cb (main advanced with #380/#73). Both commits replayed byte-identical in code; the only rebase conflict was in docs/decisions.md/docs/domain-model.md (both sessions appended) — resolved by keeping all entries. No source change since the reviewed 567486cf, so the prior MERGEABLE verdict + L1 fix stand. Re-verified on the rebased tree: 42 tests green (touched namespaces), check:api clean.

Review-verdict: MERGEABLE @ 1be45fbe88

**Rebased onto `d38181cb`** (main advanced with #380/#73). Both commits replayed **byte-identical in code**; the only rebase conflict was in `docs/decisions.md`/`docs/domain-model.md` (both sessions appended) — resolved by keeping all entries. No source change since the reviewed `567486cf`, so the prior MERGEABLE verdict + L1 fix stand. Re-verified on the rebased tree: 42 tests green (touched namespaces), `check:api` clean. Review-verdict: MERGEABLE @ 1be45fbe8862d43f75f600702e7276c1028cf7be
timothy merged commit ed4b0d45b1 into main 2026-07-17 12:43:50 +02:00
timothy deleted branch feat/384-smartcollection-members 2026-07-17 12:43:51 +02:00
Sign in to join this conversation.