From a29875b5bfe7a06c5270d56fa0cf47aabc93aaa3 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 6 Jan 2023 12:02:44 -0700 Subject: [PATCH] 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. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 10 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 5 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 10 +- .../list/selection/SelectionViewModel.kt | 2 +- .../java/org/oxycblt/auxio/music/Library.kt | 183 ++++++++++++++++++ .../org/oxycblt/auxio/music/MusicStore.kt | 103 +--------- .../auxio/music/picker/PickerViewModel.kt | 2 +- .../org/oxycblt/auxio/music/system/Indexer.kt | 128 ++---------- .../playback/state/PlaybackStateDatabase.kt | 11 +- .../playback/state/PlaybackStateManager.kt | 6 +- .../auxio/playback/system/PlaybackService.kt | 3 +- .../oxycblt/auxio/search/SearchViewModel.kt | 9 +- 12 files changed, 218 insertions(+), 254 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/Library.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0ef6b3054..37545b526 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index efa3d86f6..c88f25b8e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -333,10 +333,7 @@ class HomeFragment : } } - private fun setupCompleteState( - binding: FragmentHomeBinding, - result: Result - ) { + private fun setupCompleteState(binding: FragmentHomeBinding, result: Result) { if (result.isSuccess) { logD("Received ok response") binding.homeFab.show() diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index f9e88e3ef..9661e3e2e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 754e8ca08..925d79bb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/Library.kt new file mode 100644 index 000000000..6bbb91023 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/Library.kt @@ -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 . + */ + +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, 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 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.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.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.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.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): List { + // 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, albums: List): List { + // 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>() + + 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): List { + // 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>() + 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 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index f59356a3d..6b5ea25b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -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, - val albums: List, - val artists: List, - val genres: List, - ) { - private val uidMap = HashMap() - - 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 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.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.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.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.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 { /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index 0050a8bae..15e33b3ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index adb25242a..ac2af7502 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -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? = null + @Volatile private var lastResponse: Result? = 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 { + private suspend fun loadRawSongs(metadataExtractor: MetadataExtractor): List { 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() val rawSongs = mutableListOf() 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): List { - // 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, albums: List): List { - // 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>() - - 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): List { - // 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>() - 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) { + private suspend fun emitCompletion(result: Result) { 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) : State() + data class Complete(val result: Result) : 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 { /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index f63e5baf4..cb5f86284 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -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 { + private fun readQueue(library: Library): List { val queue = mutableListOf() readableDatabase.queryAll(TABLE_QUEUE) { cursor -> val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 639fa6381..ce556e0a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -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") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 521bb5949..ece6fba6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 72ea04fae..5b57859c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -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 { + private fun searchImpl(library: Library, query: String): List { val sort = Sort(Sort.Mode.ByName, true) val filterMode = settings.searchFilterMode val results = mutableListOf()