feat(web+api): media browse, search & trash for SPA parity (#141) #168

Merged
timothy merged 5 commits from feat/141-media-browse into main 2026-07-07 16:20:23 +02:00
Owner

Implements the main slice of #141 (SPA parity: media browsing, search & trash) + the core of API gap #161.

Backend (#161)

  • GET /api/library/browse extended to ALL media kinds: Episode/MusicVideo/Song/OtherVideo/Image/RemoteStream added to LibraryBrowseMediaType with hydrators mirroring the existing four; optional Subtitle field gives per-kind context ("Show - S1E2", "Artist - Album", …). The null/all-kinds browse now spans all 10 kinds (behavior change — makes ChannelBuilder's untyped library browse surface episodes/songs within a library).
  • New GET /api/search?query= — grouped multi-kind results (stable 10-group contract, reuses the browse hydration via per-kind dispatch, mirrors Blazor Search.razor's type:{kind} AND (query)); 422 on empty query. state:FileNotFound passes through for trash.
  • New DELETE /api/media-items (body {ids}) → DeleteItemsFromDatabase; 422 on empty, 204 on success. Security + error-contract tests included.

Frontend (#141)

  • MediaBrowseScreen (/app/media) — one screen, in-screen kind switcher for all 9 top-level kinds (deviation from Blazor's 9 nav rows, noted), debounced per-kind search, 100/page paging.
  • SearchScreen (/app/search) — grouped sections + counts + "See all" into browse; TopBar search box now actually works.
  • TrashScreen (/app/trash) — FileNotFound grid, multi-select + delete, Empty Trash (existing endpoint), confirm dialogs.
  • Shared media-kind maps/poster card reused by ChannelBuilder + CollectionsScreen. Includes reconcile merge of main (post-#167).

Deferred (stay open in #141/#161): media detail endpoints/pages, GetMediaItemInfo, image folder browser, remote-stream detail.

Verification: build clean; ErsatzTV.Tests 608 green; Core.Tests 493 (+1 skipped); web typecheck/vitest 209 (20 files)/build/eslint clean. Live E2E on empty scratch DB: all new-kind browses + search groups + 422s + delete 204 + empty_trash verified; SPA routes serve.

Refs #141, #161.

🤖 Generated with Claude Code

Implements the main slice of #141 (SPA parity: media browsing, search & trash) + the core of API gap #161. **Backend (#161)** - `GET /api/library/browse` extended to ALL media kinds: `Episode/MusicVideo/Song/OtherVideo/Image/RemoteStream` added to `LibraryBrowseMediaType` with hydrators mirroring the existing four; optional `Subtitle` field gives per-kind context ("Show - S1E2", "Artist - Album", …). The `null`/all-kinds browse now spans all 10 kinds (behavior change — makes ChannelBuilder's untyped library browse surface episodes/songs within a library). - New `GET /api/search?query=` — grouped multi-kind results (stable 10-group contract, reuses the browse hydration via per-kind dispatch, mirrors Blazor Search.razor's `type:{kind} AND (query)`); 422 on empty query. `state:FileNotFound` passes through for trash. - New `DELETE /api/media-items` (body `{ids}`) → `DeleteItemsFromDatabase`; 422 on empty, 204 on success. Security + error-contract tests included. **Frontend (#141)** - `MediaBrowseScreen` (`/app/media`) — one screen, in-screen kind switcher for all 9 top-level kinds (deviation from Blazor's 9 nav rows, noted), debounced per-kind search, 100/page paging. - `SearchScreen` (`/app/search`) — grouped sections + counts + "See all" into browse; TopBar search box now actually works. - `TrashScreen` (`/app/trash`) — FileNotFound grid, multi-select + delete, Empty Trash (existing endpoint), confirm dialogs. - Shared media-kind maps/poster card reused by ChannelBuilder + CollectionsScreen. Includes reconcile merge of main (post-#167). **Deferred (stay open in #141/#161)**: media detail endpoints/pages, GetMediaItemInfo, image folder browser, remote-stream detail. **Verification**: build clean; ErsatzTV.Tests 608 green; Core.Tests 493 (+1 skipped); web typecheck/vitest 209 (20 files)/build/eslint clean. Live E2E on empty scratch DB: all new-kind browses + search groups + 422s + delete 204 + empty_trash verified; SPA routes serve. Refs #141, #161. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 3 commits 2026-07-07 15:52:15 +02:00
- Extend /api/library/browse to episodes, music videos, songs, other videos,
  images and remote streams (new LibraryBrowseMediaType values + hydrators);
  add optional Subtitle to LibraryBrowseItemResponseModel for leaf-item context
- Add GET /api/search: grouped per-kind results reusing the browse query/shape;
  empty query -> 422
- Add DELETE /api/media-items: body { ids }, empty -> 422, success -> 204
- Tests: SearchController, MediaItemsController, security + OpenAPI contract entries
- Regenerate openapi v1.json + web v1.d.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MediaBrowseScreen (/app/media): one parameterized screen for all 9 top-level
  kinds with an in-screen kind switcher, per-kind search box, 100/page paging
- SearchScreen (/app/search): grouped per-kind results with counts + 'See all';
  TopBar search input now navigates here
- TrashScreen (/app/trash): state:FileNotFound results, multi-select + Empty Trash
  via DELETE /api/media-items and /api/maintenance/empty_trash (confirm dialogs)
- Shared media/mediaKinds (icon/label maps, hueOf, duration helpers) + MediaPosterCard;
  ChannelBuilder and CollectionsScreen now reuse the single-source maps (their
  exhaustive Record<LibraryBrowseMediaType> had to cover the 6 new kinds anyway)
- api: search.ts, mediaItems.ts, maintenance.ts (+ URL-assert tests)
- One consolidated 'Browse' nav entry with an in-screen kind switcher instead of
  9 per-kind nav rows (deviation from Blazor's per-kind Media links)
- Nav: Media group gains Browse/Search/Trash; Settings Classic-UI help trimmed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into feat/141-media-browse
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
7083301ce6
# Conflicts:
#	ErsatzTV.Tests/Controllers/OpenApiErrorResponseContractTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
timothy added 1 commit 2026-07-07 16:06:53 +02:00
fix(web): scope library/add-item pickers to their 4 media kinds (#168 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
72b6d0a76a
GET /api/library/browse without mediaType now spans all 10 media kinds
(was 4: Movie/TelevisionShow/TelevisionSeason/Artist), which flooded two
pickers with episodes/songs/etc. mixed in with the intended top-level
items. Fan both consumers out per-kind and merge, restoring pre-#168
scoping without touching the API:

- ChannelBuilder's library browse tab now fans out Movie/TelevisionShow/
  TelevisionSeason/Artist per page (mirrors the existing loadCollections
  pattern) and sums per-kind totalCount so paging/load-more still works.
- CollectionsScreen's add-items search now fans out over the same 4
  addable kinds instead of filtering one unscoped page client-side.

Extended App.test.tsx and CollectionsScreen.test.tsx to assert the
per-kind requests and updated a few fixtures that relied on the old
unscoped browse call returning collection-kind items under the library
tab.
timothy added 1 commit 2026-07-07 16:07:52 +02:00
Merge remote-tracking branch 'origin/main' into feat/141-media-browse
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
f1fae6397e
timothy merged commit 3c37ec5f38 into main 2026-07-07 16:20:23 +02:00
timothy deleted branch feat/141-media-browse 2026-07-07 16:20:24 +02:00
Sign in to join this conversation.