From deabcafd49e9cccb3821aa98cb2e84196cca7a80 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 16 May 2026 11:09:50 +0200 Subject: [PATCH] Add audiobook support to Android Auto --- .../sessionbrowser/LibraryPageElement.kt | 6 ++ .../mobile/sessionbrowser/LibraryRoute.kt | 6 +- .../sessionbrowser/SessionBrowserCallback.kt | 2 + .../page/AudioBooksLibraryPage.kt | 38 +++++++++ .../sessionbrowser/page/RootLibraryPage.kt | 6 +- .../page/UserViewLibraryPage.kt | 79 +++++++++++++------ app/src/main/res/values/strings.xml | 1 + 7 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/AudioBooksLibraryPage.kt diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryPageElement.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryPageElement.kt index 6ab5fb22..c98b99af 100644 --- a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryPageElement.kt +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryPageElement.kt @@ -60,6 +60,12 @@ sealed interface LibraryPageElement { tag = albumPrimaryImageTag, ).toUri() + parentId != null && parentPrimaryImageTag != null -> api.imageApi.getItemImageUrl( + itemId = requireNotNull(parentId), + imageType = ImageType.PRIMARY, + tag = parentPrimaryImageTag, + ).toUri() + else -> null } } diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryRoute.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryRoute.kt index 7bd0a40a..11674c34 100644 --- a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryRoute.kt +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/LibraryRoute.kt @@ -4,6 +4,7 @@ package org.jellyfin.mobile.sessionbrowser import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID @@ -22,7 +23,7 @@ sealed interface LibraryRoute { data class Search(val query: String? = null) : LibraryRoute @Serializable - data class Library(val libraryId: UUID) : LibraryRoute + data class Library(val libraryId: UUID, val collectionType: CollectionType? = null) : LibraryRoute @Serializable data class Albums(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute @@ -30,6 +31,9 @@ sealed interface LibraryRoute { @Serializable data class Album(val albumId: UUID) : LibraryRoute + @Serializable + data class AudioBooks(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute + @Serializable data class Artists(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/SessionBrowserCallback.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/SessionBrowserCallback.kt index da5c9058..9f3fcbf6 100644 --- a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/SessionBrowserCallback.kt +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/SessionBrowserCallback.kt @@ -22,6 +22,7 @@ import org.jellyfin.mobile.sessionbrowser.page.AlbumLibraryPage import org.jellyfin.mobile.sessionbrowser.page.AlbumsLibraryPage import org.jellyfin.mobile.sessionbrowser.page.ArtistLibraryPage import org.jellyfin.mobile.sessionbrowser.page.ArtistsLibraryPage +import org.jellyfin.mobile.sessionbrowser.page.AudioBooksLibraryPage import org.jellyfin.mobile.sessionbrowser.page.FavoritesLibraryPage import org.jellyfin.mobile.sessionbrowser.page.GenreLibraryPage import org.jellyfin.mobile.sessionbrowser.page.GenresLibraryPage @@ -54,6 +55,7 @@ class SessionBrowserCallback( UserViewLibraryPage(context), AlbumsLibraryPage(api), AlbumLibraryPage(api), + AudioBooksLibraryPage(api), ArtistsLibraryPage(api), ArtistLibraryPage(api), FavoritesLibraryPage(api), diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/AudioBooksLibraryPage.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/AudioBooksLibraryPage.kt new file mode 100644 index 00000000..a436de88 --- /dev/null +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/AudioBooksLibraryPage.kt @@ -0,0 +1,38 @@ +package org.jellyfin.mobile.sessionbrowser.page + +import org.jellyfin.mobile.sessionbrowser.LibraryItemAction +import org.jellyfin.mobile.sessionbrowser.LibraryPageElement +import org.jellyfin.mobile.sessionbrowser.LibraryRoute +import org.jellyfin.mobile.sessionbrowser.libraryPage +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.api.ItemSortBy + +val AudioBooksLibraryPage = { api: ApiClient -> + libraryPage(grid = true) { route, offset, limit -> + if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) } + + val result by api.itemsApi.getItems( + parentId = route.libraryId, + includeItemTypes = listOf(BaseItemKind.AUDIO_BOOK), + sortBy = listOf(ItemSortBy.SORT_NAME), + recursive = true, + imageTypeLimit = 1, + enableImageTypes = listOf(ImageType.PRIMARY), + nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null, + nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter, + startIndex = offset, + limit = limit, + ) + + result.items.map { + LibraryPageElement.baseItem( + api = api, + item = it, + action = LibraryItemAction.Play(it), + ) + } + } +} diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/RootLibraryPage.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/RootLibraryPage.kt index 8fb5135a..4923b50e 100644 --- a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/RootLibraryPage.kt +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/RootLibraryPage.kt @@ -8,6 +8,8 @@ import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.model.api.CollectionType +private val collectionTypes = setOf(CollectionType.MUSIC, CollectionType.BOOKS) + /** * Root library page that returns the available libraries (user views) for Android Auto playback. * Note that this should ideally not exceed 4 items. @@ -17,7 +19,7 @@ val RootLibraryPage = { api: ApiClient -> val userViews by api.userViewsApi.getUserViews() userViews .items - .filter { it.collectionType == CollectionType.MUSIC } + .filter { collectionTypes.contains(it.collectionType) } .drop(offset) .take(limit) .map { @@ -25,7 +27,7 @@ val RootLibraryPage = { api: ApiClient -> api = api, item = it, image = null, - action = LibraryItemAction.Navigate(LibraryRoute.Library(it.id)), + action = LibraryItemAction.Navigate(LibraryRoute.Library(it.id, it.collectionType)), ) } } diff --git a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/UserViewLibraryPage.kt b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/UserViewLibraryPage.kt index 4e26359f..cbaea9c5 100644 --- a/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/UserViewLibraryPage.kt +++ b/app/src/main/java/org/jellyfin/mobile/sessionbrowser/page/UserViewLibraryPage.kt @@ -6,39 +6,66 @@ import org.jellyfin.mobile.sessionbrowser.LibraryItemAction import org.jellyfin.mobile.sessionbrowser.LibraryPageElement import org.jellyfin.mobile.sessionbrowser.LibraryRoute import org.jellyfin.mobile.sessionbrowser.libraryPage +import org.jellyfin.sdk.model.api.CollectionType val UserViewLibraryPage = { context: Context -> libraryPage { route, offset, limit -> - listOf( - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_albums), - action = LibraryItemAction.Navigate(LibraryRoute.Albums(route.libraryId)), - ), + buildList { + val isMusic = route.collectionType == CollectionType.MUSIC + val isBooks = route.collectionType == CollectionType.BOOKS - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_artists), - action = LibraryItemAction.Navigate(LibraryRoute.Artists(route.libraryId)), - ), + if (isMusic) { + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_albums), + action = LibraryItemAction.Navigate(LibraryRoute.Albums(route.libraryId)), + ), + ) - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_favorites), - action = LibraryItemAction.Navigate(LibraryRoute.Favorites(route.libraryId)), - ), + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_artists), + action = LibraryItemAction.Navigate(LibraryRoute.Artists(route.libraryId)), + ), + ) + } - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_genres), - action = LibraryItemAction.Navigate(LibraryRoute.Genres(route.libraryId)), - ), + if (isBooks) { + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_audiobooks), + action = LibraryItemAction.Navigate(LibraryRoute.AudioBooks(route.libraryId)), + ), + ) + } - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_playlists), - action = LibraryItemAction.Navigate(LibraryRoute.Playlists(route.libraryId)), - ), + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_favorites), + action = LibraryItemAction.Navigate(LibraryRoute.Favorites(route.libraryId)), + ), + ) - LibraryPageElement.Item( - title = context.getString(R.string.media_service_car_section_recents), - action = LibraryItemAction.Navigate(LibraryRoute.Recent(route.libraryId)), - ), - ).drop(offset).take(limit) + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_genres), + action = LibraryItemAction.Navigate(LibraryRoute.Genres(route.libraryId)), + ), + ) + + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_playlists), + action = LibraryItemAction.Navigate(LibraryRoute.Playlists(route.libraryId)), + ), + ) + + add( + LibraryPageElement.Item( + title = context.getString(R.string.media_service_car_section_recents), + action = LibraryItemAction.Navigate(LibraryRoute.Recent(route.libraryId)), + ), + ) + }.drop(offset).take(limit) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2d1eba9..2ff1f36f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Recently played Favorites Albums + Audiobooks Artists Songs Genres