Merge branch 'feature/auto-genres' into 'develop'

feat(auto): add Genres browsing to Android Auto

See merge request ultrasonic/ultrasonic!1382
This commit is contained in:
birdbird committed 2026-10-01 21:44:19 +00:00
commit c4febfc371
1 file changed
+93
@@ -91,6 +91,9 @@ private const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
private const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
private const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
private const val MEDIA_GENRE_ID = "MEDIA_GENRE_ID"
private const val MEDIA_GENRE_ITEM = "MEDIA_GENRE_ITEM"
private const val MEDIA_GENRE_SONG_ITEM = "MEDIA_GENRE_SONG_ITEM"
// Currently the display limit for long lists is 100 items
private const val DISPLAY_LIMIT = 100
@@ -124,6 +127,7 @@ class MediaLibrarySessionCallback(
private var starredSongsCache: List<Track>? = null
private var randomSongsCache: List<Track>? = null
private var searchSongsCache: List<Track>? = null
private var genreSongsCache: List<Track>? = null
private val musicService get() = musicServiceFactory.getMusicService()
private val isOffline get() = activeServerProvider.isOffline()
@@ -659,6 +663,10 @@ class MediaLibrarySessionCallback(
MEDIA_ARTIST_PLAY_ID -> playAllForArtist(mediaIdParts[1], mediaIdParts[2])
MEDIA_GENRE_ITEM -> playGenre(mediaIdParts[1])
MEDIA_GENRE_SONG_ITEM -> playGenreSong(mediaIdParts[1])
else -> null
}
@@ -731,6 +739,10 @@ class MediaLibrarySessionCallback(
MEDIA_PODCAST_ITEM -> getPodcastEpisodes(parentIdParts[1])
MEDIA_GENRE_ID -> getGenres()
MEDIA_GENRE_ITEM -> getSongsForGenre(parentIdParts[1])
else -> Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
}
}
@@ -846,6 +858,10 @@ class MediaLibrarySessionCallback(
MEDIA_ARTIST_PLAY_ID -> playAllForArtist(mediaIdParts[1], mediaIdParts[2])
MEDIA_GENRE_ITEM -> playGenre(mediaIdParts[1])
MEDIA_GENRE_SONG_ITEM -> playGenreSong(mediaIdParts[1])
else -> {
listOf()
}
@@ -879,6 +895,72 @@ class MediaLibrarySessionCallback(
}.ifEmpty { null }
}
private fun getGenres(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return mainScope.future {
val genres = serviceScope.future {
callWithErrorHandling { musicService.getGenres(false) }
}.await()
genres?.sortedBy { it.name }?.map { genre ->
mediaItems.add(
genre.name,
listOf(MEDIA_GENRE_ITEM, genre.name).joinToString("|"),
mediaType = MEDIA_TYPE_FOLDER_MIXED,
isBrowsable = true
)
}
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
private fun getSongsForGenre(
genre: String
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return mainScope.future {
val songs = serviceScope.future {
callWithErrorHandling { musicService.getSongsByGenre(genre, DISPLAY_LIMIT, 0) }
}.await()
if (songs != null) {
val tracks = songs.getTracks()
genreSongsCache = tracks
if (tracks.size > 1) {
mediaItems.addPlayAllItem(listOf(MEDIA_GENRE_ITEM, genre).joinToString("|"))
}
tracks.map { song ->
mediaItems.add(
song.toMediaItem(
listOf(MEDIA_GENRE_SONG_ITEM, song.id).joinToString("|")
)
)
}
}
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
private fun playGenre(genre: String): List<Track>? {
if (genreSongsCache == null) {
val content = serviceScope.future {
callWithErrorHandling { musicService.getSongsByGenre(genre, DISPLAY_LIMIT, 0) }
}.get()
genreSongsCache = content?.getTracks()
}
return genreSongsCache
}
private fun playGenreSong(songId: String): List<Track>? {
val song = genreSongsCache?.firstOrNull { it.id == songId }
if (song != null) return listOf(song)
return null
}
private fun getRootItems(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
@@ -922,6 +1004,17 @@ class MediaLibrarySessionCallback(
icon = R.drawable.ic_menu_playlists
)
if (!isOffline) {
mediaItems.add(
R.string.main_genres_title,
MEDIA_GENRE_ID,
null,
isBrowsable = true,
mediaType = MEDIA_TYPE_FOLDER_MIXED,
icon = R.drawable.ic_menu_browse
)
}
return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null))
}