feat: batch re-enrich for local tracks, SAF FD refactor, Ogg quality fix

- Replace batch Share action with batch Re-enrich in local album selection bar
  - Full native/FFmpeg re-enrich flow with SAF write-back support
  - Triggers incremental local library scan after completion to refresh metadata
- Queue tab: switch first selection action to Re-enrich when all selected items are local-only
- Refactor SAF FD handoff in MainActivity: drop detachFd/dup pattern, pass procfs
  path to Go and let Go re-open it to avoid fdsan double-close race conditions
- Handle /proc/self/fd/ path in output_fd.go: re-open via O_WRONLY|O_TRUNC instead
  of taking raw FD ownership
- Fix Ogg duration/bitrate calculation in audio_metadata.go:
  - Use float64 arithmetic and math.Round for accurate duration
  - Compute bitrate from file size / float duration at the source
  - Validate Ogg page header fields (version, headerType, segment table) to avoid
    false positives from payload bytes during backward scan
  - Guard against corrupted granule values (>24h duration, <8kbps bitrate)
- Rename trackReEnrich label from 'Re-enrich Metadata' to 'Re-enrich' across all
  13 locales and ARB files
- Update CHANGELOG.md with 3.7.0 entry
This commit is contained in:
zarzet
2026-02-18 19:29:59 +07:00
parent 5605930aef
commit 4df96db809
36 changed files with 843 additions and 192 deletions
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode
@@ -666,22 +665,12 @@ class MainActivity: FlutterFragmentActivity() {
val pfd = contentResolver.openFileDescriptor(document.uri, "rw")
?: return errorJson("Failed to open SAF file")
var fdHandedOffToGo = false
try {
// Keep the original PFD open so the document provider receives close signaling.
// Pass a duplicated FD to Go and detach only the duplicate.
val writerPfd = ParcelFileDescriptor.dup(pfd.fileDescriptor)
val detachedFd = writerPfd.detachFd()
try {
writerPfd.close()
} catch (_: Exception) {}
// After detach, ownership is intended for Go. Kotlin must never close this FD,
// otherwise Android fdsan may abort on double-close during cancellation races.
fdHandedOffToGo = true
req.put("output_path", "/proc/self/fd/$detachedFd")
req.put("output_fd", detachedFd)
// Keep SAF PFD ownership in Kotlin and pass only procfs path to Go.
// Go re-opens this procfs FD path for writing to avoid raw FD ownership handoff.
req.put("output_path", "/proc/self/fd/${pfd.fd}")
req.put("output_fd", 0)
req.put("output_ext", outputExt)
val response = downloader(req.toString())
val respObj = JSONObject(response)
@@ -696,9 +685,6 @@ class MainActivity: FlutterFragmentActivity() {
document.delete()
return errorJson("SAF download failed: ${e.message}")
} finally {
if (!fdHandedOffToGo) {
android.util.Log.w("SpotiFLAC", "SAF writer FD was not handed off to Go")
}
try {
pfd.close()
} catch (_: Exception) {}