playback: handle media item playback
This commit is contained in:
parent
64b9557793
commit
7503accada
5 changed files with 418 additions and 135 deletions
|
@ -32,15 +32,16 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
import org.oxycblt.auxio.playback.state.DeferredPlayback
|
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.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.Progression
|
import org.oxycblt.auxio.playback.state.Progression
|
||||||
import org.oxycblt.auxio.playback.state.QueueChange
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
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.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -59,8 +60,8 @@ constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
private val persistenceRepository: PersistenceRepository,
|
private val persistenceRepository: PersistenceRepository,
|
||||||
|
private val commandFactory: PlaybackCommand.Factory,
|
||||||
private val listSettings: ListSettings,
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
|
||||||
) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||||
private var lastPositionJob: Job? = null
|
private var lastPositionJob: Job? = null
|
||||||
|
|
||||||
|
@ -189,21 +190,21 @@ constructor(
|
||||||
|
|
||||||
fun play(song: Song, with: PlaySong) {
|
fun play(song: Song, with: PlaySong) {
|
||||||
logD("Playing $song with $with")
|
logD("Playing $song with $with")
|
||||||
playWithImpl(song, with, isImplicitlyShuffled())
|
playWithImpl(song, with, ShuffleMode.IMPLICIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playExplicit(song: Song, with: PlaySong) {
|
fun playExplicit(song: Song, with: PlaySong) {
|
||||||
playWithImpl(song, with, false)
|
playWithImpl(song, with, ShuffleMode.OFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shuffleExplicit(song: Song, with: PlaySong) {
|
fun shuffleExplicit(song: Song, with: PlaySong) {
|
||||||
playWithImpl(song, with, true)
|
playWithImpl(song, with, ShuffleMode.ON)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shuffle all songs in the music library. */
|
/** Shuffle all songs in the music library. */
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
logD("Shuffling all songs")
|
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.
|
* be prompted on what artist to play. Defaults to null.
|
||||||
*/
|
*/
|
||||||
fun playFromArtist(song: Song, artist: Artist? = 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.
|
* be prompted on what artist to play. Defaults to null.
|
||||||
*/
|
*/
|
||||||
fun playFromGenre(song: Song, genre: Genre? = 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, shuffle: ShuffleMode) {
|
||||||
|
|
||||||
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
|
|
||||||
when (with) {
|
when (with) {
|
||||||
is PlaySong.FromAll -> playFromAllImpl(song, shuffled)
|
is PlaySong.FromAll -> playFromAllImpl(song, shuffle)
|
||||||
is PlaySong.FromAlbum -> playFromAlbumImpl(song, shuffled)
|
is PlaySong.FromAlbum -> playFromAlbumImpl(song, shuffle)
|
||||||
is PlaySong.FromArtist -> playFromArtistImpl(song, with.which, shuffled)
|
is PlaySong.FromArtist -> playFromArtistImpl(song, with.which, shuffle)
|
||||||
is PlaySong.FromGenre -> playFromGenreImpl(song, with.which, shuffled)
|
is PlaySong.FromGenre -> playFromGenreImpl(song, with.which, shuffle)
|
||||||
is PlaySong.FromPlaylist -> playFromPlaylistImpl(song, with.which, shuffled)
|
is PlaySong.FromPlaylist -> playFromPlaylistImpl(song, with.which, shuffle)
|
||||||
is PlaySong.ByItself -> playItselfImpl(song, shuffled)
|
is PlaySong.ByItself -> playItselfImpl(song, shuffle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playFromAllImpl(song: Song?, shuffled: Boolean) {
|
private fun playItselfImpl(song: Song, shuffle: ShuffleMode) {
|
||||||
playImpl(song, null, shuffled)
|
playbackManager.play(
|
||||||
|
requireNotNull(commandFactory.song(song, shuffle)) {
|
||||||
|
"Invalid playback parameters [$song $shuffle]"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playFromAlbumImpl(song: Song, shuffled: Boolean) {
|
private fun playFromAllImpl(song: Song?, shuffle: ShuffleMode) {
|
||||||
playImpl(song, song.album, shuffled)
|
val params =
|
||||||
|
if (song != null) {
|
||||||
|
commandFactory.songFromAll(song, shuffle)
|
||||||
|
} else {
|
||||||
|
commandFactory.all(shuffle)
|
||||||
|
}
|
||||||
|
|
||||||
|
playImpl(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playFromArtistImpl(song: Song, artist: Artist?, shuffled: Boolean) {
|
private fun playFromAlbumImpl(song: Song, shuffle: ShuffleMode) {
|
||||||
if (artist != null) {
|
logD("Playing $song from album")
|
||||||
logD("Playing $song from $artist")
|
playImpl(commandFactory.songFromAlbum(song, shuffle))
|
||||||
playImpl(song, artist, shuffled)
|
}
|
||||||
} else if (song.artists.size == 1) {
|
|
||||||
logD("$song has one artist, playing from it")
|
private fun playFromArtistImpl(song: Song, artist: Artist?, shuffle: ShuffleMode) {
|
||||||
playImpl(song, song.artists[0], shuffled)
|
val params = commandFactory.songFromArtist(song, artist, shuffle)
|
||||||
} else {
|
if (params != null) {
|
||||||
logD("$song has multiple artists, showing choice dialog")
|
playbackManager.play(params)
|
||||||
startPlaybackDecision(PlaybackDecision.PlayFromArtist(song))
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private fun playFromGenreImpl(song: Song, genre: Genre?, shuffle: ShuffleMode) {
|
||||||
if (genre != null) {
|
val params = commandFactory.songFromGenre(song, genre, shuffle)
|
||||||
logD("Playing $song from $genre")
|
if (params != null) {
|
||||||
playImpl(song, genre, shuffled)
|
playbackManager.play(params)
|
||||||
} 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))
|
|
||||||
}
|
}
|
||||||
|
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")
|
logD("Playing $song from $playlist")
|
||||||
playImpl(song, playlist, shuffled)
|
playImpl(commandFactory.songFromPlaylist(song, playlist, shuffle))
|
||||||
}
|
|
||||||
|
|
||||||
private fun playItselfImpl(song: Song, shuffled: Boolean) {
|
|
||||||
playImpl(song, listOf(song), shuffled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlaybackDecision(decision: PlaybackDecision) {
|
private fun startPlaybackDecision(decision: PlaybackDecision) {
|
||||||
|
@ -300,7 +304,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun play(album: Album) {
|
fun play(album: Album) {
|
||||||
logD("Playing $album")
|
logD("Playing $album")
|
||||||
playImpl(null, album, false)
|
playImpl(commandFactory.album(album, ShuffleMode.OFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -310,7 +314,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun shuffle(album: Album) {
|
fun shuffle(album: Album) {
|
||||||
logD("Shuffling $album")
|
logD("Shuffling $album")
|
||||||
playImpl(null, album, true)
|
playImpl(commandFactory.album(album, ShuffleMode.ON))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,7 +324,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun play(artist: Artist) {
|
fun play(artist: Artist) {
|
||||||
logD("Playing $artist")
|
logD("Playing $artist")
|
||||||
playImpl(null, artist, false)
|
playImpl(commandFactory.artist(artist, ShuffleMode.OFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -330,7 +334,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun shuffle(artist: Artist) {
|
fun shuffle(artist: Artist) {
|
||||||
logD("Shuffling $artist")
|
logD("Shuffling $artist")
|
||||||
playImpl(null, artist, true)
|
playImpl(commandFactory.artist(artist, ShuffleMode.ON))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -340,7 +344,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun play(genre: Genre) {
|
fun play(genre: Genre) {
|
||||||
logD("Playing $genre")
|
logD("Playing $genre")
|
||||||
playImpl(null, genre, false)
|
playImpl(commandFactory.genre(genre, ShuffleMode.OFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -350,7 +354,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun shuffle(genre: Genre) {
|
fun shuffle(genre: Genre) {
|
||||||
logD("Shuffling $genre")
|
logD("Shuffling $genre")
|
||||||
playImpl(null, genre, true)
|
playImpl(commandFactory.genre(genre, ShuffleMode.ON))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -360,7 +364,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun play(playlist: Playlist) {
|
fun play(playlist: Playlist) {
|
||||||
logD("Playing $playlist")
|
logD("Playing $playlist")
|
||||||
playImpl(null, playlist, false)
|
playImpl(commandFactory.playlist(playlist, ShuffleMode.OFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -370,7 +374,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun shuffle(playlist: Playlist) {
|
fun shuffle(playlist: Playlist) {
|
||||||
logD("Shuffling $playlist")
|
logD("Shuffling $playlist")
|
||||||
playImpl(null, playlist, true)
|
playImpl(commandFactory.playlist(playlist, ShuffleMode.ON))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,7 +384,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun play(songs: List<Song>) {
|
fun play(songs: List<Song>) {
|
||||||
logD("Playing ${songs.size} songs")
|
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<Song>) {
|
fun shuffle(songs: List<Song>) {
|
||||||
logD("Shuffling ${songs.size} songs")
|
logD("Shuffling ${songs.size} songs")
|
||||||
playbackManager.play(null, null, songs, true)
|
playImpl(commandFactory.songs(songs, ShuffleMode.ON))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playImpl(song: Song?, queue: List<Song>, shuffled: Boolean) {
|
private fun playImpl(command: PlaybackCommand?) {
|
||||||
check(song == null || queue.contains(song)) { "Song to play not in queue" }
|
playbackManager.play(requireNotNull(command) { "Invalid playback parameters" })
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -617,49 +604,6 @@ constructor(
|
||||||
}
|
}
|
||||||
_openPanel.put(panel)
|
_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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Song>
|
||||||
|
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<Song>, 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<Song>,
|
||||||
|
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<Song>, 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 <T : MusicParent> newCommand(
|
||||||
|
song: Song,
|
||||||
|
parent: T?,
|
||||||
|
parents: List<T>,
|
||||||
|
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<Song>,
|
||||||
|
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<Song>,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,13 +111,9 @@ interface PlaybackStateManager {
|
||||||
/**
|
/**
|
||||||
* Start new playback.
|
* Start new playback.
|
||||||
*
|
*
|
||||||
* @param song A particular [Song] to play, or null to play the first [Song] in the new queue.
|
* @param command The parameters to start playback with.
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
fun play(song: Song?, parent: MusicParent?, queue: List<Song>, 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
|
* 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 ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, 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<Song>, shuffled: Boolean) {
|
||||||
val stateHolder = stateHolder ?: return
|
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
|
// Played something, so we are initialized now
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
stateHolder.newPlayback(queue, song, parent, shuffled)
|
stateHolder.newPlayback(queue, song, parent, shuffled)
|
||||||
|
@ -476,7 +476,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
override fun playNext(songs: List<Song>) {
|
override fun playNext(songs: List<Song>) {
|
||||||
if (currentSong == null) {
|
if (currentSong == null) {
|
||||||
logD("Nothing playing, short-circuiting to new playback")
|
logD("Nothing playing, short-circuiting to new playback")
|
||||||
play(songs[0], null, songs, false)
|
play(null, null, songs, false)
|
||||||
} else {
|
} else {
|
||||||
val stateHolder = stateHolder ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Adding ${songs.size} songs to start of queue")
|
logD("Adding ${songs.size} songs to start of queue")
|
||||||
|
@ -488,7 +488,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
override fun addToQueue(songs: List<Song>) {
|
override fun addToQueue(songs: List<Song>) {
|
||||||
if (currentSong == null) {
|
if (currentSong == null) {
|
||||||
logD("Nothing playing, short-circuiting to new playback")
|
logD("Nothing playing, short-circuiting to new playback")
|
||||||
play(songs[0], null, songs, false)
|
play(null, null, songs, false)
|
||||||
} else {
|
} else {
|
||||||
val stateHolder = stateHolder ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Adding ${songs.size} songs to end of queue")
|
logD("Adding ${songs.size} songs to end of queue")
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -101,11 +101,13 @@ import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||||
import org.oxycblt.auxio.playback.service.BetterShuffleOrder
|
import org.oxycblt.auxio.playback.service.BetterShuffleOrder
|
||||||
import org.oxycblt.auxio.playback.state.DeferredPlayback
|
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.PlaybackStateHolder
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.Progression
|
import org.oxycblt.auxio.playback.state.Progression
|
||||||
import org.oxycblt.auxio.playback.state.RawQueue
|
import org.oxycblt.auxio.playback.state.RawQueue
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
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.playback.state.StateAck
|
||||||
import org.oxycblt.auxio.util.getPlural
|
import org.oxycblt.auxio.util.getPlural
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
|
@ -137,6 +139,7 @@ class AuxioService :
|
||||||
private var currentIndexJob: Job? = null
|
private var currentIndexJob: Job? = null
|
||||||
|
|
||||||
@Inject lateinit var playbackManager: PlaybackStateManager
|
@Inject lateinit var playbackManager: PlaybackStateManager
|
||||||
|
@Inject lateinit var commandFactory: PlaybackCommand.Factory
|
||||||
@Inject lateinit var playbackSettings: PlaybackSettings
|
@Inject lateinit var playbackSettings: PlaybackSettings
|
||||||
@Inject lateinit var persistenceRepository: PersistenceRepository
|
@Inject lateinit var persistenceRepository: PersistenceRepository
|
||||||
@Inject lateinit var mediaSourceFactory: MediaSource.Factory
|
@Inject lateinit var mediaSourceFactory: MediaSource.Factory
|
||||||
|
@ -518,7 +521,7 @@ class AuxioService :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
||||||
player.addMediaItems(songs.map { it.toMediaItem(this, null) })
|
player.addToQueue(songs)
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
@ -556,17 +559,18 @@ class AuxioService :
|
||||||
is DeferredPlayback.ShuffleAll -> {
|
is DeferredPlayback.ShuffleAll -> {
|
||||||
logD("Shuffling all tracks")
|
logD("Shuffling all tracks")
|
||||||
playbackManager.play(
|
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
|
// Open -> Try to find the Song for the given file and then play it from all songs
|
||||||
is DeferredPlayback.Open -> {
|
is DeferredPlayback.Open -> {
|
||||||
logD("Opening specified file")
|
logD("Opening specified file")
|
||||||
deviceLibrary.findSongForUri(workerContext, action.uri)?.let { song ->
|
deviceLibrary.findSongForUri(workerContext, action.uri)?.let { song ->
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
song,
|
requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) {
|
||||||
null,
|
"Invalid playback parameters"
|
||||||
listSettings.songSort.songs(deviceLibrary.songs),
|
})
|
||||||
player.shuffleModeEnabled && playbackSettings.keepShuffle)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -735,6 +739,15 @@ class AuxioService :
|
||||||
return Futures.immediateFuture(result)
|
return Futures.immediateFuture(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onGetItem(
|
||||||
|
session: MediaLibrarySession,
|
||||||
|
browser: MediaSession.ControllerInfo,
|
||||||
|
mediaId: String
|
||||||
|
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||||
|
// TODO
|
||||||
|
return super.onGetItem(session, browser, mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetChildren(
|
override fun onGetChildren(
|
||||||
session: MediaLibrarySession,
|
session: MediaLibrarySession,
|
||||||
browser: MediaSession.ControllerInfo,
|
browser: MediaSession.ControllerInfo,
|
||||||
|
@ -815,6 +828,101 @@ class AuxioService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSetMediaItems(
|
||||||
|
mediaSession: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
mediaItems: MutableList<MediaItem>,
|
||||||
|
startIndex: Int,
|
||||||
|
startPositionMs: Long
|
||||||
|
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
||||||
|
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<MediaItem>,
|
||||||
|
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<MediaItem>
|
||||||
|
): ListenableFuture<MutableList<MediaItem>> {
|
||||||
|
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(
|
override fun onCustomCommand(
|
||||||
session: MediaSession,
|
session: MediaSession,
|
||||||
controller: MediaSession.ControllerInfo,
|
controller: MediaSession.ControllerInfo,
|
||||||
|
@ -1146,6 +1254,10 @@ class NeoPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addToQueue(songs: List<Song>) {
|
||||||
|
addMediaItems(songs.map { it.toMediaItem(context, null) })
|
||||||
|
}
|
||||||
|
|
||||||
fun move(from: Int, to: Int) {
|
fun move(from: Int, to: Int) {
|
||||||
val indices = unscrambleQueueIndices()
|
val indices = unscrambleQueueIndices()
|
||||||
if (indices.isEmpty()) {
|
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
|
class NeoBitmapLoader
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
|
Loading…
Reference in a new issue