diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt index 8cc456ff..96e7ac6b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -7,6 +7,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.RequestMetadata import androidx.media3.common.MediaMetadata import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -19,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil import com.cappielloantonio.tempo.util.Preferences.getImageSize import java.util.Date +@UnstableApi @Keep @Entity(tableName = "session_media_item") class SessionMediaItem() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java index 2421ebec..8f1ef4f5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -4,8 +4,10 @@ package com.cappielloantonio.tempo.repository; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.UnstableApi; import androidx.media3.session.LibraryResult; import com.cappielloantonio.tempo.App; @@ -183,7 +185,7 @@ public class AutomotiveRepository { return listenableFuture; } - public ListenableFuture>> getStarredArtists(String prefix, boolean playFromThere) { + public ListenableFuture>> getStarredArtists(String prefix) { final SettableFuture>> listenableFuture = SettableFuture.create(); App.getSubsonicClientInstance(false) @@ -195,6 +197,8 @@ public class AutomotiveRepository { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getArtists() != null) { List artists = response.body().getSubsonicResponse().getStarred2().getArtists(); + Collections.shuffle(artists); + List mediaItems = new ArrayList<>(); for (ArtistID3 artist : artists) { @@ -202,8 +206,8 @@ public class AutomotiveRepository { MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setTitle(artist.getName()) - .setIsBrowsable(!playFromThere) - .setIsPlayable(playFromThere) + .setIsBrowsable(true) + .setIsPlayable(false) .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) .setArtworkUri(artworkUri) .build(); @@ -461,7 +465,7 @@ public class AutomotiveRepository { return listenableFuture; } - public ListenableFuture>> getNewestPodcastEpisodes(String prefix, int count) { + public ListenableFuture>> getNewestPodcastEpisodes(int count) { final SettableFuture>> listenableFuture = SettableFuture.create(); App.getSubsonicClientInstance(false) @@ -487,7 +491,7 @@ public class AutomotiveRepository { .build(); MediaItem mediaItem = new MediaItem.Builder() - .setMediaId(prefix + episode.getId()) + .setMediaId(episode.getId()) .setMediaMetadata(mediaMetadata) .setUri(MusicUtil.getStreamUri(episode.getStreamId())) .build(); @@ -514,7 +518,7 @@ public class AutomotiveRepository { return listenableFuture; } - public ListenableFuture>> getInternetRadioStations(String prefix) { + public ListenableFuture>> getInternetRadioStations() { final SettableFuture>> listenableFuture = SettableFuture.create(); App.getSubsonicClientInstance(false) @@ -538,7 +542,7 @@ public class AutomotiveRepository { .build(); MediaItem mediaItem = new MediaItem.Builder() - .setMediaId(prefix + radioStation.getId()) + .setMediaId(radioStation.getId()) .setMediaMetadata(mediaMetadata) .setUri(radioStation.getStreamUrl()) .build(); @@ -575,7 +579,6 @@ public class AutomotiveRepository { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null && response.body().getSubsonicResponse().getAlbum().getSongs() != null) { - List tracks = response.body().getSubsonicResponse().getAlbum().getSongs(); setChildrenMetadata(tracks); @@ -599,6 +602,58 @@ public class AutomotiveRepository { return listenableFuture; } + public ListenableFuture>> getArtistAlbum(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + public ListenableFuture>> getPlaylistSongs(String id) { final SettableFuture>> listenableFuture = SettableFuture.create(); @@ -630,6 +685,118 @@ public class AutomotiveRepository { return listenableFuture; } + public ListenableFuture>> getMadeForYou(String id, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(id, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null && response.body().getSubsonicResponse().getSimilarSongs2().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSimilarSongs2().getSongs(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> search(String query, String albumPrefix, String artistPrefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 20, 20, 20) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSearchResult3() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { + for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(artistPrefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { + for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(albumPrefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSearchResult3().getSongs(); + setChildrenMetadata(tracks); + mediaItems.addAll(MappingUtil.mapMediaItems(tracks)); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + @OptIn(markerClass = UnstableApi.class) public void setChildrenMetadata(List children) { long timestamp = System.currentTimeMillis(); ArrayList sessionMediaItems = new ArrayList<>(); @@ -645,6 +812,7 @@ public class AutomotiveRepository { thread.start(); } + @OptIn(markerClass = UnstableApi.class) public void setPodcastEpisodesMetadata(List podcastEpisodes) { long timestamp = System.currentTimeMillis(); ArrayList sessionMediaItems = new ArrayList<>(); @@ -660,6 +828,7 @@ public class AutomotiveRepository { thread.start(); } + @OptIn(markerClass = UnstableApi.class) public void setInternetRadioStationsMetadata(List internetRadioStations) { long timestamp = System.currentTimeMillis(); ArrayList sessionMediaItems = new ArrayList<>(); @@ -736,11 +905,11 @@ public class AutomotiveRepository { } } + @OptIn(markerClass = UnstableApi.class) private static class GetMediaItemsThreadSafe implements Runnable { private final SessionMediaItemDao sessionMediaItemDao; private final Long timestamp; - - private List mediaItems = new ArrayList<>(); + private final List mediaItems = new ArrayList<>(); public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) { this.sessionMediaItemDao = sessionMediaItemDao; @@ -758,43 +927,6 @@ public class AutomotiveRepository { } } - /* private static class InsertThreadSafe implements Runnable { - private final SessionMediaItemDao sessionMediaItemDao; - private final List children; - private final List podcastEpisodes; - private final List internetRadioStations; - private final long timestamp; - - public InsertThreadSafe(SessionMediaItemDao sessionMediaItemDao, List children, List podcastEpisodes, List internetRadioStations, long timestamp) { - this.sessionMediaItemDao = sessionMediaItemDao; - this.children = children; - this.podcastEpisodes = podcastEpisodes; - this.internetRadioStations = internetRadioStations; - this.timestamp = timestamp; - } - - @Override - public void run() { - if (children != null) { - SessionMediaItem sessionMediaItem = new SessionMediaItem(children); - sessionMediaItem.setTimestamp(timestamp); - sessionMediaItemDao.insert(sessionMediaItem); - } - - if (podcastEpisodes != null) { - SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisodes); - sessionMediaItem.setTimestamp(timestamp); - sessionMediaItemDao.insert(sessionMediaItem); - } - - if (internetRadioStations != null) { - SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStations); - sessionMediaItem.setTimestamp(timestamp); - sessionMediaItemDao.insert(sessionMediaItem); - } - } - } */ - private static class InsertAllThreadSafe implements Runnable { private final SessionMediaItemDao sessionMediaItemDao; private final List sessionMediaItems; diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt index 82a1b6ef..438ab0b4 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt @@ -1,20 +1,15 @@ package com.cappielloantonio.tempo.service -import android.content.Context import android.net.Uri -import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaMetadata import androidx.media3.session.LibraryResult -import androidx.media3.session.MediaLibraryService -import com.cappielloantonio.tempo.model.SessionMediaItem import com.cappielloantonio.tempo.repository.AutomotiveRepository import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import java.lang.Exception object MediaBrowserTree { @@ -36,7 +31,6 @@ object MediaBrowserTree { private const val MOST_PLAYED_ID = "[mostPlayedID]" private const val LAST_PLAYED_ID = "[lastPlayedID]" private const val RECENTLY_ADDED_ID = "[recentlyAddedID]" - private const val BEST_OF_ID = "[bestOfID]" private const val MADE_FOR_YOU_ID = "[madeForYouID]" private const val STARRED_TRACKS_ID = "[starredTracksID]" private const val STARRED_ALBUMS_ID = "[starredAlbumsID]" @@ -51,7 +45,9 @@ object MediaBrowserTree { // Second level OTHER_ID private const val PODCAST_ID = "[podcastID]" private const val RADIO_ID = "[radioID]" - private const val DOWNLOAD_ID = "[downloadID]" + + private const val ALBUM_ID = "[albumID]" + private const val ARTIST_ID = "[artistID]" private class MediaItemNode(val item: MediaItem) { private val children: MutableList = ArrayList() @@ -103,7 +99,7 @@ object MediaBrowserTree { .build() } - fun initialize(context: Context, automotiveRepository: AutomotiveRepository) { + fun initialize(automotiveRepository: AutomotiveRepository) { this.automotiveRepository = automotiveRepository if (isInitialized) return @@ -197,17 +193,6 @@ object MediaBrowserTree { ) ) - treeNodes[BEST_OF_ID] = - MediaItemNode( - buildMediaItem( - title = "Best of", - mediaId = BEST_OF_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS - ) - ) - treeNodes[MADE_FOR_YOU_ID] = MediaItemNode( buildMediaItem( @@ -255,11 +240,10 @@ object MediaBrowserTree { treeNodes[HOME_ID]!!.addChild(MOST_PLAYED_ID) treeNodes[HOME_ID]!!.addChild(LAST_PLAYED_ID) treeNodes[HOME_ID]!!.addChild(RECENTLY_ADDED_ID) - treeNodes[HOME_ID]!!.addChild(BEST_OF_ID) treeNodes[HOME_ID]!!.addChild(MADE_FOR_YOU_ID) - // treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID) - // treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID) - // treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID) + treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID) + treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID) + treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID) // Second level LIBRARY_ID @@ -312,20 +296,8 @@ object MediaBrowserTree { ) ) - treeNodes[DOWNLOAD_ID] = - MediaItemNode( - buildMediaItem( - title = "Downloads", - mediaId = DOWNLOAD_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS - ) - ) - treeNodes[OTHER_ID]!!.addChild(PODCAST_ID) treeNodes[OTHER_ID]!!.addChild(RADIO_ID) - // treeNodes[OTHER_ID]!!.addChild(DOWNLOAD_ID) } fun getRootItem(): MediaItem { @@ -333,8 +305,7 @@ object MediaBrowserTree { } fun getChildren( - id: String, - params: MediaLibraryService.LibraryParams? + id: String ): ListenableFuture>> { return when (id) { ROOT_ID -> treeNodes[ROOT_ID]?.getChildren()!! @@ -345,17 +316,14 @@ object MediaBrowserTree { MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 100) LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 100) RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100) - BEST_OF_ID -> automotiveRepository.getStarredArtists(id, true) - MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id, true) + MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id) STARRED_TRACKS_ID -> automotiveRepository.starredSongs STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id) - STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id, false) + STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id) FOLDER_ID -> automotiveRepository.getMusicFolders(id) PLAYLIST_ID -> automotiveRepository.getPlaylists(id) - PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(id, 100) - RADIO_ID -> automotiveRepository.getInternetRadioStations(id) - - DOWNLOAD_ID -> Futures.immediateFuture(null) + PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100) + RADIO_ID -> automotiveRepository.internetRadioStations else -> { if (id.startsWith(MOST_PLAYED_ID)) { @@ -382,12 +350,30 @@ object MediaBrowserTree { ) } - if (id.startsWith(BEST_OF_ID)) { - + if (id.startsWith(MADE_FOR_YOU_ID)) { + return automotiveRepository.getMadeForYou( + id.removePrefix( + MADE_FOR_YOU_ID + ), + 20 + ) } - if (id.startsWith(MADE_FOR_YOU_ID)) { + if (id.startsWith(STARRED_ALBUMS_ID)) { + return automotiveRepository.getAlbumTracks( + id.removePrefix( + STARRED_ALBUMS_ID + ) + ) + } + if (id.startsWith(STARRED_ARTISTS_ID)) { + return automotiveRepository.getArtistAlbum( + STARRED_ALBUMS_ID, + id.removePrefix( + STARRED_ARTISTS_ID + ) + ) } if (id.startsWith(FOLDER_ID)) { @@ -425,12 +411,21 @@ object MediaBrowserTree { ) } - if (id.startsWith(PODCAST_ID)) { - + if (id.startsWith(ALBUM_ID)) { + return automotiveRepository.getAlbumTracks( + id.removePrefix( + ALBUM_ID + ) + ) } - if (id.startsWith(RADIO_ID)) { - + if (id.startsWith(ARTIST_ID)) { + return automotiveRepository.getArtistAlbum( + ALBUM_ID, + id.removePrefix( + ARTIST_ID + ) + ) } return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) @@ -461,4 +456,12 @@ object MediaBrowserTree { return updatedMediaItems } + + fun search(query: String): ListenableFuture>> { + return automotiveRepository.search( + query, + ALBUM_ID, + ARTIST_ID + ) + } } diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index e9f67e45..747a45b2 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -22,10 +22,9 @@ open class MediaLibrarySessionCallback( automotiveRepository: AutomotiveRepository ) : MediaLibraryService.MediaLibrarySession.Callback { - private val TAG = "MediaLibraryServiceCall" init { - MediaBrowserTree.initialize(context, automotiveRepository) + MediaBrowserTree.initialize(automotiveRepository) } private val customLayoutCommandButtons: List = listOf( @@ -118,7 +117,7 @@ open class MediaLibrarySessionCallback( pageSize: Int, params: MediaLibraryService.LibraryParams? ): ListenableFuture>> { - return MediaBrowserTree.getChildren(parentId, params) + return MediaBrowserTree.getChildren(parentId) } override fun onAddMediaItems( @@ -133,17 +132,17 @@ open class MediaLibrarySessionCallback( ) } - /* override fun onSearch( + override fun onSearch( session: MediaLibraryService.MediaLibrarySession, browser: MediaSession.ControllerInfo, query: String, params: MediaLibraryService.LibraryParams? ): ListenableFuture> { - session.notifySearchResultChanged(browser, query, MediaBrowserTree.search(query).size, params) + session.notifySearchResultChanged(browser, query, 60, params) return Futures.immediateFuture(LibraryResult.ofVoid()) - } */ + } - /* override fun onGetSearchResult( + override fun onGetSearchResult( session: MediaLibraryService.MediaLibrarySession, browser: MediaSession.ControllerInfo, query: String, @@ -151,12 +150,8 @@ open class MediaLibrarySessionCallback( pageSize: Int, params: MediaLibraryService.LibraryParams? ): ListenableFuture>> { - return Futures.immediateFuture( - LibraryResult.ofItemList( - MediaBrowserTree.search(query), params - ) - ) - } */ + return MediaBrowserTree.search(query) + } companion object { private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =