refactor(android): move SAF scan checkpointing out of MainActivitySafScan.kt

The resumable-scan checkpoint helpers (state file path, load,
reconcile against written rows, row count and output repair) are a
self-contained unit, so they move verbatim into SafScanCheckpoint.kt.
They were file-private and become internal because scanSafTree stays
in MainActivitySafScan.kt. The checkpoint file name, its line format
and the reconciled NDJSON keys are unchanged.
This commit is contained in:
zarzet committed 2026-10-19 05:16:33 +07:00
1 parent 461e449eb2
commit bb930827fa
2 files changed
+94 -87

No files matched your search

@@ -12,7 +12,6 @@ import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.security.MessageDigest
import java.util.Locale
import java.util.concurrent.ExecutionException
@@ -91,92 +90,6 @@ internal fun MainActivity.loadExistingFilesFromSnapshot(snapshotPath: String): M
return result
}
private fun safScanCheckpointPath(outputPath: String): String = "$outputPath.state"
private fun loadSafScanCheckpoint(path: String): MutableMap<String, Long> {
val checkpoint = File(path)
if (!checkpoint.exists()) return mutableMapOf()
val entries = mutableMapOf<String, Long>()
try {
checkpoint.forEachLine(Charsets.UTF_8) { line ->
val separator = line.indexOf('\t')
if (separator <= 0) return@forEachLine
val uri = line.substring(0, separator)
val modified = line.substring(separator + 1).toLongOrNull() ?: return@forEachLine
entries[uri] = modified
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed reading checkpoint: ${e.message}")
}
return entries
}
private fun reconcileSafScanCheckpoint(
outputPath: String,
loaded: MutableMap<String, Long>,
): MutableMap<String, Long> {
val output = File(outputPath)
if (!output.exists()) return loaded
val outputEntries = mutableMapOf<String, Long>()
try {
output.forEachLine(Charsets.UTF_8) { line ->
if (line.isBlank()) return@forEachLine
try {
val obj = JSONObject(line)
val filePath = obj.optString("filePath", "").trim()
if (filePath.isNotBlank()) {
val modified = obj.optLong("fileModTime", 0L)
val cueMarker = filePath.indexOf("#track")
val key = if (cueMarker > 0) filePath.substring(0, cueMarker) else filePath
outputEntries[key] = modified
}
} catch (_: Exception) {}
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed reconciling checkpoint: ${e.message}")
return loaded
}
loaded.clear()
loaded.putAll(outputEntries)
return loaded
}
private fun countSafScanRows(path: String): Int {
val output = File(path)
if (!output.exists()) return 0
return try {
output.useLines(Charsets.UTF_8) { lines -> lines.count { it.isNotBlank() } }
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed counting existing rows: ${e.message}")
0
}
}
private fun repairSafScanOutput(path: String) {
val output = File(path)
if (!output.exists()) return
try {
RandomAccessFile(output, "rw").use { file ->
var position = file.length() - 1
while (position >= 0) {
file.seek(position)
if (file.readByte().toInt() == '\n'.code) {
file.setLength(position + 1)
return
}
position--
}
// No complete line survived the interruption.
file.setLength(0)
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed repairing output: ${e.message}")
}
}
/**
* Extract the audio filename referenced by a CUE sheet file.
* Reads the FILE "name" TYPE line from the .cue text.
@@ -0,0 +1,94 @@
package com.zarz.spotiflac
import org.json.JSONObject
import java.io.File
import java.io.RandomAccessFile
// Resumable SAF scan state: the `<output>.state` checkpoint beside the NDJSON
// output, its reconciliation with the rows already written, and output repair
// after an interrupted scan.
internal fun safScanCheckpointPath(outputPath: String): String = "$outputPath.state"
internal fun loadSafScanCheckpoint(path: String): MutableMap<String, Long> {
val checkpoint = File(path)
if (!checkpoint.exists()) return mutableMapOf()
val entries = mutableMapOf<String, Long>()
try {
checkpoint.forEachLine(Charsets.UTF_8) { line ->
val separator = line.indexOf('\t')
if (separator <= 0) return@forEachLine
val uri = line.substring(0, separator)
val modified = line.substring(separator + 1).toLongOrNull() ?: return@forEachLine
entries[uri] = modified
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed reading checkpoint: ${e.message}")
}
return entries
}
internal fun reconcileSafScanCheckpoint(
outputPath: String,
loaded: MutableMap<String, Long>,
): MutableMap<String, Long> {
val output = File(outputPath)
if (!output.exists()) return loaded
val outputEntries = mutableMapOf<String, Long>()
try {
output.forEachLine(Charsets.UTF_8) { line ->
if (line.isBlank()) return@forEachLine
try {
val obj = JSONObject(line)
val filePath = obj.optString("filePath", "").trim()
if (filePath.isNotBlank()) {
val modified = obj.optLong("fileModTime", 0L)
val cueMarker = filePath.indexOf("#track")
val key = if (cueMarker > 0) filePath.substring(0, cueMarker) else filePath
outputEntries[key] = modified
}
} catch (_: Exception) {}
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed reconciling checkpoint: ${e.message}")
return loaded
}
loaded.clear()
loaded.putAll(outputEntries)
return loaded
}
internal fun countSafScanRows(path: String): Int {
val output = File(path)
if (!output.exists()) return 0
return try {
output.useLines(Charsets.UTF_8) { lines -> lines.count { it.isNotBlank() } }
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed counting existing rows: ${e.message}")
0
}
}
internal fun repairSafScanOutput(path: String) {
val output = File(path)
if (!output.exists()) return
try {
RandomAccessFile(output, "rw").use { file ->
var position = file.length() - 1
while (position >= 0) {
file.seek(position)
if (file.readByte().toInt() == '\n'.code) {
file.setLength(position + 1)
return
}
position--
}
// No complete line survived the interruption.
file.setLength(0)
}
} catch (e: Exception) {
android.util.Log.w("SpotiFLAC", "SAF scan: failed repairing output: ${e.message}")
}
}