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) :
|
class MutableFolderCovers(private val context: Context) :
|
||||||
FolderCovers(context), MutableCovers<FolderCover> {
|
FolderCovers(context), MutableCovers<FolderCover> {
|
||||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<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()
|
val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss()
|
||||||
return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri))
|
return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package org.oxycblt.musikr.fs.device
|
package org.oxycblt.musikr.fs.device
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
|
|
||||||
|
@ -30,8 +31,8 @@ sealed interface DeviceNode {
|
||||||
data class DeviceDirectory(
|
data class DeviceDirectory(
|
||||||
override val uri: Uri,
|
override val uri: Uri,
|
||||||
override val path: Path,
|
override val path: Path,
|
||||||
val parent: DeviceDirectory?,
|
val parent: Deferred<DeviceDirectory>?,
|
||||||
var children: Flow<DeviceNode>
|
val children: List<DeviceNode>
|
||||||
) : DeviceNode
|
) : DeviceNode
|
||||||
|
|
||||||
data class DeviceFile(
|
data class DeviceFile(
|
||||||
|
@ -40,5 +41,5 @@ data class DeviceFile(
|
||||||
val modifiedMs: Long,
|
val modifiedMs: Long,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
val parent: DeviceDirectory
|
val parent: Deferred<DeviceDirectory>
|
||||||
) : DeviceNode
|
) : DeviceNode
|
||||||
|
|
|
@ -22,16 +22,21 @@ import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
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.emitAll
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
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.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>): Flow<DeviceNode>
|
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context, ignoreHidden: Boolean): DeviceFiles =
|
fun from(context: Context, ignoreHidden: Boolean): DeviceFiles =
|
||||||
|
@ -44,38 +49,32 @@ private class DeviceFilesImpl(
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
private val ignoreHidden: Boolean
|
private val ignoreHidden: Boolean
|
||||||
) : DeviceFiles {
|
) : DeviceFiles {
|
||||||
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceNode> =
|
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
|
||||||
locations.flatMapMerge { location ->
|
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
|
// Set up the children flow for the root directory
|
||||||
rootDirectory.children =
|
|
||||||
exploreDirectoryImpl(
|
exploreDirectoryImpl(
|
||||||
contentResolver,
|
|
||||||
location.uri,
|
location.uri,
|
||||||
DocumentsContract.getTreeDocumentId(location.uri),
|
DocumentsContract.getTreeDocumentId(location.uri),
|
||||||
location.path,
|
location.path,
|
||||||
rootDirectory,
|
null
|
||||||
ignoreHidden)
|
)
|
||||||
|
|
||||||
// Return a flow that emits the root directory
|
|
||||||
flow { emit(rootDirectory) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exploreDirectoryImpl(
|
private fun exploreDirectoryImpl(
|
||||||
contentResolver: ContentResolver,
|
|
||||||
rootUri: Uri,
|
rootUri: Uri,
|
||||||
treeDocumentId: String,
|
treeDocumentId: String,
|
||||||
relativePath: Path,
|
relativePath: Path,
|
||||||
parent: DeviceDirectory,
|
parent: Deferred<DeviceDirectory>?
|
||||||
ignoreHidden: Boolean
|
): Flow<DeviceFile> = flow {
|
||||||
): Flow<DeviceNode> = 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(
|
contentResolver.useQuery(
|
||||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
uri, PROJECTION
|
||||||
PROJECTION) { cursor ->
|
) { cursor ->
|
||||||
val childUriIndex =
|
val childUriIndex =
|
||||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||||
val displayNameIndex =
|
val displayNameIndex =
|
||||||
|
@ -98,26 +97,19 @@ private class DeviceFilesImpl(
|
||||||
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 lastModified = cursor.getLong(lastModifiedIndex)
|
||||||
val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
|
|
||||||
|
|
||||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||||
// Create a directory node with empty children flow initially
|
recursive.add(
|
||||||
val directory =
|
|
||||||
DeviceDirectory(
|
|
||||||
uri = childUri,
|
|
||||||
path = newPath,
|
|
||||||
parent = parent,
|
|
||||||
children = emptyFlow())
|
|
||||||
|
|
||||||
// Set up the children flow for this directory
|
|
||||||
directory.children =
|
|
||||||
exploreDirectoryImpl(
|
exploreDirectoryImpl(
|
||||||
contentResolver, rootUri, childId, newPath, directory, ignoreHidden)
|
rootUri,
|
||||||
|
childId,
|
||||||
// Emit the directory node
|
newPath,
|
||||||
emit(directory)
|
directoryDeferred
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val size = cursor.getLong(sizeIndex)
|
val size = cursor.getLong(sizeIndex)
|
||||||
|
val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
|
||||||
emit(
|
emit(
|
||||||
DeviceFile(
|
DeviceFile(
|
||||||
uri = childUri,
|
uri = childUri,
|
||||||
|
@ -125,10 +117,14 @@ private class DeviceFilesImpl(
|
||||||
path = newPath,
|
path = newPath,
|
||||||
size = size,
|
size = size,
|
||||||
modifiedMs = lastModified,
|
modifiedMs = lastModified,
|
||||||
parent = parent))
|
parent = directoryDeferred
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children))
|
||||||
|
emitAll(recursive.asFlow().flattenMerge())
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
@ -138,6 +134,7 @@ private class DeviceFilesImpl(
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
DocumentsContract.Document.COLUMN_SIZE,
|
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.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.filter
|
||||||
import kotlinx.coroutines.flow.flattenMerge
|
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
|
||||||
|
@ -59,7 +60,8 @@ private class ExploreStepImpl(
|
||||||
val audios =
|
val audios =
|
||||||
deviceFiles
|
deviceFiles
|
||||||
.explore(locations.asFlow())
|
.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)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer()
|
.buffer()
|
||||||
val playlists =
|
val playlists =
|
||||||
|
@ -69,20 +71,6 @@ 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 {
|
||||||
|
|
Loading…
Reference in a new issue