music: decouple library from musicstore/indexer

De-couple the library data structure (and library grouping) from
MusicStore and Indexer.

This should make library creation *much* easier to test.
This commit is contained in:
Alexander Capehart 2023-01-06 12:02:44 -07:00
parent a5ea4af5c4
commit a29875b5bf
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 218 additions and 254 deletions

View file

@ -32,13 +32,7 @@ import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.*
@ -137,7 +131,7 @@ class DetailViewModel(application: Application) :
musicStore.removeListener(this)
}
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library == null) {
// Nothing to do.
return

View file

@ -333,10 +333,7 @@ class HomeFragment :
}
}
private fun setupCompleteState(
binding: FragmentHomeBinding,
result: Result<MusicStore.Library>
) {
private fun setupCompleteState(binding: FragmentHomeBinding, result: Result<Library>) {
if (result.isSuccess) {
logD("Received ok response")
binding.homeFab.show()

View file

@ -24,13 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
@ -104,7 +98,7 @@ class HomeViewModel(application: Application) :
settings.removeListener(this)
}
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library != null) {
logD("Library changed, refreshing library")
// Get the each list of items in the library to use as our list data.

View file

@ -38,7 +38,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
musicStore.addListener(this)
}
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library == null) {
return
}

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2023 Auxio Project
*
* 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.music
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
/**
* Organized music library information.
*
* This class allows for the creation of a well-formed music library graph from raw song
* information. It's generally not expected to create this yourself and instead use [MusicStore].
*
* @author Alexander Capehart
*/
class Library(rawSongs: List<Song.Raw>, settings: Settings) {
/** All [Song]s that were detected on the device. */
val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) })
/** All [Album]s found on the device. */
val albums = buildAlbums(songs)
/** All [Artist]s found on the device. */
val artists = buildArtists(songs, albums)
/** All [Genre]s found on the device. */
val genres = buildGenres(songs)
private val uidMap = buildMap {
// We need to finalize the newly-created music and also add it to a mapping to make
// de-serializing music from UIDs much faster. Do these in the same loop for efficiency.
for (music in (songs + albums + artists + genres)) {
music._finalize()
this[music.uid] = music
}
}
/**
* Finds a [Music] item [T] in the library by it's [Music.UID].
* @param uid The [Music.UID] to search for.
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or
* the [Music.UID] did not correspond to a [T].
*/
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
/**
* Convert a [Song] from an another library into a [Song] in this [Library].
* @param song The [Song] to convert.
* @return The analogous [Song] in this [Library], or null if it does not exist.
*/
fun sanitize(song: Song) = find<Song>(song.uid)
/**
* Convert a [Album] from an another library into a [Album] in this [Library].
* @param album The [Album] to convert.
* @return The analogous [Album] in this [Library], or null if it does not exist.
*/
fun sanitize(album: Album) = find<Album>(album.uid)
/**
* Convert a [Artist] from an another library into a [Artist] in this [Library].
* @param artist The [Artist] to convert.
* @return The analogous [Artist] in this [Library], or null if it does not exist.
*/
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
/**
* Convert a [Genre] from an another library into a [Genre] in this [Library].
* @param genre The [Genre] to convert.
* @return The analogous [Genre] in this [Library], or null if it does not exist.
*/
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
/**
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].
* @param context [Context] required to analyze the [Uri].
* @param uri [Uri] to search for.
* @return A [Song] corresponding to the given [Uri], or null if one could not be found.
*/
fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open.
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}
/**
* Build a list of [Album]s from the given [Song]s.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [Album]s when created.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* with parent [Artist] instances in order to be usable.
*/
private fun buildAlbums(songs: List<Song>): List<Album> {
// Group songs by their singular raw album, then map the raw instances and their
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
val songsByAlbum = songs.groupBy { it._rawAlbum }
val albums = songsByAlbum.map { Album(it.key, it.value) }
logD("Successfully built ${albums.size} albums")
return albums
}
/**
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
* they group into [Artist] instances much differently, with [Song]s being grouped primarily by
* artist names, and [Album]s being grouped primarily by album artist names.
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* of [Song]s and [Album]s.
*/
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
// Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists.
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
for (song in songs) {
for (rawArtist in song._rawArtists) {
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song)
}
}
for (album in albums) {
for (rawArtist in album._rawArtists) {
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(album)
}
}
// Convert the combined mapping into artist instances.
val artists = musicByArtist.map { Artist(it.key, it.value) }
logD("Successfully built ${artists.size} artists")
return artists
}
/**
* Group up [Song]s into [Genre] instances.
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* @return A non-empty list of [Genre]s.
*/
private fun buildGenres(songs: List<Song>): List<Genre> {
// Add every raw genre credited to each Song to the grouping. This way,
// different multi-genre combinations are not treated as different genres.
val songsByGenre = mutableMapOf<Genre.Raw, MutableList<Song>>()
for (song in songs) {
for (rawGenre in song._rawGenres) {
songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
}
}
// Convert the mapping into genre instances.
val genres = songsByGenre.map { Genre(it.key, it.value) }
logD("Successfully built ${genres.size} genres")
return genres
}
}

View file

@ -17,14 +17,8 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.useQuery
/**
* A repository granting access to the music library..
* A repository granting access to the music library.
*
* This can be used to obtain certain music items, or await changes to the music library. It is
* generally recommended to use this over Indexer to keep track of the library state, as the
@ -72,101 +66,6 @@ class MusicStore private constructor() {
listeners.remove(listener)
}
/**
* A library of [Music] instances.
* @param songs All [Song]s loaded from the device.
* @param albums All [Album]s that could be created.
* @param artists All [Artist]s that could be created.
* @param genres All [Genre]s that could be created.
*/
data class Library(
val songs: List<Song>,
val albums: List<Album>,
val artists: List<Artist>,
val genres: List<Genre>,
) {
private val uidMap = HashMap<Music.UID, Music>()
init {
// The data passed to Library initially are complete, but are still volitaile.
// Finalize them to ensure they are well-formed. Also initialize the UID map in
// the same loop for efficiency.
for (song in songs) {
song._finalize()
uidMap[song.uid] = song
}
for (album in albums) {
album._finalize()
uidMap[album.uid] = album
}
for (artist in artists) {
artist._finalize()
uidMap[artist.uid] = artist
}
for (genre in genres) {
genre._finalize()
uidMap[genre.uid] = genre
}
}
/**
* Finds a [Music] item [T] in the library by it's [Music.UID].
* @param uid The [Music.UID] to search for.
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be found
* or the [Music.UID] did not correspond to a [T].
*/
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
/**
* Convert a [Song] from an another library into a [Song] in this [Library].
* @param song The [Song] to convert.
* @return The analogous [Song] in this [Library], or null if it does not exist.
*/
fun sanitize(song: Song) = find<Song>(song.uid)
/**
* Convert a [Album] from an another library into a [Album] in this [Library].
* @param album The [Album] to convert.
* @return The analogous [Album] in this [Library], or null if it does not exist.
*/
fun sanitize(album: Album) = find<Album>(album.uid)
/**
* Convert a [Artist] from an another library into a [Artist] in this [Library].
* @param artist The [Artist] to convert.
* @return The analogous [Artist] in this [Library], or null if it does not exist.
*/
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
/**
* Convert a [Genre] from an another library into a [Genre] in this [Library].
* @param genre The [Genre] to convert.
* @return The analogous [Genre] in this [Library], or null if it does not exist.
*/
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
/**
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].
* @param context [Context] required to analyze the [Uri].
* @param uri [Uri] to search for.
* @return A [Song] corresponding to the given [Uri], or null if one could not be found.
*/
fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open.
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}
}
/** A listener for changes in the music library. */
interface Listener {
/**

View file

@ -50,7 +50,7 @@ class PickerViewModel : ViewModel(), MusicStore.Listener {
musicStore.removeListener(this)
}
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library != null) {
refreshChoices()
}

View file

@ -24,17 +24,10 @@ import android.os.Build
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.extractor.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
@ -52,7 +45,7 @@ import org.oxycblt.auxio.util.logW
* @author Alexander Capehart (OxygenCobalt)
*/
class Indexer private constructor() {
@Volatile private var lastResponse: Result<MusicStore.Library>? = null
@Volatile private var lastResponse: Result<Library>? = null
@Volatile private var indexingState: Indexing? = null
@Volatile private var controller: Controller? = null
@Volatile private var listener: Listener? = null
@ -198,11 +191,11 @@ class Indexer private constructor() {
* @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
* be written, but no cache entries will be loaded into the new library.
* @return A newly-loaded [MusicStore.Library].
* @return A newly-loaded [Library].
* @throws NoPermissionException If [PERMISSION_READ_AUDIO] was not granted.
* @throws NoMusicException If no music was found on the device.
*/
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library {
private suspend fun indexImpl(context: Context, withCache: Boolean): Library {
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) {
// No permissions, signal that we can't do anything.
@ -218,7 +211,6 @@ class Indexer private constructor() {
} else {
WriteOnlyCacheExtractor(context)
}
val mediaStoreExtractor =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
@ -227,33 +219,24 @@ class Indexer private constructor() {
Api29MediaStoreExtractor(context, cacheDatabase)
else -> Api21MediaStoreExtractor(context, cacheDatabase)
}
val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor)
val songs =
buildSongs(metadataExtractor, Settings(context)).ifEmpty { throw NoMusicException() }
val rawSongs = loadRawSongs(metadataExtractor).ifEmpty { throw NoMusicException() }
// Build the rest of the music library from the song list. This is much more powerful
// and reliable compared to using MediaStore to obtain grouping information.
val buildStart = System.currentTimeMillis()
val albums = buildAlbums(songs)
val artists = buildArtists(songs, albums)
val genres = buildGenres(songs)
val library = Library(rawSongs, Settings(context))
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
return MusicStore.Library(songs, albums, artists, genres)
return library
}
/**
* Load a list of [Song]s from the device.
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw]
* instances.
* @param settings [Settings] required to create [Song] instances.
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked
* with parent [Album], [Artist], and [Genre] items in order to be usable.
*/
private suspend fun buildSongs(
metadataExtractor: MetadataExtractor,
settings: Settings
): List<Song> {
private suspend fun loadRawSongs(metadataExtractor: MetadataExtractor): List<Song.Raw> {
logD("Starting indexing process")
val start = System.currentTimeMillis()
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
@ -263,104 +246,23 @@ class Indexer private constructor() {
yield()
// Note: We use a set here so we can eliminate song duplicates.
val songs = mutableSetOf<Song>()
val rawSongs = mutableListOf<Song.Raw>()
metadataExtractor.extract().collect { rawSong ->
songs.add(Song(rawSong, settings))
rawSongs.add(rawSong)
// Now we can signal a defined progress by showing how many songs we have
// loaded, and the projected amount of songs we found in the library
// (obtained by the extractors)
yield()
emitIndexing(Indexing.Songs(songs.size, total))
emitIndexing(Indexing.Songs(rawSongs.size, total))
}
// Finalize the extractors with the songs we have now loaded. There is no ETA
// on this process, so go back to an indeterminate state.
emitIndexing(Indexing.Indeterminate)
metadataExtractor.finalize(rawSongs)
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
// Ensure that sorting order is consistent so that grouping is also consistent.
// Rolling this into the set is not an option, as songs with the same sort result
// would be lost.
return Sort(Sort.Mode.ByName, true).songs(songs)
}
/**
* Build a list of [Album]s from the given [Song]s.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [Album]s when created.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* with parent [Artist] instances in order to be usable.
*/
private fun buildAlbums(songs: List<Song>): List<Album> {
// Group songs by their singular raw album, then map the raw instances and their
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
val songsByAlbum = songs.groupBy { it._rawAlbum }
val albums = songsByAlbum.map { Album(it.key, it.value) }
logD("Successfully built ${albums.size} albums")
return albums
}
/**
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
* they group into [Artist] instances much differently, with [Song]s being grouped primarily by
* artist names, and [Album]s being grouped primarily by album artist names.
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* of [Song]s and [Album]s.
*/
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
// Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists.
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
for (song in songs) {
for (rawArtist in song._rawArtists) {
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song)
}
}
for (album in albums) {
for (rawArtist in album._rawArtists) {
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(album)
}
}
// Convert the combined mapping into artist instances.
val artists = musicByArtist.map { Artist(it.key, it.value) }
logD("Successfully built ${artists.size} artists")
return artists
}
/**
* Group up [Song]s into [Genre] instances.
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* @return A non-empty list of [Genre]s.
*/
private fun buildGenres(songs: List<Song>): List<Genre> {
// Add every raw genre credited to each Song to the grouping. This way,
// different multi-genre combinations are not treated as different genres.
val songsByGenre = mutableMapOf<Genre.Raw, MutableList<Song>>()
for (song in songs) {
for (rawGenre in song._rawGenres) {
songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
}
}
// Convert the mapping into genre instances.
val genres = songsByGenre.map { Genre(it.key, it.value) }
logD("Successfully built ${genres.size} genres")
return genres
logD(
"Successfully loaded ${rawSongs.size} raw songs in ${System.currentTimeMillis() - start}ms")
return rawSongs
}
/**
@ -387,7 +289,7 @@ class Indexer private constructor() {
* @param result The new [Result] to emit, representing the outcome of the music loading
* process.
*/
private suspend fun emitCompletion(result: Result<MusicStore.Library>) {
private suspend fun emitCompletion(result: Result<Library>) {
yield()
// Swap to the Main thread so that downstream callbacks don't crash from being on
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
@ -418,7 +320,7 @@ class Indexer private constructor() {
* Music loading has completed.
* @param result The outcome of the music loading process.
*/
data class Complete(val result: Result<MusicStore.Library>) : State()
data class Complete(val result: Result<Library>) : State()
}
/**
@ -456,7 +358,7 @@ class Indexer private constructor() {
*
* This is only useful for code that absolutely must show the current loading process.
* Otherwise, [MusicStore.Listener] is highly recommended due to it's updates only consisting of
* the [MusicStore.Library].
* the [Library].
*/
interface Listener {
/**

View file

@ -23,10 +23,7 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import androidx.core.database.sqlite.transaction
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.*
/**
@ -72,10 +69,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
/**
* Read a persisted [SavedState] from the database.
* @param library [MusicStore.Library] required to restore [SavedState].
* @param library [Library] required to restore [SavedState].
* @return A persisted [SavedState], or null if one could not be found.
*/
fun read(library: MusicStore.Library): SavedState? {
fun read(library: Library): SavedState? {
requireBackgroundThread()
// Read the saved state and queue. If the state is non-null, that must imply an
// existent, albeit possibly empty, queue.
@ -123,7 +120,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
}
private fun readQueue(library: MusicStore.Library): List<Song> {
private fun readQueue(library: Library): List<Song> {
val queue = mutableListOf<Song>()
readableDatabase.queryAll(TABLE_QUEUE) { cursor ->
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)

View file

@ -486,11 +486,11 @@ class PlaybackStateManager private constructor() {
}
/**
* Update the playback state to align with a new [MusicStore.Library].
* @param newLibrary The new [MusicStore.Library] that was recently loaded.
* Update the playback state to align with a new [Library].
* @param newLibrary The new [Library] that was recently loaded.
*/
@Synchronized
fun sanitize(newLibrary: MusicStore.Library) {
fun sanitize(newLibrary: Library) {
// if (!isInitialized) {
// // Nothing playing, nothing to do.
// logD("Not initialized, no need to sanitize")

View file

@ -43,6 +43,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
@ -302,7 +303,7 @@ class PlaybackService :
// --- MUSICSTORE OVERRIDES ---
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library != null) {
// We now have a library, see if we have anything we need to do.
playbackManager.requestAction(this)

View file

@ -30,10 +30,7 @@ import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
@ -63,7 +60,7 @@ class SearchViewModel(application: Application) :
musicStore.removeListener(this)
}
override fun onLibraryChanged(library: MusicStore.Library?) {
override fun onLibraryChanged(library: Library?) {
if (library != null) {
// Make sure our query is up to date with the music library.
search(lastQuery)
@ -96,7 +93,7 @@ class SearchViewModel(application: Application) :
}
}
private fun searchImpl(library: MusicStore.Library, query: String): List<Item> {
private fun searchImpl(library: Library, query: String): List<Item> {
val sort = Sort(Sort.Mode.ByName, true)
val filterMode = settings.searchFilterMode
val results = mutableListOf<Item>()