refactor(android): move SAF CUE helpers out of MainActivitySafScan.kt

The CUE sheet helpers used by both SAF scans (FILE line extraction,
the per-directory child lookup cache and the audio sibling resolver)
move verbatim into SafCueScan.kt. rememberCueDirectoryListing becomes
internal because the scans stay behind; the sibling extension list and
the lookup builder are only used in the new file and stay private.
This commit is contained in:
zarzet committed 2026-10-19 05:16:33 +07:00
1 parent bb930827fa
commit 2af0fa95e5
2 files changed
+103 -95

No files matched your search

@@ -90,40 +90,6 @@ internal fun MainActivity.loadExistingFilesFromSnapshot(snapshotPath: String): M
return result
}
/**
* Extract the audio filename referenced by a CUE sheet file.
* Reads the FILE "name" TYPE line from the .cue text.
* Returns just the filename (no path), or null if not found.
*/
internal fun MainActivity.extractCueAudioFileName(cueTempPath: String): String? {
try {
val lines = File(cueTempPath).readLines()
for (line in lines) {
val trimmed = line.trim().let { l ->
if (l.startsWith("\uFEFF")) l.removePrefix("\uFEFF").trim() else l
}
if (trimmed.uppercase(Locale.ROOT).startsWith("FILE ")) {
val rest = trimmed.substring(5).trim()
val filename = if (rest.startsWith("\"")) {
val endQuote = rest.indexOf('"', 1)
if (endQuote > 0) rest.substring(1, endQuote) else rest
} else {
val parts = rest.split("\\s+".toRegex())
if (parts.size >= 2) parts.dropLast(1).joinToString(" ") else rest
}
return filename.substringAfterLast("/").substringAfterLast("\\")
}
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "Failed to extract audio filename from CUE: ${e.message}")
}
return null
}
private val cueSiblingAudioExtensions = listOf(
".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a", ".mp4", ".aac"
)
// Keep the SAF folder walk aligned with the backend's supported audio formats.
// CUE files are handled separately.
private val libraryScanAudioExtensions = setOf(
@@ -131,36 +97,6 @@ internal fun MainActivity.extractCueAudioFileName(cueTempPath: String): String?
".ape", ".wv", ".mpc", ".wav", ".aiff", ".aif", ".dsf", ".dff"
)
internal fun MainActivity.getSafChildFileLookup(
dir: DocumentFile,
cache: MutableMap<String, Map<String, DocumentFile>>,
): Map<String, DocumentFile> {
val dirKey = dir.uri.toString()
return cache.getOrPut(dirKey) { safChildFileLookup(listSafChildrenOrThrow(dir)) }
}
private fun safChildFileLookup(children: List<SafChildEntry>): Map<String, DocumentFile> =
buildMap {
for (child in children) {
if (child.isDirectory) continue
val childName = child.name?.trim().orEmpty()
if (childName.isBlank()) continue
put(childName.lowercase(Locale.ROOT), child.doc)
}
}
/** Keeps a traversal listing for CUE sibling lookup so the directory is not
* queried again; on network providers every listing is a round trip. */
private fun rememberCueDirectoryListing(
dir: DocumentFile,
children: List<SafChildEntry>,
cache: MutableMap<String, Map<String, DocumentFile>>,
) {
if (children.any { !it.isDirectory && it.name?.endsWith(".cue", ignoreCase = true) == true }) {
cache[dir.uri.toString()] = safChildFileLookup(children)
}
}
// Provider round trips dominate on SD cards, USB drives and network shares.
private const val SAF_LIST_CONCURRENCY = 4
private const val SAF_READ_WORKERS = 6
@@ -266,37 +202,6 @@ private fun <T, R> runSafReadsInOrder(
}
}
internal fun MainActivity.resolveCueAudioSibling(
parentDir: DocumentFile,
cueName: String,
audioFileName: String?,
childLookupCache: MutableMap<String, Map<String, DocumentFile>>,
): DocumentFile? {
val childLookup = getSafChildFileLookup(parentDir, childLookupCache)
val directMatch = audioFileName
?.trim()
?.takeIf { it.isNotEmpty() }
?.substringAfterLast("/")
?.substringAfterLast("\\")
?.lowercase(Locale.ROOT)
?.let(childLookup::get)
if (directMatch != null) {
return directMatch
}
val cueBaseName = cueName.substringBeforeLast('.').trim()
if (cueBaseName.isBlank()) {
return null
}
val cueBaseKey = cueBaseName.lowercase(Locale.ROOT)
for (ext in cueSiblingAudioExtensions) {
childLookup["$cueBaseKey$ext"]?.let { return it }
}
return null
}
internal data class SafChildEntry(
val doc: DocumentFile,
val name: String?,
@@ -0,0 +1,103 @@
package com.zarz.spotiflac
import androidx.documentfile.provider.DocumentFile
import java.io.File
import java.util.Locale
// CUE sheet helpers for the SAF library scan: the audio file a sheet names,
// cached per-directory child lookups, and resolving the sheet's audio sibling.
/**
* Extract the audio filename referenced by a CUE sheet file.
* Reads the FILE "name" TYPE line from the .cue text.
* Returns just the filename (no path), or null if not found.
*/
internal fun MainActivity.extractCueAudioFileName(cueTempPath: String): String? {
try {
val lines = File(cueTempPath).readLines()
for (line in lines) {
val trimmed = line.trim().let { l ->
if (l.startsWith("\uFEFF")) l.removePrefix("\uFEFF").trim() else l
}
if (trimmed.uppercase(Locale.ROOT).startsWith("FILE ")) {
val rest = trimmed.substring(5).trim()
val filename = if (rest.startsWith("\"")) {
val endQuote = rest.indexOf('"', 1)
if (endQuote > 0) rest.substring(1, endQuote) else rest
} else {
val parts = rest.split("\\s+".toRegex())
if (parts.size >= 2) parts.dropLast(1).joinToString(" ") else rest
}
return filename.substringAfterLast("/").substringAfterLast("\\")
}
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "Failed to extract audio filename from CUE: ${e.message}")
}
return null
}
private val cueSiblingAudioExtensions = listOf(
".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a", ".mp4", ".aac"
)
internal fun MainActivity.getSafChildFileLookup(
dir: DocumentFile,
cache: MutableMap<String, Map<String, DocumentFile>>,
): Map<String, DocumentFile> {
val dirKey = dir.uri.toString()
return cache.getOrPut(dirKey) { safChildFileLookup(listSafChildrenOrThrow(dir)) }
}
private fun safChildFileLookup(children: List<SafChildEntry>): Map<String, DocumentFile> =
buildMap {
for (child in children) {
if (child.isDirectory) continue
val childName = child.name?.trim().orEmpty()
if (childName.isBlank()) continue
put(childName.lowercase(Locale.ROOT), child.doc)
}
}
/** Keeps a traversal listing for CUE sibling lookup so the directory is not
* queried again; on network providers every listing is a round trip. */
internal fun rememberCueDirectoryListing(
dir: DocumentFile,
children: List<SafChildEntry>,
cache: MutableMap<String, Map<String, DocumentFile>>,
) {
if (children.any { !it.isDirectory && it.name?.endsWith(".cue", ignoreCase = true) == true }) {
cache[dir.uri.toString()] = safChildFileLookup(children)
}
}
internal fun MainActivity.resolveCueAudioSibling(
parentDir: DocumentFile,
cueName: String,
audioFileName: String?,
childLookupCache: MutableMap<String, Map<String, DocumentFile>>,
): DocumentFile? {
val childLookup = getSafChildFileLookup(parentDir, childLookupCache)
val directMatch = audioFileName
?.trim()
?.takeIf { it.isNotEmpty() }
?.substringAfterLast("/")
?.substringAfterLast("\\")
?.lowercase(Locale.ROOT)
?.let(childLookup::get)
if (directMatch != null) {
return directMatch
}
val cueBaseName = cueName.substringBeforeLast('.').trim()
if (cueBaseName.isBlank()) {
return null
}
val cueBaseKey = cueBaseName.lowercase(Locale.ROOT)
for (ext in cueSiblingAudioExtensions) {
childLookup["$cueBaseKey$ext"]?.let { return it }
}
return null
}