musikr: avoid redundant dir child queries
Make parents async rather than children
This commit is contained in:
parent
4de42a3a55
commit
d62c85f8a5
4 changed files with 78 additions and 92 deletions
|
@ -63,7 +63,7 @@ open class FolderCovers(private val context: Context) : Covers<FolderCover> {
|
|||
class MutableFolderCovers(private val context: Context) :
|
||||
FolderCovers(context), MutableCovers<FolderCover> {
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FolderCover> {
|
||||
val parent = file.parent
|
||||
val parent = file.parent.await()
|
||||
val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss()
|
||||
return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri))
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.oxycblt.musikr.fs.device
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.oxycblt.musikr.fs.Path
|
||||
|
||||
|
@ -30,8 +31,8 @@ sealed interface DeviceNode {
|
|||
data class DeviceDirectory(
|
||||
override val uri: Uri,
|
||||
override val path: Path,
|
||||
val parent: DeviceDirectory?,
|
||||
var children: Flow<DeviceNode>
|
||||
val parent: Deferred<DeviceDirectory>?,
|
||||
val children: List<DeviceNode>
|
||||
) : DeviceNode
|
||||
|
||||
data class DeviceFile(
|
||||
|
@ -40,5 +41,5 @@ data class DeviceFile(
|
|||
val modifiedMs: Long,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val parent: DeviceDirectory
|
||||
val parent: Deferred<DeviceDirectory>
|
||||
) : DeviceNode
|
||||
|
|
|
@ -22,16 +22,21 @@ import android.content.ContentResolver
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.musikr.fs.MusicLocation
|
||||
import org.oxycblt.musikr.fs.Path
|
||||
|
||||
internal interface DeviceFiles {
|
||||
fun explore(locations: Flow<MusicLocation>): Flow<DeviceNode>
|
||||
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
||||
|
||||
companion object {
|
||||
fun from(context: Context, ignoreHidden: Boolean): DeviceFiles =
|
||||
|
@ -44,91 +49,82 @@ private class DeviceFilesImpl(
|
|||
private val contentResolver: ContentResolver,
|
||||
private val ignoreHidden: Boolean
|
||||
) : DeviceFiles {
|
||||
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceNode> =
|
||||
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
|
||||
locations.flatMapMerge { location ->
|
||||
// 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,
|
||||
location.uri,
|
||||
DocumentsContract.getTreeDocumentId(location.uri),
|
||||
location.path,
|
||||
rootDirectory,
|
||||
ignoreHidden)
|
||||
|
||||
// Return a flow that emits the root directory
|
||||
flow { emit(rootDirectory) }
|
||||
exploreDirectoryImpl(
|
||||
location.uri,
|
||||
DocumentsContract.getTreeDocumentId(location.uri),
|
||||
location.path,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun exploreDirectoryImpl(
|
||||
contentResolver: ContentResolver,
|
||||
rootUri: Uri,
|
||||
treeDocumentId: String,
|
||||
relativePath: Path,
|
||||
parent: DeviceDirectory,
|
||||
ignoreHidden: Boolean
|
||||
): Flow<DeviceNode> = flow {
|
||||
parent: Deferred<DeviceDirectory>?
|
||||
): Flow<DeviceFile> = flow {
|
||||
// Make a kotlin future
|
||||
val uri =
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId)
|
||||
val directoryDeferred = CompletableDeferred<DeviceDirectory>()
|
||||
val recursive = mutableListOf<Flow<DeviceFile>>()
|
||||
val children = mutableListOf<DeviceNode>()
|
||||
contentResolver.useQuery(
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
||||
PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val lastModifiedIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
uri, PROJECTION
|
||||
) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val lastModifiedIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val childId = cursor.getString(childUriIndex)
|
||||
val displayName = cursor.getString(displayNameIndex)
|
||||
while (cursor.moveToNext()) {
|
||||
val childId = cursor.getString(childUriIndex)
|
||||
val displayName = cursor.getString(displayNameIndex)
|
||||
|
||||
// Skip hidden files/directories if ignoreHidden is true
|
||||
if (ignoreHidden && displayName.startsWith(".")) {
|
||||
continue
|
||||
}
|
||||
// Skip hidden files/directories if ignoreHidden is true
|
||||
if (ignoreHidden && displayName.startsWith(".")) {
|
||||
continue
|
||||
}
|
||||
|
||||
val newPath = relativePath.file(displayName)
|
||||
val mimeType = cursor.getString(mimeTypeIndex)
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
val newPath = relativePath.file(displayName)
|
||||
val mimeType = cursor.getString(mimeTypeIndex)
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
recursive.add(
|
||||
exploreDirectoryImpl(
|
||||
rootUri,
|
||||
childId,
|
||||
newPath,
|
||||
directoryDeferred
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val size = cursor.getLong(sizeIndex)
|
||||
val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
|
||||
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
// Create a directory node with empty children flow initially
|
||||
val directory =
|
||||
DeviceDirectory(
|
||||
uri = childUri,
|
||||
path = newPath,
|
||||
parent = parent,
|
||||
children = emptyFlow())
|
||||
|
||||
// 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)
|
||||
emit(
|
||||
DeviceFile(
|
||||
uri = childUri,
|
||||
mimeType = mimeType,
|
||||
path = newPath,
|
||||
size = size,
|
||||
modifiedMs = lastModified,
|
||||
parent = parent))
|
||||
}
|
||||
emit(
|
||||
DeviceFile(
|
||||
uri = childUri,
|
||||
mimeType = mimeType,
|
||||
path = newPath,
|
||||
size = size,
|
||||
modifiedMs = lastModified,
|
||||
parent = directoryDeferred
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children))
|
||||
emitAll(recursive.asFlow().flattenMerge())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
@ -138,6 +134,7 @@ private class DeviceFilesImpl(
|
|||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
@ -59,7 +60,8 @@ private class ExploreStepImpl(
|
|||
val audios =
|
||||
deviceFiles
|
||||
.explore(locations.asFlow())
|
||||
.flattenFilter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
|
||||
.filter { it.mimeType.startsWith("audio/") && it.mimeType != M3U.MIME_TYPE }
|
||||
.map { ExploreNode.Audio(it) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer()
|
||||
val playlists =
|
||||
|
@ -69,20 +71,6 @@ private class ExploreStepImpl(
|
|||
.buffer()
|
||||
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 {
|
||||
|
|
Loading…
Reference in a new issue