Skip to content

Commit f2b1ba7

Browse files
committed
musikr: add subfolder exclusion prototype
1 parent 7f672d6 commit f2b1ba7

3 files changed

Lines changed: 73 additions & 21 deletions

File tree

musikr/src/main/java/org/oxycblt/musikr/Musikr.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ interface Musikr {
5757
locations: List<MusicLocation>,
5858
onProgress: suspend (IndexingProgress) -> Unit = {}
5959
): LibraryResult
60+
61+
/**
62+
* Update a music location with a set of subdirectories to exclude from scanning.
63+
*
64+
* @param context The context to use for loading resources.
65+
* @param uri The URI of the music location to update.
66+
* @param excludedSubdirs The set of subdirectory names to exclude when scanning this location.
67+
* @return The updated [MusicLocation] or null if the location doesn't exist.
68+
*/
69+
fun updateLocationExclusions(context: Context, uri: android.net.Uri, excludedSubdirs: Set<String>): MusicLocation?
70+
71+
/**
72+
* Create a new music location with a set of subdirectories to exclude from scanning.
73+
*
74+
* @param context The context to use for loading resources.
75+
* @param uri The URI of the music location to create.
76+
* @param excludedSubdirs The set of subdirectory names to exclude when scanning this location.
77+
* @return The new [MusicLocation] or null if the location couldn't be created.
78+
*/
79+
fun newLocationWithExclusions(context: Context, uri: android.net.Uri, excludedSubdirs: Set<String>): MusicLocation?
6080

6181
companion object {
6282
/**
@@ -116,6 +136,14 @@ private class MusikrImpl(
116136
private val extractStep: ExtractStep,
117137
private val evaluateStep: EvaluateStep
118138
) : Musikr {
139+
override fun updateLocationExclusions(context: Context, uri: android.net.Uri, excludedSubdirs: Set<String>): MusicLocation? {
140+
return MusicLocation.existing(context, uri, excludedSubdirs)
141+
}
142+
143+
override fun newLocationWithExclusions(context: Context, uri: android.net.Uri, excludedSubdirs: Set<String>): MusicLocation? {
144+
return MusicLocation.new(context, uri, excludedSubdirs)
145+
}
146+
119147
override suspend fun run(
120148
locations: List<MusicLocation>,
121149
onProgress: suspend (IndexingProgress) -> Unit

musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ import org.oxycblt.musikr.fs.device.contentResolverSafe
2626
import org.oxycblt.musikr.fs.path.DocumentPathFactory
2727
import org.oxycblt.musikr.util.splitEscaped
2828

29-
class MusicLocation private constructor(val uri: Uri, val path: Path) {
29+
class MusicLocation private constructor(val uri: Uri, val path: Path, val excludedSubdirs: Set<String> = emptySet()) {
3030
override fun equals(other: Any?) = other is MusicLocation && uri == other.uri
3131

3232
override fun hashCode() = 31 * uri.hashCode()
3333

3434
override fun toString(): String = uri.toString()
3535

3636
companion object {
37-
fun new(context: Context, uri: Uri): MusicLocation? {
37+
fun new(context: Context, uri: Uri, excludedSubdirs: Set<String> = emptySet()): MusicLocation? {
3838
if (!DocumentsContract.isTreeUri(uri)) return null
3939
val documentPathFactory = DocumentPathFactory.from(context)
4040
val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null
@@ -47,10 +47,10 @@ class MusicLocation private constructor(val uri: Uri, val path: Path) {
4747
uri,
4848
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
4949
}
50-
return MusicLocation(uri, path)
50+
return MusicLocation(uri, path, excludedSubdirs)
5151
}
5252

53-
fun existing(context: Context, uri: Uri): MusicLocation? {
53+
fun existing(context: Context, uri: Uri, excludedSubdirs: Set<String> = emptySet()): MusicLocation? {
5454
val documentPathFactory = DocumentPathFactory.from(context)
5555
if (!DocumentsContract.isTreeUri(uri)) return null
5656
val notPersisted =
@@ -59,14 +59,28 @@ class MusicLocation private constructor(val uri: Uri, val path: Path) {
5959
}
6060
if (notPersisted) return null
6161
val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null
62-
return MusicLocation(uri, path)
62+
return MusicLocation(uri, path, excludedSubdirs)
6363
}
6464

65-
fun toString(list: List<MusicLocation>) =
66-
list.joinToString(";") { it.uri.toString().replace(";", "\\;") }
65+
fun toString(list: List<MusicLocation>): String {
66+
return list.joinToString(";") { location ->
67+
val uriStr = location.uri.toString().replace(";", "\\;")
68+
val excludedSubdirsStr = location.excludedSubdirs.joinToString(",") { it.replace(",", "\\,").replace(";", "\\;") }
69+
if (excludedSubdirsStr.isEmpty()) uriStr else "$uriStr|$excludedSubdirsStr"
70+
}
71+
}
6772

6873
fun existing(context: Context, string: String): List<MusicLocation> {
69-
return string.splitEscaped { it == ';' }.mapNotNull { existing(context, Uri.parse(it)) }
74+
return string.splitEscaped { it == ';' }.mapNotNull { entry ->
75+
val parts = entry.split("|", limit = 2)
76+
val uriStr = parts[0]
77+
val excludedSubdirs = if (parts.size > 1) {
78+
parts[1].splitEscaped { it == ',' }.toSet()
79+
} else {
80+
emptySet()
81+
}
82+
existing(context, Uri.parse(uriStr), excludedSubdirs)
83+
}
7084
}
7185
}
7286
}

musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import android.content.ContentResolver
2222
import android.content.Context
2323
import android.net.Uri
2424
import android.provider.DocumentsContract
25-
import android.util.Log
2625
import androidx.core.net.toUri
2726
import kotlinx.coroutines.CoroutineScope
2827
import kotlinx.coroutines.Dispatchers
@@ -40,6 +39,7 @@ import kotlinx.coroutines.flow.mapNotNull
4039
import kotlinx.coroutines.flow.merge
4140
import kotlinx.coroutines.flow.shareIn
4241
import kotlinx.coroutines.flow.takeWhile
42+
import kotlinx.coroutines.flow.transform
4343
import org.oxycblt.musikr.fs.MusicLocation
4444
import org.oxycblt.musikr.fs.Path
4545

@@ -76,7 +76,7 @@ private class DeviceFSImpl(
7676
if (modifiedMs == null) {
7777
return null
7878
}
79-
return query(location.uri, treeDocumentId, location.path, modifiedMs, null, fileTree)
79+
return query(location.uri, treeDocumentId, location.path, modifiedMs, null, fileTree, location.excludedSubdirs)
8080
}
8181

8282
private suspend fun query(
@@ -85,13 +85,14 @@ private class DeviceFSImpl(
8585
path: Path,
8686
modifiedMs: Long,
8787
parent: DeviceDirectory?,
88-
fileTree: FileTree
88+
fileTree: FileTree,
89+
excludedSubdirs: Set<String> = emptySet()
8990
): DeviceDirectory = coroutineScope {
9091
val uri = DocumentsContract.buildDocumentUriUsingTree(rootUri, treeDocumentId)
9192
val cached = fileTree.queryDirectory(uri)
9293
if (cached != null && cached.modifiedMs == modifiedMs) {
9394
return@coroutineScope hydrateCached(
94-
cached = cached, parentDir = parent, path = path, fileTree = fileTree)
95+
cached = cached, parentDir = parent, path = path, fileTree = fileTree, excludedSubdirs = excludedSubdirs)
9596
}
9697
val dir =
9798
DeviceDirectoryImpl(
@@ -102,7 +103,6 @@ private class DeviceFSImpl(
102103
children = emptyFlow())
103104
dir.children =
104105
flow {
105-
Log.d("DeviceFS", "Querying $uri")
106106
contentResolver.useQuery(
107107
DocumentsContract.buildChildDocumentsUriUsingTree(
108108
rootUri,
@@ -129,8 +129,6 @@ private class DeviceFSImpl(
129129
val childSubdirUris = mutableListOf<String>()
130130
val childFileUris = mutableListOf<String>()
131131

132-
val subqueries = mutableListOf<DeviceDirectory>()
133-
134132
while (cursor.moveToNext()) {
135133
val childId = cursor.getString(childUriIndex)
136134
val displayName = cursor.getString(displayNameIndex)
@@ -145,18 +143,23 @@ private class DeviceFSImpl(
145143
val lastModified = cursor.getLong(lastModifiedIndex)
146144

147145
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
146+
// Skip this directory if it's in the excluded list
147+
if (displayName in excludedSubdirs) {
148+
continue
149+
}
150+
148151
val subdir =
149152
query(
150153
rootUri = rootUri,
151154
treeDocumentId = childId,
152155
path = newPath,
153156
modifiedMs = modifiedMs,
154157
parent = dir,
155-
fileTree = fileTree)
158+
fileTree = fileTree,
159+
excludedSubdirs = excludedSubdirs)
156160
childSubdirUris.add(subdir.uri.toString())
157161
emit(StreamedFile.More(subdir))
158162
} else {
159-
Log.d("DeviceFS", "Querying file $childId")
160163
val size = cursor.getLong(sizeIndex)
161164
val childUri =
162165
DocumentsContract.buildDocumentUriUsingTree(
@@ -202,7 +205,8 @@ private class DeviceFSImpl(
202205
cached: CachedDirectory,
203206
parentDir: DeviceDirectory?,
204207
path: Path,
205-
fileTree: FileTree
208+
fileTree: FileTree,
209+
excludedSubdirs: Set<String> = emptySet()
206210
): DeviceDirectory = coroutineScope {
207211
val dir =
208212
DeviceDirectoryImpl(
@@ -215,17 +219,23 @@ private class DeviceFSImpl(
215219
flow {
216220
emitAll(
217221
merge(
218-
cached.subdirUris.asFlow().map { subdirUriString ->
222+
cached.subdirUris.asFlow().transform { subdirUriString ->
219223
val subdirUri = subdirUriString.toUri()
220224
val cachedSubdir =
221225
requireNotNull(fileTree.queryDirectory(subdirUri)) {
222226
"No cached subdir for $subdirUri, malformed cache! Rescan needed."
223227
}
224-
hydrateCached(
228+
// Skip this directory if it's in the excluded list
229+
if (cachedSubdir.name in excludedSubdirs) {
230+
return@transform
231+
}
232+
233+
emit(hydrateCached(
225234
cachedSubdir,
226235
dir,
227236
dir.path.file(cachedSubdir.name),
228-
fileTree)
237+
fileTree,
238+
excludedSubdirs))
229239
},
230240
cached.fileUris.asFlow().map {
231241
val cachedFile = requireNotNull(fileTree.queryFile(it.toUri()))

0 commit comments

Comments
 (0)