From e80ae1b1da660fcae480a35fdeaf3f7a649f0267 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:57:54 +0700 Subject: [PATCH] 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. --- .../com/zarz/spotiflac/MainActivitySafScan.kt | 148 +++++++----------- .../kotlin/com/zarz/spotiflac/SafCueScan.kt | 50 ++++++ 2 files changed, 105 insertions(+), 93 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt index a0ede526..35284d27 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt @@ -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() 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() 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() for (audio in audioFiles) { if (safScanStopRequested(cancelled)) return cancelledIncrementalResult() diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/SafCueScan.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/SafCueScan.kt index 301eeaa0..f2418f32 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/SafCueScan.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/SafCueScan.kt @@ -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>, +): 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) +}