fix(native-worker): never delete a published download over bookkeeping failures

The finalizer's catch-all cleanup deleted the finished output for any
exception, including ones thrown after the file was already published
to SAF. A failed history insert (SQLITE_BUSY against the concurrently
open Flutter connection, disk full, or a schema-version mismatch that
would throw on every download), an external .lrc write error, or a
quality-metadata refresh failure destroyed a complete, valid track and
forced a full re-download.

- Track outputPublished and skip cleanupFailedFinalizationOutput once
  the file reached its final destination
- Make the history insert, external LRC write, and quality metadata
  refresh non-fatal (log + history_written:false instead of failing
  the item)
This commit is contained in:
zarzet
2026-07-10 07:03:16 +07:00
parent 7f7e408623
commit fb3fad279c
@@ -185,6 +185,10 @@ object NativeDownloadFinalizer {
),
)
// Once the output has been published to its final destination the audio
// is complete; failures past that point are bookkeeping and must never
// trigger the destructive cleanup below.
var outputPublished = result.optBoolean("already_exists", false)
try {
var qualityMetadataRefreshed = false
if (!result.optBoolean("already_exists", false)) {
@@ -198,7 +202,11 @@ object NativeDownloadFinalizer {
checkCancelled(shouldCancel)
finalizeMetadata(context, effectiveInput, state)
checkCancelled(shouldCancel)
writeExternalLrc(context, effectiveInput, state)
try {
writeExternalLrc(context, effectiveInput, state)
} catch (e: Exception) {
android.util.Log.w(TAG, "External LRC write failed (non-fatal): ${e.message}")
}
checkCancelled(shouldCancel)
runPostProcessing(context, effectiveInput, state, shouldCancel)
checkCancelled(shouldCancel)
@@ -206,22 +214,37 @@ object NativeDownloadFinalizer {
if (replayGain != null) result.put("replaygain", replayGain)
checkCancelled(shouldCancel)
if (isDeferredSafPublish(effectiveInput)) {
refreshFinalAudioQualityMetadata(context, result, state)
try {
refreshFinalAudioQualityMetadata(context, result, state)
} catch (e: Exception) {
android.util.Log.w(TAG, "Quality metadata refresh failed (non-fatal): ${e.message}")
}
qualityMetadataRefreshed = true
publishDeferredSafOutput(context, effectiveInput, state)
} else {
promoteStagedSafOutputIfNeeded(context, effectiveInput, state)
}
outputPublished = true
}
checkCancelled(shouldCancel)
if (!qualityMetadataRefreshed) {
refreshFinalAudioQualityMetadata(context, result, state)
try {
refreshFinalAudioQualityMetadata(context, result, state)
} catch (e: Exception) {
android.util.Log.w(TAG, "Quality metadata refresh failed (non-fatal): ${e.message}")
}
}
val saveDownloadHistory = parseObject(settingsJson)
.optBoolean("save_download_history", true)
val history = if (saveDownloadHistory) {
buildHistoryRow(effectiveInput, state).also { upsertHistory(context, it) }
try {
buildHistoryRow(effectiveInput, state).also { upsertHistory(context, it) }
} catch (e: Exception) {
// History is bookkeeping; never fail (and never delete) a
// finished download because the insert failed.
android.util.Log.w(TAG, "History write failed (non-fatal): ${e.message}")
null
}
} else {
null
}
@@ -233,13 +256,17 @@ object NativeDownloadFinalizer {
result.put("history_written", history != null)
if (history != null) result.put("history_item", historyToJson(history))
} catch (e: CancellationException) {
cleanupFailedFinalizationOutput(context, result, initialPath, state.filePath)
if (!outputPublished) {
cleanupFailedFinalizationOutput(context, result, initialPath, state.filePath)
}
result.put("success", false)
result.put("error", "Native finalization cancelled")
result.put("error_type", "cancelled")
result.put("native_finalized", false)
} catch (e: Exception) {
cleanupFailedFinalizationOutput(context, result, initialPath, state.filePath)
if (!outputPublished) {
cleanupFailedFinalizationOutput(context, result, initialPath, state.filePath)
}
result.put("success", false)
result.put("error", "Native finalization failed: ${e.message}")
result.put("error_type", "unknown")