music: connect saf indexer to libraries

Largely temporary, to be replaced with Interpreter
This commit is contained in:
Alexander Capehart 2024-11-19 15:17:05 -07:00
parent e51b2817e9
commit f76eafc9d4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 41 additions and 48 deletions

View file

@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.stack.interpreter.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.util.DEFAULT_TIMEOUT import org.oxycblt.auxio.util.DEFAULT_TIMEOUT

View file

@ -24,6 +24,7 @@ import android.provider.OpenableColumns
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -35,7 +36,7 @@ import org.oxycblt.auxio.music.stack.fs.Path
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
import org.oxycblt.auxio.music.stack.fs.useQuery import org.oxycblt.auxio.music.stack.fs.useQuery
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.stack.interpreter.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.util.forEachWithTimeout import org.oxycblt.auxio.util.forEachWithTimeout
import org.oxycblt.auxio.util.sendWithTimeout import org.oxycblt.auxio.util.sendWithTimeout
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -123,8 +124,8 @@ interface DeviceLibrary {
* the instance. * the instance.
*/ */
suspend fun create( suspend fun create(
rawSongs: Channel<RawSong>, rawSongs: Flow<RawSong>,
processedSongs: Channel<RawSong>, onSongProcessed: () -> Unit,
separators: Separators, separators: Separators,
nameFactory: Name.Known.Factory nameFactory: Name.Known.Factory
): DeviceLibraryImpl ): DeviceLibraryImpl
@ -133,8 +134,8 @@ interface DeviceLibrary {
class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory { class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
override suspend fun create( override suspend fun create(
rawSongs: Channel<RawSong>, rawSongs: Flow<RawSong>,
processedSongs: Channel<RawSong>, onSongProcessed: () -> Unit,
separators: Separators, separators: Separators,
nameFactory: Name.Known.Factory nameFactory: Name.Known.Factory
): DeviceLibraryImpl { ): DeviceLibraryImpl {
@ -144,7 +145,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
val genreGrouping = mutableMapOf<String?, Grouping<RawGenre, SongImpl>>() val genreGrouping = mutableMapOf<String?, Grouping<RawGenre, SongImpl>>()
// All music information is grouped as it is indexed by other components. // All music information is grouped as it is indexed by other components.
rawSongs.forEachWithTimeout { rawSong -> rawSongs.collect { rawSong ->
val song = SongImpl(rawSong, nameFactory, separators) val song = SongImpl(rawSong, nameFactory, separators)
// At times the indexer produces duplicate songs, try to filter these. Comparing by // At times the indexer produces duplicate songs, try to filter these. Comparing by
// UID is sufficient for something like this, and also prevents collisions from // UID is sufficient for something like this, and also prevents collisions from
@ -153,11 +154,8 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
L.w( L.w(
"Duplicate song found: ${song.path} " + "Duplicate song found: ${song.path} " +
"collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}") "collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}")
// We still want to say that we "processed" the song so that the user doesn't onSongProcessed()
// get confused at why the bar was only partly filled by the end of the loading return@collect
// process.
processedSongs.sendWithTimeout(rawSong)
return@forEachWithTimeout
} }
songGrouping[song.uid] = song songGrouping[song.uid] = song
@ -180,7 +178,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
appendToNameTree(song, rawGenre, genreGrouping) { old, new -> new.name < old.name } appendToNameTree(song, rawGenre, genreGrouping) { old, new -> new.name < old.name }
} }
processedSongs.sendWithTimeout(rawSong) onSongProcessed()
} }
// Now that all songs are processed, also process albums and group them into their // Now that all songs are processed, also process albums and group them into their
@ -343,7 +341,7 @@ class DeviceLibraryImpl(
) : DeviceLibrary { ) : DeviceLibrary {
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } // private val songPathMap = buildMap { songs.forEach { put(it.path, it) } }
private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } }
private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } }
@ -365,17 +363,17 @@ class DeviceLibraryImpl(
override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
override fun findSongByPath(path: Path) = songPathMap[path] override fun findSongByPath(path: Path) = null
override fun findSongForUri(context: Context, uri: Uri) = override fun findSongForUri(context: Context, uri: Uri) = null
context.contentResolverSafe.useQuery( // context.contentResolverSafe.useQuery(
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> // uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst() // cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a // // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open. // // song. Do what we can to hopefully find the song the user wanted to open.
val displayName = // val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) // cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) // val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size } // songs.find { it.path.name == displayName && it.size == size }
} // }
} }

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.stack.interpreter.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.positiveOrNull import org.oxycblt.auxio.util.positiveOrNull

View file

@ -1,7 +1,5 @@
package org.oxycblt.auxio.music.stack.interpreter package org.oxycblt.auxio.music.metadata
import org.oxycblt.auxio.music.metadata.CharSeparators
import org.oxycblt.auxio.music.metadata.NoSeparators
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
import org.oxycblt.auxio.music.stack.extractor.splitEscaped import org.oxycblt.auxio.music.stack.extractor.splitEscaped

View file

@ -29,7 +29,6 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.stack.interpreter.Separators
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import timber.log.Timber as L import timber.log.Timber as L

View file

@ -14,27 +14,33 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.toList
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.cache.TagCache import org.oxycblt.auxio.music.stack.cache.TagCache
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
import org.oxycblt.auxio.music.stack.extractor.TagResult import org.oxycblt.auxio.music.stack.extractor.TagResult
import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.fs.DeviceFile
import org.oxycblt.auxio.music.stack.fs.DeviceFiles import org.oxycblt.auxio.music.stack.fs.DeviceFiles
import org.oxycblt.auxio.music.stack.interpreter.Interpreter import org.oxycblt.auxio.music.user.UserLibrary
import javax.inject.Inject import javax.inject.Inject
interface Indexer { interface Indexer {
suspend fun run(uris: List<Uri>): DeviceLibrary suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory): LibraryResult
} }
data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: UserLibrary)
class IndexerImpl @Inject constructor( class IndexerImpl @Inject constructor(
private val deviceFiles: DeviceFiles, private val deviceFiles: DeviceFiles,
private val tagCache: TagCache, private val tagCache: TagCache,
private val tagExtractor: ExoPlayerTagExtractor, private val tagExtractor: ExoPlayerTagExtractor,
private val interpreter: Interpreter private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory
) : Indexer { ) : Indexer {
override suspend fun run(uris: List<Uri>) = coroutineScope { override suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory) = coroutineScope {
val deviceFiles = deviceFiles.explore(uris.asFlow()) val deviceFiles = deviceFiles.explore(uris.asFlow())
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
@ -52,10 +58,12 @@ class IndexerImpl @Inject constructor(
started = SharingStarted.WhileSubscribed(), started = SharingStarted.WhileSubscribed(),
replay = Int.MAX_VALUE replay = Int.MAX_VALUE
) )
val tagWrite = async { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) } val tagWrite = async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) }
val deviceLibrary = async { interpreter.interpret(sharedExtractorSongs) }.await() val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() }
val deviceLibrary = deviceLibraryFactory.create(merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory)
val userLibrary = userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory)
tagWrite.await() tagWrite.await()
deviceLibrary LibraryResult(deviceLibrary, userLibrary)
} }
private fun Flow<TagResult>.split(): Pair<Flow<DeviceFile>, Flow<RawSong>> { private fun Flow<TagResult>.split(): Pair<Flow<DeviceFile>, Flow<RawSong>> {

View file

@ -1,9 +0,0 @@
package org.oxycblt.auxio.music.stack.interpreter
import kotlinx.coroutines.flow.Flow
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong
interface Interpreter {
suspend fun interpret(rawSong: Flow<RawSong>): DeviceLibrary
}

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.metadata
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.stack.interpreter.Separators
class SeparatorsTest { class SeparatorsTest {
@Test @Test