music: parallelize library creation

Make it so that the DeviceLibrary constructor streams all song
information instead of building the library on completion.

This has no measurable effect on loading times, but does appear visibly
faster to the user since the loading process is no longer stuck on the
"Loading your music library" step.
This commit is contained in:
Alexander Capehart 2023-06-07 19:32:22 -06:00
parent 8edfcd22c7
commit 07eefda67a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 205 additions and 213 deletions

View file

@ -372,6 +372,7 @@ constructor(
// Do the initial query of the cache and media databases in parallel. // Do the initial query of the cache and media databases in parallel.
logD("Starting MediaStore query") logD("Starting MediaStore query")
val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() }
val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() }
val cache = val cache =
if (withCache) { if (withCache) {
logD("Reading cache") logD("Reading cache")
@ -388,6 +389,7 @@ constructor(
logD("Starting song discovery") logD("Starting song discovery")
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
val processedSongs = Channel<RawSong>(Channel.UNLIMITED)
logD("Started MediaStore discovery") logD("Started MediaStore discovery")
val mediaStoreJob = val mediaStoreJob =
worker.scope.tryAsync { worker.scope.tryAsync {
@ -400,10 +402,17 @@ constructor(
tagExtractor.consume(incompleteSongs, completeSongs) tagExtractor.consume(incompleteSongs, completeSongs)
completeSongs.close() completeSongs.close()
} }
logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Default) {
deviceLibraryFactory.create(completeSongs, processedSongs).also {
processedSongs.close()
}
}
// Await completed raw songs as they are processed. // Await completed raw songs as they are processed.
val rawSongs = LinkedList<RawSong>() val rawSongs = LinkedList<RawSong>()
for (rawSong in completeSongs) { for (rawSong in processedSongs) {
rawSongs.add(rawSong) rawSongs.add(rawSong)
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
} }
@ -417,29 +426,22 @@ constructor(
throw NoMusicException() throw NoMusicException()
} }
// Successfully loaded the library, now save the cache, create the library, and // Successfully loaded the library, now save the cache and read playlist information
// read playlist information in parallel. // in parallel.
logD("Discovered ${rawSongs.size} songs, starting finalization") logD("Discovered ${rawSongs.size} songs, starting finalization")
// TODO: Indicate playlist state in loading process? // TODO: Indicate playlist state in loading process?
emitLoading(IndexingProgress.Indeterminate) emitLoading(IndexingProgress.Indeterminate)
val deviceLibraryChannel = Channel<DeviceLibrary>() logD("Starting UserLibrary query")
logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Default) {
deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) }
}
logD("Starting UserLibrary creation")
val userLibraryJob =
worker.scope.tryAsync {
userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() }
}
if (cache == null || cache.invalidated) { if (cache == null || cache.invalidated) {
logD("Writing cache [why=${cache?.invalidated}]") logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs) cacheRepository.writeCache(rawSongs)
} }
logD("Awaiting library creation") logD("Awaiting UserLibrary query")
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
logD("Awaiting DeviceLibrary creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow() val deviceLibrary = deviceLibraryJob.await().getOrThrow()
val userLibrary = userLibraryJob.await().getOrThrow() logD("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
emitComplete(null) emitComplete(null)

View file

@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.device
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Organized music library information obtained from device storage. * Organized music library information obtained from device storage.
@ -46,13 +46,13 @@ import org.oxycblt.auxio.util.logW
*/ */
interface DeviceLibrary { interface DeviceLibrary {
/** All [Song]s in this [DeviceLibrary]. */ /** All [Song]s in this [DeviceLibrary]. */
val songs: List<Song> val songs: Collection<Song>
/** All [Album]s in this [DeviceLibrary]. */ /** All [Album]s in this [DeviceLibrary]. */
val albums: List<Album> val albums: Collection<Album>
/** All [Artist]s in this [DeviceLibrary]. */ /** All [Artist]s in this [DeviceLibrary]. */
val artists: List<Artist> val artists: Collection<Artist>
/** All [Genre]s in this [DeviceLibrary]. */ /** All [Genre]s in this [DeviceLibrary]. */
val genres: List<Genre> val genres: Collection<Genre>
/** /**
* Find a [Song] instance corresponding to the given [Music.UID]. * Find a [Song] instance corresponding to the given [Music.UID].
@ -97,38 +97,157 @@ interface DeviceLibrary {
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
interface Factory { interface Factory {
/** suspend fun create(
* Create a new [DeviceLibrary]. rawSongs: Channel<RawSong>,
* processedSongs: Channel<RawSong>
* @param rawSongs [RawSong] instances to create a [DeviceLibrary] from. ): DeviceLibraryImpl
*/
suspend fun create(rawSongs: List<RawSong>): DeviceLibrary
}
companion object {
/**
* Create an instance of [DeviceLibrary].
*
* @param rawSongs [RawSong]s to create the library out of.
* @param settings [MusicSettings] required.
*/
fun from(rawSongs: List<RawSong>, settings: MusicSettings): DeviceLibrary =
DeviceLibraryImpl(rawSongs, settings)
} }
} }
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) : class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
DeviceLibrary.Factory { DeviceLibrary.Factory {
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary = override suspend fun create(
DeviceLibraryImpl(rawSongs, musicSettings) rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
): DeviceLibraryImpl {
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()
val genreGrouping = mutableMapOf<RawGenre.Key, Grouping<RawGenre, SongImpl>>()
// All music information is grouped as it is indexed by other components.
for (rawSong in rawSongs) {
val song = SongImpl(rawSong, musicSettings)
// At times the indexer produces duplicate songs, try to filter these. Comparing by
// UID is sufficient for something like this, and also prevents collisions from
// causing severe issues elsewhere.
if (songGrouping.containsKey(song.uid)) {
logW(
"Duplicate song found: ${song.path} in " +
"collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}")
processedSongs.send(rawSong)
continue
}
songGrouping[song.uid] = song
// Group the new song into an album.
val albumKey = song.rawAlbum.key
val albumBody = albumGrouping[albumKey]
if (albumBody != null) {
albumBody.music.add(song)
val prioritized = albumBody.raw.src
// Since albums are grouped fuzzily, we pick the song with the earliest track to
// use for album information to ensure consistent metadata and UIDs. Fall back to
// the name otherwise.
val trackLower =
song.track != null && (prioritized.track == null || song.track < prioritized.track)
val nameLower =
song.name < prioritized.name
if (trackLower || nameLower) {
albumBody.raw = PrioritizedRaw(song.rawAlbum, song)
}
} else {
// Need to initialize this grouping.
albumGrouping[albumKey] =
Grouping(PrioritizedRaw(song.rawAlbum, song), mutableListOf(song))
}
// Group the song into each of it's artists.
for (rawArtist in song.rawArtists) {
val artistKey = rawArtist.key
val artistBody = artistGrouping[artistKey]
if (artistBody != null) {
// Since artists are not guaranteed to have songs, song artist information is
// de-prioritized compared to album artist information.
artistBody.music.add(song)
} else {
// Need to initialize this grouping.
artistGrouping[artistKey] =
Grouping(PrioritizedRaw(rawArtist, song), mutableListOf(song))
}
}
// Group the song into each of it's genres.
for (rawGenre in song.rawGenres) {
val genreKey = rawGenre.key
val genreBody = genreGrouping[genreKey]
if (genreBody != null) {
genreBody.music.add(song)
// Genre information from higher songs in ascending alphabetical order are
// prioritized.
val prioritized = genreBody.raw.src
val nameLower = song.name < prioritized.name
if (nameLower) {
genreBody.raw = PrioritizedRaw(rawGenre, song)
}
} else {
// Need to initialize this grouping.
genreGrouping[genreKey] =
Grouping(PrioritizedRaw(rawGenre, song), mutableListOf(song))
}
}
processedSongs.send(rawSong)
}
// Now that all songs are processed, also process albums and group them into their
// respective artists.
val albums =
albumGrouping.values.map { AlbumImpl(it.raw.inner, musicSettings, it.music) }
for (album in albums) {
for (rawArtist in album.rawArtists) {
val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(album)
when (val prioritized = body.raw.src) {
// Immediately replace any songs that initially held the priority position.
is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album)
is AlbumImpl -> {
// Album information from later dates is prioritized, as it is more likely to
// contain the "modern" name of the artist if the information really is
// in-consistent. Fall back to the name otherwise.
val dateEarlier =
album.dates != null && (prioritized.dates == null || album.dates < prioritized.dates)
val nameLower =
album.name < prioritized.name
if (dateEarlier || nameLower) {
body.raw = PrioritizedRaw(rawArtist, album)
}
}
else -> throw IllegalStateException()
}
} else {
// Need to initialize this grouping.
artistGrouping[key] =
Grouping(PrioritizedRaw(rawArtist, album), mutableListOf(album))
}
}
}
// Artists and genres do not need to be grouped and can be processed immediately.
val artists =
artistGrouping.values.map { ArtistImpl(it.raw.inner, musicSettings, it.music) }
val genres =
genreGrouping.values.map { GenreImpl(it.raw.inner, musicSettings, it.music) }
return DeviceLibraryImpl(songGrouping.values, albums, artists, genres)
}
private data class Grouping<R, M : Music>(
var raw: PrioritizedRaw<R, M>,
val music: MutableList<M>
)
private data class PrioritizedRaw<R, M : Music>(val inner: R, val src: M)
} }
private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : DeviceLibrary { class DeviceLibraryImpl(
override val songs = buildSongs(rawSongs, settings) override val songs: Collection<SongImpl>,
override val albums = buildAlbums(songs, settings) override val albums: Collection<AlbumImpl>,
override val artists = buildArtists(songs, albums, settings) override val artists: Collection<ArtistImpl>,
override val genres = buildGenres(songs, settings) override val genres: Collection<GenreImpl>
) : DeviceLibrary {
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
@ -139,12 +258,13 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs
override fun hashCode() = songs.hashCode() override fun hashCode() = songs.hashCode()
override fun toString() = override fun toString() =
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})" "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " +
"artists=${artists.size}, genres=${genres.size})"
override fun findSong(uid: Music.UID) = songUidMap[uid] override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
override fun findAlbum(uid: Music.UID) = albumUidMap[uid] override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
override fun findArtist(uid: Music.UID) = artistUidMap[uid] override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
override fun findGenre(uid: Music.UID) = genreUidMap[uid] override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
override fun findSongForUri(context: Context, uri: Uri) = override fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(
@ -157,135 +277,4 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size } songs.find { it.path.name == displayName && it.size == size }
} }
private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings): List<SongImpl> {
val start = System.currentTimeMillis()
val uidSet = LinkedHashSet<Music.UID>(rawSongs.size)
val songs = LinkedList<SongImpl>()
for (rawSong in rawSongs) {
val song = SongImpl(rawSong, settings)
if (uidSet.add(song.uid)) {
songs.add(song)
} else {
logW("Duplicate song found: $song")
}
}
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
return songs
}
private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
val start = System.currentTimeMillis()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl, SongImpl>>()
for (song in songs) {
val key = RawAlbum.Key(song.rawAlbum)
val body = albumGrouping[key]
if (body != null) {
body.music.add(song)
val dominantSong = body.dominantRaw.derived
val dominates =
song.track != null &&
(dominantSong.track == null || song.track < dominantSong.track)
if (dominates) {
body.dominantRaw = DominantRaw(song.rawAlbum, song)
}
} else {
albumGrouping[key] = Grouping(DominantRaw(song.rawAlbum, song), mutableListOf(song))
}
}
// 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 albums =
albumGrouping.values.map { AlbumImpl(it.dominantRaw.inner, settings, it.music) }
logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms")
return albums
}
private fun buildArtists(
songs: List<SongImpl>,
albums: List<AlbumImpl>,
settings: MusicSettings
): List<ArtistImpl> {
val start = System.currentTimeMillis()
// Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists.
// Songs and albums are grouped by artist and album artist respectively.
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, AlbumImpl, Music>>()
for (song in songs) {
for (rawArtist in song.rawArtists) {
val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(song)
} else {
artistGrouping[key] =
Grouping(DominantRaw(rawArtist, albums.first()), mutableListOf(song))
}
}
}
for (album in albums) {
for (rawArtist in album.rawArtists) {
val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(album)
val dominantAlbum = body.dominantRaw.derived
val dominates =
album.dates != null &&
(dominantAlbum.dates == null || album.dates < dominantAlbum.dates)
if (dominates) {
body.dominantRaw = DominantRaw(rawArtist, album)
}
} else {
artistGrouping[key] =
Grouping(DominantRaw(rawArtist, album), mutableListOf(album))
}
}
}
// Convert the combined mapping into artist instances.
val artists =
artistGrouping.values.map { ArtistImpl(it.dominantRaw.inner, settings, it.music) }
logD(
"Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms")
return artists
}
private fun buildGenres(songs: List<SongImpl>, settings: MusicSettings): List<GenreImpl> {
val start = System.currentTimeMillis()
// 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<RawGenre.Key, Grouping<RawGenre, SongImpl, SongImpl>>()
for (song in songs) {
for (rawGenre in song.rawGenres) {
val key = RawGenre.Key(rawGenre)
val body = songsByGenre[key]
if (body != null) {
body.music.add(song)
val dominantSong = body.dominantRaw.derived
if (song.date != null && song.name < dominantSong.name) {
body.dominantRaw = DominantRaw(rawGenre, song)
}
} else {
songsByGenre[key] = Grouping(DominantRaw(rawGenre, song), mutableListOf(song))
}
}
}
// Convert the mapping into genre instances.
val genres =
songsByGenre.map { GenreImpl(it.value.dominantRaw.inner, settings, it.value.music) }
logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms")
return genres
}
data class DominantRaw<R, M : Music>(val inner: R, val derived: M)
data class Grouping<R, D : Music, M : Music>(
var dominantRaw: DominantRaw<R, D>,
val music: MutableList<M>
)
} }

View file

@ -156,8 +156,8 @@ private class TagWorkerImpl(
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
rawSong.albumArtistNames = it rawSong.albumArtistNames = it
} }
(textFrames["TXXX:albumartistssort"] ?: textFrames["TXXX:albumartists_sort"] (textFrames["TXXX:albumartistssort"]
?: textFrames["TSO2"]) ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
@ -248,14 +248,15 @@ private class TagWorkerImpl(
// Artist // Artist
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
(comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"] )?.let { (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let {
rawSong.artistSortNames = it rawSong.artistSortNames = it
} }
// Album artist // Album artist
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
(comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it }
(comments["albumartistssort"] ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) (comments["albumartistssort"]
?: comments["albumartists_sort"] ?: comments["albumartistsort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.user
import java.lang.Exception import java.lang.Exception
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
@ -37,6 +36,8 @@ import org.oxycblt.auxio.util.logE
* is also not backed by library information, rather an app database with in-memory caching. It is * is also not backed by library information, rather an app database with in-memory caching. It is
* generally not expected to create this yourself, and instead rely on MusicRepository. * generally not expected to create this yourself, and instead rely on MusicRepository.
* *
* TODO: Communicate errors
*
* @author Alexander Capehart * @author Alexander Capehart
*/ */
interface UserLibrary { interface UserLibrary {
@ -61,15 +62,12 @@ interface UserLibrary {
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */ /** Constructs a [UserLibrary] implementation in an asynchronous manner. */
interface Factory { interface Factory {
/** suspend fun query(): List<RawPlaylist>
* Create a new [UserLibrary].
* suspend fun create(
* @param deviceLibraryChannel Asynchronously populated [DeviceLibrary] that can be obtained rawPlaylists: List<RawPlaylist>,
* later. This allows database information to be read before the actual instance is deviceLibrary: DeviceLibrary
* constructed. ): MutableUserLibrary
* @return A new [MutableUserLibrary] with the required implementation.
*/
suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary
} }
} }
@ -123,17 +121,19 @@ class UserLibraryFactoryImpl
@Inject @Inject
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) : constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
UserLibrary.Factory { UserLibrary.Factory {
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary { override suspend fun query() =
// While were waiting for the library, read our playlists out. try {
val rawPlaylists = playlistDao.readRawPlaylists()
try { } catch (e: Exception) {
playlistDao.readRawPlaylists() logE("Unable to read playlists: $e")
} catch (e: Exception) { listOf()
logE("Unable to read playlists: $e") }
return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings)
} override suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
): MutableUserLibrary {
logD("Successfully read ${rawPlaylists.size} playlists") logD("Successfully read ${rawPlaylists.size} playlists")
val deviceLibrary = deviceLibraryChannel.receive()
// Convert the database playlist information to actual usable playlists. // Convert the database playlist information to actual usable playlists.
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>() val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) { for (rawPlaylist in rawPlaylists) {

View file

@ -58,11 +58,11 @@ interface SearchEngine {
* @param playlists A list of [Playlist], null if empty. * @param playlists A list of [Playlist], null if empty.
*/ */
data class Items( data class Items(
val songs: List<Song>?, val songs: Collection<Song>?,
val albums: List<Album>?, val albums: Collection<Album>?,
val artists: List<Artist>?, val artists: Collection<Artist>?,
val genres: List<Genre>?, val genres: Collection<Genre>?,
val playlists: List<Playlist>? val playlists: Collection<Playlist>?
) )
} }
@ -90,7 +90,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
* initially. This can be used to compare against additional attributes to improve search * initially. This can be used to compare against additional attributes to improve search
* result quality. * result quality.
*/ */
private inline fun <T : Music> List<T>.searchListImpl( private inline fun <T : Music> Collection<T>.searchListImpl(
query: String, query: String,
fallback: (String, T) -> Boolean = { _, _ -> false } fallback: (String, T) -> Boolean = { _, _ -> false }
) = ) =