mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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:
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}")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user