diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index b1ad1d4e..b0b47717 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2677,7 +2677,7 @@ class MainActivity: FlutterFragmentActivity() { } } catch (e: Exception) { android.util.Log.e("SpotiFLAC", "readFileMetadata failed: ${e.message}", e) - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -2718,7 +2718,7 @@ class MainActivity: FlutterFragmentActivity() { } } catch (e: Exception) { android.util.Log.e("SpotiFLAC", "editFileMetadata failed: ${e.message}", e) - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -2731,7 +2731,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.writeM4AFreeformTags(filePath, metadataJson) } catch (e: Exception) { android.util.Log.e("SpotiFLAC", "writeM4AFreeformTags failed: ${e.message}", e) - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -2744,7 +2744,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.ensureAC4Config(filePath, sourcePath) } catch (e: Exception) { android.util.Log.e("SpotiFLAC", "ensureAC4Config failed: ${e.message}", e) - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -2758,7 +2758,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.writeAC4Metadata(filePath, metadataJson, coverPath) } catch (e: Exception) { android.util.Log.e("SpotiFLAC", "writeAC4Metadata failed: ${e.message}", e) - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -2966,7 +2966,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.reEnrichFile(requestJson) } } catch (e: Exception) { - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -3685,7 +3685,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.readAudioMetadataJSON(filePath) } } catch (e: Exception) { - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) @@ -3759,7 +3759,7 @@ class MainActivity: FlutterFragmentActivity() { Gobackend.parseCueSheet(cuePath, audioDir) } } catch (e: Exception) { - """{"error":"${e.message?.replace("\"", "'")}"}""" + """{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}""" } } result.success(response) diff --git a/go_backend/ac4_config.go b/go_backend/ac4_config.go index cd52f8a3..0d044fcf 100644 --- a/go_backend/ac4_config.go +++ b/go_backend/ac4_config.go @@ -3,6 +3,7 @@ package gobackend import ( "encoding/binary" "fmt" + "io" "os" ) @@ -295,6 +296,23 @@ func EnsureAC4ConfigBox(decryptedPath, sourcePath string) error { f.Close() return err } + // A non-MP4 decrypt output (e.g. a raw FLAC stream) is not an AC-4 file; + // bail out before the box parser reports it as a corrupt MP4. A real + // ISO-BMFF box type is four printable ASCII bytes. + var head [8]byte + if _, err := f.ReadAt(head[:], 0); err != nil { + f.Close() + if err == io.EOF || err == io.ErrUnexpectedEOF { + return nil + } + return err + } + for _, c := range head[4:8] { + if c < 0x20 || c > 0x7e { + f.Close() + return nil + } + } moovBuf, moovOffset, moovFound, err := loadTopLevelMP4Box(f, info.Size(), "moov") if err != nil || !moovFound { f.Close() diff --git a/lib/providers/download_queue_provider_finalization.dart b/lib/providers/download_queue_provider_finalization.dart index 096c6aaa..db0a7d20 100644 --- a/lib/providers/download_queue_provider_finalization.dart +++ b/lib/providers/download_queue_provider_finalization.dart @@ -11,6 +11,13 @@ class _DecryptOutcome { const _DecryptOutcome(this.path, {this.newFileName, this.failStage}); } +/// AC-4 repair only applies to MP4 containers; decrypt can also emit raw +/// FLAC, which the native MP4 box parser would reject as corrupt. +bool _isMp4Container(String path) { + final lower = path.toLowerCase(); + return lower.endsWith('.m4a') || lower.endsWith('.mp4'); +} + extension _DownloadQueueFinalization on DownloadQueueNotifier { /// Builds the [DownloadHistoryItem] shared by the native-worker and inline /// completion paths. Fields whose source/derivation legitimately differs @@ -297,7 +304,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier { return null; } addCleanup(decryptedTempPath); - if (repairAc4) { + if (repairAc4 && _isMp4Container(decryptedTempPath)) { try { await PlatformBridge.ensureAC4Config(decryptedTempPath, tempPath); } catch (e) { @@ -345,10 +352,12 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier { failStage: DownloadQueueNotifier._decryptStageDecrypt, ); } - try { - await PlatformBridge.ensureAC4Config(decryptedPath, filePath); - } catch (e) { - _log.w('AC-4 container repair skipped: $e'); + if (_isMp4Container(decryptedPath)) { + try { + await PlatformBridge.ensureAC4Config(decryptedPath, filePath); + } catch (e) { + _log.w('AC-4 container repair skipped: $e'); + } } try { await deleteFile(filePath);