Merge pull request 'feat(384): auto-tune DetailPanel content-source member read endpoint' (#397) from feat/384-smartcollection-members into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 3m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m4s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 29m57s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 3m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m4s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 29m57s
This commit was merged in pull request #397.
This commit is contained in:
@@ -316,6 +316,21 @@ both requiring the standard credential (session-or-key, per §9 — no `[SkipApi
|
||||
| POST | `/api/v1/channels/auto-tune/preview` | `PreviewAutoTuneChannels` | Preview auto-tuned channels |
|
||||
| POST | `/api/v1/channels/auto-tune` | `CreateAutoTunedChannels` | Create auto-tuned channels |
|
||||
|
||||
**Endpoint inventory addition (#384, Auto-Tune DetailPanel backend)**: one read-only `ChannelController`
|
||||
GET, standard credential (catalog-read tier — no `[RequiresAuthentication]`):
|
||||
|
||||
| Method | Path | Operation | Summary |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/v1/channels/auto-tune/members` | `GetAutoTuneChannelMembers` | List a proposed auto-tune channel's distinct content-source members |
|
||||
|
||||
It takes `?axis=&value=&pageNum=&pageSize=` and reuses the existing `PagedLibraryBrowseItemsResponseModel`
|
||||
/ `LibraryBrowseItemResponseModel` DTOs (no new schema). The handler runs the server-owned
|
||||
`AutoTuneAxisMap.GenerateQuery(axis, value)` through `ISearchIndex.Search` (client never sends Lucene, per
|
||||
the #69 PR1 decision), then rolls matching leaf items up to their distinct content sources — parent shows for
|
||||
the TV axes (`ItemCount` = query-matching episodes, **not** the show's total), movies for the movie-genre
|
||||
axis. Paging is clamped (`pageNum` floored at 0; `pageSize` defaults to 100, clamped 1–200) per the §1 Logs
|
||||
precedent. See `docs/decisions.md` 2026-07-17 (#384) for the search-index-vs-EF-enumeration rationale.
|
||||
|
||||
**Resolved wart (#287)**: `DayOfWeek` previously serialized as an integer in the OpenAPI schema while
|
||||
the runtime JSON payload is the enum's **name string** ("Sunday".."Saturday"). It is now added to
|
||||
`Startup.UseStringEnumSchemas`'s hand-list, so the "v1" schema emits it as a **string enum** matching
|
||||
|
||||
@@ -86,6 +86,7 @@ in-file entries.
|
||||
- [2026-07-17 — Clock-boundary schedule padding already exists (FillerMode.Pad); #77 verified, convenience toggle deferred](#2026-07-17--clock-boundary-schedule-padding-already-exists-fillermodepad-77-verified-convenience-toggle-deferred)
|
||||
- [2026-07-17 — Shuffle-source construction extracted to `ShuffleSourceBuilder`; per-family seam, not a god-factory (#380)](#2026-07-17--shuffle-source-construction-extracted-to-shufflesourcebuilder-per-family-seam-not-a-god-factory-380)
|
||||
- [2026-07-17 — Seasonal / date-conditional scheduling already exists (alternate schedules / playout templates); #73 closed as implemented](#2026-07-17--seasonal--date-conditional-scheduling-already-exists-alternate-schedules--playout-templates-73-closed-as-implemented)
|
||||
- [2026-07-17 — Auto-Tune DetailPanel member list = live search-index roll-up, not EF enumeration (#384)](#2026-07-17--auto-tune-detailpanel-member-list--live-search-index-roll-up-not-ef-enumeration-384)
|
||||
|
||||
---
|
||||
|
||||
@@ -1198,3 +1199,29 @@ channel" has no reason to look under "Alternate Schedules", and on finding that
|
||||
Fixed with a task-shaped **"Recipe: seasonal / holiday programming"** section in `channels.md` (both engines,
|
||||
plus the gotchas above) and a `domain-model.md` glossary row. No production code changed, so no live-E2E
|
||||
(same reasoning as #77).
|
||||
## 2026-07-17 — Auto-Tune DetailPanel member list = live search-index roll-up, not EF enumeration (#384)
|
||||
|
||||
The Auto-Tune DetailPanel (#383) shows, per proposed channel, the distinct **content sources** its
|
||||
generated SmartCollection resolves to (a genre channel's shows/movies), each weightable in the #385
|
||||
write path. `GET /api/v1/channels/auto-tune/members?axis=&value=` backs that list.
|
||||
|
||||
**Why the search index, not an EF distinct+count query** — even though #69's *preview* enumeration uses
|
||||
EF (`PreviewAutoTuneChannelsHandler.EnumerateTvShows/…`). The created channel's playout is built from a
|
||||
**SmartCollection** whose members come from `ISearchIndex.Search` (`MediaCollectionRepository.GetSmartCollectionItems`).
|
||||
For the DetailPanel to faithfully preview *what the built channel will actually contain*, the member list
|
||||
must run the **same** query through the **same** index — so the handler calls the server-owned
|
||||
`AutoTuneAxisMap.GenerateQuery(axis, value)` (client never sends Lucene, per #69 PR1) and rolls the matching
|
||||
leaf items up to their distinct parents. #69's preview is a different granularity (enumerating candidate
|
||||
axis *values* with EF exact counts to drive the min-items threshold); this is enumerating the *members of one
|
||||
value*, where index-fidelity matters more than count-exactness. The two coexist deliberately.
|
||||
|
||||
**Roll-up + shape.** Episode axes (TvShow/TvGenre) → distinct parent shows (Episode→Season→ShowId), with
|
||||
`ItemCount` = the **query-matching** episode count, not the show's total (a show contributes only its matching
|
||||
episodes to a genre channel). Movie-genre axis → the matching movies are themselves the sources. Both reuse
|
||||
the existing `PagedLibraryBrowseItemsResponseModel` / `LibraryBrowseItemResponseModel` DTOs and
|
||||
`LibraryBrowseItemMapper.GetShows/GetMovies` (no new schema), ordered by title then id, paged in memory
|
||||
(the distinct-source set is bounded — dozens for a genre). Search pulls up to the 10k cap
|
||||
`GetSmartCollectionItems` already uses; a value resolving to >10k leaf items could under-report sources past
|
||||
the cap — the same staleness bound the smart-collection path accepts. Read-only, catalog-read tier (no
|
||||
`[RequiresAuthentication]`), so a cold review sufficed. Sibling backend child #385 (write-path overrides +
|
||||
weights) and SPA child #386 remain open under the #383 milestone.
|
||||
|
||||
@@ -78,7 +78,7 @@ Channel (1) ──< Playout (0..N per channel; ChannelPlayoutSource distinguishe
|
||||
| **MediaItemState** | Health flag on a media item: Normal/FileNotFound/Unavailable/RemoteOnly. Drives the Trash screen. | `MediaItemState` | `/app/trash` |
|
||||
| **PlayoutItem** | One materialized, built entry in a playout's timeline (the actual thing that will play at a given time). | `PlayoutItem` | (generated, not directly edited) |
|
||||
| **PlayoutHistory** | Rotation/rerun bookkeeping per block (`BlockId`) + collection `Key`/`ChildKey`, used by block-playout schedulers to avoid repeats; inspectable via Troubleshooting. | `PlayoutHistory` | `/app/troubleshooting/blocks` |
|
||||
| **Channel concepts** | `Number` (validated by `Channel.NumberValidator` regex), `Group`, `PlayoutSource` (Generated/Mirror; Mirror channels relay another channel via `MirrorSourceChannelId`+`PlayoutOffset`), `PlayoutMode` (Continuous/OnDemand), `TranscodeMode` (OnDemand only, today), `IdleBehavior` (StopOnDisconnect/KeepRunning), `StreamingMode` (TransportStream/HttpLiveStreamingDirect/HttpLiveStreamingSegmenter/TransportStreamHybrid). | `Channel` | `/app/channels`, `/app/edit-channel/{id}`, `/app/new-channel`, `/app/auto-tune` (bulk-generate from library metadata, #69) |
|
||||
| **Channel concepts** | `Number` (validated by `Channel.NumberValidator` regex), `Group`, `PlayoutSource` (Generated/Mirror; Mirror channels relay another channel via `MirrorSourceChannelId`+`PlayoutOffset`), `PlayoutMode` (Continuous/OnDemand), `TranscodeMode` (OnDemand only, today), `IdleBehavior` (StopOnDisconnect/KeepRunning), `StreamingMode` (TransportStream/HttpLiveStreamingDirect/HttpLiveStreamingSegmenter/TransportStreamHybrid). | `Channel` | `/app/channels`, `/app/edit-channel/{id}`, `/app/new-channel`, `/app/auto-tune` (bulk-generate from library metadata, #69; per-channel DetailPanel content-source members read via `GET /api/v1/channels/auto-tune/members`, #384) |
|
||||
| **Guide / EPG (XMLTV)** | Per-channel programme guide generated from playout items; channels with `ShowInEpg=false` are excluded. | `GetChannelGuideHandler` | `/app/guide` (viewer); settings at `/app/settings/xmltv` |
|
||||
| **M3U** | The channel lineup playlist Jellyfin/Dispatcharr consume. | `ChannelPlaylist.ToM3U()` | — |
|
||||
| **IPTV base URL** | Optional advertised base URL for the IPTV surface (#340). Stored as a single `ConfigElement` (`ConfigElementKey.IptvBaseUrl`, key `iptv.base_url`, no EF migration); when set, `GetChannelPlaylistHandler` (M3U) and `GetChannelGuideHandler` (XMLTV) pin their absolute URLs to its scheme/host/base instead of the request `Host` (blank/invalid → request-derived). Not applied to HDHomeRun; distinct from `ETV_BASE_URL`. Parsed by `ErsatzTV.Core/Iptv/AdvertisedBaseUrl.cs`. | `ConfigElementKey.IptvBaseUrl` | `/app/settings` → IPTV (`GET`/`PUT /api/v1/settings/iptv`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
*Generated by `scripts/generate-endpoint-index.py` from `ErsatzTV/wwwroot/openapi/v1.json`. Do not edit by hand -- regenerated by `scripts/update-openapi.sh`.*
|
||||
|
||||
164 endpoints, 247 operations.
|
||||
165 endpoints, 248 operations.
|
||||
|
||||
## Artists
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|---|---|---|---|
|
||||
| POST | `/api/v1/channels` | ChannelCreate | Create a channel |
|
||||
| POST | `/api/v1/channels/auto-tune` | CreateAutoTunedChannels | Create auto-tuned channels |
|
||||
| GET | `/api/v1/channels/auto-tune/members` | GetAutoTuneChannelMembers | List a proposed auto-tune channel's distinct content-source members |
|
||||
| POST | `/api/v1/channels/auto-tune/preview` | PreviewAutoTuneChannels | Preview auto-tuned channels |
|
||||
| POST | `/api/v1/channels/bulk/delete` | ChannelBulkDelete | Delete channels |
|
||||
| POST | `/api/v1/channels/bulk/group` | ChannelBulkMoveToGroup | Move channels to a group |
|
||||
|
||||
Reference in New Issue
Block a user