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 510fb97c..cfa4d209 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivitySafScan.kt @@ -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 { - val checkpoint = File(path) - if (!checkpoint.exists()) return mutableMapOf() - val entries = mutableMapOf() - 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, -): MutableMap { - val output = File(outputPath) - if (!output.exists()) return loaded - - val outputEntries = mutableMapOf() - 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. diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/SafScanCheckpoint.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/SafScanCheckpoint.kt new file mode 100644 index 00000000..7aeea5b9 --- /dev/null +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/SafScanCheckpoint.kt @@ -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 `.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 { + val checkpoint = File(path) + if (!checkpoint.exists()) return mutableMapOf() + val entries = mutableMapOf() + 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, +): MutableMap { + val output = File(outputPath) + if (!output.exists()) return loaded + + val outputEntries = mutableMapOf() + 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}") + } +}