music: improve indexing progress
This commit is contained in:
parent
0ba5ddce51
commit
b0c6dd2b74
10 changed files with 76 additions and 190 deletions
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) :
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue