refactor(android): share the SAF scan percent and CUE helpers

The two SAF scans repeated the same progress arithmetic and CUE steps.
Only the runs that are identical are shared:

- safScanProgressPct(done, total) is the Int/Int to Double * 100.0
  expression all six percent sites used. The incremental scan keeps its
  zero-total guard (100.0), the full scan's call sites keep relying on a
  non-zero total as before.
- reportScanned() replaces the two identical scanned/error/percent
  updates in scanSafTree. The CUE-referenced audio skip keeps its own
  update, which never set errorCount.
- reportProcessed() moves above the changed-CUE loop, whose inline copy
  of its body it now replaces.
- resolveCueSheetAudio() covers the FILE line extraction plus sibling
  lookup that both changed-CUE loops and the unchanged-CUE loop ran.
- scanSafCueTracks() covers the audio name, timestamp, cover key and
  scanCueFromUri steps. The scans fall back to different timestamps
  when the audio's own cannot be read, so the fallback is a lambda and
  still runs only on that failure.

Cancellation checks, checkpoint writes, flushes, the copy/delete of the
temporary CUE file and every counter update stay in place and order;
progress values and the scan output are unchanged.
This commit is contained in:
zarzet committed 2026-10-19 05:16:33 +07:00
1 parent eeda955363
commit e80ae1b1da
2 files changed
+105 -93

No files matched your search

@@ -100,6 +100,10 @@ internal fun MainActivity.safScanStopRequested(cancelled: () -> Boolean = { fals
return safScanCancel || cancelled()
}
/** The `progress_pct` value for [done] of [total] items; callers guard zero. */
private fun safScanProgressPct(done: Int, total: Int): Double =
done.toDouble() / total.toDouble() * 100.0
internal fun MainActivity.scanSafTree(
treeUriStr: String,
ndjsonOutputPath: String? = null,
@@ -231,7 +235,7 @@ internal fun MainActivity.scanSafTree(
it.totalFiles = totalItems
if (totalItems > 0 && earlyCheckpoint.isNotEmpty()) {
it.scannedFiles = earlyCheckpoint.size.coerceAtMost(totalItems)
it.progressPct = it.scannedFiles.toDouble() / totalItems.toDouble() * 100.0
it.progressPct = safScanProgressPct(it.scannedFiles, totalItems)
}
}
@@ -301,6 +305,15 @@ internal fun MainActivity.scanSafTree(
var scanned = checkpoint.size.coerceAtMost(totalItems)
var errors = traversalErrors
fun reportScanned() {
val pct = safScanProgressPct(scanned, totalItems)
updateSafScanProgress {
it.scannedFiles = scanned
it.errorCount = errors
it.progressPct = pct
}
}
val cueReferencedAudioUris = mutableSetOf<String>()
for (cue in cueFiles) {
@@ -327,13 +340,11 @@ internal fun MainActivity.scanSafTree(
continue
}
val audioFileName = extractCueAudioFileName(tempCuePath)
val audioDoc = resolveCueAudioSibling(
parentDir = parentDir,
cueName = cueName,
audioFileName = audioFileName,
childLookupCache = safChildLookupCache,
val audioDoc = resolveCueSheetAudio(
tempCuePath,
parentDir,
cueName,
safChildLookupCache,
)
if (audioDoc == null) {
@@ -349,26 +360,12 @@ internal fun MainActivity.scanSafTree(
continue
}
val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
val audioLastModified = try { audioDoc.lastModified() } catch (_: Exception) { cueDoc.lastModified() }
val coverCacheKey = buildLibraryCoverCacheKey(
audioDoc.uri.toString(),
audioLastModified,
)
val cueLastModified = cue.lastModified
val cueResultsJson = scanCueFromUri(
coreBackend,
val cueArray = scanSafCueTracks(
tempCuePath,
audioDoc.uri,
audioName,
cueDoc.uri.toString(),
cueLastModified,
coverCacheKey,
)
val cueArray = JSONArray(cueResultsJson)
audioDoc,
cueUri,
cue.lastModified,
) { cueDoc.lastModified() }
for (j in 0 until cueArray.length()) {
putResult(cueArray.getJSONObject(j))
}
@@ -383,12 +380,7 @@ internal fun MainActivity.scanSafTree(
}
scanned++
val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
updateSafScanProgress {
it.scannedFiles = scanned
it.errorCount = errors
it.progressPct = pct
}
reportScanned()
}
data class SafAudioScanOutcome(
@@ -417,7 +409,7 @@ internal fun MainActivity.scanSafTree(
if (cueReferencedAudioUris.contains(stableUri)) {
recordCheckpoint(stableUri, lastModified)
scanned++
val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
val pct = safScanProgressPct(scanned, totalItems)
updateSafScanProgress {
it.scannedFiles = scanned
it.progressPct = pct
@@ -485,12 +477,7 @@ internal fun MainActivity.scanSafTree(
}
scanned++
val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
updateSafScanProgress {
it.scannedFiles = scanned
it.errorCount = errors
it.progressPct = pct
}
reportScanned()
}
if (!completed) {
ndjsonWriter?.close()
@@ -717,6 +704,20 @@ internal fun MainActivity.scanSafTreeIncremental(
var scanned = 0
var errors = traversalErrors
fun reportProcessed() {
val processed = skippedCount + scanned
val pct = if (totalFiles > 0) {
safScanProgressPct(processed, totalFiles)
} else {
100.0
}
updateSafScanProgress {
it.scannedFiles = processed
it.errorCount = errors
it.progressPct = pct
}
}
val cueReferencedAudioUris = mutableSetOf<String>()
for ((cueDoc, parentDir, cueName, cueLastModified) in cueFilesToScan) {
@@ -744,13 +745,11 @@ internal fun MainActivity.scanSafTreeIncremental(
continue
}
val audioFileName = extractCueAudioFileName(tempCuePath)
val audioDoc = resolveCueAudioSibling(
parentDir = parentDir,
cueName = cueName,
audioFileName = audioFileName,
childLookupCache = safChildLookupCache,
val audioDoc = resolveCueSheetAudio(
tempCuePath,
parentDir,
cueName,
safChildLookupCache,
)
if (audioDoc == null) {
@@ -762,24 +761,12 @@ internal fun MainActivity.scanSafTreeIncremental(
cueReferencedAudioUris.add(audioDoc.uri.toString())
val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
val audioLastModified = try { audioDoc.lastModified() } catch (_: Exception) { cueLastModified }
val coverCacheKey = buildLibraryCoverCacheKey(
audioDoc.uri.toString(),
audioLastModified,
)
val cueResultsJson = scanCueFromUri(
coreBackend,
val cueArray = scanSafCueTracks(
tempCuePath,
audioDoc.uri,
audioName,
audioDoc,
cueDoc.uri.toString(),
cueLastModified,
coverCacheKey,
)
val cueArray = JSONArray(cueResultsJson)
) { cueLastModified }
for (j in 0 until cueArray.length()) {
val trackObj = cueArray.getJSONObject(j)
putFile(trackObj)
@@ -797,17 +784,7 @@ internal fun MainActivity.scanSafTreeIncremental(
}
scanned++
val processed = skippedCount + scanned
val pct = if (totalFiles > 0) {
processed.toDouble() / totalFiles.toDouble() * 100.0
} else {
100.0
}
updateSafScanProgress {
it.scannedFiles = processed
it.errorCount = errors
it.progressPct = pct
}
reportProcessed()
}
for ((cueDoc, parentDir, cueName) in unchangedCueFiles) {
@@ -815,12 +792,11 @@ internal fun MainActivity.scanSafTreeIncremental(
try {
tempCue = copyUriToTemp(coreBackend, cueDoc.uri, ".cue", cueName)
if (tempCue != null) {
val audioFileName = extractCueAudioFileName(tempCue)
val audioDoc = resolveCueAudioSibling(
parentDir = parentDir,
cueName = cueName,
audioFileName = audioFileName,
childLookupCache = safChildLookupCache,
val audioDoc = resolveCueSheetAudio(
tempCue,
parentDir,
cueName,
safChildLookupCache,
)
if (audioDoc != null) {
cueReferencedAudioUris.add(audioDoc.uri.toString())
@@ -845,20 +821,6 @@ internal fun MainActivity.scanSafTreeIncremental(
return result.toString()
}
fun reportProcessed() {
val processed = skippedCount + scanned
val pct = if (totalFiles > 0) {
processed.toDouble() / totalFiles.toDouble() * 100.0
} else {
100.0
}
updateSafScanProgress {
it.scannedFiles = processed
it.errorCount = errors
it.progressPct = pct
}
}
val pendingAudio = mutableListOf<ChangedAudio>()
for (audio in audioFiles) {
if (safScanStopRequested(cancelled)) return cancelledIncrementalResult()
@@ -1,6 +1,7 @@
package com.zarz.spotiflac
import androidx.documentfile.provider.DocumentFile
import org.json.JSONArray
import java.io.File
import java.util.Locale
@@ -101,3 +102,52 @@ internal fun MainActivity.resolveCueAudioSibling(
}
return null
}
/** Resolves the audio document named by a CUE sheet copied to [tempCuePath]. */
internal fun MainActivity.resolveCueSheetAudio(
tempCuePath: String,
parentDir: DocumentFile,
cueName: String,
childLookupCache: MutableMap<String, Map<String, DocumentFile>>,
): DocumentFile? {
val audioFileName = extractCueAudioFileName(tempCuePath)
return resolveCueAudioSibling(
parentDir = parentDir,
cueName = cueName,
audioFileName = audioFileName,
childLookupCache = childLookupCache,
)
}
/**
* Reads the virtual track rows of a CUE sheet against its resolved audio.
* [audioLastModifiedFallback] runs only when the audio's own timestamp
* cannot be read; the two scans fall back to different values.
*/
internal fun MainActivity.scanSafCueTracks(
tempCuePath: String,
audioDoc: DocumentFile,
cueUri: String,
cueLastModified: Long,
audioLastModifiedFallback: () -> Long,
): JSONArray {
val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
val audioLastModified = try { audioDoc.lastModified() } catch (_: Exception) { audioLastModifiedFallback() }
val coverCacheKey = buildLibraryCoverCacheKey(
audioDoc.uri.toString(),
audioLastModified,
)
val cueResultsJson = scanCueFromUri(
coreBackend,
tempCuePath,
audioDoc.uri,
audioName,
cueUri,
cueLastModified,
coverCacheKey,
)
return JSONArray(cueResultsJson)
}