music: improve indexing progress

This commit is contained in:
Alexander Capehart 2024-11-26 13:07:24 -07:00
parent 0ba5ddce51
commit b0c6dd2b74
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 76 additions and 190 deletions

View file

@ -58,7 +58,6 @@ import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectionFragment import org.oxycblt.auxio.list.SelectionFragment
import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.IndexingProgress
import org.oxycblt.auxio.music.IndexingState import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
@ -70,6 +69,7 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.PlaylistMessage
import org.oxycblt.auxio.music.external.M3U import org.oxycblt.auxio.music.external.M3U
import org.oxycblt.auxio.music.stack.IndexingProgress
import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
@ -384,19 +384,12 @@ class HomeFragment :
binding.homeIndexingActions.visibility = View.INVISIBLE binding.homeIndexingActions.visibility = View.INVISIBLE
binding.homeIndexingStatus.setText(R.string.lng_indexing) binding.homeIndexingStatus.setText(R.string.lng_indexing)
when (progress) {
is IndexingProgress.Indeterminate -> { // Actively loading songs, show the current progress.
// In a query/initialization state, show a generic loading status. binding.homeIndexingProgress.apply {
binding.homeIndexingProgress.isIndeterminate = true isIndeterminate = false
} max = progress.explored
is IndexingProgress.Songs -> { this.progress = progress.interpreted
// Actively loading songs, show the current progress.
binding.homeIndexingProgress.apply {
isIndeterminate = false
max = progress.total
this.progress = progress.current
}
}
} }
} }

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.os.Build import android.os.Build
import org.oxycblt.auxio.music.stack.IndexingProgress
/** Version-aware permission identifier for reading audio files. */ /** Version-aware permission identifier for reading audio files. */
val PERMISSION_READ_AUDIO = val PERMISSION_READ_AUDIO =
@ -50,24 +51,6 @@ sealed interface IndexingState {
data class Completed(val error: Exception?) : IndexingState data class Completed(val error: Exception?) : IndexingState
} }
/**
* Represents the current progress of music loading.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface IndexingProgress {
/** Other work is being done that does not have a defined progress. */
data object Indeterminate : IndexingProgress
/**
* Songs are currently being loaded.
*
* @param current The current amount of songs loaded.
* @param total The projected total amount of songs.
*/
data class Songs(val current: Int, val total: Int) : IndexingProgress
}
/** /**
* Thrown by the music loader when [PERMISSION_READ_AUDIO] was not granted. * Thrown by the music loader when [PERMISSION_READ_AUDIO] was not granted.
* *

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.Indexer import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.stack.IndexingProgress
import org.oxycblt.auxio.music.stack.interpret.Interpretation import org.oxycblt.auxio.music.stack.interpret.Interpretation
import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary import org.oxycblt.auxio.music.stack.interpret.model.MutableLibrary
import timber.log.Timber as L import timber.log.Timber as L
@ -363,26 +364,8 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
Name.Known.SimpleFactory Name.Known.SimpleFactory
} }
var explored = 0
var loaded = 0
val newLibrary = val newLibrary =
indexer.run(listOf(), Interpretation(nameFactory, separators)) { indexer.run(listOf(), Interpretation(nameFactory, separators), ::emitIndexingProgress)
when (it) {
is Indexer.Event.Discovered -> {
explored = it.amount
emitIndexingProgress(IndexingProgress.Songs(loaded, explored))
}
is Indexer.Event.Extracted -> {
loaded = it.amount
emitIndexingProgress(IndexingProgress.Songs(loaded, explored))
}
is Indexer.Event.Interpret -> {
if (explored == loaded) {
emitIndexingProgress(IndexingProgress.Indeterminate)
}
}
}
}
// We want to make sure that all reads and writes are synchronized due to the sheer // We want to make sure that all reads and writes are synchronized due to the sheer
// amount of consumers of MusicRepository. // amount of consumers of MusicRepository.

View file

@ -25,7 +25,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.ForegroundServiceNotification
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.IndexingProgress import org.oxycblt.auxio.music.stack.IndexingProgress
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent
import timber.log.Timber as L import timber.log.Timber as L
@ -61,33 +61,19 @@ class IndexingNotification(private val context: Context) :
* @return true if the notification updated, false otherwise * @return true if the notification updated, false otherwise
*/ */
fun updateIndexingState(progress: IndexingProgress): Boolean { fun updateIndexingState(progress: IndexingProgress): Boolean {
when (progress) { // Determinate state, show an active progress meter. Since these updates arrive
is IndexingProgress.Indeterminate -> { // highly rapidly, only update every 1.5 seconds to prevent notification rate
// Indeterminate state, use a vaguer description and in-determinate progress. // limiting.
// These events are not very frequent, and thus we don't need to safeguard val now = SystemClock.elapsedRealtime()
// against rate limiting. if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) {
L.d("Updating state to $progress") return false
lastUpdateTime = -1
setContentText(context.getString(R.string.lng_indexing))
setProgress(0, 0, true)
return true
}
is IndexingProgress.Songs -> {
// Determinate state, show an active progress meter. Since these updates arrive
// highly rapidly, only update every 1.5 seconds to prevent notification rate
// limiting.
val now = SystemClock.elapsedRealtime()
if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) {
return false
}
lastUpdateTime = SystemClock.elapsedRealtime()
L.d("Updating state to $progress")
setContentText(
context.getString(R.string.fmt_indexing, progress.current, progress.total))
setProgress(progress.total, progress.current, false)
return true
}
} }
lastUpdateTime = SystemClock.elapsedRealtime()
L.d("Updating state to $progress")
setContentText(
context.getString(R.string.fmt_indexing, progress.interpreted, progress.explored))
setProgress(progress.explored, progress.interpreted, false)
return true
} }
} }

View file

@ -24,7 +24,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import org.oxycblt.auxio.music.stack.Indexer.Event
import org.oxycblt.auxio.music.stack.explore.Explorer import org.oxycblt.auxio.music.stack.explore.Explorer
import org.oxycblt.auxio.music.stack.interpret.Interpretation import org.oxycblt.auxio.music.stack.interpret.Interpretation
import org.oxycblt.auxio.music.stack.interpret.Interpreter import org.oxycblt.auxio.music.stack.interpret.Interpreter
@ -34,31 +33,38 @@ interface Indexer {
suspend fun run( suspend fun run(
uris: List<Uri>, uris: List<Uri>,
interpretation: Interpretation, interpretation: Interpretation,
eventHandler: suspend (Event) -> Unit = {} onProgress: suspend (IndexingProgress) -> Unit = {}
): MutableLibrary ): MutableLibrary
sealed interface Event {
data class Discovered(
val amount: Int,
) : Event
data class Extracted(val amount: Int) : Event
data class Interpret(val amount: Int) : Event
}
} }
/**
* Represents the current progress of music loading.
*
* @author Alexander Capehart (OxygenCobalt)
*/
data class IndexingProgress(val interpreted: Int, val explored: Int)
class IndexerImpl class IndexerImpl
@Inject @Inject
constructor(private val explorer: Explorer, private val interpreter: Interpreter) : Indexer { constructor(private val explorer: Explorer, private val interpreter: Interpreter) : Indexer {
override suspend fun run( override suspend fun run(
uris: List<Uri>, uris: List<Uri>,
interpretation: Interpretation, interpretation: Interpretation,
eventHandler: suspend (Event) -> Unit onProgress: suspend (IndexingProgress) -> Unit
) = coroutineScope { ) = coroutineScope {
val files = explorer.explore(uris, eventHandler) var interpreted = 0
var explored = 0
val files =
explorer.explore(uris) {
explored++
onProgress(IndexingProgress(interpreted, explored))
}
val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer() val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer()
val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer() val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer()
interpreter.interpret(audioFiles, playlistFiles, interpretation, eventHandler)
interpreter.interpret(audioFiles, playlistFiles, interpretation) {
interpreted++
onProgress(IndexingProgress(interpreted, explored))
}
} }
} }

View file

@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.flow.withIndex
import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.stack.explore.cache.CacheResult import org.oxycblt.auxio.music.stack.explore.cache.CacheResult
import org.oxycblt.auxio.music.stack.explore.cache.TagCache import org.oxycblt.auxio.music.stack.explore.cache.TagCache
import org.oxycblt.auxio.music.stack.explore.extractor.TagExtractor import org.oxycblt.auxio.music.stack.explore.extractor.TagExtractor
@ -44,7 +43,7 @@ import org.oxycblt.auxio.music.stack.explore.fs.DeviceFiles
import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists
interface Explorer { interface Explorer {
fun explore(uris: List<Uri>, eventHandler: suspend (Indexer.Event) -> Unit): Files fun explore(uris: List<Uri>, onExplored: suspend () -> Unit): Files
} }
data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>) data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
@ -58,14 +57,14 @@ constructor(
private val storedPlaylists: StoredPlaylists private val storedPlaylists: StoredPlaylists
) : Explorer { ) : Explorer {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun explore(uris: List<Uri>, eventHandler: suspend (Indexer.Event) -> Unit): Files { override fun explore(uris: List<Uri>, onExplored: suspend () -> Unit): Files {
var discovered = 0 var discovered = 0
val deviceFiles = val deviceFiles =
deviceFiles deviceFiles
.explore(uris.asFlow()) .explore(uris.asFlow())
.onEach { .onEach {
discovered++ discovered++
eventHandler(Indexer.Event.Discovered(discovered)) onExplored()
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
@ -75,15 +74,9 @@ constructor(
uncachedDeviceFiles uncachedDeviceFiles
.split(8) .split(8)
.map { tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() } .map { tagExtractor.extract(it).flowOn(Dispatchers.IO).buffer() }
.asFlow()
.flattenMerge() .flattenMerge()
val writtenAudioFiles = tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer() val writtenAudioFiles = tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer()
var loaded = 0 val audioFiles = merge(cachedAudioFiles, writtenAudioFiles)
val audioFiles =
merge(cachedAudioFiles, writtenAudioFiles).onEach {
loaded++
eventHandler(Indexer.Event.Extracted(loaded))
}
val playlistFiles = storedPlaylists.read() val playlistFiles = storedPlaylists.read()
return Files(audioFiles, playlistFiles) return Files(audioFiles, playlistFiles)
} }
@ -96,11 +89,11 @@ constructor(
return files to songs return files to songs
} }
private fun <T> Flow<T>.split(n: Int): Array<Flow<T>> { private fun <T> Flow<T>.split(n: Int): Flow<Flow<T>> {
val indexed = withIndex() val indexed = withIndex()
val shared = val shared =
indexed.shareIn( indexed.shareIn(
CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0) CoroutineScope(Dispatchers.Main), SharingStarted.WhileSubscribed(), replay = 0)
return Array(n) { shared.filter { it.index % n == 0 }.map { it.value } } return Array(n) { shared.filter { it.index % n == 0 }.map { it.value } }.asFlow()
} }
} }

View file

@ -23,7 +23,6 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.stack.explore.fs.Path import org.oxycblt.auxio.music.stack.explore.fs.Path
import org.oxycblt.auxio.music.stack.interpret.model.SongImpl
data class DeviceFile( data class DeviceFile(
val uri: Uri, val uri: Uri,
@ -33,11 +32,6 @@ data class DeviceFile(
val lastModified: Long val lastModified: Long
) )
/**
* Raw information about a [SongImpl] obtained from the filesystem/Extractor instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
data class AudioFile( data class AudioFile(
val deviceFile: DeviceFile, val deviceFile: DeviceFile,
val durationMs: Long, val durationMs: Long,

View file

@ -42,21 +42,19 @@ class DeviceFilesImpl
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val volumeManager: VolumeManager @Inject private val documentPathFactory: DocumentPathFactory
) : DeviceFiles { ) : DeviceFiles {
private val contentResolver = context.contentResolverSafe private val contentResolver = context.contentResolverSafe
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> = override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
uris.flatMapMerge { rootUri -> exploreImpl(contentResolver, rootUri, Components.nil()) } uris.flatMapMerge { rootUri -> exploreImpl(contentResolver, rootUri,
requireNotNull(documentPathFactory.unpackDocumentTreeUri(rootUri))) }
private fun exploreImpl( private fun exploreImpl(
contentResolver: ContentResolver, contentResolver: ContentResolver,
uri: Uri, uri: Uri,
relativePath: Components relativePath: Path
): Flow<DeviceFile> = flow { ): Flow<DeviceFile> = flow {
// TODO: Temporary to maintain path api parity
// Figure out what we actually want to do to paths now in saf world.
val external = volumeManager.getInternalVolume()
contentResolver.useQuery(uri, PROJECTION) { cursor -> contentResolver.useQuery(uri, PROJECTION) { cursor ->
val childUriIndex = val childUriIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
@ -73,7 +71,7 @@ constructor(
val childId = cursor.getString(childUriIndex) val childId = cursor.getString(childUriIndex)
val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId) val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId)
val displayName = cursor.getString(displayNameIndex) val displayName = cursor.getString(displayNameIndex)
val path = relativePath.child(displayName) val path = relativePath.file(displayName)
val mimeType = cursor.getString(mimeTypeIndex) val mimeType = cursor.getString(mimeTypeIndex)
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
// This does NOT block the current coroutine. Instead, we will // This does NOT block the current coroutine. Instead, we will
@ -85,7 +83,7 @@ constructor(
// rather than just being a glorified async. // rather than just being a glorified async.
val lastModified = cursor.getLong(lastModifiedIndex) val lastModified = cursor.getLong(lastModifiedIndex)
val size = cursor.getLong(sizeIndex) val size = cursor.getLong(sizeIndex)
emit(DeviceFile(childUri, mimeType, Path(external, path), size, lastModified)) emit(DeviceFile(childUri, mimeType, path, size, lastModified))
} }
} }
// Hypothetically, we could just emitAll as we recurse into a new directory, // Hypothetically, we could just emitAll as we recurse into a new directory,

View file

@ -21,13 +21,12 @@ package org.oxycblt.auxio.music.stack.interpret
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.stack.Indexer
import org.oxycblt.auxio.music.stack.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.AudioFile
import org.oxycblt.auxio.music.stack.explore.PlaylistFile import org.oxycblt.auxio.music.stack.explore.PlaylistFile
import org.oxycblt.auxio.music.stack.interpret.linker.AlbumLinker import org.oxycblt.auxio.music.stack.interpret.linker.AlbumLinker
@ -50,7 +49,7 @@ interface Interpreter {
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation, interpretation: Interpretation,
eventHandler: suspend (Indexer.Event) -> Unit onInterpret: suspend () -> Unit
): MutableLibrary ): MutableLibrary
} }
@ -59,7 +58,7 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation, interpretation: Interpretation,
eventHandler: suspend (Indexer.Event) -> Unit onInterpret: suspend () -> Unit
): MutableLibrary { ): MutableLibrary {
val preSongs = val preSongs =
preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main).buffer() preparer.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main).buffer()
@ -69,24 +68,19 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
val artistLinker = ArtistLinker() val artistLinker = ArtistLinker()
val artistLinkedSongs = val artistLinkedSongs =
artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).buffer() artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).toList()
// This is intentional. Song and album instances are dependent on artist // This is intentional. Song and album instances are dependent on artist
// data, so we need to ensure that all of the linked artist data is resolved // data, so we need to ensure that all of the linked artist data is resolved
// before we go any further. // before we go any further.
val genres = genreLinker.resolve() val genres = genreLinker.resolve()
val artists = artistLinker.resolve() val artists = artistLinker.resolve()
var interpreted = 0
val albumLinker = AlbumLinker() val albumLinker = AlbumLinker()
val albumLinkedSongs = val albumLinkedSongs =
albumLinker albumLinker
.register(artistLinkedSongs) .register(artistLinkedSongs.asFlow())
.flowOn(Dispatchers.Main)
.onEach {
interpreted++
eventHandler(Indexer.Event.Interpret(interpreted))
}
.map { LinkedSongImpl(it) } .map { LinkedSongImpl(it) }
.flowOn(Dispatchers.Main)
.toList() .toList()
val albums = albumLinker.resolve() val albums = albumLinker.resolve()
@ -96,17 +90,13 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
val uid = it.preSong.computeUid() val uid = it.preSong.computeUid()
val other = uidMap[uid] val other = uidMap[uid]
if (other == null) { if (other == null) {
SongImpl(it) SongImpl(it).also { onInterpret() }
} else { } else {
L.d("Song @ $uid already exists at ${other.path}, ignoring") L.d("Song @ $uid already exists at ${other.path}, ignoring")
null null
} }
} }
return LibraryImpl( return LibraryImpl(songs, albums, artists, genres)
songs,
albums.onEach { it.finalize() },
artists.onEach { it.finalize() },
genres.onEach { it.finalize() })
} }
private data class LinkedSongImpl(private val albumLinkedSong: AlbumLinker.LinkedSong) : private data class LinkedSongImpl(private val albumLinkedSong: AlbumLinker.LinkedSong) :

View file

@ -122,15 +122,6 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
} }
hashCode = 31 * hashCode + song.hashCode() hashCode = 31 * hashCode + song.hashCode()
} }
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Album].
*/
fun finalize(): Album {
return this
}
} }
/** /**
@ -147,12 +138,15 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist {
override val songs = mutableSetOf<Song>() override val songs = mutableSetOf<Song>()
private val albums = mutableSetOf<Album>() override var explicitAlbums = mutableSetOf<Album>()
private val albumMap = mutableMapOf<Album, Boolean>() override var implicitAlbums = mutableSetOf<Album>()
override lateinit var explicitAlbums: Set<Album>
override lateinit var implicitAlbums: Set<Album>
override lateinit var genres: List<Genre> override val genres: List<Genre> by lazy {
// TODO: Not sure how to integrate this into music loading.
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
.genres(songs.flatMapTo(mutableSetOf()) { it.genres })
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
}
override var durationMs = 0L override var durationMs = 0L
override lateinit var cover: ParentCover override lateinit var cover: ParentCover
@ -176,40 +170,15 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist {
fun link(song: SongImpl) { fun link(song: SongImpl) {
songs.add(song) songs.add(song)
durationMs += song.durationMs durationMs += song.durationMs
if (albumMap[song.album] == null) { if (!explicitAlbums.contains(song.album)) {
albumMap[song.album] = false implicitAlbums.add(song.album)
} }
hashCode = 31 * hashCode + song.hashCode() hashCode = 31 * hashCode + song.hashCode()
} }
fun link(album: AlbumImpl) { fun link(album: AlbumImpl) {
albums.add(album) explicitAlbums.add(album)
albumMap[album] = true implicitAlbums.remove(album)
}
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Artist].
*/
fun finalize(): Artist {
// There are valid artist configurations:
// 1. No songs, no implicit albums, some explicit albums
// 2. Some songs, no implicit albums, some explicit albums
// 3. Some songs, some implicit albums, no implicit albums
// 4. Some songs, some implicit albums, some explicit albums
// I'm pretty sure the latter check could be reduced to just explicitAlbums.isNotEmpty,
// but I can't be 100% certain.
check(songs.isNotEmpty() || (implicitAlbums.size + explicitAlbums.size) > 0) {
"Malformed artist $name: Empty"
}
explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true }
implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true }
genres =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
.genres(songs.flatMapTo(mutableSetOf()) { it.genres })
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
return this
} }
} }
@ -242,13 +211,4 @@ class GenreImpl(private val preGenre: PreGenre) : Genre {
durationMs += song.durationMs durationMs += song.durationMs
hashCode = 31 * hashCode + song.hashCode() hashCode = 31 * hashCode + song.hashCode()
} }
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Genre].
*/
fun finalize(): Genre {
return this
}
} }