musikr: refactor devicefiles into tree

This commit is contained in:
Alexander Capehart 2025-03-03 12:14:40 -07:00
parent fce77ec8a0
commit 8104985a4e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 110 additions and 54 deletions

View file

@ -30,7 +30,7 @@ import org.oxycblt.musikr.cover.CoverResult
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.FileCover import org.oxycblt.musikr.cover.FileCover
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata
open class CompatCovers(private val context: Context, private val inner: Covers<FileCover>) : open class CompatCovers(private val context: Context, private val inner: Covers<FileCover>) :

View file

@ -22,7 +22,7 @@ import android.content.Context
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.CoverResult
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata
class NullCovers(private val context: Context) : MutableCovers<NullCover> { class NullCovers(private val context: Context) : MutableCovers<NullCover> {

View file

@ -31,7 +31,7 @@ import org.oxycblt.musikr.cover.FileCover
import org.oxycblt.musikr.cover.FileCovers import org.oxycblt.musikr.cover.FileCovers
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.MutableFileCovers import org.oxycblt.musikr.cover.MutableFileCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.fs.app.AppFiles import org.oxycblt.musikr.fs.app.AppFiles
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.cache
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
abstract class Cache { abstract class Cache {

View file

@ -34,7 +34,7 @@ import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.CoverResult
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.cache
import android.content.Context import android.content.Context
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
interface StoredCache { interface StoredCache {

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import java.io.InputStream import java.io.InputStream
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata
interface Covers<T : Cover> { interface Covers<T : Cover> {

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.fs.app.AppFile import org.oxycblt.musikr.fs.app.AppFile
import org.oxycblt.musikr.fs.app.AppFiles import org.oxycblt.musikr.fs.app.AppFiles
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata

View file

@ -16,14 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.musikr.fs package org.oxycblt.musikr.fs.device
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.flow.Flow
import org.oxycblt.musikr.fs.Path
sealed interface DeviceNode {
val uri: Uri
val path: Path
}
data class DeviceDirectory(
override val uri: Uri,
override val path: Path,
val parent: DeviceDirectory?,
var children: Flow<DeviceNode>
) : DeviceNode
data class DeviceFile( data class DeviceFile(
val uri: Uri, override val uri: Uri,
override val path: Path,
val modifiedMs: Long,
val mimeType: String, val mimeType: String,
val path: Path,
val size: Long, val size: Long,
val modifiedMs: Long val parent: DeviceDirectory
) ) : DeviceNode

View file

@ -24,17 +24,14 @@ import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.musikr.fs.DeviceFile import kotlinx.coroutines.flow.flatMapMerge
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
internal interface DeviceFiles { internal interface DeviceFiles {
fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean = true): Flow<DeviceFile> fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean = true): Flow<DeviceNode>
companion object { companion object {
fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe) fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe)
@ -43,23 +40,38 @@ internal interface DeviceFiles {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
override fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean): Flow<DeviceFile> = override fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean): Flow<DeviceNode> =
locations.flatMapMerge { location -> locations.flatMapMerge { location ->
exploreImpl( // Create a root directory for each location
val rootDirectory = DeviceDirectory(
uri = location.uri,
path = location.path,
parent = null,
children = emptyFlow()
)
// Set up the children flow for the root directory
rootDirectory.children = exploreDirectoryImpl(
contentResolver, contentResolver,
location.uri, location.uri,
DocumentsContract.getTreeDocumentId(location.uri), DocumentsContract.getTreeDocumentId(location.uri),
location.path, location.path,
ignoreHidden) rootDirectory,
ignoreHidden
)
// Return a flow that emits the root directory
flow { emit(rootDirectory) }
} }
private fun exploreImpl( private fun exploreDirectoryImpl(
contentResolver: ContentResolver, contentResolver: ContentResolver,
rootUri: Uri, rootUri: Uri,
treeDocumentId: String, treeDocumentId: String,
relativePath: Path, relativePath: Path,
parent: DeviceDirectory,
ignoreHidden: Boolean ignoreHidden: Boolean
): Flow<DeviceFile> = flow { ): Flow<DeviceNode> = flow {
contentResolver.useQuery( contentResolver.useQuery(
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
PROJECTION) { cursor -> PROJECTION) { cursor ->
@ -72,7 +84,7 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
val lastModifiedIndex = val lastModifiedIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
val recursive = mutableListOf<Flow<DeviceFile>>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val childId = cursor.getString(childUriIndex) val childId = cursor.getString(childUriIndex)
val displayName = cursor.getString(displayNameIndex) val displayName = cursor.getString(displayNameIndex)
@ -84,27 +96,44 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De
val newPath = relativePath.file(displayName) val newPath = relativePath.file(displayName)
val mimeType = cursor.getString(mimeTypeIndex) val mimeType = cursor.getString(mimeTypeIndex)
val lastModified = cursor.getLong(lastModifiedIndex)
val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
// This does NOT block the current coroutine. Instead, we will // Create a directory node with empty children flow initially
// evaluate this flow in parallel later to maximize throughput. val directory = DeviceDirectory(
recursive.add( uri = childUri,
exploreImpl(contentResolver, rootUri, childId, newPath, ignoreHidden)) path = newPath,
} else if (mimeType.startsWith("audio/") && mimeType != "audio/x-mpegurl") { parent = parent,
// Immediately emit all files given that it's just an O(1) op. children = emptyFlow()
// This also just makes sure the outer flow has a reason to exist )
// rather than just being a glorified async.
val lastModified = cursor.getLong(lastModifiedIndex) // Set up the children flow for this directory
directory.children = exploreDirectoryImpl(
contentResolver,
rootUri,
childId,
newPath,
directory,
ignoreHidden
)
// Emit the directory node
emit(directory)
} else {
val size = cursor.getLong(sizeIndex) val size = cursor.getLong(sizeIndex)
emit( emit(
DeviceFile( DeviceFile(
DocumentsContract.buildDocumentUriUsingTree(rootUri, childId), uri = childUri,
mimeType, mimeType = mimeType,
newPath, path = newPath,
size, size = size,
lastModified)) modifiedMs = lastModified,
parent = parent
)
)
} }
} }
emitAll(recursive.asFlow().flattenMerge())
} }
} }

View file

@ -22,7 +22,7 @@ import android.os.ParcelFileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
internal interface MetadataExtractor { internal interface MetadataExtractor {
suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata? suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata?

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
import android.util.Log import android.util.Log
import java.io.FileInputStream import java.io.FileInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) { internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) {
private val channel = fis.channel private val channel = fis.channel

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.metadata package org.oxycblt.musikr.metadata
import java.io.FileInputStream import java.io.FileInputStream
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
internal object TagLibJNI { internal object TagLibJNI {
init { init {

View file

@ -20,17 +20,20 @@ package org.oxycblt.musikr.pipeline
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceDirectory
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.fs.device.DeviceNode
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.device.DeviceFiles import org.oxycblt.musikr.fs.device.DeviceFiles
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
@ -54,12 +57,8 @@ private class ExploreStepImpl(
val audios = val audios =
deviceFiles deviceFiles
.explore(locations.asFlow()) .explore(locations.asFlow())
.mapNotNull { .flattenFilter {
when { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE
it.mimeType == M3U.MIME_TYPE -> null
it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it)
else -> null
}
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
@ -70,6 +69,19 @@ private class ExploreStepImpl(
.buffer() .buffer()
return merge(audios, playlists) return merge(audios, playlists)
} }
@OptIn(ExperimentalCoroutinesApi::class)
private fun Flow<DeviceNode>.flattenFilter(block: (DeviceFile) -> Boolean): Flow<ExploreNode> = flow {
collect {
val recurse = mutableListOf<Flow<ExploreNode>>()
when {
it is DeviceFile && block(it) -> emit(ExploreNode.Audio(it))
it is DeviceDirectory -> recurse.add(it.children.flattenFilter(block))
else -> {}
}
emitAll(recurse.asFlow().flattenMerge())
}
}
} }
internal sealed interface ExploreNode { internal sealed interface ExploreNode {

View file

@ -37,7 +37,7 @@ import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.CoverResult
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile

View file

@ -18,7 +18,7 @@
package org.oxycblt.musikr.pipeline package org.oxycblt.musikr.pipeline
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.interpret.PrePlaylist import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.interpret
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Format
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Disc