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>
This commit is contained in:
89
docs/agent-tasks/06-podcast-kind.md
Normal file
89
docs/agent-tasks/06-podcast-kind.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user