feat(browse): add Artists, Albums (A–Z) and Genres categories - #42
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #37.
Adds three browse categories, selectable through the existing Browse categories setting.
getArtistAlbumspath)SortNameDesign 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 withnameStartsWith; the#bucket usesnameLessThan = "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, notparentId, sogetItemChildren's generic query returns nothing for one — hence aMEDIA_TYPE_GENREbranch. Same reason the genre shuffle gets its ownSHUFFLE_GENRE:prefix: reusingSHUFFLE_FOLDER:would have sent a genre id into aparentIdquery and come back empty.genresApiis used becausemusicGenresApiis@Deprecatedin 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
getChildrenas 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:assembleDebugpasses.:automotive:testDebugUnitTest— 362 tests, 0 failures, 0 skipped (--rerun-tasks, not cached). 12 of those are new inJellyfinMediaTreeBrowseTestand assert the actual Jellyfin query shapes rather than just the item tree:nameStartsWithvsnameLessThanper bucket,genreIdsfor 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:
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 forCOLLECTION_FOLDERinJellyfinMediaTree.kt.Albums → #returning only numeric/symbol-titled albums.Can't create mediaItem for MusicGenrein logcat.Known limitation
Letter buckets are shown unconditionally, so a library with nothing under
Qpresents 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