music: emulate old music loading process
This commit is contained in:
parent
c74b744aec
commit
ae449ded45
6 changed files with 88 additions and 50 deletions
|
@ -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 -> {
|
||||||
// Actively loading songs, show the current progress.
|
// In a query/initialization state, show a generic loading status.
|
||||||
binding.homeIndexingProgress.apply {
|
binding.homeIndexingStatus.text = getString(R.string.lng_indexing)
|
||||||
isIndeterminate = false
|
binding.homeIndexingProgress.isIndeterminate = true
|
||||||
max = progress.explored
|
}
|
||||||
this.progress = progress.interpreted
|
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.loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,19 +61,33 @@ 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 {
|
||||||
// Determinate state, show an active progress meter. Since these updates arrive
|
when (progress) {
|
||||||
// highly rapidly, only update every 1.5 seconds to prevent notification rate
|
is IndexingProgress.Indeterminate -> {
|
||||||
// limiting.
|
// Indeterminate state, use a vaguer description and in-determinate progress.
|
||||||
val now = SystemClock.elapsedRealtime()
|
// These events are not very frequent, and thus we don't need to safeguard
|
||||||
if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) {
|
// against rate limiting.
|
||||||
return false
|
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.
|
||||||
|
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.loaded, progress.explored))
|
||||||
|
setProgress(progress.loaded, progress.explored, 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue