diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt index 4949d78d7..251e8ce7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CompatCovers.kt @@ -30,7 +30,7 @@ import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.FileCover 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 open class CompatCovers(private val context: Context, private val inner: Covers) : diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt index cde9db80a..cffa626d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt @@ -22,7 +22,7 @@ import android.content.Context import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverResult 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 class NullCovers(private val context: Context) : MutableCovers { diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt index 7a326d483..d40112c20 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt @@ -31,7 +31,7 @@ import org.oxycblt.musikr.cover.FileCover import org.oxycblt.musikr.cover.FileCovers import org.oxycblt.musikr.cover.MutableCovers 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.metadata.Metadata diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt index 43f6aae12..670ee2f31 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -20,7 +20,7 @@ package org.oxycblt.musikr.cache import org.oxycblt.musikr.cover.Cover 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 abstract class Cache { diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index c332946a7..19ba41ab2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -34,7 +34,7 @@ import androidx.room.TypeConverters import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverResult 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.pipeline.RawSong import org.oxycblt.musikr.tag.Date diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt index e15e019b9..c4107c3a5 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt @@ -21,7 +21,7 @@ package org.oxycblt.musikr.cache import android.content.Context import org.oxycblt.musikr.cover.Cover 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 interface StoredCache { diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index f6e505393..8d7b19eec 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -19,7 +19,7 @@ package org.oxycblt.musikr.cover import java.io.InputStream -import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata interface Covers { diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt index f4d93c3ca..3edcfd6d8 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt @@ -19,7 +19,7 @@ package org.oxycblt.musikr.cover 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.AppFiles import org.oxycblt.musikr.metadata.Metadata diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt similarity index 63% rename from musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt index 25b0dfbbe..6590491a6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt @@ -16,14 +16,29 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.fs +package org.oxycblt.musikr.fs.device 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 data class DeviceFile( - val uri: Uri, + override val uri: Uri, + override val path: Path, + val modifiedMs: Long, val mimeType: String, - val path: Path, val size: Long, - val modifiedMs: Long -) + val parent: DeviceDirectory +) : DeviceNode diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt index 3490ed69f..0fbee972f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt @@ -24,17 +24,14 @@ import android.net.Uri import android.provider.DocumentsContract import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.emptyFlow 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.Path internal interface DeviceFiles { - fun explore(locations: Flow, ignoreHidden: Boolean = true): Flow + fun explore(locations: Flow, ignoreHidden: Boolean = true): Flow companion object { fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe) @@ -43,23 +40,38 @@ internal interface DeviceFiles { @OptIn(ExperimentalCoroutinesApi::class) private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { - override fun explore(locations: Flow, ignoreHidden: Boolean): Flow = + override fun explore(locations: Flow, ignoreHidden: Boolean): Flow = 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, location.uri, DocumentsContract.getTreeDocumentId(location.uri), location.path, - ignoreHidden) + rootDirectory, + ignoreHidden + ) + + // Return a flow that emits the root directory + flow { emit(rootDirectory) } } - private fun exploreImpl( + private fun exploreDirectoryImpl( contentResolver: ContentResolver, rootUri: Uri, treeDocumentId: String, relativePath: Path, + parent: DeviceDirectory, ignoreHidden: Boolean - ): Flow = flow { + ): Flow = flow { contentResolver.useQuery( DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), PROJECTION) { cursor -> @@ -72,7 +84,7 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) val lastModifiedIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - val recursive = mutableListOf>() + while (cursor.moveToNext()) { val childId = cursor.getString(childUriIndex) val displayName = cursor.getString(displayNameIndex) @@ -84,27 +96,44 @@ private class DeviceFilesImpl(private val contentResolver: ContentResolver) : De val newPath = relativePath.file(displayName) val mimeType = cursor.getString(mimeTypeIndex) + val lastModified = cursor.getLong(lastModifiedIndex) + val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId) + if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { - // This does NOT block the current coroutine. Instead, we will - // evaluate this flow in parallel later to maximize throughput. - recursive.add( - exploreImpl(contentResolver, rootUri, childId, newPath, ignoreHidden)) - } else if (mimeType.startsWith("audio/") && mimeType != "audio/x-mpegurl") { - // Immediately emit all files given that it's just an O(1) op. - // 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) + // 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( - DocumentsContract.buildDocumentUriUsingTree(rootUri, childId), - mimeType, - newPath, - size, - lastModified)) + uri = childUri, + mimeType = mimeType, + path = newPath, + size = size, + modifiedMs = lastModified, + parent = parent + ) + ) } } - emitAll(recursive.asFlow().flattenMerge()) } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt index 0b0cfc48d..7ce64d949 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt @@ -22,7 +22,7 @@ import android.os.ParcelFileDescriptor import java.io.FileInputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.fs.device.DeviceFile internal interface MetadataExtractor { suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata? diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt index c7486e220..213e68ea7 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt @@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata import android.util.Log import java.io.FileInputStream 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) { private val channel = fis.channel diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt index d5105f3a8..e0b48ee00 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt @@ -19,7 +19,7 @@ package org.oxycblt.musikr.metadata import java.io.FileInputStream -import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.fs.device.DeviceFile internal object TagLibJNI { init { diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt index 1e77659e1..94da7e590 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -20,17 +20,20 @@ package org.oxycblt.musikr.pipeline import android.content.Context import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge 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.device.DeviceFiles import org.oxycblt.musikr.playlist.PlaylistFile @@ -54,12 +57,8 @@ private class ExploreStepImpl( val audios = deviceFiles .explore(locations.asFlow()) - .mapNotNull { - when { - it.mimeType == M3U.MIME_TYPE -> null - it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it) - else -> null - } + .flattenFilter { + it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } .flowOn(Dispatchers.IO) .buffer() @@ -70,6 +69,19 @@ private class ExploreStepImpl( .buffer() return merge(audios, playlists) } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun Flow.flattenFilter(block: (DeviceFile) -> Boolean): Flow = flow { + collect { + val recurse = mutableListOf>() + 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 { diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index d47ed5e9d..1aab2c562 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -37,7 +37,7 @@ import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverResult 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.Properties import org.oxycblt.musikr.playlist.PlaylistFile diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt index 1f6efc892..65d7525f5 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt @@ -18,7 +18,7 @@ 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.interpret.PrePlaylist import org.oxycblt.musikr.tag.interpret.PreSong diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index edb6cfed8..de1447c8b 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.interpret import org.oxycblt.musikr.Interpretation 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.pipeline.RawSong import org.oxycblt.musikr.tag.Disc