music: keep changes when unshuffling/reshuffling

Keep changes when unshuffling and reshuffling the queue.

This quirk was a hold-over from the old queue system, and now it's
removed.

Note that sorting is still based on parent, and so sort orders might
remain somewhat wonky. I only see myself really tackling that come
gapless playback, as I have to remove that last vestige to get that
system working.
This commit is contained in:
Alexander Capehart 2022-09-16 20:12:00 -06:00
parent 9e9e1a007d
commit 765f2f9a18
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
21 changed files with 129 additions and 162 deletions

View file

@ -131,7 +131,7 @@ class AlbumDetailFragment :
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
when (settings.detailPlaybackMode) {
null, MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
@ -151,11 +151,11 @@ class AlbumDetailFragment :
}
override fun onPlayParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onShuffleParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), true)
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onShowSortMenu(anchor: View) {

View file

@ -123,7 +123,7 @@ class ArtistDetailFragment :
is Song -> {
when (settings.detailPlaybackMode) {
null, MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
@ -150,11 +150,11 @@ class ArtistDetailFragment :
}
override fun onPlayParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onShuffleParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), true)
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onShowSortMenu(anchor: View) {

View file

@ -123,7 +123,7 @@ class GenreDetailFragment :
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
when (settings.detailPlaybackMode) {
null -> playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value))
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
@ -144,11 +144,11 @@ class GenreDetailFragment :
}
override fun onPlayParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onShuffleParent() {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), true)
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onShowSortMenu(anchor: View) {

View file

@ -113,7 +113,7 @@ class SongListFragment : HomeListFragment<Song>() {
override fun onItemClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.java}" }
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {

View file

@ -26,6 +26,7 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Date.Companion.from
import org.oxycblt.auxio.music.extractor.parseId3GenreNames
import org.oxycblt.auxio.music.extractor.parseMultiValue
import org.oxycblt.auxio.music.extractor.parseReleaseType
import org.oxycblt.auxio.settings.Settings
@ -40,6 +41,8 @@ import java.util.UUID
import kotlin.math.max
import kotlin.math.min
// TODO: Make empty parents a hard error
// --- MUSIC MODELS ---
/** [Item] variant that represents a music item. */
@ -204,11 +207,11 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
update(raw.albumName)
update(raw.date)
update(raw.artistNames)
update(raw.albumArtistNames)
update(raw.track)
update(raw.disc)
update(raw.artistNames)
update(raw.albumArtistNames)
}
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
@ -317,7 +320,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
}
)
val _rawGenres = raw.genreNames.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw(null)) }
val _rawGenres = raw.genreNames.parseId3GenreNames(settings)
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw(null)) }
fun _link(album: Album) {
_album = album
@ -379,7 +383,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun resolveName(context: Context) = rawName
/** The latest date this album was released. */
/** The earliest date this album was released. */
val date: Date?
/** The release type of this album, such as "EP". Defaults to "Album". */
@ -435,7 +439,6 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
}
totalDuration += song.durationMs
}
date = earliestDate
@ -528,11 +531,11 @@ class Genre constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
val durationMs: Long
init {
val totalDuration = 0L
var totalDuration = 0L
for (song in songs) {
song._link(this)
durationMs += song.durationMs
totalDuration += song.durationMs
}
durationMs = totalDuration

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.extractor
import org.oxycblt.auxio.music.Song
/** TODO: Stub class, not implemented yet */
class CacheLayer {
class CacheDatabase {
fun init() {
}

View file

@ -68,9 +68,7 @@ import java.io.File
* to something that actually works, not even in Android 12. ID3v2.4 has been around for *21
* years.* *It can drink now.*
*
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
* table, so we have to go for the less efficient "make a big query on all the songs lol" method so
* that songs don't end up fragmented across artists. Pretty much every OEM has added some extension
* Not to mention all the other infuriating quirks. Pretty much every OEM has added some extension
* or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH) crippling the
* normal tables so that you're railroaded into their music app. I have to use a semi-deprecated
* field to work with file paths, and the supposedly "modern" method is SLOWER and causes even more
@ -82,12 +80,12 @@ import java.io.File
* Is there anything we can do about it? No. Google has routinely shut down issues that begged
* google to fix glaring issues with MediaStore or to just take the API behind the woodshed and
* shoot it. Largely because they have zero incentive to improve it given how "obscure" local music
* listening is. As a result, Auxio exposes an option to use an internal parser based on ExoPlayer
* that at least tries to correct the insane metadata that this API returns, but not only is that
* system horrifically slow and bug-prone, it also faces the even larger issue of how google keeps
* trying to kill the filesystem and force you into their ContentResolver API. In the future
* MediaStore could be the only system we have, which is also the day that greenland melts and
* birthdays stop happening forever.
* listening is. As a result, I am forced to write my own extractor (Which is the contents of the
* rest of this module) based on ExoPlayer that at least tries to correct the insane metadata that
* this API returns, but not only is that system horrifically slow and bug-prone, it also faces the
* even larger issue of how google keeps trying to kill the filesystem and force you into their
* ContentResolver API. In the future MediaStore could be the only system we have, which is also
* the day that greenland melts and birthdays stop happening forever.
*
* I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and
* probably deprecated eventually for a "new" API that just coincidentally excludes music indexing.
@ -102,7 +100,7 @@ import java.io.File
* music loading process.
* @author OxygenCobalt
*/
abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheLayer) {
abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheDatabase) {
private var cursor: Cursor? = null
private var idIndex = -1
@ -249,7 +247,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
* This returns true if the song could be restored from cache, false if metadata had to be
* re-extracted, and null if the cursor is exhausted.
*/
fun populateRaw(raw: Song.Raw): Boolean? {
fun populateRawSong(raw: Song.Raw): Boolean? {
val cursor = requireNotNull(cursor) { "MediaStoreLayer is not properly initialized" }
if (!cursor.moveToNext()) {
logD("Cursor is exhausted")
@ -374,7 +372,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
* API 21 onwards to API 29.
* @author OxygenCobalt
*/
class Api21MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
class Api21MediaStoreLayer(context: Context, cacheLayer: CacheDatabase) :
MediaStoreLayer(context, cacheLayer) {
private var trackIndex = -1
private var dataIndex = -1
@ -440,7 +438,7 @@ class Api21MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.Q)
open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheDatabase) :
MediaStoreLayer(context, cacheLayer) {
private var volumeIndex = -1
private var relativePathIndex = -1
@ -496,7 +494,7 @@ open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.Q)
open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheDatabase) :
BaseApi29MediaStoreLayer(context, cacheLayer) {
private var trackIndex = -1
@ -528,7 +526,7 @@ open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.R)
class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
class Api30MediaStoreLayer(context: Context, cacheLayer: CacheDatabase) :
BaseApi29MediaStoreLayer(context, cacheLayer) {
private var trackIndex: Int = -1
private var discIndex: Int = -1

View file

@ -55,7 +55,7 @@ class MetadataLayer(private val context: Context, private val mediaStoreLayer: M
suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
while (true) {
val raw = Song.Raw()
if (mediaStoreLayer.populateRaw(raw) ?: break) {
if (mediaStoreLayer.populateRawSong(raw) ?: break) {
// No need to extract metadata that was successfully restored from the cache
emit(raw)
continue

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.dirs
package org.oxycblt.auxio.music.settings
import android.view.View
import android.view.ViewGroup

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.dirs
package org.oxycblt.auxio.music.settings
import org.oxycblt.auxio.music.Directory

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.dirs
package org.oxycblt.auxio.music.settings
import android.net.Uri
import android.os.Bundle

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.separators
package org.oxycblt.auxio.music.settings
import android.os.Bundle
import android.view.LayoutInflater

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer
import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer
import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
import org.oxycblt.auxio.music.extractor.CacheLayer
import org.oxycblt.auxio.music.extractor.CacheDatabase
import org.oxycblt.auxio.music.extractor.MetadataLayer
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
@ -202,7 +202,7 @@ class Indexer {
// experience. This is technically dependency injection. Except it doesn't increase
// your compile times by 3x. Isn't that nice.
val cacheLayer = CacheLayer()
val cacheLayer = CacheDatabase()
val mediaStoreLayer =
when {

View file

@ -38,7 +38,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logE
/**
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
@ -92,72 +91,70 @@ class PlaybackViewModel(application: Application) :
// --- PLAYING FUNCTIONS ---
/** Play a [song] from all songs. */
fun play(song: Song) {
fun playFromAll(song: Song) {
playbackManager.play(song, null, settings)
}
/** Play a song from it's album. */
fun playFromAlbum(song: Song) {
playbackManager.play(song, song.album, settings)
}
/** Play a song from it's artist. */
fun playFromArtist(song: Song) {
playbackManager.play(song, song.album.artist, settings)
}
/** Play a song from the specific genre that contains the song. */
fun playFromGenre(song: Song, genre: Genre) {
if (!genre.songs.contains(song)) {
logE("Genre does not contain song, not playing")
return
}
playbackManager.play(song, genre, settings)
}
/**
* Play an [album].
* @param shuffled Whether to shuffle the new queue
*/
fun play(album: Album, shuffled: Boolean) {
if (album.songs.isEmpty()) {
logE("Album is empty, Not playing")
return
}
playbackManager.play(album, shuffled, settings)
}
/**
* Play an [artist].
* @param shuffled Whether to shuffle the new queue
*/
fun play(artist: Artist, shuffled: Boolean) {
if (artist.songs.isEmpty()) {
logE("Artist is empty, Not playing")
return
}
playbackManager.play(artist, shuffled, settings)
}
/**
* Play a [genre].
* @param shuffled Whether to shuffle the new queue
*/
fun play(genre: Genre, shuffled: Boolean) {
if (genre.songs.isEmpty()) {
logE("Genre is empty, Not playing")
return
}
playbackManager.play(genre, shuffled, settings)
}
/** Shuffle all songs */
fun shuffleAll() {
playbackManager.shuffleAll(settings)
playbackManager.play(null, null, settings, true)
}
/** Play a song from it's album. */
fun playFromAlbum(song: Song) {
playbackManager.play(song, song.album, settings, false)
}
/** Play a song from it's artist. */
fun playFromArtist(song: Song) {
playbackManager.play(song, song.album.artist, settings, false)
}
/** Play a song from the specific genre that contains the song. */
fun playFromGenre(song: Song, genre: Genre) {
playbackManager.play(song, genre, settings, false)
}
/**
* Play an [album].
*/
fun play(album: Album) {
playbackManager.play(null, album, settings, false)
}
/**
* Play an [artist].
*/
fun play(artist: Artist) {
playbackManager.play(null, artist, settings, false)
}
/**
* Play a [genre].
*/
fun play(genre: Genre) {
playbackManager.play(null, genre, settings, false)
}
/**
* Shuffle an [album].
*/
fun shuffle(album: Album) {
playbackManager.play(null, album, settings, true)
}
/**
* Shuffle an [artist].
*/
fun shuffle(artist: Artist) {
playbackManager.play(null, artist, settings, true)
}
/**
* Shuffle a [genre].
*/
fun shuffle(genre: Genre) {
playbackManager.play(null, genre, settings, true)
}
/**

View file

@ -151,54 +151,27 @@ class PlaybackStateManager private constructor() {
/** Play a song from a parent that contains the song. */
@Synchronized
fun play(song: Song, parent: MusicParent?, settings: Settings) {
fun play(
song: Song?,
parent: MusicParent?,
settings: Settings,
shuffled: Boolean = settings.keepShuffle && isShuffled
) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
this.parent = parent
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)
_queue = (parent?.songs ?: library.songs).toMutableList()
orderQueue(settings, shuffled, song)
notifyNewPlayback()
notifyShuffledChanged()
internalPlayer.loadSong(song, true)
internalPlayer.loadSong(this.song, true)
isInitialized = true
}
/** Play a [parent], such as an artist or album. */
@Synchronized
fun play(parent: MusicParent, shuffled: Boolean, settings: Settings) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
this.parent = parent
applyNewQueue(library, settings, shuffled, null)
notifyNewPlayback()
notifyShuffledChanged()
internalPlayer.loadSong(song, true)
isInitialized = true
}
/** Shuffle all songs. */
@Synchronized
fun shuffleAll(settings: Settings) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
parent = null
applyNewQueue(library, settings, true, null)
notifyNewPlayback()
notifyShuffledChanged()
internalPlayer.loadSong(song, true)
isInitialized = true
}
// --- QUEUE FUNCTIONS ---
/** Go to the next song, along with doing all the checks that entails. */
@ -288,27 +261,24 @@ class PlaybackStateManager private constructor() {
/** Set whether this instance is [shuffled]. Updates the queue accordingly. */
@Synchronized
fun reshuffle(shuffled: Boolean, settings: Settings) {
val library = musicStore.library ?: return
val song = song ?: return
applyNewQueue(library, settings, shuffled, song)
orderQueue(settings, shuffled, song)
notifyQueueReworked()
notifyShuffledChanged()
}
private fun applyNewQueue(
library: MusicStore.Library,
private fun orderQueue(
settings: Settings,
shuffled: Boolean,
keep: Song?
) {
val newQueue = (parent?.songs ?: library.songs).toMutableList()
val newIndex: Int
if (shuffled) {
newQueue.shuffle()
_queue.shuffle()
if (keep != null) {
newQueue.add(0, newQueue.removeAt(newQueue.indexOf(keep)))
_queue.add(0, _queue.removeAt(_queue.indexOf(keep)))
}
newIndex = 0
@ -323,12 +293,11 @@ class PlaybackStateManager private constructor() {
}
}
sort.songsInPlace(newQueue)
newIndex = keep?.let(newQueue::indexOf) ?: 0
sort.songsInPlace(_queue)
newIndex = keep?.let(_queue::indexOf) ?: 0
}
_queue = newQueue
_queue = queue
index = newIndex
isShuffled = shuffled
}

View file

@ -163,7 +163,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
}
song.album.date?.let {
song.date?.let {
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString())
}

View file

@ -382,7 +382,7 @@ class PlaybackService :
}
}
is InternalPlayer.Action.ShuffleAll -> {
playbackManager.shuffleAll(settings)
playbackManager.play(null, null, settings, true)
}
is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song ->

View file

@ -151,7 +151,7 @@ class SearchFragment :
override fun onItemClick(item: Item) {
when (item) {
is Song -> when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {

View file

@ -30,7 +30,7 @@ import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.dirs.MusicDirs
import org.oxycblt.auxio.music.settings.MusicDirs
import org.oxycblt.auxio.playback.BarAction
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp

View file

@ -96,10 +96,10 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
R.id.action_play -> {
playbackModel.play(album, false)
playbackModel.play(album)
}
R.id.action_shuffle -> {
playbackModel.play(album, true)
playbackModel.shuffle(album)
}
R.id.action_play_next -> {
playbackModel.playNext(album)
@ -131,10 +131,10 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
R.id.action_play -> {
playbackModel.play(artist, false)
playbackModel.play(artist)
}
R.id.action_shuffle -> {
playbackModel.play(artist, true)
playbackModel.shuffle(artist)
}
R.id.action_play_next -> {
playbackModel.playNext(artist)
@ -163,10 +163,10 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
R.id.action_play -> {
playbackModel.play(genre, false)
playbackModel.play(genre)
}
R.id.action_shuffle -> {
playbackModel.play(genre, true)
playbackModel.shuffle(genre)
}
R.id.action_play_next -> {
playbackModel.playNext(genre)

View file

@ -83,12 +83,12 @@
tools:layout="@layout/dialog_pre_amp" />
<dialog
android:id="@+id/music_dirs_dialog"
android:name="org.oxycblt.auxio.music.dirs.MusicDirsDialog"
android:name="org.oxycblt.auxio.music.settings.MusicDirsDialog"
android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" />
<dialog
android:id="@+id/separators_dialog"
android:name="org.oxycblt.auxio.music.separators.SeparatorsDialog"
android:name="org.oxycblt.auxio.music.settings.SeparatorsDialog"
android:label="music_dirs_dialog"
tools:layout="@layout/dialog_separators" />