fix(metadata): stop the endless FLAC-write failures on misnamed files

When a provider delivers a lossy/unknown stream, the native finalizer
preserved the container but kept the requested .flac name, so every
metadata/ReplayGain write treated the file as FLAC and failed with
"failed to write FLAC metadata: failed to parse FLAC file: fLaC head
incorrect" — on the first download, on every retry (the file is never
deleted from public storage), on every manual edit attempt, and the
history backfill re-probed the same file on every app launch forever.

- Finalizer now renames a preserved lossy/unknown container away from
  its .flac name to match the real content (aac/MP4 -> .m4a, mp3, opus),
  mirroring the Dart pipeline, so the correct tag writer is picked
- EditFileMetadata sniffs MP4 content before taking the .flac branch
  and reports what is actually wrong (rename to .m4a) instead of the
  cryptic parse error
- The history audio-metadata backfill remembers paths whose probe
  failed permanently (unparseable content) and stops reselecting them
  on every launch; transient failures (missing file, unmounted volume)
  still retry
This commit is contained in:
zarzet
2026-07-10 12:07:11 +07:00
parent df1ac30e07
commit 364b2aa138
3 changed files with 125 additions and 4 deletions
@@ -584,6 +584,12 @@ object NativeDownloadFinalizer {
val isAlreadyNativeFlac = codec == "flac" && isNativeFlacFile(localInput)
if (!isLosslessAudioCodec(codec)) {
Log.d(TAG, "Preserving native container; audio codec is ${codec.ifBlank { "unknown" }}")
// The preserved stream is not FLAC but still carries the
// requested .flac name. Rename to the real container so the
// metadata/ReplayGain writers pick the right format — an
// MP4 stream under a .flac name fails "fLaC head incorrect"
// on every subsequent write and the file is never repaired.
adoptPreservedContainerExtension(state, localInput, codec)
return
}
if (isAlreadyNativeFlac) {
@@ -614,6 +620,63 @@ object NativeDownloadFinalizer {
}
}
/// Renames a preserved lossy/unknown stream away from its requested .flac
/// name to match its actual container (mirrors the Dart pipeline's
/// post-download rename). Local files only: legacy content:// outputs are
/// left untouched. No-op when the container cannot be identified.
private fun adoptPreservedContainerExtension(
state: FinalizeState,
localInput: String,
codec: String,
) {
if (state.filePath.startsWith("content://")) return
val currentFile = File(state.filePath)
if (!currentFile.exists()) return
if (!currentFile.name.lowercase(Locale.ROOT).endsWith(".flac")) return
val newExt = when {
codec == "aac" && isMP4ContainerFile(localInput) -> ".m4a"
codec == "mp3" -> ".mp3"
codec == "opus" -> ".opus"
isMP4ContainerFile(localInput) -> ".m4a"
else -> return
}
val renamed = File(
currentFile.parentFile,
currentFile.name.dropLast(".flac".length) + newExt,
)
if (renamed.exists() && !renamed.delete()) {
Log.w(TAG, "Cannot adopt container extension; ${renamed.name} already exists")
return
}
if (!currentFile.renameTo(renamed)) {
Log.w(TAG, "Failed to rename preserved container to ${renamed.name}")
return
}
Log.i(TAG, "Preserved container renamed: ${currentFile.name} -> ${renamed.name}")
state.filePath = renamed.absolutePath
if (state.fileName.isNotBlank()) {
state.fileName = renamed.name
}
state.audioCodec = normalizeAudioCodec(codec)
}
private fun isMP4ContainerFile(path: String): Boolean {
return try {
File(path).inputStream().use { stream ->
val header = ByteArray(12)
val read = stream.read(header)
read >= 8 &&
header[4] == 'f'.code.toByte() &&
header[5] == 't'.code.toByte() &&
header[6] == 'y'.code.toByte() &&
header[7] == 'p'.code.toByte()
}
} catch (_: Exception) {
false
}
}
private fun finalizeMetadata(context: Context, input: FinalizeInput, state: FinalizeState) {
if (!input.request.optBoolean("embed_metadata", false)) return
if (!state.filePath.startsWith("content://")) {