musikr: avoid redundant dir child queries

Make parents async rather than children
This commit is contained in:
Alexander Capehart 2025-03-04 15:48:40 -07:00
parent 4de42a3a55
commit d62c85f8a5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 78 additions and 92 deletions

View file

@ -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))
} }

View file

@ -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

View file

@ -15,23 +15,28 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* 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.device package org.oxycblt.musikr.fs.device
import android.content.ContentResolver 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,91 +49,82 @@ 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( location.uri,
contentResolver, DocumentsContract.getTreeDocumentId(location.uri),
location.uri, location.path,
DocumentsContract.getTreeDocumentId(location.uri), null
location.path, )
rootDirectory,
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 =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val mimeTypeIndex = val mimeTypeIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
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)
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)
// Skip hidden files/directories if ignoreHidden is true // Skip hidden files/directories if ignoreHidden is true
if (ignoreHidden && displayName.startsWith(".")) { if (ignoreHidden && displayName.startsWith(".")) {
continue continue
} }
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)
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) val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
emit(
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { DeviceFile(
// Create a directory node with empty children flow initially uri = childUri,
val directory = mimeType = mimeType,
DeviceDirectory( path = newPath,
uri = childUri, size = size,
path = newPath, modifiedMs = lastModified,
parent = parent, parent = directoryDeferred
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))
}
} }
} }
}
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
)
} }
} }

View file

@ -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 {