From fb3fad279c83adb8ea8677eaae2f6d82d6e52d77 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 10 Jul 2026 07:03:16 +0700 Subject: [PATCH] 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) --- .../zarz/spotiflac/NativeDownloadFinalizer.kt | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt index 00c53eb4..f3e3a32b 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -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")