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.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.
binding.homeIndexingStatus.text =
getString(R.string.fmt_indexing, progress.loaded, progress.explored)
binding.homeIndexingProgress.apply {
isIndeterminate = false
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
*/
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
// highly rapidly, only update every 1.5 seconds to prevent notification rate
// limiting.
@ -71,10 +83,12 @@ class IndexingNotification(private val context: Context) :
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)
context.getString(R.string.fmt_indexing, progress.loaded, progress.explored))
setProgress(progress.loaded, progress.explored, false)
return true
}
}
}
}
/**

View file

@ -22,7 +22,9 @@ import android.net.Uri
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.oxycblt.auxio.music.stack.explore.Explorer
import org.oxycblt.auxio.music.stack.interpret.Interpretation
@ -42,7 +44,11 @@ interface Indexer {
*
* @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
@Inject
@ -52,20 +58,25 @@ constructor(private val explorer: Explorer, private val interpreter: Interpreter
interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit
) = coroutineScope {
var interpreted = 0
var explored = 0
onProgress(IndexingProgress(interpreted, explored))
val files =
explorer.explore(uris) {
explored++
onProgress(IndexingProgress(interpreted, explored))
}
val audioFiles = files.audios.flowOn(Dispatchers.IO).buffer()
val files = explorer.explore(uris, onProgress)
val audioFiles =
files.audios
.cap(
start = { onProgress(IndexingProgress.Songs(0, 0)) },
end = { onProgress(IndexingProgress.Indeterminate) })
.flowOn(Dispatchers.IO)
.buffer()
val playlistFiles = files.playlists.flowOn(Dispatchers.IO).buffer()
interpreter.interpret(audioFiles, playlistFiles, interpretation)
}
interpreter.interpret(audioFiles, playlistFiles, interpretation) {
interpreted++
onProgress(IndexingProgress(interpreted, explored))
private fun <T> Flow<T>.cap(start: suspend () -> Unit, end: suspend () -> Unit): Flow<T> =
flow {
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.receiveAsFlow
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.extractor.TagExtractor
import org.oxycblt.auxio.music.stack.explore.fs.DeviceFiles
import org.oxycblt.auxio.music.stack.explore.playlists.StoredPlaylists
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>)
@ -54,14 +55,18 @@ constructor(
private val storedPlaylists: StoredPlaylists
) : Explorer {
@OptIn(ExperimentalCoroutinesApi::class)
override fun explore(uris: List<Uri>, onExplored: suspend () -> Unit): Files {
var discovered = 0
override fun explore(
uris: List<Uri>,
onProgress: suspend (IndexingProgress.Songs) -> Unit
): Files {
var loaded = 0
var explored = 0
val deviceFiles =
deviceFiles
.explore(uris.asFlow())
.onEach {
discovered++
onExplored()
explored++
onProgress(IndexingProgress.Songs(loaded, explored))
}
.flowOn(Dispatchers.IO)
.buffer()
@ -73,8 +78,13 @@ constructor(
// val writtenAudioFiles =
// tagCache.write(extractedAudioFiles).flowOn(Dispatchers.IO).buffer()
// val audioFiles = merge(cachedAudioFiles, writtenAudioFiles)
val audioFiles =
extractedAudioFiles.onEach {
loaded++
onProgress(IndexingProgress.Songs(loaded, explored))
}
val playlistFiles = storedPlaylists.read()
return Files(extractedAudioFiles, playlistFiles)
return Files(audioFiles, playlistFiles)
}
/** 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.flow
import org.oxycblt.auxio.music.stack.explore.DeviceFile
import timber.log.Timber
interface DeviceFiles {
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.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.stack.explore.AudioFile
@ -49,8 +48,7 @@ interface Interpreter {
suspend fun interpret(
audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation,
onInterpret: suspend () -> Unit
interpretation: Interpretation
): MutableLibrary
}
@ -58,8 +56,7 @@ class InterpreterImpl @Inject constructor(private val preparer: Preparer) : Inte
override suspend fun interpret(
audioFiles: Flow<AudioFile>,
playlistFiles: Flow<PlaylistFile>,
interpretation: Interpretation,
onInterpret: suspend () -> Unit
interpretation: Interpretation
): MutableLibrary {
val preSongs =
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 artistLinkedSongs =
artistLinker
.register(genreLinkedSongs)
.onEach { onInterpret() }
.flowOn(Dispatchers.Main)
.toList()
artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).toList()
// 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
// before we go any further.