ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt

1244 lines
47 KiB
Kotlin
Raw Normal View History

2021-07-18 13:17:29 +02:00
/*
2022-04-04 21:18:07 +02:00
* CustomMediaLibrarySessionCallback.kt
* Copyright (C) 2009-2022 Ultrasonic developers
2021-07-18 13:17:29 +02:00
*
* Distributed under terms of the GNU GPLv3 license.
*/
2022-04-04 21:18:07 +02:00
package org.moire.ultrasonic.playback
2022-04-04 21:18:07 +02:00
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
2022-07-02 01:27:12 +02:00
import androidx.media3.common.HeartRating
2022-04-04 21:18:07 +02:00
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
import androidx.media3.common.Player
2022-07-02 01:27:12 +02:00
import androidx.media3.common.Rating
2022-04-04 21:18:07 +02:00
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
2022-07-02 01:27:12 +02:00
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE
import androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN
import androidx.media3.session.SessionResult.RESULT_SUCCESS
2022-04-04 21:18:07 +02:00
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.FutureCallback
2022-04-04 21:18:07 +02:00
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
2022-04-04 21:18:07 +02:00
import kotlinx.coroutines.guava.future
import kotlinx.coroutines.launch
2022-04-04 21:18:07 +02:00
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
2021-07-18 11:33:39 +02:00
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
2022-04-04 21:18:07 +02:00
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
2021-07-18 11:33:39 +02:00
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Track
2022-04-04 21:18:07 +02:00
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory
2022-07-03 18:23:22 +02:00
import org.moire.ultrasonic.util.MainThreadExecutor
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
2021-07-18 13:17:29 +02:00
private const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
private const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
private const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
private const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
private const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
private const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
private const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
private const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
private const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
private const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
private const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
private const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
private const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
private const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
private const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
private const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
private const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
private const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
private const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
private const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
private const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
private const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
private const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
private const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
private const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
private const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
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"
// Currently the display limit for long lists is 100 items
2021-07-18 13:17:29 +02:00
private const val DISPLAY_LIMIT = 100
private const val SEARCH_LIMIT = 10
// List of available custom SessionCommands
const val SESSION_CUSTOM_SET_RATING = "SESSION_CUSTOM_SET_RATING"
/**
* MediaBrowserService implementation for e.g. Android Auto
*/
2022-04-05 21:56:13 +02:00
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
2022-07-03 18:23:22 +02:00
class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) :
2022-06-19 18:21:33 +02:00
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
private val mediaPlayerController by inject<MediaPlayerController>()
private val activeServerProvider: ActiveServerProvider by inject()
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var playlistCache: List<Track>? = null
private var starredSongsCache: List<Track>? = null
private var randomSongsCache: List<Track>? = null
private var searchSongsCache: List<Track>? = null
private val musicService get() = MusicServiceFactory.getMusicService()
private val isOffline get() = ActiveServerProvider.isOffline()
private val useId3Tags get() = Settings.shouldUseId3Tags
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
2022-04-04 21:18:07 +02:00
/**
* Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link
* MediaBrowser#getLibraryRoot(LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} may differ from the given {@link LibraryParams params}
* if the session can't provide a root that matches with the {@code params}.
*
* <p>To allow browsing the media library, return a {@link LibraryResult} with {@link
* LibraryResult#RESULT_SUCCESS} and a root {@link MediaItem} with a valid {@link
* MediaItem#mediaId}. The media id is required for the browser to get the children under the
* root.
*
* <p>Interoperability: If this callback is called because a legacy {@link
* android.support.v4.media.MediaBrowserCompat} has requested a {@link
* androidx.media.MediaBrowserServiceCompat.BrowserRoot}, then the main thread may be blocked
* until the returned future is done. If your service may be queried by a legacy {@link
* android.support.v4.media.MediaBrowserCompat}, you should ensure that the future completes
* quickly to avoid blocking the main thread for a long period of time.
*
* @param session The session for this event.
* @param browser The browser information.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a root media item.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT
*/
override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(
LibraryResult.ofItem(
buildMediaItem(
"Root Folder",
MEDIA_ROOT_ID,
isPlayable = false,
folderType = FOLDER_TYPE_MIXED
),
params
)
)
}
2022-07-02 01:27:12 +02:00
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
2022-07-05 18:34:24 +02:00
/*
* TODO: Currently we need to create a custom session command, see https://github.com/androidx/media/issues/107
* When this issue is fixed we should be able to remove this method again
*/
availableSessionCommands.add(SessionCommand(SESSION_CUSTOM_SET_RATING, Bundle()))
2022-07-02 01:27:12 +02:00
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
2022-04-04 21:18:07 +02:00
override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
playFromMediaId(mediaId)
2022-04-05 21:56:13 +02:00
// TODO:
2022-04-05 10:10:24 +02:00
// Create LRU Cache of MediaItems, fill it in the other calls
// and retrieve it here.
2022-04-04 21:18:07 +02:00
return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
}
override fun onGetChildren(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
// TODO: params???
return onLoadChildren(parentId)
}
2022-07-02 01:27:12 +02:00
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
var customCommandFuture: ListenableFuture<SessionResult>? = null
when (customCommand.customAction) {
SESSION_CUSTOM_SET_RATING -> {
/*
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value
* See https://github.com/androidx/media/issues/33
*/
val track = mediaPlayerController.currentPlayingLegacy?.track
if (track != null) {
customCommandFuture = onSetRating(
2022-07-02 01:27:12 +02:00
session,
controller,
HeartRating(!track.starred)
2022-07-02 01:27:12 +02:00
)
Futures.addCallback(
customCommandFuture,
object : FutureCallback<SessionResult> {
override fun onSuccess(result: SessionResult) {
track.starred = !track.starred
2022-07-03 18:23:22 +02:00
// This needs to be called on the main Thread
libraryService.onUpdateNotification(session)
}
override fun onFailure(t: Throwable) {
Toast.makeText(
mediaPlayerController.context,
"There was an error updating the rating",
LENGTH_SHORT
).show()
}
2022-07-02 21:58:45 +02:00
},
2022-07-03 18:23:22 +02:00
MainThreadExecutor()
2022-07-02 01:27:12 +02:00
)
}
}
else -> {
Timber.d(
"CustomCommand not recognized %s with extra %s",
customCommand.customAction,
customCommand.customExtras.toString()
)
}
2022-07-02 01:27:12 +02:00
}
if (customCommandFuture != null)
return customCommandFuture
2022-07-02 01:27:12 +02:00
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onSetRating(
session: MediaSession,
controller: MediaSession.ControllerInfo,
rating: Rating
): ListenableFuture<SessionResult> {
if (session.player.currentMediaItem != null)
return onSetRating(
session,
controller,
session.player.currentMediaItem!!.mediaId,
rating
)
return super.onSetRating(session, controller, rating)
}
override fun onSetRating(
session: MediaSession,
controller: MediaSession.ControllerInfo,
mediaId: String,
rating: Rating
): ListenableFuture<SessionResult> {
return serviceScope.future {
if (rating is HeartRating) {
try {
if (rating.isHeart) {
musicService.star(mediaId, null, null)
} else {
musicService.unstar(mediaId, null, null)
}
} catch (all: Exception) {
Timber.e(all)
// TODO: Better handle exception
return@future SessionResult(RESULT_ERROR_UNKNOWN)
}
return@future SessionResult(RESULT_SUCCESS)
}
return@future SessionResult(RESULT_ERROR_BAD_VALUE)
}
}
2022-06-20 09:49:49 +02:00
/*
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
* and thereby customarily it is required to rebuild it..
* See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error
*/
2022-06-19 18:21:33 +02:00
override fun onAddMediaItems(
mediaSession: MediaSession,
2022-04-04 21:18:07 +02:00
controller: MediaSession.ControllerInfo,
2022-06-19 18:21:33 +02:00
mediaItems: MutableList<MediaItem>
): ListenableFuture<MutableList<MediaItem>> {
val updatedMediaItems = mediaItems.map { mediaItem ->
mediaItem.buildUpon()
.setUri(mediaItem.requestMetadata.mediaUri)
.build()
}
2022-06-19 18:21:33 +02:00
return Futures.immediateFuture(updatedMediaItems.toMutableList())
2022-04-04 21:18:07 +02:00
}
@Suppress("ReturnCount", "ComplexMethod")
fun onLoadChildren(
parentId: String,
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
val parentIdParts = parentId.split('|')
when (parentIdParts.first()) {
MEDIA_ROOT_ID -> return getRootItems()
MEDIA_LIBRARY_ID -> return getLibrary()
MEDIA_ARTIST_ID -> return getArtists()
MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1])
MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME)
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
)
MEDIA_PLAYLIST_ID -> return getPlaylists()
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT)
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST)
MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT)
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM)
MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED)
MEDIA_SONG_RANDOM_ID -> return getRandomSongs()
MEDIA_SONG_STARRED_ID -> return getStarredSongs()
MEDIA_SHARE_ID -> return getShares()
MEDIA_BOOKMARK_ID -> return getBookmarks()
MEDIA_PODCAST_ID -> return getPodcasts()
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2])
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
parentIdParts[1], parentIdParts[2]
)
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2])
MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1])
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1])
else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
}
2022-04-04 21:18:07 +02:00
}
2022-04-04 21:18:07 +02:00
fun onSearch(
query: String,
extras: Bundle?,
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
Timber.d("AutoMediaBrowserService onSearch query: %s", query)
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT)
val searchResult = callWithErrorHandling { musicService.search(criteria) }
2022-04-04 21:18:07 +02:00
// TODO Add More... button to categories
if (searchResult != null) {
searchResult.artists.map { artist ->
mediaItems.add(
artist.name ?: "",
listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"),
FOLDER_TYPE_ARTISTS
)
}
searchResult.albums.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
FOLDER_TYPE_ALBUMS
)
}
searchSongsCache = searchResult.songs
searchResult.songs.map { song ->
mediaItems.add(
buildMediaItemFromTrack(
song,
listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"),
isPlayable = true
)
)
}
}
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
@Suppress("MagicNumber", "ComplexMethod")
private fun playFromMediaId(mediaId: String?) {
Timber.d(
"AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s",
mediaId
)
if (mediaId == null) return
val mediaIdParts = mediaId.split('|')
when (mediaIdParts.first()) {
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
)
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
)
MEDIA_SONG_STARRED_ID -> playStarredSongs()
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
MEDIA_SONG_RANDOM_ITEM -> playRandomSong(mediaIdParts[1])
MEDIA_SHARE_ITEM -> playShare(mediaIdParts[1])
MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2])
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
mediaIdParts[1], mediaIdParts[2]
)
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
}
}
private fun playFromSearchCommand(query: String?) {
Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query)
if (query.isNullOrBlank()) playRandomSongs()
serviceScope.launch {
val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT)
val searchResult = callWithErrorHandling { musicService.search(criteria) }
// Try to find the best match
if (searchResult != null) {
val song = searchResult.songs
.asSequence()
.sortedByDescending { song -> song.starred }
.sortedByDescending { song -> song.averageRating }
.sortedByDescending { song -> song.userRating }
.sortedByDescending { song -> song.closeness }
.firstOrNull()
if (song != null) playSong(song)
}
}
}
2021-07-18 13:17:29 +02:00
private fun playSearch(id: String) {
2021-07-18 11:33:39 +02:00
serviceScope.launch {
// If there is no cache, we can't play the selected song.
if (searchSongsCache != null) {
val song = searchSongsCache!!.firstOrNull { x -> x.id == id }
if (song != null) playSong(song)
}
}
}
2022-04-04 21:18:07 +02:00
private fun getRootItems(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
if (!isOffline)
mediaItems.add(
R.string.music_library_label,
MEDIA_LIBRARY_ID,
null
)
mediaItems.add(
R.string.main_artists_title,
MEDIA_ARTIST_ID,
2022-04-04 21:18:07 +02:00
null,
folderType = FOLDER_TYPE_ARTISTS
)
2021-07-18 11:33:39 +02:00
if (!isOffline)
mediaItems.add(
R.string.main_albums_title,
MEDIA_ALBUM_ID,
2022-04-04 21:18:07 +02:00
null,
folderType = FOLDER_TYPE_ALBUMS
2021-07-18 11:33:39 +02:00
)
mediaItems.add(
R.string.playlist_label,
MEDIA_PLAYLIST_ID,
2022-04-04 21:18:07 +02:00
null,
folderType = FOLDER_TYPE_PLAYLISTS
)
2022-04-04 21:18:07 +02:00
return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null))
}
2022-04-04 21:18:07 +02:00
private fun getLibrary(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
// Songs
mediaItems.add(
R.string.main_songs_random,
MEDIA_SONG_RANDOM_ID,
2022-04-04 21:18:07 +02:00
R.string.main_songs_title,
folderType = FOLDER_TYPE_TITLES
)
mediaItems.add(
R.string.main_songs_starred,
MEDIA_SONG_STARRED_ID,
2022-04-04 21:18:07 +02:00
R.string.main_songs_title,
folderType = FOLDER_TYPE_TITLES
)
// Albums
mediaItems.add(
R.string.main_albums_newest,
MEDIA_ALBUM_NEWEST_ID,
R.string.main_albums_title
)
mediaItems.add(
R.string.main_albums_recent,
MEDIA_ALBUM_RECENT_ID,
2022-04-04 21:18:07 +02:00
R.string.main_albums_title,
folderType = FOLDER_TYPE_ALBUMS
)
mediaItems.add(
R.string.main_albums_frequent,
MEDIA_ALBUM_FREQUENT_ID,
2022-04-04 21:18:07 +02:00
R.string.main_albums_title,
folderType = FOLDER_TYPE_ALBUMS
)
mediaItems.add(
R.string.main_albums_random,
MEDIA_ALBUM_RANDOM_ID,
2022-04-04 21:18:07 +02:00
R.string.main_albums_title,
folderType = FOLDER_TYPE_ALBUMS
)
mediaItems.add(
R.string.main_albums_starred,
MEDIA_ALBUM_STARRED_ID,
2022-04-04 21:18:07 +02:00
R.string.main_albums_title,
folderType = FOLDER_TYPE_ALBUMS
)
// Other
2022-04-04 21:18:07 +02:00
mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null)
mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null)
mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null)
2022-04-04 21:18:07 +02:00
return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null))
}
2021-07-18 13:17:29 +02:00
private fun getArtists(
section: String? = null
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val childMediaId: String
var artists = if (!isOffline && useId3Tags) {
2021-07-18 11:33:39 +02:00
childMediaId = MEDIA_ARTIST_ITEM
// TODO this list can be big so we're not refreshing.
// Maybe a refresh menu item can be added
2021-07-18 11:33:39 +02:00
callWithErrorHandling { musicService.getArtists(false) }
} else {
2021-07-18 11:33:39 +02:00
// This will be handled at getSongsForAlbum, which supports navigation
childMediaId = MEDIA_ALBUM_ITEM
callWithErrorHandling { musicService.getIndexes(musicFolderId, false) }
}
2021-07-18 11:33:39 +02:00
if (artists != null) {
if (section != null)
artists = artists.filter { artist ->
getSectionFromName(artist.name ?: "") == section
}
2021-07-18 11:33:39 +02:00
// If there are too many artists, create alphabetic index of them
2021-07-18 13:17:29 +02:00
if (section == null && artists.count() > DISPLAY_LIMIT) {
2021-07-18 11:33:39 +02:00
val index = mutableListOf<String>()
// TODO This sort should use ignoredArticles somehow...
artists = artists.sortedBy { artist -> artist.name }
artists.map { artist ->
val currentSection = getSectionFromName(artist.name ?: "")
if (!index.contains(currentSection)) {
index.add(currentSection)
mediaItems.add(
currentSection,
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_ARTISTS
2021-07-18 11:33:39 +02:00
)
}
}
} else {
artists.map { artist ->
mediaItems.add(
2021-07-18 11:33:39 +02:00
artist.name ?: "",
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_ARTISTS
)
}
}
2021-07-18 11:33:39 +02:00
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
private fun getAlbumsForArtist(
id: String,
name: String
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val albums = if (!isOffline && useId3Tags) {
2021-07-18 13:17:29 +02:00
callWithErrorHandling { musicService.getArtist(id, name, false) }
} else {
callWithErrorHandling {
musicService.getMusicDirectory(id, name, false).getAlbums()
}
2021-07-18 11:33:39 +02:00
}
albums?.map { album ->
2021-07-18 11:33:39 +02:00
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_ALBUMS
2021-07-18 11:33:39 +02:00
)
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
private fun getSongsForAlbum(
id: String,
name: String
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val songs = listSongsInMusicService(id, name)
if (songs != null) {
Merge update build tools #755 by Holger Müller Squashed commit of the following: commit 4491c65b1bfa8f507e9c998878254d61bc52f962 Merge: 51ff716f 77865a14 Author: tzugen <67737443+tzugen@users.noreply.github.com> Date: Tue Jun 21 20:50:05 2022 +0200 Merge branch 'develop' into gradle-update commit 51ff716ff5fc8aeb29bc557844747c0127ab1f78 Author: Holger Müller <github@euhm.de> Date: Tue Jun 21 20:38:52 2022 +0200 fixed lint warning commit 18c31a5704e7d92a04009e2f3ed8f8b9ca28b577 Author: Holger Müller <github@euhm.de> Date: Tue Jun 21 20:38:35 2022 +0200 fixed lint warning commit 603654c262ed86a66b4127513764cd252cdedfff Author: Holger Müller <github@euhm.de> Date: Tue Jun 21 20:37:51 2022 +0200 API is > lollipop ... target removed commit b38a7211de0f206465640d39f8f3189695afbd38 Author: Holger Müller <github@euhm.de> Date: Tue Jun 21 20:37:07 2022 +0200 new created after fixes commit 4929a526f7e9154755cae2c16fdf431550ae2c2a Author: tzugen <tzugen@riseup.net> Date: Tue Jun 21 10:43:16 2022 +0200 Disable ObsoleteLintCustomCheck commit d0c30f0b6b1d4de2f3c46a166a4f1058223ad3a8 Author: tzugen <tzugen@riseup.net> Date: Tue Jun 21 10:14:06 2022 +0200 Update more libs commit e2fa447bbfbd19119393297c19de0f3237e1ccd3 Merge: d4ead495 ff9c7b24 Author: tzugen <67737443+tzugen@users.noreply.github.com> Date: Tue Jun 21 09:47:03 2022 +0200 Merge branch 'develop' into gradle-update commit d4ead49548d11f51dd9c29478972dcd4553d352a Merge: 2dac6a7e 9a73d72f Author: Holger Müller <github@euhm.de> Date: Tue Jun 21 08:50:42 2022 +0200 merged with develop branch commit 2dac6a7e01e4ce5dc619426553ea08743bd9524e Author: Holger Müller <github@euhm.de> Date: Mon Jun 20 21:45:22 2022 +0200 update to android image tag 2022.06.1 commit f3dc259c390c256a66f96c3f003e34daf0fdea14 Author: Holger Müller <github@euhm.de> Date: Mon Jun 20 20:56:37 2022 +0200 rebuild lint-baseline.xml commit c71bc1212a8570f8ee99a80926c7b3b6036c672f Author: Holger Müller <github@euhm.de> Date: Mon Jun 20 20:55:00 2022 +0200 removed unneeded cast commit eca136dabedab838e8a02be1a6fd740c4d0934ec Author: Holger Müller <github@euhm.de> Date: Fri Jun 17 23:58:37 2022 +0200 commit signed commit 540f47633485cddeb4e68626532e54707a66bab8 Author: Holger Müller <github@euhm.de> Date: Fri Jun 17 23:40:59 2022 +0200 commit signed Signed-off-by: Holger Müller <github@euhm.de> commit 986bd013a49afc56ff8992634f2ce126339eecfc Author: Holger Müller <github@euhm.de> Date: Fri Jun 17 23:27:20 2022 +0200 push to latest gradle version, set targetSdk to 33 Signed-off-by: tzugen <tzugen@riseup.net>
2022-06-21 21:05:58 +02:00
if (songs.getChildren(includeDirs = true, includeFiles = false).isEmpty() &&
songs.getChildren(includeDirs = false, includeFiles = true).isNotEmpty()
2021-07-18 11:33:39 +02:00
)
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getTracks().take(DISPLAY_LIMIT)
2021-07-18 11:33:39 +02:00
items.map { item ->
if (item.isDirectory)
mediaItems.add(
2022-04-04 21:18:07 +02:00
item.title ?: "",
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"),
FOLDER_TYPE_TITLES
2021-07-18 11:33:39 +02:00
)
else
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
item,
listOf(
MEDIA_ALBUM_SONG_ITEM,
id,
name,
item.id
).joinToString("|"),
isPlayable = true
2021-07-18 11:33:39 +02:00
)
)
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
private fun getAlbums(
2021-07-18 11:33:39 +02:00
type: AlbumListType,
page: Int? = null
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
2021-07-18 13:17:29 +02:00
val offset = (page ?: 0) * DISPLAY_LIMIT
2021-07-18 11:33:39 +02:00
val albums = if (useId3Tags) {
2021-07-18 13:17:29 +02:00
callWithErrorHandling {
musicService.getAlbumList2(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
2021-07-18 11:33:39 +02:00
} else {
2021-07-18 13:17:29 +02:00
callWithErrorHandling {
musicService.getAlbumList(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
2021-07-18 11:33:39 +02:00
}
albums?.map { album ->
2021-07-18 11:33:39 +02:00
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_ALBUMS
2021-07-18 11:33:39 +02:00
)
}
if (albums?.size ?: 0 >= DISPLAY_LIMIT)
2021-07-18 11:33:39 +02:00
mediaItems.add(
R.string.search_more,
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"),
null
)
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
2022-04-04 21:18:07 +02:00
private fun getPlaylists(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val playlists = callWithErrorHandling { musicService.getPlaylists(true) }
playlists?.map { playlist ->
mediaItems.add(
playlist.name,
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
.joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_PLAYLISTS
)
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
2021-07-18 13:17:29 +02:00
private fun getPlaylist(
id: String,
name: String,
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
2021-07-18 11:33:39 +02:00
if (content != null) {
if (content.size > 1)
2021-07-18 11:33:39 +02:00
mediaItems.addPlayAllItem(
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
)
2021-07-18 11:33:39 +02:00
// Playlist should be cached as it may contain random elements
playlistCache = content.getTracks()
2021-07-18 13:17:29 +02:00
playlistCache!!.take(DISPLAY_LIMIT).map { item ->
2021-07-18 11:33:39 +02:00
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
item,
listOf(
MEDIA_PLAYLIST_SONG_ITEM,
id,
name,
item.id
).joinToString("|"),
isPlayable = true
2021-07-18 11:33:39 +02:00
)
)
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
private fun playPlaylist(id: String, name: String) {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
2021-07-18 11:33:39 +02:00
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getTracks()
}
2022-04-03 23:57:50 +02:00
if (playlistCache != null) playSongs(playlistCache!!)
}
}
private fun playPlaylistSong(id: String, name: String, songId: String) {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
2021-07-18 11:33:39 +02:00
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getTracks()
}
2021-07-18 13:17:29 +02:00
val song = playlistCache?.firstOrNull { x -> x.id == songId }
2021-07-18 11:33:39 +02:00
if (song != null) playSong(song)
}
}
private fun playAlbum(id: String, name: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
if (songs != null) playSongs(songs.getTracks())
2021-07-18 11:33:39 +02:00
}
}
private fun playAlbumSong(id: String, name: String, songId: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
val song = songs?.getTracks()?.firstOrNull { x -> x.id == songId }
2021-07-18 11:33:39 +02:00
if (song != null) playSong(song)
}
}
2022-04-04 21:18:07 +02:00
private fun getPodcasts(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val podcasts = callWithErrorHandling { musicService.getPodcastsChannels(false) }
podcasts?.map { podcast ->
mediaItems.add(
podcast.title ?: "",
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_MIXED
)
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
}
}
2021-07-18 11:33:39 +02:00
private fun getPodcastEpisodes(
id: String
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
if (episodes.getTracks().count() > 1)
2021-07-18 11:33:39 +02:00
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
episodes.getTracks().map { episode ->
2021-07-18 13:17:29 +02:00
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
episode,
listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id)
.joinToString("|"),
isPlayable = true
2021-07-18 13:17:29 +02:00
)
)
2021-07-18 11:33:39 +02:00
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
private fun playPodcast(id: String) {
serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
playSongs(episodes.getTracks())
2021-07-18 11:33:39 +02:00
}
}
}
private fun playPodcastEpisode(id: String, episodeId: String) {
serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
val selectedEpisode = episodes
.getTracks()
2021-07-18 11:33:39 +02:00
.firstOrNull { episode -> episode.id == episodeId }
if (selectedEpisode != null) playSong(selectedEpisode)
}
}
}
2022-04-04 21:18:07 +02:00
private fun getBookmarks(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks)
songs.getTracks().map { song ->
2021-07-18 13:17:29 +02:00
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
song,
listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|"),
isPlayable = true
2021-07-18 13:17:29 +02:00
)
)
2021-07-18 11:33:39 +02:00
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
private fun playBookmark(id: String) {
serviceScope.launch {
val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks)
val song = songs.getTracks().firstOrNull { song -> song.id == id }
2021-07-18 11:33:39 +02:00
if (song != null) playSong(song)
}
}
}
2022-04-04 21:18:07 +02:00
private fun getShares(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val shares = callWithErrorHandling { musicService.getShares(false) }
shares?.map { share ->
mediaItems.add(
share.name ?: "",
listOf(MEDIA_SHARE_ITEM, share.id)
.joinToString("|"),
2022-04-04 21:18:07 +02:00
FOLDER_TYPE_MIXED
2021-07-18 11:33:39 +02:00
)
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
2021-07-18 11:33:39 +02:00
private fun getSongsForShare(
id: String
2022-04-04 21:18:07 +02:00
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val shares = callWithErrorHandling { musicService.getShares(false) }
2021-07-18 13:17:29 +02:00
val selectedShare = shares?.firstOrNull { share -> share.id == id }
2021-07-18 11:33:39 +02:00
if (selectedShare != null) {
if (selectedShare.getEntries().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
selectedShare.getEntries().map { song ->
2021-07-18 13:17:29 +02:00
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
song,
listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|"),
isPlayable = true
2021-07-18 13:17:29 +02:00
)
)
2021-07-18 11:33:39 +02:00
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
2021-07-18 11:33:39 +02:00
private fun playShare(id: String) {
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
2021-07-18 13:17:29 +02:00
val selectedShare = shares?.firstOrNull { share -> share.id == id }
2021-07-18 11:33:39 +02:00
if (selectedShare != null) {
playSongs(selectedShare.getEntries())
}
}
}
2021-07-18 11:33:39 +02:00
private fun playShareSong(id: String, songId: String) {
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
2021-07-18 13:17:29 +02:00
val selectedShare = shares?.firstOrNull { share -> share.id == id }
2021-07-18 11:33:39 +02:00
if (selectedShare != null) {
2021-07-18 13:17:29 +02:00
val song = selectedShare.getEntries().firstOrNull { x -> x.id == songId }
2021-07-18 11:33:39 +02:00
if (song != null) playSong(song)
}
}
}
2022-04-04 21:18:07 +02:00
private fun getStarredSongs(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 11:33:39 +02:00
val songs = listStarredSongsInMusicService()
if (songs != null) {
if (songs.songs.count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
2021-07-18 13:17:29 +02:00
val items = songs.songs.take(DISPLAY_LIMIT)
2021-07-18 11:33:39 +02:00
starredSongsCache = items
items.map { song ->
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
song,
listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|"),
isPlayable = true
2021-07-18 11:33:39 +02:00
)
)
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
2021-07-18 11:33:39 +02:00
private fun playStarredSongs() {
serviceScope.launch {
if (starredSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = listStarredSongsInMusicService()
starredSongsCache = content?.songs
}
2022-04-03 23:57:50 +02:00
if (starredSongsCache != null) playSongs(starredSongsCache!!)
2021-07-18 11:33:39 +02:00
}
}
2021-07-18 11:33:39 +02:00
private fun playStarredSong(songId: String) {
serviceScope.launch {
if (starredSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = listStarredSongsInMusicService()
starredSongsCache = content?.songs
}
2021-07-18 13:17:29 +02:00
val song = starredSongsCache?.firstOrNull { x -> x.id == songId }
2021-07-18 11:33:39 +02:00
if (song != null) playSong(song)
}
}
2022-04-04 21:18:07 +02:00
private fun getRandomSongs(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val mediaItems: MutableList<MediaItem> = ArrayList()
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
return serviceScope.future {
2021-07-18 13:17:29 +02:00
val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
2021-07-18 11:33:39 +02:00
if (songs != null) {
if (songs.size > 1)
2021-07-18 11:33:39 +02:00
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getTracks()
2021-07-18 11:33:39 +02:00
randomSongsCache = items
items.map { song ->
mediaItems.add(
2022-04-04 21:18:07 +02:00
buildMediaItemFromTrack(
song,
listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|"),
isPlayable = true
2021-07-18 11:33:39 +02:00
)
)
}
}
2022-04-04 21:18:07 +02:00
return@future LibraryResult.ofItemList(mediaItems, null)
2021-07-18 11:33:39 +02:00
}
}
private fun playRandomSongs() {
serviceScope.launch {
if (randomSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
// In this case we request a new set of random songs
2021-07-18 13:17:29 +02:00
val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
randomSongsCache = content?.getTracks()
2021-07-18 11:33:39 +02:00
}
2022-04-03 23:57:50 +02:00
if (randomSongsCache != null) playSongs(randomSongsCache!!)
2021-07-18 11:33:39 +02:00
}
}
private fun playRandomSong(songId: String) {
serviceScope.launch {
// If there is no cache, we can't play the selected song.
if (randomSongsCache != null) {
val song = randomSongsCache!!.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
}
private fun listSongsInMusicService(id: String, name: String): MusicDirectory? {
return if (!ActiveServerProvider.isOffline() && Settings.shouldUseId3Tags) {
2021-07-18 11:33:39 +02:00
callWithErrorHandling { musicService.getAlbum(id, name, false) }
} else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
}
}
private fun listStarredSongsInMusicService(): SearchResult? {
return if (Settings.shouldUseId3Tags) {
2021-07-18 11:33:39 +02:00
callWithErrorHandling { musicService.getStarred2() }
} else {
callWithErrorHandling { musicService.getStarred() }
}
}
2022-04-04 21:18:07 +02:00
private fun MutableList<MediaItem>.add(
title: String,
mediaId: String,
2022-04-04 21:18:07 +02:00
folderType: Int
) {
2021-07-18 11:33:39 +02:00
2022-04-04 21:18:07 +02:00
val mediaItem = buildMediaItem(
title,
mediaId,
isPlayable = false,
folderType = folderType
)
this.add(mediaItem)
}
2022-04-04 21:18:07 +02:00
private fun MutableList<MediaItem>.add(
resId: Int,
mediaId: String,
groupNameId: Int?,
2022-04-04 21:18:07 +02:00
browsable: Boolean = true,
folderType: Int = FOLDER_TYPE_MIXED
) {
2022-04-04 21:18:07 +02:00
val applicationContext = UApp.applicationContext()
2022-04-04 21:18:07 +02:00
val mediaItem = buildMediaItem(
applicationContext.getString(resId),
mediaId,
isPlayable = false,
folderType = folderType
)
this.add(mediaItem)
}
2022-04-04 21:18:07 +02:00
private fun MutableList<MediaItem>.addPlayAllItem(
2021-07-18 11:33:39 +02:00
mediaId: String,
) {
this.add(
R.string.select_album_play_all,
mediaId,
null,
false
)
}
private fun getSectionFromName(name: String): String {
var section = name.first().uppercaseChar()
if (!section.isLetter()) section = '#'
return section.toString()
}
2021-07-18 11:33:39 +02:00
2022-04-03 23:57:50 +02:00
private fun playSongs(songs: List<Track>) {
mediaPlayerController.addToPlaylist(
2021-07-18 11:33:39 +02:00
songs,
2022-04-03 23:57:50 +02:00
cachePermanently = false,
2021-07-18 11:33:39 +02:00
autoPlay = true,
shuffle = false,
2022-04-03 23:57:50 +02:00
insertionMode = MediaPlayerController.InsertionMode.CLEAR
2021-07-18 11:33:39 +02:00
)
}
private fun playSong(song: Track) {
mediaPlayerController.addToPlaylist(
2021-07-18 11:33:39 +02:00
listOf(song),
2022-04-03 23:57:50 +02:00
cachePermanently = false,
2021-07-18 11:33:39 +02:00
autoPlay = false,
shuffle = false,
2022-04-03 23:57:50 +02:00
insertionMode = MediaPlayerController.InsertionMode.AFTER_CURRENT
2021-07-18 11:33:39 +02:00
)
2022-04-03 23:57:50 +02:00
if (mediaPlayerController.mediaItemCount > 1) mediaPlayerController.next()
else mediaPlayerController.play()
2021-07-18 11:33:39 +02:00
}
private fun <T> callWithErrorHandling(function: () -> T): T? {
// TODO Implement better error handling
return try {
function()
} catch (all: Exception) {
Timber.i(all)
null
}
}
2022-04-04 21:18:07 +02:00
private fun buildMediaItemFromTrack(
track: Track,
mediaId: String,
isPlayable: Boolean
): MediaItem {
return buildMediaItem(
title = track.title ?: "",
mediaId = mediaId,
isPlayable = isPlayable,
folderType = FOLDER_TYPE_NONE,
album = track.album,
artist = track.artist,
genre = track.genre,
2022-07-02 01:27:12 +02:00
starred = track.starred
2022-04-04 21:18:07 +02:00
)
}
@Suppress("LongParameterList")
private fun buildMediaItem(
title: String,
mediaId: String,
isPlayable: Boolean,
@MediaMetadata.FolderType folderType: Int,
album: String? = null,
artist: String? = null,
genre: String? = null,
sourceUri: Uri? = null,
imageUri: Uri? = null,
2022-07-02 01:27:12 +02:00
starred: Boolean = false
2022-04-04 21:18:07 +02:00
): MediaItem {
val metadata =
MediaMetadata.Builder()
.setAlbumTitle(album)
.setTitle(title)
.setArtist(artist)
.setGenre(genre)
2022-07-02 01:27:12 +02:00
.setUserRating(HeartRating(starred))
2022-04-04 21:18:07 +02:00
.setFolderType(folderType)
.setIsPlayable(isPlayable)
.setArtworkUri(imageUri)
.build()
return MediaItem.Builder()
.setMediaId(mediaId)
.setMediaMetadata(metadata)
.setUri(sourceUri)
.build()
}
2022-04-05 10:10:24 +02:00
}