diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 483a36d99..4326e993a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -32,15 +32,16 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.state.DeferredPlayback +import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode +import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD @@ -59,8 +60,8 @@ constructor( private val playbackManager: PlaybackStateManager, private val playbackSettings: PlaybackSettings, private val persistenceRepository: PersistenceRepository, + private val commandFactory: PlaybackCommand.Factory, private val listSettings: ListSettings, - private val musicRepository: MusicRepository, ) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener { private var lastPositionJob: Job? = null @@ -189,21 +190,21 @@ constructor( fun play(song: Song, with: PlaySong) { logD("Playing $song with $with") - playWithImpl(song, with, isImplicitlyShuffled()) + playWithImpl(song, with, ShuffleMode.IMPLICIT) } fun playExplicit(song: Song, with: PlaySong) { - playWithImpl(song, with, false) + playWithImpl(song, with, ShuffleMode.OFF) } fun shuffleExplicit(song: Song, with: PlaySong) { - playWithImpl(song, with, true) + playWithImpl(song, with, ShuffleMode.ON) } /** Shuffle all songs in the music library. */ fun shuffleAll() { logD("Shuffling all songs") - playFromAllImpl(null, true) + playFromAllImpl(null, ShuffleMode.ON) } /** @@ -214,7 +215,7 @@ constructor( * be prompted on what artist to play. Defaults to null. */ fun playFromArtist(song: Song, artist: Artist? = null) { - playFromArtistImpl(song, artist, isImplicitlyShuffled()) + playFromArtistImpl(song, artist, ShuffleMode.IMPLICIT) } /** @@ -225,63 +226,66 @@ constructor( * be prompted on what artist to play. Defaults to null. */ fun playFromGenre(song: Song, genre: Genre? = null) { - playFromGenreImpl(song, genre, isImplicitlyShuffled()) + playFromGenreImpl(song, genre, ShuffleMode.IMPLICIT) } - private fun isImplicitlyShuffled() = playbackManager.isShuffled && playbackSettings.keepShuffle - - private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) { + private fun playWithImpl(song: Song, with: PlaySong, shuffle: ShuffleMode) { when (with) { - is PlaySong.FromAll -> playFromAllImpl(song, shuffled) - is PlaySong.FromAlbum -> playFromAlbumImpl(song, shuffled) - is PlaySong.FromArtist -> playFromArtistImpl(song, with.which, shuffled) - is PlaySong.FromGenre -> playFromGenreImpl(song, with.which, shuffled) - is PlaySong.FromPlaylist -> playFromPlaylistImpl(song, with.which, shuffled) - is PlaySong.ByItself -> playItselfImpl(song, shuffled) + is PlaySong.FromAll -> playFromAllImpl(song, shuffle) + is PlaySong.FromAlbum -> playFromAlbumImpl(song, shuffle) + is PlaySong.FromArtist -> playFromArtistImpl(song, with.which, shuffle) + is PlaySong.FromGenre -> playFromGenreImpl(song, with.which, shuffle) + is PlaySong.FromPlaylist -> playFromPlaylistImpl(song, with.which, shuffle) + is PlaySong.ByItself -> playItselfImpl(song, shuffle) } } - private fun playFromAllImpl(song: Song?, shuffled: Boolean) { - playImpl(song, null, shuffled) + private fun playItselfImpl(song: Song, shuffle: ShuffleMode) { + playbackManager.play( + requireNotNull(commandFactory.song(song, shuffle)) { + "Invalid playback parameters [$song $shuffle]" + }) } - private fun playFromAlbumImpl(song: Song, shuffled: Boolean) { - playImpl(song, song.album, shuffled) + private fun playFromAllImpl(song: Song?, shuffle: ShuffleMode) { + val params = + if (song != null) { + commandFactory.songFromAll(song, shuffle) + } else { + commandFactory.all(shuffle) + } + + playImpl(params) } - private fun playFromArtistImpl(song: Song, artist: Artist?, shuffled: Boolean) { - if (artist != null) { - logD("Playing $song from $artist") - playImpl(song, artist, shuffled) - } else if (song.artists.size == 1) { - logD("$song has one artist, playing from it") - playImpl(song, song.artists[0], shuffled) - } else { - logD("$song has multiple artists, showing choice dialog") - startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) + private fun playFromAlbumImpl(song: Song, shuffle: ShuffleMode) { + logD("Playing $song from album") + playImpl(commandFactory.songFromAlbum(song, shuffle)) + } + + private fun playFromArtistImpl(song: Song, artist: Artist?, shuffle: ShuffleMode) { + val params = commandFactory.songFromArtist(song, artist, shuffle) + if (params != null) { + playbackManager.play(params) } + logD( + "Cannot use given artist parameter for $song [$artist from ${song.artists}], showing choice dialog") + startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } - private fun playFromGenreImpl(song: Song, genre: Genre?, shuffled: Boolean) { - if (genre != null) { - logD("Playing $song from $genre") - playImpl(song, genre, shuffled) - } else if (song.genres.size == 1) { - logD("$song has one genre, playing from it") - playImpl(song, song.genres[0], shuffled) - } else { - logD("$song has multiple genres, showing choice dialog") - startPlaybackDecision(PlaybackDecision.PlayFromGenre(song)) + private fun playFromGenreImpl(song: Song, genre: Genre?, shuffle: ShuffleMode) { + val params = commandFactory.songFromGenre(song, genre, shuffle) + if (params != null) { + playbackManager.play(params) } + logD( + "Cannot use given genre parameter for $song [$genre from ${song.genres}], showing choice dialog") + startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } - private fun playFromPlaylistImpl(song: Song, playlist: Playlist, shuffled: Boolean) { + private fun playFromPlaylistImpl(song: Song, playlist: Playlist, shuffle: ShuffleMode) { logD("Playing $song from $playlist") - playImpl(song, playlist, shuffled) - } - - private fun playItselfImpl(song: Song, shuffled: Boolean) { - playImpl(song, listOf(song), shuffled) + playImpl(commandFactory.songFromPlaylist(song, playlist, shuffle)) } private fun startPlaybackDecision(decision: PlaybackDecision) { @@ -300,7 +304,7 @@ constructor( */ fun play(album: Album) { logD("Playing $album") - playImpl(null, album, false) + playImpl(commandFactory.album(album, ShuffleMode.OFF)) } /** @@ -310,7 +314,7 @@ constructor( */ fun shuffle(album: Album) { logD("Shuffling $album") - playImpl(null, album, true) + playImpl(commandFactory.album(album, ShuffleMode.ON)) } /** @@ -320,7 +324,7 @@ constructor( */ fun play(artist: Artist) { logD("Playing $artist") - playImpl(null, artist, false) + playImpl(commandFactory.artist(artist, ShuffleMode.OFF)) } /** @@ -330,7 +334,7 @@ constructor( */ fun shuffle(artist: Artist) { logD("Shuffling $artist") - playImpl(null, artist, true) + playImpl(commandFactory.artist(artist, ShuffleMode.ON)) } /** @@ -340,7 +344,7 @@ constructor( */ fun play(genre: Genre) { logD("Playing $genre") - playImpl(null, genre, false) + playImpl(commandFactory.genre(genre, ShuffleMode.OFF)) } /** @@ -350,7 +354,7 @@ constructor( */ fun shuffle(genre: Genre) { logD("Shuffling $genre") - playImpl(null, genre, true) + playImpl(commandFactory.genre(genre, ShuffleMode.ON)) } /** @@ -360,7 +364,7 @@ constructor( */ fun play(playlist: Playlist) { logD("Playing $playlist") - playImpl(null, playlist, false) + playImpl(commandFactory.playlist(playlist, ShuffleMode.OFF)) } /** @@ -370,7 +374,7 @@ constructor( */ fun shuffle(playlist: Playlist) { logD("Shuffling $playlist") - playImpl(null, playlist, true) + playImpl(commandFactory.playlist(playlist, ShuffleMode.ON)) } /** @@ -380,7 +384,7 @@ constructor( */ fun play(songs: List) { logD("Playing ${songs.size} songs") - playbackManager.play(null, null, songs, false) + playImpl(commandFactory.songs(songs, ShuffleMode.OFF)) } /** @@ -390,28 +394,11 @@ constructor( */ fun shuffle(songs: List) { logD("Shuffling ${songs.size} songs") - playbackManager.play(null, null, songs, true) + playImpl(commandFactory.songs(songs, ShuffleMode.ON)) } - private fun playImpl(song: Song?, queue: List, shuffled: Boolean) { - check(song == null || queue.contains(song)) { "Song to play not in queue" } - playbackManager.play(song, null, queue, shuffled) - } - - private fun playImpl(song: Song?, parent: MusicParent?, shuffled: Boolean) { - check(song == null || parent == null || parent.songs.contains(song)) { - "Song to play not in parent" - } - val deviceLibrary = musicRepository.deviceLibrary ?: return - val queue = - when (parent) { - is Genre -> listSettings.genreSongSort.songs(parent.songs) - is Artist -> listSettings.artistSongSort.songs(parent.songs) - is Album -> listSettings.albumSongSort.songs(parent.songs) - is Playlist -> parent.songs - null -> listSettings.songSort.songs(deviceLibrary.songs) - } - playbackManager.play(song, parent, queue, shuffled) + private fun playImpl(command: PlaybackCommand?) { + playbackManager.play(requireNotNull(command) { "Invalid playback parameters" }) } /** @@ -617,49 +604,6 @@ constructor( } _openPanel.put(panel) } - - // --- SAVE/RESTORE FUNCTIONS --- - - /** - * Force-save the current playback state. - * - * @param onDone Called when the save is completed with true if successful, and false otherwise. - */ - fun savePlaybackState(onDone: (Boolean) -> Unit) { - logD("Saving playback state") - viewModelScope.launch { - onDone(persistenceRepository.saveState(playbackManager.toSavedState())) - } - } - - /** - * Clear the current playback state. - * - * @param onDone Called when the wipe is completed with true if successful, and false otherwise. - */ - fun wipePlaybackState(onDone: (Boolean) -> Unit) { - logD("Wiping playback state") - viewModelScope.launch { onDone(persistenceRepository.saveState(null)) } - } - - /** - * Force-restore the current playback state. - * - * @param onDone Called when the restoration is completed with true if successful, and false - * otherwise. - */ - fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { - logD("Force-restoring playback state") - viewModelScope.launch { - val savedState = persistenceRepository.readState() - if (savedState != null) { - playbackManager.applySavedState(savedState, true) - onDone(true) - return@launch - } - onDone(false) - } - } } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt new file mode 100644 index 000000000..90e435a26 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaybackCommand.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback.state + +import javax.inject.Inject +import org.oxycblt.auxio.list.ListSettings +import org.oxycblt.auxio.list.sort.Sort +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaybackSettings + +/** + * @param song A particular [Song] to play, or null to play the first [Song] in the new queue. + * @param queue The queue of [Song]s to play from. + * @param parent The [MusicParent] to play from, or null if to play from an non-specific collection + * of "All [Song]s". + * @param shuffled Whether to shuffle or not. + */ +interface PlaybackCommand { + val song: Song? + val parent: MusicParent? + val queue: List + val shuffled: Boolean + + interface Factory { + fun song(song: Song, shuffle: ShuffleMode): PlaybackCommand? + + fun songFromAll(song: Song, shuffle: ShuffleMode): PlaybackCommand? + + fun songFromAlbum(song: Song, shuffle: ShuffleMode): PlaybackCommand? + + fun songFromArtist(song: Song, artist: Artist?, shuffle: ShuffleMode): PlaybackCommand? + + fun songFromGenre(song: Song, genre: Genre?, shuffle: ShuffleMode): PlaybackCommand? + + fun songFromPlaylist(song: Song, playlist: Playlist, shuffle: ShuffleMode): PlaybackCommand? + + fun all(shuffle: ShuffleMode): PlaybackCommand? + + fun songs(songs: List, shuffle: ShuffleMode): PlaybackCommand? + + fun album(album: Album, shuffle: ShuffleMode): PlaybackCommand? + + fun artist(artist: Artist, shuffle: ShuffleMode): PlaybackCommand? + + fun genre(genre: Genre, shuffle: ShuffleMode): PlaybackCommand? + + fun playlist(playlist: Playlist, shuffle: ShuffleMode): PlaybackCommand? + } +} + +enum class ShuffleMode { + ON, + OFF, + IMPLICIT +} + +class PlaybackCommandFactoryImpl +@Inject +constructor( + val playbackManager: PlaybackStateManager, + val playbackSettings: PlaybackSettings, + val listSettings: ListSettings, + val musicRepository: MusicRepository +) : PlaybackCommand.Factory { + data class PlaybackCommandImpl( + override val song: Song?, + override val parent: MusicParent?, + override val queue: List, + override val shuffled: Boolean + ) : PlaybackCommand + + override fun song(song: Song, shuffle: ShuffleMode) = newCommand(song, shuffle) + + override fun songFromAll(song: Song, shuffle: ShuffleMode) = newCommand(song, shuffle) + + override fun songFromAlbum(song: Song, shuffle: ShuffleMode) = + newCommand(song, song.album, listSettings.albumSongSort, shuffle) + + override fun songFromArtist(song: Song, artist: Artist?, shuffle: ShuffleMode) = + newCommand(song, artist, song.artists, listSettings.artistSongSort, shuffle) + + override fun songFromGenre(song: Song, genre: Genre?, shuffle: ShuffleMode) = + newCommand(song, genre, song.genres, listSettings.genreSongSort, shuffle) + + override fun songFromPlaylist(song: Song, playlist: Playlist, shuffle: ShuffleMode) = + newCommand(song, playlist, playlist.songs, listSettings.playlistSort, shuffle) + + override fun all(shuffle: ShuffleMode) = newCommand(null, shuffle) + + override fun songs(songs: List, shuffle: ShuffleMode) = + newCommand(null, null, songs, shuffle) + + override fun album(album: Album, shuffle: ShuffleMode) = + newCommand(null, album, listSettings.albumSongSort, shuffle) + + override fun artist(artist: Artist, shuffle: ShuffleMode) = + newCommand(null, artist, listSettings.artistSongSort, shuffle) + + override fun genre(genre: Genre, shuffle: ShuffleMode) = + newCommand(null, genre, listSettings.genreSongSort, shuffle) + + override fun playlist(playlist: Playlist, shuffle: ShuffleMode) = + newCommand(null, playlist, playlist.songs, shuffle) + + private fun newCommand( + song: Song, + parent: T?, + parents: List, + sort: Sort, + shuffle: ShuffleMode + ): PlaybackCommand? { + return if (parent != null) { + newCommand(song, parent, sort, shuffle) + } else if (song.genres.size == 1) { + newCommand(song, parents.first(), sort, shuffle) + } else { + null + } + } + + private fun newCommand(song: Song?, shuffle: ShuffleMode): PlaybackCommand? { + val deviceLibrary = musicRepository.deviceLibrary ?: return null + return newCommand(song, null, deviceLibrary.songs, listSettings.songSort, shuffle) + } + + private fun newCommand( + song: Song?, + parent: MusicParent, + sort: Sort, + shuffle: ShuffleMode + ): PlaybackCommand? { + val songs = sort.songs(parent.songs) + return newCommand(song, parent, songs, sort, shuffle) + } + + private fun newCommand( + song: Song?, + parent: MusicParent?, + queue: Collection, + sort: Sort, + shuffle: ShuffleMode + ): PlaybackCommand? { + if (queue.isEmpty() || song !in queue) { + return null + } + return newCommand(song, parent, sort.songs(queue), shuffle) + } + + private fun newCommand( + song: Song?, + parent: MusicParent?, + queue: List, + shuffle: ShuffleMode + ): PlaybackCommand { + return PlaybackCommandImpl(song, parent, queue, isShuffled(shuffle)) + } + + private fun isShuffled(shuffle: ShuffleMode) = + when (shuffle) { + ShuffleMode.ON -> true + ShuffleMode.OFF -> false + ShuffleMode.IMPLICIT -> playbackSettings.keepShuffle && playbackManager.isShuffled + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index fceb344a5..19e0b3f86 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -111,13 +111,9 @@ interface PlaybackStateManager { /** * Start new playback. * - * @param song A particular [Song] to play, or null to play the first [Song] in the new queue. - * @param queue The queue of [Song]s to play from. - * @param parent The [MusicParent] to play from, or null if to play from an non-specific - * collection of "All [Song]s". - * @param shuffled Whether to shuffle or not. + * @param command The parameters to start playback with. */ - fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) + fun play(command: PlaybackCommand) /** * Go to the next [Song] in the queue. Will go to the first [Song] in the queue if there is no @@ -441,9 +437,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { // --- PLAYING FUNCTIONS --- @Synchronized - override fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) { + override fun play(command: PlaybackCommand) { + play(command.song, command.parent, command.queue, command.shuffled) + } + + private fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) { val stateHolder = stateHolder ?: return - logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]") + logD("Playing ${song} from $parent in ${queue.size}-song queue [shuffled=${shuffled}]") // Played something, so we are initialized now isInitialized = true stateHolder.newPlayback(queue, song, parent, shuffled) @@ -476,7 +476,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun playNext(songs: List) { if (currentSong == null) { logD("Nothing playing, short-circuiting to new playback") - play(songs[0], null, songs, false) + play(null, null, songs, false) } else { val stateHolder = stateHolder ?: return logD("Adding ${songs.size} songs to start of queue") @@ -488,7 +488,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun addToQueue(songs: List) { if (currentSong == null) { logD("Nothing playing, short-circuiting to new playback") - play(songs[0], null, songs, false) + play(null, null, songs, false) } else { val stateHolder = stateHolder ?: return logD("Adding ${songs.size} songs to end of queue") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateModule.kt new file mode 100644 index 000000000..2b76ed971 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaybackStateModule.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback.state + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface PlaybackStateModule { + @Binds fun playbackCommandFactory(factory: PlaybackCommandFactoryImpl): PlaybackCommand.Factory +} diff --git a/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt index 75c7e8375..711582831 100644 --- a/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt @@ -101,11 +101,13 @@ import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.service.BetterShuffleOrder import org.oxycblt.auxio.playback.state.DeferredPlayback +import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateHolder import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.RawQueue import org.oxycblt.auxio.playback.state.RepeatMode +import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.StateAck import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getSystemServiceCompat @@ -137,6 +139,7 @@ class AuxioService : private var currentIndexJob: Job? = null @Inject lateinit var playbackManager: PlaybackStateManager + @Inject lateinit var commandFactory: PlaybackCommand.Factory @Inject lateinit var playbackSettings: PlaybackSettings @Inject lateinit var persistenceRepository: PersistenceRepository @Inject lateinit var mediaSourceFactory: MediaSource.Factory @@ -518,7 +521,7 @@ class AuxioService : } override fun addToQueue(songs: List, ack: StateAck.AddToQueue) { - player.addMediaItems(songs.map { it.toMediaItem(this, null) }) + player.addToQueue(songs) playbackManager.ack(this, ack) deferSave() } @@ -556,17 +559,18 @@ class AuxioService : is DeferredPlayback.ShuffleAll -> { logD("Shuffling all tracks") playbackManager.play( - null, null, listSettings.songSort.songs(deviceLibrary.songs), true) + requireNotNull(commandFactory.all(ShuffleMode.ON)) { + "Invalid playback parameters" + }) } // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { logD("Opening specified file") deviceLibrary.findSongForUri(workerContext, action.uri)?.let { song -> playbackManager.play( - song, - null, - listSettings.songSort.songs(deviceLibrary.songs), - player.shuffleModeEnabled && playbackSettings.keepShuffle) + requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { + "Invalid playback parameters" + }) } } } @@ -735,6 +739,15 @@ class AuxioService : return Futures.immediateFuture(result) } + override fun onGetItem( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + // TODO + return super.onGetItem(session, browser, mediaId) + } + override fun onGetChildren( session: MediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -815,6 +828,101 @@ class AuxioService : } } + override fun onSetMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList, + startIndex: Int, + startPositionMs: Long + ): ListenableFuture { + val deviceLibrary = + musicRepository.deviceLibrary + ?: return Futures.immediateFailedFuture(Exception("Invalid state")) + val result = + if (mediaItems.size > 1) { + playMediaItemSelection(mediaItems, startIndex, deviceLibrary) + } else { + playSingleMediaItem(mediaItems.first(), deviceLibrary) + } + return if (result) { + // This will not actually do anything to the player, I patched that out + Futures.immediateFuture( + MediaSession.MediaItemsWithStartPosition(listOf(), C.INDEX_UNSET, C.TIME_UNSET)) + } else { + Futures.immediateFailedFuture(Exception("Invalid state")) + } + } + + private fun playMediaItemSelection( + mediaItems: List, + startIndex: Int, + deviceLibrary: DeviceLibrary + ): Boolean { + val targetSong = mediaItems.getOrNull(startIndex)?.toSong(deviceLibrary) + val songs = mediaItems.mapNotNull { it.toSong(deviceLibrary) } + var index = startIndex + if (targetSong != null) { + while (songs.getOrNull(index)?.uid != targetSong.uid) { + index-- + } + } + playbackManager.play(commandFactory.songs(songs, ShuffleMode.OFF) ?: return false) + return true + } + + private fun playSingleMediaItem(mediaItem: MediaItem, deviceLibrary: DeviceLibrary): Boolean { + val uid = ExternalUID.fromString(mediaItem.mediaId) ?: return false + val music: Music + var parent: MusicParent? = null + when (uid) { + is ExternalUID.Single -> { + music = musicRepository.find(uid.uid) ?: return false + } + is ExternalUID.Joined -> { + music = musicRepository.find(uid.childUid) ?: return false + parent = musicRepository.find(uid.parentUid) as? MusicParent ?: return false + } + else -> return false + } + + val command = + when (music) { + is Song -> inferSongFromParentCommand(music, parent) + is Album -> commandFactory.album(music, ShuffleMode.OFF) + is Artist -> commandFactory.artist(music, ShuffleMode.OFF) + is Genre -> commandFactory.genre(music, ShuffleMode.OFF) + is Playlist -> commandFactory.playlist(music, ShuffleMode.OFF) + } + + playbackManager.play(command ?: return false) + + return true + } + + private fun inferSongFromParentCommand(music: Song, parent: MusicParent?) = + when (parent) { + is Album -> commandFactory.songFromAlbum(music, ShuffleMode.IMPLICIT) + is Artist -> commandFactory.songFromArtist(music, parent, ShuffleMode.IMPLICIT) + ?: commandFactory.songFromArtist(music, music.artists[0], ShuffleMode.IMPLICIT) + is Genre -> commandFactory.songFromGenre(music, parent, ShuffleMode.IMPLICIT) + ?: commandFactory.songFromGenre(music, music.genres[0], ShuffleMode.IMPLICIT) + is Playlist -> commandFactory.songFromPlaylist(music, parent, ShuffleMode.IMPLICIT) + null -> commandFactory.songFromAll(music, ShuffleMode.IMPLICIT) + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + val deviceLibrary = + musicRepository.deviceLibrary ?: return Futures.immediateFuture(mutableListOf()) + val songs = mediaItems.mapNotNull { it.toSong(deviceLibrary) } + playbackManager.addToQueue(songs) + // This will not actually do anything to the player, I patched that out + return Futures.immediateFuture(mutableListOf()) + } + override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, @@ -1146,6 +1254,10 @@ class NeoPlayer( } } + fun addToQueue(songs: List) { + addMediaItems(songs.map { it.toMediaItem(context, null) }) + } + fun move(from: Int, to: Int) { val indices = unscrambleQueueIndices() if (indices.isEmpty()) { @@ -1423,6 +1535,17 @@ private fun MediaItem.toSong(deviceLibrary: DeviceLibrary): Song? { } } +private fun MediaItem.toParent(deviceLibrary: DeviceLibrary): MusicParent? { + val uid = ExternalUID.fromString(mediaId) ?: return null + return when (uid) { + is ExternalUID.Joined -> { + deviceLibrary.findArtist(uid.parentUid) + } + is ExternalUID.Single -> null + is ExternalUID.Category -> null + } +} + class NeoBitmapLoader @Inject constructor(