music: emulate old music loading process

This commit is contained in:
Alexander Capehart 2024-11-27 09:40:59 -07:00
parent c74b744aec
commit ae449ded45
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 88 additions and 50 deletions

View file

@ -360,13 +360,22 @@ class HomeFragment :
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingActions.visibility = View.INVISIBLE binding.homeIndexingActions.visibility = View.INVISIBLE
binding.homeIndexingStatus.setText(R.string.lng_indexing) when (progress) {
is IndexingProgress.Indeterminate -> {
// In a query/initialization state, show a generic loading status.
binding.homeIndexingStatus.text = getString(R.string.lng_indexing)
binding.homeIndexingProgress.isIndeterminate = true
}
is IndexingProgress.Songs -> {
// Actively loading songs, show the current progress. // Actively loading songs, show the current progress.
binding.homeIndexingStatus.text =
getString(R.string.fmt_indexing, progress.loaded, progress.explored)
binding.homeIndexingProgress.apply { binding.homeIndexingProgress.apply {
isIndeterminate = false isIndeterminate = false
max = progress.explored max = progress.explored
this.progress = progress.interpreted this.progress = progress.loaded
}
}
} }
} }

View file

@ -61,6 +61,18 @@ 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) {
is IndexingProgress.Indeterminate -> {
// Indeterminate state, use a vaguer description and in-determinate progress.
// These events are not very frequent, and thus we don't need to safeguard
// against rate limiting.
L.d("Updating state to $progress")
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 // Determinate state, show an active progress meter. Since these updates arrive
// highly rapidly, only update every 1.5 seconds to prevent notification rate // highly rapidly, only update every 1.5 seconds to prevent notification rate
// limiting. // limiting.
@ -71,11 +83,13 @@ class IndexingNotification(private val context: Context) :
lastUpdateTime = SystemClock.elapsedRealtime() lastUpdateTime = SystemClock.elapsedRealtime()
L.d("Updating state to $progress") L.d("Updating state to $progress")
setContentText( setContentText(
context.getString(R.string.fmt_indexing, progress.interpreted, progress.explored)) context.getString(R.string.fmt_indexing, progress.loaded, progress.explored))
setProgress(progress.explored, progress.interpreted, false) setProgress(progress.loaded, progress.explored, false)
return true return true
} }
} }
}
}
/** /**
* A static [ForegroundServiceNotification] that signals to the user that the app is currently * A static [ForegroundServiceNotification] that signals to the user that the app is currently

View file

@ -22,7 +22,9 @@ import android.net.Uri
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
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
@ -42,7 +44,11 @@ interface Indexer {
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class IndexingProgress(val interpreted: Int, val explored: Int) sealed interface IndexingProgress {
data class Songs(val loaded: Int, val explored: Int) : IndexingProgress
data object Indeterminate : IndexingProgress
}
class IndexerImpl class IndexerImpl
@Inject @Inject
@ -52,20 +58,25 @@ constructor(private val explorer: Explorer, private val interpreter: Interpreter
interpretation: Interpretation, interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit onProgress: suspend (IndexingProgress) -> Unit
) = coroutineScope { ) = coroutineScope {
var interpreted = 0 val files = explorer.explore(uris, onProgress)
var explored = 0 val audioFiles =
onProgress(IndexingProgress(interpreted, explored)) files.audios
val files = .cap(
explorer.explore(uris) { start = { onProgress(IndexingProgress.Songs(0, 0)) },
explored++ end = { onProgress(IndexingProgress.Indeterminate) })
onProgress(IndexingProgress(interpreted, explored)) .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)
}
interpreter.interpret(audioFiles, playlistFiles, interpretation) { private fun <T> Flow<T>.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow<T> =
interpreted++ flow {
onProgress(IndexingProgress(interpreted, explored)) start()
try {
collect { emit(it) }
} finally {
end()
} }
} }
} }

View file

@ -34,13 +34,14 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.flow.withIndex
import org.oxycblt.auxio.music.stack.IndexingProgress
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
import org.oxycblt.auxio.music.stack.explore.fs.DeviceFiles 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>, onExplored: suspend () -> Unit): Files fun explore(uris: List<Uri>, onProgress: suspend (IndexingProgress.Songs) -> Unit): Files
} }
data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>) data class Files(val audios: Flow<AudioFile>, val playlists: Flow<PlaylistFile>)
@ -54,14 +55,18 @@ constructor(
private val storedPlaylists: StoredPlaylists private val storedPlaylists: StoredPlaylists
) : Explorer { ) : Explorer {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun explore(uris: List<Uri>, onExplored: suspend () -> Unit): Files { override fun explore(
var discovered = 0 uris: List<Uri>,
onProgress: suspend (IndexingProgress.Songs) -> Unit
): Files {
var loaded = 0
var explored = 0
val deviceFiles = val deviceFiles =
deviceFiles deviceFiles
.explore(uris.asFlow()) .explore(uris.asFlow())
.onEach { .onEach {
discovered++ explored++
onExplored() onProgress(IndexingProgress.Songs(loaded, explored))
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
@ -73,8 +78,13 @@ constructor(
// val writtenAudioFiles = // val writtenAudioFiles =
// tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer() // tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer()
// val audioFiles = merge(cachedAudioFiles, writtenAudioFiles) // val audioFiles = merge(cachedAudioFiles, writtenAudioFiles)
val audioFiles =
extractedAudioFiles.onEach {
loaded++
onProgress(IndexingProgress.Songs(loaded, explored))
}
val playlistFiles = storedPlaylists.read() val playlistFiles = storedPlaylists.read()
return Files(extractedAudioFiles, playlistFiles) return Files(audioFiles, playlistFiles)
} }
/** Temporarily split a flow into 8 parallel threads and then */ /** Temporarily split a flow into 8 parallel threads and then */

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.auxio.music.stack.explore.DeviceFile import org.oxycblt.auxio.music.stack.explore.DeviceFile
import timber.log.Timber
interface DeviceFiles { interface DeviceFiles {
fun explore(uris: Flow<Uri>): Flow<DeviceFile> fun explore(uris: Flow<Uri>): Flow<DeviceFile>

View file

@ -25,7 +25,6 @@ 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.explore.AudioFile import org.oxycblt.auxio.music.stack.explore.AudioFile
@ -49,8 +48,7 @@ interface Interpreter {
suspend fun interpret( suspend fun interpret(
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation, interpretation: Interpretation
onInterpret: suspend () -> Unit
): MutableLibrary ): MutableLibrary
} }
@ -58,8 +56,7 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
override suspend fun interpret( override suspend fun interpret(
audioFiles: Flow<AudioFile>, audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>, playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation, interpretation: Interpretation
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,11 +66,7 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
val artistLinker = ArtistLinker() val artistLinker = ArtistLinker()
val artistLinkedSongs = val artistLinkedSongs =
artistLinker artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).toList()
.register(genreLinkedSongs)
.onEach { onInterpret() }
.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.