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:
parent
a5ea4af5c4
commit
a29875b5bf
12 changed files with 218 additions and 254 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
183
app/src/main/java/org/oxycblt/auxio/music/Library.kt
Normal file
183
app/src/main/java/org/oxycblt/auxio/music/Library.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()
|
||||
|
|
Loading…
Reference in a new issue