playback: handle media item playback

This commit is contained in:
Alexander Capehart 2024-04-09 15:17:24 -06:00
parent 64b9557793
commit 7503accada
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 418 additions and 135 deletions

View file

@ -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)
}
}
} }
/** /**

View file

@ -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
}
}

View file

@ -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")

View file

@ -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
}

View file

@ -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(