Files
musicmouse/docs/agent-tasks/06-podcast-kind.md
Martin Bauer 4cb2d085ca Add agent task files for the review's remaining fixes
Authentication (01), one input policy for skip/mute/volume steps (04), the
firmware's interleaved serial frames (05), podcast as an album kind (06), and eight
smaller cleanups (07). Each says why, what was decided, the design, the tests and
when it is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:51:27 +02:00

90 lines
4.3 KiB
Markdown

# Task: make "podcast" a kind of album
## Why
`AlbumKind` is `Literal["music", "book"]` (`python-backend/musicmouse/library/sections.py`),
and the `Kinderpodcasts` shelf is declared `kind="book"`. So "is this a podcast?" is
answered differently in each layer:
- web: `album.section === "Kinderpodcasts"` (`web/src/lib/search.ts`, `groupOf`), and
everything else goes through `groupOf(album) === "podcasts"` (App, PlayView,
BrowseView, MobileBrowse, keyboard.ts, remote.ts, ambience.ts, useTrackDetail);
- backend: `SECTIONS[album.section].album_unit == "episode"` (`MusicLibrary.latest_episode`,
`tippen/rewards.py`);
- typing game: reward kinds `"episode"` and `"podcast_episode"`
(`services/web/tippen_api.py`).
The shelf's folder name is therefore a de-facto API string the front-end depends on.
`todo.md` already asks for a cleaner podcast data model.
## What the owner decided
Add `"podcast"` to the album kind. Podcast-ness is then a property of the album, carried
in the index and the API, and nobody compares section names.
## Design
### Backend
- `AlbumKind = Literal["music", "book", "podcast"]`. `SECTIONS["Kinderpodcasts"]` gets
`kind="podcast"`.
- `Section.album_unit` stays. It is the scanner's structural fact (one album per file),
not a classification. Keep using it in the scanner (`scan_library`) and in
`podcast_feeds.find_feed_shows`. Every place that asks "is this album a podcast?"
switches to `album.kind == "podcast"`:
- `MusicLibrary.latest_episode`;
- `tippen/rewards.py` (`_chronological_episodes`, and the `album_unit == "folder"`
branches that really mean "not a podcast");
- `tippen_api._animation_kind`;
- the `is_podcast` helper, if task 04 added one: change its body, keep its callers.
- `scanner.scan_album` derives `series` only for `kind == "book"`. Check that episode
albums still get `series = folder.name` (they do, in `scan_episodes`) and that
nothing else branches on `"book"` meaning "book or podcast".
`grep -rn '"book"' python-backend/musicmouse` and decide per hit.
- `FigureConfig.kind` stays `Literal["music", "book"]`. A figurine holding a podcast is
not a thing. Type `Config.figure_kinds` accordingly.
- `cache._album_from_json` validates the kind: accept `"podcast"`. Bump
`_INDEX_VERSION` (with a comment line, like the existing ones), so every cached
podcast album is re-scanned with the new kind instead of being read back as
`"book"`.
- `_ANALYZED_KINDS` stays `("music",)`.
- `services/web/schemas.py`: `AlbumOut.kind` and `PlayerStateOut.kind` become the
`AlbumKind` literal instead of `str`, so the OpenAPI schema says what the values are.
### Web
- `src/api/types.ts`: `AlbumKind = "music" | "book" | "podcast"`.
- `src/lib/search.ts`, `groupOf`: map from `kind` alone (`podcast` → `"podcasts"`,
`book` → `"audiobooks"`, else `"music"`). No `section` comparison may remain
anywhere in `web/src`.
- `src/lib/covers.ts`: `isBook` and `aspectOfKind` treat `"book"` specially. Podcasts
are drawn like books today *because* they were `kind: "book"`. Keep them looking
exactly as they do now: decide explicitly whether `isBook` / `aspectOfKind` include
`"podcast"`, and check BrowseView's `mostlyBooks` / `allPodcasts` tiles and the play
view. The result must be visually unchanged; compare screenshots of the podcast
shelf, a podcast category and the podcast play view before and after.
- Anything else that reads `state.kind` or `album.kind`: grep and decide per hit.
### Leave alone
The `section` field stays in the API; it is still useful for display and debugging.
Don't rename the shelves, the folders or the `Group` names.
## Tests
- `tests/test_library.py`: a `Kinderpodcasts` episode scans as `kind == "podcast"`; a
cache written at the previous `_INDEX_VERSION` is discarded; `latest_episode` still
works.
- `tests/test_tippen_rewards.py` / `test_tippen_api.py`: episode rewards still resolve
and still animate as `podcast_episode`.
- `tests/test_web.py`: `/api/library` reports `"podcast"`.
- `web/src/lib/__tests__/search.test.ts`: `groupOf` uses `kind`; an album whose section
says `Kinderpodcasts` but whose kind is `music` is music. That proves nothing compares
the section any more.
## Done when
`scripts/check.sh` passes, `grep -rn "Kinderpodcasts" web/src` finds nothing, the
backend's "is it a podcast" checks all read `album.kind`, and the podcast UI looks the
same as before.