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:
parent
8edfcd22c7
commit
07eefda67a
5 changed files with 205 additions and 213 deletions
|
@ -372,6 +372,7 @@ constructor(
|
|||
// Do the initial query of the cache and media databases in parallel.
|
||||
logD("Starting MediaStore query")
|
||||
val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() }
|
||||
val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() }
|
||||
val cache =
|
||||
if (withCache) {
|
||||
logD("Reading cache")
|
||||
|
@ -388,6 +389,7 @@ constructor(
|
|||
logD("Starting song discovery")
|
||||
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
val processedSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
logD("Started MediaStore discovery")
|
||||
val mediaStoreJob =
|
||||
worker.scope.tryAsync {
|
||||
|
@ -400,10 +402,17 @@ constructor(
|
|||
tagExtractor.consume(incompleteSongs, completeSongs)
|
||||
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.
|
||||
val rawSongs = LinkedList<RawSong>()
|
||||
for (rawSong in completeSongs) {
|
||||
for (rawSong in processedSongs) {
|
||||
rawSongs.add(rawSong)
|
||||
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
||||
}
|
||||
|
@ -417,29 +426,22 @@ constructor(
|
|||
throw NoMusicException()
|
||||
}
|
||||
|
||||
// Successfully loaded the library, now save the cache, create the library, and
|
||||
// read playlist information in parallel.
|
||||
// Successfully loaded the library, now save the cache and read playlist information
|
||||
// in parallel.
|
||||
logD("Discovered ${rawSongs.size} songs, starting finalization")
|
||||
// TODO: Indicate playlist state in loading process?
|
||||
emitLoading(IndexingProgress.Indeterminate)
|
||||
val deviceLibraryChannel = Channel<DeviceLibrary>()
|
||||
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() }
|
||||
}
|
||||
logD("Starting UserLibrary query")
|
||||
if (cache == null || cache.invalidated) {
|
||||
logD("Writing cache [why=${cache?.invalidated}]")
|
||||
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 userLibrary = userLibraryJob.await().getOrThrow()
|
||||
logD("Starting UserLibrary creation")
|
||||
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
|
||||
|
||||
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
|
||||
emitComplete(null)
|
||||
|
|
|
@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.device
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import java.util.LinkedList
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
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.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.fs.useQuery
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* Organized music library information obtained from device storage.
|
||||
|
@ -46,13 +46,13 @@ import org.oxycblt.auxio.util.logW
|
|||
*/
|
||||
interface DeviceLibrary {
|
||||
/** All [Song]s in this [DeviceLibrary]. */
|
||||
val songs: List<Song>
|
||||
val songs: Collection<Song>
|
||||
/** All [Album]s in this [DeviceLibrary]. */
|
||||
val albums: List<Album>
|
||||
val albums: Collection<Album>
|
||||
/** All [Artist]s in this [DeviceLibrary]. */
|
||||
val artists: List<Artist>
|
||||
val artists: Collection<Artist>
|
||||
/** All [Genre]s in this [DeviceLibrary]. */
|
||||
val genres: List<Genre>
|
||||
val genres: Collection<Genre>
|
||||
|
||||
/**
|
||||
* Find a [Song] instance corresponding to the given [Music.UID].
|
||||
|
@ -97,38 +97,157 @@ interface DeviceLibrary {
|
|||
|
||||
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
|
||||
interface Factory {
|
||||
/**
|
||||
* Create a new [DeviceLibrary].
|
||||
*
|
||||
* @param rawSongs [RawSong] instances to create a [DeviceLibrary] from.
|
||||
*/
|
||||
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)
|
||||
suspend fun create(
|
||||
rawSongs: Channel<RawSong>,
|
||||
processedSongs: Channel<RawSong>
|
||||
): DeviceLibraryImpl
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
|
||||
DeviceLibrary.Factory {
|
||||
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary =
|
||||
DeviceLibraryImpl(rawSongs, musicSettings)
|
||||
override suspend fun create(
|
||||
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 {
|
||||
override val songs = buildSongs(rawSongs, settings)
|
||||
override val albums = buildAlbums(songs, settings)
|
||||
override val artists = buildArtists(songs, albums, settings)
|
||||
override val genres = buildGenres(songs, settings)
|
||||
|
||||
class DeviceLibraryImpl(
|
||||
override val songs: Collection<SongImpl>,
|
||||
override val albums: Collection<AlbumImpl>,
|
||||
override val artists: Collection<ArtistImpl>,
|
||||
override val genres: Collection<GenreImpl>
|
||||
) : DeviceLibrary {
|
||||
// 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 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 hashCode() = songs.hashCode()
|
||||
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 findAlbum(uid: Music.UID) = albumUidMap[uid]
|
||||
override fun findArtist(uid: Music.UID) = artistUidMap[uid]
|
||||
override fun findGenre(uid: Music.UID) = genreUidMap[uid]
|
||||
override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
|
||||
override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
|
||||
override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
|
||||
override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
|
||||
|
||||
override fun findSongForUri(context: Context, uri: Uri) =
|
||||
context.contentResolverSafe.useQuery(
|
||||
|
@ -157,135 +277,4 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
|
|||
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -156,8 +156,8 @@ private class TagWorkerImpl(
|
|||
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
|
||||
rawSong.albumArtistNames = it
|
||||
}
|
||||
(textFrames["TXXX:albumartistssort"] ?: textFrames["TXXX:albumartists_sort"]
|
||||
?: textFrames["TSO2"])
|
||||
(textFrames["TXXX:albumartistssort"]
|
||||
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])
|
||||
?.let { rawSong.albumArtistSortNames = it }
|
||||
|
||||
// Genre
|
||||
|
@ -248,14 +248,15 @@ private class TagWorkerImpl(
|
|||
// Artist
|
||||
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = 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
|
||||
}
|
||||
|
||||
// Album artist
|
||||
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = 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 }
|
||||
|
||||
// Genre
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.user
|
|||
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
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
|
||||
* generally not expected to create this yourself, and instead rely on MusicRepository.
|
||||
*
|
||||
* TODO: Communicate errors
|
||||
*
|
||||
* @author Alexander Capehart
|
||||
*/
|
||||
interface UserLibrary {
|
||||
|
@ -61,15 +62,12 @@ interface UserLibrary {
|
|||
|
||||
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
|
||||
interface Factory {
|
||||
/**
|
||||
* Create a new [UserLibrary].
|
||||
*
|
||||
* @param deviceLibraryChannel Asynchronously populated [DeviceLibrary] that can be obtained
|
||||
* later. This allows database information to be read before the actual instance is
|
||||
* constructed.
|
||||
* @return A new [MutableUserLibrary] with the required implementation.
|
||||
*/
|
||||
suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary
|
||||
suspend fun query(): List<RawPlaylist>
|
||||
|
||||
suspend fun create(
|
||||
rawPlaylists: List<RawPlaylist>,
|
||||
deviceLibrary: DeviceLibrary
|
||||
): MutableUserLibrary
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,17 +121,19 @@ class UserLibraryFactoryImpl
|
|||
@Inject
|
||||
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
||||
UserLibrary.Factory {
|
||||
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary {
|
||||
// While were waiting for the library, read our playlists out.
|
||||
val rawPlaylists =
|
||||
try {
|
||||
playlistDao.readRawPlaylists()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to read playlists: $e")
|
||||
return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings)
|
||||
}
|
||||
override suspend fun query() =
|
||||
try {
|
||||
playlistDao.readRawPlaylists()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to read playlists: $e")
|
||||
listOf()
|
||||
}
|
||||
|
||||
override suspend fun create(
|
||||
rawPlaylists: List<RawPlaylist>,
|
||||
deviceLibrary: DeviceLibrary
|
||||
): MutableUserLibrary {
|
||||
logD("Successfully read ${rawPlaylists.size} playlists")
|
||||
val deviceLibrary = deviceLibraryChannel.receive()
|
||||
// Convert the database playlist information to actual usable playlists.
|
||||
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
||||
for (rawPlaylist in rawPlaylists) {
|
||||
|
|
|
@ -58,11 +58,11 @@ interface SearchEngine {
|
|||
* @param playlists A list of [Playlist], null if empty.
|
||||
*/
|
||||
data class Items(
|
||||
val songs: List<Song>?,
|
||||
val albums: List<Album>?,
|
||||
val artists: List<Artist>?,
|
||||
val genres: List<Genre>?,
|
||||
val playlists: List<Playlist>?
|
||||
val songs: Collection<Song>?,
|
||||
val albums: Collection<Album>?,
|
||||
val artists: Collection<Artist>?,
|
||||
val genres: Collection<Genre>?,
|
||||
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
|
||||
* result quality.
|
||||
*/
|
||||
private inline fun <T : Music> List<T>.searchListImpl(
|
||||
private inline fun <T : Music> Collection<T>.searchListImpl(
|
||||
query: String,
|
||||
fallback: (String, T) -> Boolean = { _, _ -> false }
|
||||
) =
|
||||
|
|
Loading…
Reference in a new issue