Skip to content

feat(browse): add Artists, Albums (A–Z) and Genres categories - #42

Merged
chamika merged 1 commit into
mainfrom
feat/browse-artists-albums-genres
Aug 16, 2026
Merged

feat(browse): add Artists, Albums (A–Z) and Genres categories#42
chamika merged 1 commit into
mainfrom
feat/browse-artists-albums-genres

Conversation

@chamika

@chamika chamika commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #37.

Adds three browse categories, selectable through the existing Browse categories setting.

Category Behaviour
Artists A–Z index → album artists → their albums (existing getArtistAlbums path)
Albums A–Z index → albums sorted by SortName
Genres genre list → "Shuffle all" + that genre's albums

Design notes

Why an A–Z index rather than a flat list. Every category is capped at MAX_ITEMS = 120, so a flat library-wide list would silently truncate — useless past "C" on a real library. Buckets query with nameStartsWith; the # bucket uses nameLessThan = "A", since names sorting before "A" are exactly the digits and symbols it should collect. No client-side filtering needed.

Why genres need their own plumbing. A genre links to its albums by genreIds, not parentId, so getItemChildren's generic query returns nothing for one — hence a MEDIA_TYPE_GENRE branch. Same reason the genre shuffle gets its own SHUFFLE_GENRE: prefix: reusing SHUFFLE_FOLDER: would have sent a genre id into a parentId query and come back empty. genresApi is used because musicGenresApi is @Deprecated in the SDK.

Sync stays shallow for all three, alongside Folders. Recursing would fan out to 27 buckets × every artist × every album — a library-sized fetch for what is meant to be a routine sync. Deeper levels still cache lazily via getChildren as the user browses.

No upgrade churn. New keys are appended to canonicalOrder, so existing users' root tab order does not shift. The picker keeps min 2 / max 4 — AAOS surfaces roughly four root tabs.

Verification

:automotive:assembleDebug passes. :automotive:testDebugUnitTest362 tests, 0 failures, 0 skipped (--rerun-tasks, not cached). 12 of those are new in JellyfinMediaTreeBrowseTest and assert the actual Jellyfin query shapes rather than just the item tree: nameStartsWith vs nameLessThan per bucket, genreIds for genre albums and genre shuffle, and that a shuffle entry is omitted for an empty genre.

Not yet verified on device

No emulator was attached and this needs a live Jellyfin server. Worth watching for on first run:

  • Cold-cache genre resolution. After a sync evicts the tree cache, a genre resolves via userLibraryApi.getItem(uuid). Jellyfin should serve genres from that endpoint, but if it 404s you would get "Media isn't available" — the same failure mode already documented for COLLECTION_FOLDER in JellyfinMediaTree.kt.
  • Albums → # returning only numeric/symbol-titled albums.
  • No Can't create mediaItem for MusicGenre in logcat.

Known limitation

Letter buckets are shown unconditionally, so a library with nothing under Q presents an empty list. Hiding empty buckets would need a count probe per letter — 27 round trips every time the category opens — which is the wrong trade on a car's connection. Deferred deliberately.

https://claude.ai/code/session_014yXRpvsEw9v9NiwvwKayNk

Closes #37.

The browse root only offered Latest / Favourites / Books / Playlists /
Random / Folders, with no way to reach the whole library by artist,
alphabetically by album, or by genre.

Artists and Albums open an A-Z index (26 letters plus "#") rather than a
flat list. A flat list would silently truncate at MAX_ITEMS, and long
unsorted scrolls are the wrong shape for a driving context. Buckets query
with nameStartsWith; "#" uses nameLessThan="A", since names sorting before
"A" are exactly the digits and symbols that bucket is meant to collect.

Genres list via genresApi (musicGenresApi is deprecated) and open to a
"Shuffle all" entry followed by that genre's albums. A genre links to its
albums by genreIds rather than parentId, so it needs its own children
query and its own shuffle prefix — reusing SHUFFLE_FOLDER: would have sent
a genre id to a parentId query and returned nothing.

Sync treats the three new sections as shallow, alongside Folders:
recursing would fan out to 27 buckets x every artist x every album,
turning a routine sync into a library-sized fetch. Deeper levels still
cache lazily as the user browses.

Category keys are appended to canonicalOrder so existing users' root tab
order does not shift on upgrade. The picker keeps its min 2 / max 4 limit.

Verified: :automotive:assembleDebug and 362 unit tests (0 failures),
including 12 new tests asserting the Jellyfin query shapes. Not yet
verified on device against a live server.

Claude-Session: https://claude.ai/code/session_014yXRpvsEw9v9NiwvwKayNk
@chamika
chamika merged commit 2832b98 into main Aug 16, 2026
1 check passed
@chamika
chamika deleted the feat/browse-artists-albums-genres branch August 16, 2026 17:00
chamika pushed a commit that referenced this pull request Aug 26, 2026
Resolves conflicts with the Artists/Albums/Genres browse categories (#42),
which touched the same category-registration points. Both feature sets are
kept:

- MediaItemFactory: DOWNLOADS constant alongside ARTISTS/ALBUMS/GENRES;
  downloads()/downloadedContainer()/artworkUriFor() kept next to
  artists()/albums()/genres()/letterBucket(), using main's albumCategory
  signature.
- JellyfinMediaTree: "downloads" appended last in canonicalOrder so neither
  the pre-existing tabs nor the newly added ones shift; DOWNLOADS added to
  the getItem dispatch.
- MediaRepository: DOWNLOADS added to main's restructured staticIds; the
  local-only Downloads children path and the sync skip are preserved
  alongside shallowSyncSections.
- Resources: both sets of browse category entries and strings retained.
- Tests: kept main's artists/albums/genres ordering tests and updated the
  downloads ordering test to assert it stays last.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BSRADdi4UoKRejWDKeZDC
chamika pushed a commit that referenced this pull request Aug 26, 2026
Resolves conflicts with the Artists/Albums/Genres browse categories (#42),
which touched the same category-registration points. Both feature sets are
kept:

- MediaItemFactory: DOWNLOADS constant alongside ARTISTS/ALBUMS/GENRES;
  downloads()/downloadedContainer()/artworkUriFor() kept next to
  artists()/albums()/genres()/letterBucket(), using main's albumCategory
  signature.
- JellyfinMediaTree: "downloads" appended last in canonicalOrder so neither
  the pre-existing tabs nor the newly added ones shift; DOWNLOADS added to
  the getItem dispatch.
- MediaRepository: DOWNLOADS added to main's restructured staticIds; the
  local-only Downloads children path and the sync skip are preserved
  alongside shallowSyncSections.
- Resources: both sets of browse category entries and strings retained.
- Tests: kept main's artists/albums/genres ordering tests and updated the
  downloads ordering test to assert it stays last.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BSRADdi4UoKRejWDKeZDC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add browse categories: Artists, Albums (A–Z), and Genres

1 participant