musikr: refactor devicefiles into tree
This commit is contained in:
parent
fce77ec8a0
commit
8104985a4e
17 changed files with 110 additions and 54 deletions
|
@ -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<FileCover>) :
|
||||
|
|
|
@ -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<NullCover> {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<T : Cover> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,14 +16,29 @@
|
|||
* 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 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(
|
||||
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
|
|
@ -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<MusicLocation>, ignoreHidden: Boolean = true): Flow<DeviceFile>
|
||||
fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean = true): Flow<DeviceNode>
|
||||
|
||||
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<MusicLocation>, ignoreHidden: Boolean): Flow<DeviceFile> =
|
||||
override fun explore(locations: Flow<MusicLocation>, ignoreHidden: Boolean): Flow<DeviceNode> =
|
||||
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<DeviceFile> = flow {
|
||||
): Flow<DeviceNode> = 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<Flow<DeviceFile>>()
|
||||
|
||||
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)
|
||||
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)
|
||||
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(
|
||||
DocumentsContract.buildDocumentUriUsingTree(rootUri, childId),
|
||||
mimeType,
|
||||
newPath,
|
||||
size,
|
||||
lastModified))
|
||||
uri = childUri,
|
||||
mimeType = mimeType,
|
||||
path = newPath,
|
||||
size = size,
|
||||
modifiedMs = lastModified,
|
||||
parent = parent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
emitAll(recursive.asFlow().flattenMerge())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue