Blazor parity conveniences: BlockPlayoutTroubleshootingScreen now persists the block-history
page-size selector to localStorage (ctv-block-history-page-size, same ctv- namespace as
ctv-theme) and restores it on mount, and gates the per-block History action on block.id >= 0
(mirrors BlockPlayoutTroubleshooting.razor, which hides it for synthesized/virtual blocks).
BlocksScreen and TemplatesScreen list screens gain a client-side name/group search filter box,
matching the filter already present on the troubleshooting blocks list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidebar "Playouts 3" badge on a fresh empty DB was design-mock
scaffolding (badge: 3 hard-coded in the routes array) never wired to
live data; Blazor had no equivalent. Removed the value but kept the
nav-badge mechanism (ScreenRoute.badge, NavItem badge/badgeTone props)
in place since it's a plausible future home for a live warnings count.
The footer "1 failing" chip reported in the same issue is NOT a bug:
summarizeHealth renders live GET /api/health data, and on a fresh
local dev instance the genuinely failing check is FFmpeg Capabilities
(local Homebrew ffmpeg lacks the subtitles/zscale filters that prod's
ffmpeg image has). No code change for that half.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three PR #222 adversarial-review findings fixed:
1. SearchScreen's `refreshing` derivation compared the last success `state.query`
against the current query even when the query was cleared to empty — `load()`
early-returns on a blank query, so `state` never updates and the "Refreshing…"
cue got stuck forever over the empty-query card. Gate on `hasQuery`.
2. `MediaPosterCard` falls back to `onOpen` whenever `onToggleSelect` is
undefined, so `selectMode && refreshing` (onToggleSelect withheld but onOpen
still derived from `!canSelect`) made a mid-select click navigate away
instead of no-op'ing. Both screens now withhold `onOpen` for the whole of
select mode, not just the "live" part of it.
3. The Select/Done toggle was `disabled={refreshing}`, which also blocked
*exiting* select mode — but exiting only clears selection, it isn't a
mutation against the stale result set. Disable only when entering
(`refreshing && !selectMode`).
Also corrected the "can never get stuck" over-claim in docs/spa-conventions.md
§3a: the param-keyed refreshing derivation is only self-correcting when every
param value actually triggers a fetch; params that suppress fetching (like an
empty search query) must be excluded from the comparison or the whole flag
gated on the same condition.
Tests added: query-cleared-to-empty shows no refreshing cue (both screens'
existing 3 race tests still green); select-mode+refreshing card click neither
selects nor navigates; select toggle disabled only while entering, not exiting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/libraries/{id}/scan-show resolved the target show via
GetShowIdByTitle, an EF.Functions.Like "%title%" substring match with
no OrderBy - non-deterministic under duplicate/overlapping titles and
capable of scanning the wrong show. The Blazor UI never had this bug
(it always passed the exact show id); this endpoint shipped days ago
in PR #216 with no external consumers, so the contract break is safe.
BREAKING CHANGE: ScanShowRequest now takes `showId: int` instead of
`showTitle: string`. Replaced ITelevisionRepository.GetShowIdByTitle
with GetShowTitle(libraryId, showId), which also enforces the show
belongs to the given library. LibrariesController.ScanShow now returns
a genuine 404 ProblemDetails (via ApiResults.NotFoundProblem, the
established pre-check pattern from TemplateController.DeleteGroup)
when the show id doesn't exist in that library, then queues
QueueShowScanByLibraryId with the DB-resolved title.
SPA: libraries.ts ScanShowParams.showId replaces showTitle;
MediaDetailScreen.tsx passes show.id. Extended
ApiErrorResponseMetadataTests and OpenApiErrorResponseContractTests
with the new 404 contract for ScanShow. Regenerated v1.json / v1.d.ts
via scripts/update-openapi.sh + npm run generate:api.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search and Media browse keep the previous successful result set rendered
during a refetch (query on Search; kind/query/page on Media browse) with no
gating, so per-card Add-to, Select/select-mode, the selection action bar, Add
all, and Save-as-smart-collection stayed live over stale, about-to-be-replaced
items. Worst path: SearchScreen.addAll only checked activeRef, so a late
GET /api/search/all-items could open a bulk-add dialog scoped to the previous
query's entire result set.
Key the success state to the request params that produced it and derive a
`refreshing` flag; while refreshing, keep cards visible but disable every
mutation surface, show a "Refreshing…" cue, and dim the grid. Card navigation
stays live. Bind addAll's completion to its query via lastQueryRef so a stale
all-items result is discarded. Same pattern applied to both screens.
Docs: spa-conventions §3a (refreshing/gating pattern) + §8 (temporal-semantics
review checklist); blazor-route-parity search/media-browse verdicts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
command (mirrors CollectionController.AddItems); controller pre-checks
playlist existence for a real 404, and the handler now rejects adds to
system (generated) playlists, matching the guard already applied to
rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
query, returning a new SearchResultAllItemsResponseModel (never expose
the VM directly) so the SPA's shared "add all to collection/playlist"
component can materialize ids before calling the add endpoints, same
two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
libraryId, title, and mediaSourceKind (serialized as a string enum via
the global StringEnumConverter) - no changes needed.
Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
- App.test.tsx: await the initial library fan-out being issued before
mockClear — passive effects flush asynchronously, so on a slow machine the
mount fan-out leaked past the clear and polluted the post-click assertion
(CI-only failure on run 383).
- WatermarksScreen: modeFromPath parses the pathname state string (incl.
search) instead of reading mutable window.location during render.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BlocksScreen: add copy-to-group dialog (mirrors TemplatesScreen), wiring the
already-existing copyBlock() API client
- WatermarksScreen: add copy via /add?from={id} prefill (mirrors
FFmpegProfilesScreen), no new endpoint needed
- TrashScreen: add Select all / Clear selection to the action bar; bump
PAGE_SIZE to the search API's max (100) — true paging needs a page param
the API doesn't have yet
- TraktListsScreen: fix stale callout text — "View matched items" now opens
the SPA's own search screen, not the Classic UI
docs/decisions.md: record the YAML-validator paste-textarea deviation, the
channel-number prompt-renumber deviation, extend the "table not calendar"
convention to the deco-templates editor, and document the trash 100-item cap.
Adds PUT /api/collections/{id}/custom-order support (updateCollectionCustomOrder)
and a reorder mode in ManualItemsView: loads every page of a manual collection
(so the wholesale-replace PUT never drops items), lets the user move items with
up/down icon buttons, and saves/cancels. Reorder is offered for any manual
collection with useCustomPlaybackOrder on, not just movies-only (server/enumerator
already support any kind).
Widens the add-items picker (ADDABLE_TYPE_LIST, MEDIA_KIND_FILTERS,
toAddItemsRequest) from 4 to all 10 addable media kinds; the default "All" search
fan-out stays Movie/Show/Artist (seasons excluded per #180), with the new kinds
reachable via their specific filter, mirroring Blazor's per-kind list pages.
The SPA needs the item's row id to call
GET /api/playouts/items/{id}/scheduling-context. Plumbed through
PlayoutItemViewModel -> PlayoutItemResponseModel as a nullable Id
(null for synthesized UNSCHEDULED gap rows, which are PlayoutGaps,
not PlayoutItems). Additive for existing consumers (Playouts.razor
reads the VM by property). Regenerated v1.json + v1.d.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend slice for the ChicoryTV playouts and collections screens.
PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
playout item's stored context by row id via a new
GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)
CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
when the collection's UseCustomPlaybackOrder is set
Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerate v1.d.ts from the aligned spec (fFmpegProfileId->ffmpegProfileId,
fFmpegProfile->ffmpegProfile) and update every SPA reader/writer and test mock
to the runtime casing:
- ChannelEditScreen: read channel.ffmpegProfileId; draft/set/select use
ffmpegProfileId.
- ChannelBuilder: template reads/writes + ADVANCED_KEYS use ffmpegProfileId.
- App.tsx channel list: read channel.ffmpegProfile (was fFmpegProfile, a latent
bug that always rendered "Unassigned" since the runtime never sent that key).
- PlaybackTroubleshootingScreen: drop the #198 escape hatch (rawChannel cast +
dual-read) and read data.channel.ffmpegProfileId directly.
- Test mocks now use runtime casing; pinning comments updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live E2E caught what unit tests could not: the runtime JSON from GET
/api/channels/{id} is Newtonsoft-cased ('ffmpegProfileId'), while the
generated OpenAPI types say 'fFmpegProfileId' (STJ naming) — so the
channel-mode default-profile read was undefined, the form held null, and
buildPlaybackUrl fell back to ffmpegProfile=0. The unit-test channel mock was
typed from the same wrong generated key, so mocks conformed to the spec, not
the server.
- Read the runtime key first via a typed escape hatch, fall back to the spec
key, then to the first profile — never 0 (see #198 for the root spec-
generation fix; ChannelEditScreen's same latent bug is #198's scope).
- buildPlaybackUrl now refuses to build a URL with a null profile, and Play
is additionally gated on ffmpegProfileId != null.
- streamSelectorMode/streamSelector verified against live runtime JSON: no
leading acronym, keys match the generated types — noted in a comment.
- Channel mock now pins the RUNTIME shape (ffmpegProfileId), and a new
channel-mode Play test asserts ffmpegProfile=2 + channel=7 + ISO start in
the URL (web suite 384 -> 385).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review MUST-FIX + SHOULD-FIX (#145):
- Repeat Play with unchanged settings was fully broken: the manifest GET is
what starts a session server-side, but an identical URL made setPlayerSrc a
state no-op, so HlsPlayer never re-attached and no request was issued —
while the poll saw the previous session's stored result and toasted a
spurious success over a dead player. HlsPlayer now takes a playToken prop
(incremented per Play) that forces teardown/re-attach even for identical src.
- First-poll race: PrepareTroubleshootingPlayback runs DB queries BEFORE
taking the playback lock, so polls right after Play can still see the stale
settled result. Replaced the manually-seeded 'running' assumption with a
play-phase machine (idle -> starting -> running -> idle): settled statuses
are ignored during 'starting' until the server has reported running at
least once, with a 30s timeout that surfaces an error and stops polling.
Play + downloads are gated and a Starting/Transcoding indicator shows for
the whole busy window.
Nits: mediaItem takes precedence over channel when both query params are
present (matching Blazor); on-mount poll comment notes the intentional
post-restart log recovery. Tests: repeat-play re-issues loadSource, stale
settled ignored until running observed, starting timeout, download gating
while busy, both-params precedence (web suite 378 -> 384).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review SHOULD-FIX (#145): the SPA graphics picker could go stale because
Blazor ran RefreshGraphicsElements (disk->DB sync) before listing, while the
API endpoint never refreshed — a newly added .yml would not appear. GET
/api/graphics-elements?refresh=true now sends RefreshGraphicsElements before
the list query; default false leaves existing callers untouched. The playback
troubleshooting screen passes refresh=true. Controller tests cover
refresh-iff-true ordering; regenerated OpenAPI v1.json (endpoint index and
generated TS schemas unchanged - query params are not part of either).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports Blazor Pages/Troubleshooting/PlaybackTroubleshooting.razor to the SPA at
/app/troubleshooting/playback. Driven by ?mediaItem={id} (media mode) or
?channel={id} (channel mode); neither shows a friendly empty state (no redirect).
- Channel mode: FFmpeg profile (channel's preselected), stream selector, and a
datetime-local Date/Time (deviation from Blazor's Chronic natural-language
parse) sent as the m3u8 'start' ISO param.
- Media mode: subtitle (mutually exclusive with stream selector), watermark +
graphics-element multi-select (repeated watermark/graphicsElement id params),
Start From Beginning (disabled for RemoteStream) + Seek Seconds.
- Play builds playback.m3u8, plays via HlsPlayer, and polls
/api/troubleshoot/playback/status every 2s; on running->completed/failed it
stops polling, shows the log tail + a colored speed badge, and a completion
notice. Polls once on mount to reflect a session running elsewhere.
- Entry points: enable the Channels table Troubleshoot action and add a
Troubleshoot Playback button to the movie detail page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds thin client wrappers (+ URL-building tests) for GET
/api/troubleshoot/playback/{stream-selectors,subtitles/{id},status}, used by
the playback troubleshooting screen (#145).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds hls.js as a web dependency and a small HlsPlayer wrapper that attaches
hls.js when MSE is available (config mirrors the legacy _Host.cshtml
previewChannel: liveDurationInfinity + unbounded manifest time-to-first-byte,
since the troubleshooting playback.m3u8 endpoint blocks until segments exist)
and falls back to native HLS on Safari. Tears down the instance on src change
and unmount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add three GET endpoints to TroubleshootController for the SPA port of the
playback troubleshooting page:
- /api/troubleshoot/playback/stream-selectors (List<string>)
- /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via
GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel)
- /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel:
idle/running/completed/failed + exitCode/speed + logs.txt tail)
Regenerate v1.json, endpoint-index.md, and the web API types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use Promise.allSettled when loading each group's playlists so one group's
transient error shows that group empty rather than blanking the whole tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a ChicoryTV SPA Playlist editor at /app/playlists over the new
playlist-CRUD REST API: a group tree (add/rename/delete groups,
add/delete playlists) plus a playlist item editor (per-item Collection
Type over the 12 playlist item types, type-conditional playback order,
count, Play All, Show In EPG, reorder/copy/remove) and a draft playout
preview. System groups and playlists are read-only, mirroring the Blazor
Playlists.razor / PlaylistEditor.razor pages.
- web/src/api/playlists.ts: group/playlist/item CRUD + preview client fns
- web/src/screens/PlaylistsScreen.tsx: group tree + editor + preview
- App.tsx: route, ScreenId, nav, dispatch wiring
- docs: blazor-route-parity + domain-model updated to SPA DONE
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add write endpoints to PlaylistController for the playlist editor: group
create/rename/delete, playlist create/read/update/delete, item list read,
and draft playout preview. Introduces a RenamePlaylistGroup command/handler
(the one missing Application-layer operation) plus request/response DTOs.
Endpoints (verb / route / route-Name):
- POST /api/playlists/groups CreatePlaylistGroup
- PUT /api/playlists/groups/{id} UpdatePlaylistGroup (rename)
- DELETE /api/playlists/groups/{id} DeletePlaylistGroup
- GET /api/playlists/{id} GetPlaylistById
- GET /api/playlists/{id}/items GetPlaylistItems
- POST /api/playlists CreatePlaylistInGroup
- PUT /api/playlists/{id} UpdatePlaylist (rename + replace items)
- DELETE /api/playlists/{id} DeletePlaylist
- POST /api/playlists/preview PreviewPlaylist
404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a
controller-side existence pre-check (mirrors TemplateController.DeleteGroup),
leaving existing shared handlers untouched; validation failures return 422.
RenamePlaylistGroup returns NotFoundError -> 404 for a missing group.
Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA
client types (web/src/api/generated/v1.d.ts). No SPA screen in this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two React SPA editor screens over the existing REST APIs, retiring the
last two "Classic UI only" media-collection editors.
MultiCollectionsScreen (/app/multi-collections, #151): list + in-screen
editor. Name + a list of manual/smart collection items, each with a
Schedule-As-Group toggle; two add-pickers dedupe already-present entries.
Items save with playbackOrder "Chronological" (matches the Blazor editor).
RerunCollectionsScreen (/app/rerun-collections, #152): list + in-screen
editor. Name, a Collection Type select restricted to the REST-supported
selection set (IsSupportedSelectionType) — Collection/MultiCollection/
SmartCollection plus the media-item types TelevisionShow/TelevisionSeason/
Artist/Movie/Episode/MusicVideo/OtherVideo/Song/Image/RemoteStream (every
one maps to a working LibraryBrowseMediaType picker); Playlist is excluded.
A conditional single-selection picker per type, and First/Rerun playback
order selects with the Blazor type-conditional option sets (reset on type
change).
Both screens toggle list<->editor via local state (mirroring
CollectionsScreen), no sub-path routing. Wired into App.tsx (imports,
ScreenId, routes, mediaNavIds, ScreenContent). Added messageFrom*Error
helpers + type re-export to the api clients.
Tests: multiCollections/rerunCollections api-client param+body tests,
both screen tests (list render, create body, rerun type-switch swaps
picker + order options, delete), and an App.tsx nav-registration test.
Docs: blazor-route-parity.md (rows moved to SPA DONE), domain-model.md.
web verify gate: 353 tests pass, eslint clean, tsc -b + vite build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a paged collection-items endpoint reusing LibraryBrowseItemResponseModel
so the SPA lists a manual collection's full contents (all media kinds), replacing
the lossy Lucene name-based preview. Confirms POST /items already returns 422 for
bogus ids (guarded by ValidateMediaItems, fb3f2856); adds endpoint-level coverage.
fixes#155
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds MultiCollectionController + RerunCollectionController (list/get/create/update/
delete) over the existing MediatR handlers, with response/request DTOs, controller +
contract tests, regenerated OpenAPI + TS types, and thin SPA api-client modules. SPA
editor screens are a follow-up. Rerun request DTO resolves the chosen entity id into
the minimal VM the handler reads (documented inline).
Refs #151, #152
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #184: ApiControllerSecurityTests scans ErsatzTV.Controllers.Api assembly
instead of a hand-maintained array (9 controllers were unlisted; 2 mutating)
- Trakt matched-items link now navigates to SPA /app/search (was Classic UI)
- GET /api/search runs its 10 per-kind queries in parallel (context-safe)
- Remove dead "New Group" header buttons from blocks/templates/decos/deco-templates
- docs/api-conventions.md §6 updated for the assembly-scan change
fixes#184
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the SPA screens over the new detail/info/image endpoints:
- Detail pages at /app/media/{movies|shows|seasons|artists}/{id}. MediaBrowseScreen
cards for movies/shows/artists (and season/show drill-in cards) navigate to them.
Shows list seasons -> seasons list episodes -> artists list music videos, each via
the browse parentId drill-in, paged. Layout: fanart/poster, title/year/plot, chip
lists, cast row, movie path + FileNotFound/Unavailable warnings. A "Media Info"
dialog (streams + chapters) backs onto GET /api/media-items/{id}/info. Add-to-
collection/playlist deferred to #153/#155 (TODO left in code).
- Image folder browser at /app/media/images/browser: lazy expandable tree, per-folder
image/subfolder counts + duration, edit dialog (PUT set/clear), and a per-folder
search link (library_folder_id:{id}). Reachable via a "Folder Browser" button on the
Images browse view.
New /app/media sub-paths are owned by a MediaRouteScreen wrapper that tracks pathname
locally + listens for popstate (App-level routing returns the same 'media' route object
for base and sub-paths). Client modules web/src/api/mediaDetail.ts + imageFolders.ts,
typed via the generated v1.d.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add REST endpoints backing the SPA media detail pages and image browser:
- GET /api/movies/{id}, /api/shows/{id}, /api/seasons/{id}, /api/artists/{id}
wrapping the existing detail queries; 404 on None.
- GET /api/media-items/{id}/info wrapping GetMediaItemInfo; UnableToLocateMediaItem
-> 404, other errors -> 422.
- GET /api/images/folders?parentId= and PUT /api/images/folders/{id}/duration
(validates null-or-positive -> 400; existence guard via new ImageFolderExists
query -> 404).
- Extend GetLibraryBrowseItems parentId drill-in to Episode (episodes of a season,
episode-number order) and MusicVideo (an artist's music videos, album/track order),
alongside the existing TelevisionSeason branch.
Response DTOs live in ErsatzTV.Core/Api/* and never expose Application VMs. Artwork
values are rooted for the SPA via a shared ErsatzTV.Core/Api/ApiArtwork helper
(mirrors the #180/#181 browse-handler logic; handles jellyfin/emby proxy prefixes,
http passthrough, empty). Regenerated OpenAPI v1.json + web v1.d.ts. New controllers
registered in ApiControllerSecurityTests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two new System-area screens over the new endpoints:
- /app/troubleshooting/blocks: pick a Block playout, browse its blocks
(grouped, name filter), drill into paged block history, click a row to
decode its details.
- /app/troubleshooting/yaml: paste YAML, toggle import schema, validate;
shows messages + JSON conversion.
Adds api/playoutHistory client + validateSequentialSchedule; regenerated
generated/v1.d.ts. Async loads follow the LogsScreen seq-guard pattern
(no synchronous setState inside effects).
Refs #145#158
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part of the #180 library picker fixes (SPA side).
Channel builder: TelevisionSeason is removed from the library grid fan-out, so a
multi-season show renders as one tile instead of one tile per season. Show tiles
gain a "Seasons" drill-in affordance (both grid and compact layouts) that opens a
dialog listing that show's seasons (via the new GET /api/library/browse?parentId=
&mediaType=TelevisionSeason), each with title, artwork and an Add button that
drops the specific season into the lineup.
Collections add-items dialog: the default search fan-out now excludes seasons,
and a media-kind filter row (All / Movies / Shows / Seasons / Artists,
default = All-without-seasons) keeps seasons reachable when explicitly selected.
Client: getLibraryBrowseItems gains an optional parentId param. Tests cover the
param mapping, the builder no longer requesting TelevisionSeason, and the
collections dialog default-excluding vs explicitly-including seasons.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>