fix: restore SAF metadata fallback and preserve active transfers

Propagate filesystem errors from complete metadata reads so Android retries unreadable SAF descriptors through temporary files while accepting valid audio without tags.

Keep established segmented and chunked transfers outside the resolution allowance. Continue charging initial requests, retries, and progress callbacks, including during parallel native reads, while preserving stall cancellation.

Add metadata, transfer, and native fallback regression tests. Validated with Go tests, race detector, vet, Android tests, targeted Flutter tests, analyzer, and formatting checks.
This commit is contained in:
zarzet
2026-09-06 16:43:24 +07:00
parent 717e8967cd
commit 781661798b
9 changed files with 327 additions and 30 deletions
@@ -6,6 +6,14 @@ import org.junit.Assert.assertNull
import org.junit.Test
class SafMetadataReadPolicyTest {
@Test fun completeMetadataReadErrorUsesTemporaryCopy() {
val metadata = mapOf("lyrics" to "words", "comment" to "notes", "track_number" to 3)
assertEquals(metadata, readSafMetadataWithFallback(
directRead = { throw Exception("failed to read metadata: permission denied") },
fallbackRead = { metadata },
))
}
@Test fun fallbackFailureIsIsolatedToOneFile() {
assertNull(readSafMetadataWithFallback<String>({ null }, { throw IllegalArgumentException("malformed metadata") }))
assertEquals("next file", readSafMetadataWithFallback({ "next file" }, { error("copy") }))
+4
View File
@@ -177,6 +177,10 @@ requests, URL resolution, retry waits, and refreshes, including HTTP headers
and the first audio byte. Native audio reads after the first byte and bounded
native FFmpeg conversion pause this clock; transfer stall limits and the
overall download timeout still apply. Progress callbacks do not reset it.
Within one native segmented or chunked download, a successfully received part
establishes the transfer: headers and the first byte of subsequent parts also
pause the clock. Retry attempts and waits still consume the allowance, and a
new native download call must establish its own transfer.
`options.resolutionTimeoutMs` reports the initial allowance, while
`utils.getResolutionRemainingMs()` reports the remaining allowance. Check for
+15 -2
View File
@@ -2,6 +2,7 @@ package gobackend
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
@@ -84,6 +85,7 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
"audio_codec": "",
}
var metadataErr error
if isFlac {
result["format"] = "flac"
result["audio_codec"] = "flac"
@@ -155,6 +157,7 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
} else if isM4A {
result["format"] = "m4a"
meta, err := ReadM4ATags(filePath)
metadataErr = err
if err == nil && meta != nil {
applyAudioMetadataToResult(result, meta)
}
@@ -179,6 +182,7 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
result["format"] = "mp3"
result["audio_codec"] = "mp3"
meta, err := ReadID3Tags(filePath)
metadataErr = err
if err == nil && meta != nil {
applyAudioMetadataToResult(result, meta)
}
@@ -195,6 +199,7 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
result["format"] = "opus"
result["audio_codec"] = "opus"
meta, err := ReadOggVorbisComments(filePath)
metadataErr = err
if err == nil && meta != nil {
applyAudioMetadataToResult(result, meta)
}
@@ -210,6 +215,7 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
result["format"] = strings.TrimPrefix(lower, ".")
result["audio_codec"] = result["format"]
apeTag, apeErr := ReadAPETags(filePath)
metadataErr = apeErr
if apeErr == nil && apeTag != nil {
meta := APETagToAudioMetadata(apeTag)
if meta != nil {
@@ -223,12 +229,12 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
if isAiff {
result["format"] = "aiff"
result["audio_codec"] = "pcm"
meta, _ = ReadAIFFTags(filePath)
meta, metadataErr = ReadAIFFTags(filePath)
quality, qualityErr = GetAIFFQuality(filePath)
} else {
result["format"] = "wav"
result["audio_codec"] = "pcm"
meta, _ = ReadWAVTags(filePath)
meta, metadataErr = ReadWAVTags(filePath)
quality, qualityErr = GetWAVQuality(filePath)
}
if meta != nil {
@@ -243,6 +249,13 @@ func ReadFileMetadataWithHint(filePath, displayNameHint string) (string, error)
return "", fmt.Errorf("unsupported file format: %s", filePath)
}
// A readable audio file can legitimately have no tags. Filesystem errors,
// however, must reach the native bridge so an unreadable SAF descriptor
// triggers its temporary-file fallback instead of returning empty tags.
var pathErr *os.PathError
if errors.As(metadataErr, &pathErr) {
return "", fmt.Errorf("failed to read metadata: %w", metadataErr)
}
return marshalJSONString(result)
}
+64 -12
View File
@@ -24,6 +24,7 @@ type resolutionBudget struct {
timer *time.Timer
generation uint64
pauses int
charges int
stopped bool
}
@@ -43,7 +44,7 @@ func (b *resolutionBudget) armLocked() {
b.timer = time.AfterFunc(b.remaining, func() {
b.mu.Lock()
defer b.mu.Unlock()
if b.stopped || b.pauses > 0 || b.generation != generation {
if b.stopped || (b.pauses > 0 && b.charges == 0) || b.generation != generation {
return
}
b.remaining = 0
@@ -57,7 +58,7 @@ func (b *resolutionBudget) pause() func() {
b.mu.Unlock()
return func() {}
}
if b.pauses == 0 {
if b.pauses == 0 && b.charges == 0 {
b.timer.Stop()
b.generation++
b.remaining -= time.Since(b.started)
@@ -76,18 +77,50 @@ func (b *resolutionBudget) pause() func() {
b.mu.Lock()
defer b.mu.Unlock()
b.pauses--
if b.pauses == 0 && !b.stopped && b.ctx.Err() == nil {
if b.pauses == 0 && b.charges == 0 && !b.stopped && b.ctx.Err() == nil {
b.armLocked()
}
})
}
}
// Resolver work must keep spending the allowance even while parallel segment
// workers are paused in native network reads.
func (b *resolutionBudget) charge() func() {
b.mu.Lock()
if b.stopped || b.ctx.Err() != nil {
b.mu.Unlock()
return func() {}
}
if b.charges == 0 && b.pauses > 0 {
b.armLocked()
}
b.charges++
b.mu.Unlock()
var once sync.Once
return func() {
once.Do(func() {
b.mu.Lock()
defer b.mu.Unlock()
b.charges--
if b.charges == 0 && b.pauses > 0 && !b.stopped && b.ctx.Err() == nil {
b.timer.Stop()
b.generation++
b.remaining -= time.Since(b.started)
if b.remaining <= 0 {
b.remaining = 0
b.cancel(context.DeadlineExceeded)
}
}
})
}
}
func (b *resolutionBudget) remainingTime() time.Duration {
b.mu.Lock()
defer b.mu.Unlock()
remaining := b.remaining
if b.pauses == 0 && !b.stopped {
if (b.pauses == 0 || b.charges > 0) && !b.stopped {
remaining -= time.Since(b.started)
}
if remaining < 0 || b.ctx.Err() != nil {
@@ -133,15 +166,25 @@ func (r *extensionRuntime) getResolutionRemainingMs(goja.FunctionCall) goja.Valu
return r.vm.ToValue(extensionResolutionTimeout.Milliseconds())
}
// Wrapping only native audio transfers keeps API/ticket bodies on the resolver
// clock. Headers and the first byte still consume the resolution allowance.
func (r *extensionRuntime) trackResolutionTransfer(resp *http.Response) {
if resp == nil || resp.Body == nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent) {
return
}
if b := r.currentResolutionBudget(); b != nil {
resp.Body = &resolutionTransferBody{ReadCloser: resp.Body, budget: b}
// A successful segment/range establishes the current native transfer. Later
// parts need no new resolution, so their headers and first byte are transfer
// time too. Initial requests and retries still spend the resolver allowance.
func (r *extensionRuntime) doResolutionTransfer(client *http.Client, req *http.Request, continuation bool) (*http.Response, error) {
b := r.currentResolutionBudget()
resp, err := func() (*http.Response, error) {
if b != nil {
if continuation {
defer b.pause()()
} else {
defer b.charge()()
}
}
return client.Do(req)
}()
if b != nil && resp != nil && resp.Body != nil && (resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusPartialContent) {
resp.Body = &resolutionTransferBody{ReadCloser: resp.Body, budget: b, receivedBytes: continuation}
}
return resp, err
}
type resolutionTransferBody struct {
@@ -155,6 +198,8 @@ func (b *resolutionTransferBody) Read(p []byte) (int, error) {
// more resolvers) and retry waits continue spending the same allowance.
if b.receivedBytes {
defer b.budget.pause()()
} else {
defer b.budget.charge()()
}
n, err := b.ReadCloser.Read(p)
if n > 0 {
@@ -162,3 +207,10 @@ func (b *resolutionTransferBody) Read(p []byte) (int, error) {
}
return n, err
}
func (r *extensionRuntime) waitResolutionRetry(ctx context.Context, delay time.Duration) error {
if b := r.currentResolutionBudget(); b != nil {
defer b.charge()()
}
return waitTransferRetry(ctx, delay)
}
@@ -3,6 +3,7 @@ package gobackend
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
@@ -82,6 +83,168 @@ func (b *resolutionSlowBody) Read(p []byte) (int, error) {
}
func (*resolutionSlowBody) Close() error { return nil }
type resolutionDelayedBody struct {
io.ReadCloser
ctx context.Context
delay time.Duration
}
func (b *resolutionDelayedBody) Read(p []byte) (int, error) {
if err := sleepRetry(b.ctx, b.delay); err != nil {
return 0, err
}
b.delay = 0
return b.ReadCloser.Read(p)
}
func TestResolutionBudgetAllowsResolvedSegments(t *testing.T) {
for _, maxParallel := range []int{1, 3} {
for _, phase := range []string{"headers", "first-byte"} {
t.Run(fmt.Sprintf("%s/parallel-%d", phase, maxParallel), func(t *testing.T) {
r := newFileDownloadTestRuntime(t, func(req *http.Request) (*http.Response, error) {
body := &resolutionDelayedBody{ReadCloser: io.NopCloser(strings.NewReader("audio")), ctx: req.Context()}
if phase == "headers" {
if err := sleepRetry(req.Context(), 90*time.Millisecond); err != nil {
return nil, err
}
} else {
body.delay = 90 * time.Millisecond
}
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: body, ContentLength: 5, Request: req}, nil
})
r.vm.Set("segments", r.fileDownloadSegments)
script := fmt.Sprintf(`
var urls = [];
for (var i = 0; i < 8; i++) urls.push("https://cdn.example.com/segment/" + i);
segments(urls, "audio.flac", {maxParallel: %d, maxAttempts: 1});
`, maxParallel)
started := time.Now()
value, err := runResolutionScript(t, r, context.Background(), 150*time.Millisecond, script)
if err != nil {
t.Fatal(err)
}
if result := value.Export().(map[string]any); result["success"] != true {
t.Fatalf("resolved segments failed: %#v", result)
}
if time.Since(started) < 250*time.Millisecond {
t.Fatal("transfer did not exceed resolution allowance")
}
data, err := os.ReadFile(filepath.Join(r.dataDir, "audio.flac"))
if err != nil || string(data) != strings.Repeat("audio", 8) {
t.Fatalf("output: %q, %v", data, err)
}
})
}
}
}
func TestResolutionBudgetAllowsResolvedChunks(t *testing.T) {
for _, phase := range []string{"headers", "first-byte"} {
t.Run(phase, func(t *testing.T) {
r := newFileDownloadTestRuntime(t, func(req *http.Request) (*http.Response, error) {
var start, end int
if _, err := fmt.Sscanf(req.Header.Get("Range"), "bytes=%d-%d", &start, &end); err != nil {
return nil, err
}
body := &resolutionDelayedBody{ReadCloser: io.NopCloser(strings.NewReader(strings.Repeat("a", end-start+1))), ctx: req.Context()}
if end-start > 1 { // The two-byte capability probe is immediate.
if phase == "headers" {
if err := sleepRetry(req.Context(), 90*time.Millisecond); err != nil {
return nil, err
}
} else {
body.delay = 90 * time.Millisecond
}
}
header := make(http.Header)
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/40", start, end))
return &http.Response{StatusCode: 206, Header: header, Body: body, ContentLength: int64(end - start + 1), Request: req}, nil
})
r.vm.Set("download", r.fileDownload)
value, err := runResolutionScript(t, r, context.Background(), 150*time.Millisecond, `download("https://cdn.example.com/audio", "audio.flac", {chunked: 5, maxAttempts: 1})`)
if err != nil {
t.Fatal(err)
}
if result := value.Export().(map[string]any); result["success"] != true {
t.Fatalf("resolved chunks failed: %#v", result)
}
data, err := os.ReadFile(filepath.Join(r.dataDir, "audio.flac"))
if err != nil || string(data) != strings.Repeat("a", 40) {
t.Fatalf("output: %q, %v", data, err)
}
})
}
}
func TestResolutionBudgetChargesSegmentCallbacksAndRetryWaits(t *testing.T) {
for _, mode := range []string{"callback", "retry-wait", "new-transfer"} {
t.Run(mode, func(t *testing.T) {
waiting := make(chan struct{})
r := newFileDownloadTestRuntime(t, func(req *http.Request) (*http.Response, error) {
if strings.HasSuffix(req.URL.Path, "/blocked") {
close(waiting)
<-req.Context().Done()
return nil, req.Context().Err()
}
header := make(http.Header)
status := http.StatusOK
if strings.HasSuffix(req.URL.Path, "/retry") {
header.Set("Retry-After", "1")
status = http.StatusServiceUnavailable
}
return &http.Response{StatusCode: status, Header: header, Body: io.NopCloser(strings.NewReader("audio")), ContentLength: 5, Request: req}, nil
})
r.vm.Set("segments", r.fileDownloadSegments)
r.vm.Set("sleep", r.sleep)
r.vm.Set("waitForNextRequest", func() {
select {
case <-waiting:
case <-time.After(time.Second):
t.Error("next segment request did not start")
}
})
script := map[string]string{
"callback": `segments(["https://cdn.example.com/first", "https://cdn.example.com/blocked"], "audio.flac", {
maxParallel: 1, maxAttempts: 1, onProgress: function() { waitForNextRequest(); sleep(1000); }
})`,
"retry-wait": `segments(["https://cdn.example.com/first", "https://cdn.example.com/second", "https://cdn.example.com/blocked", "https://cdn.example.com/retry"], "audio.flac", {maxParallel: 2, maxAttempts: 2})`,
"new-transfer": `segments(["https://cdn.example.com/first"], "first.flac", {maxAttempts: 1});
segments(["https://cdn.example.com/blocked"], "audio.flac", {maxAttempts: 1});`,
}[mode]
started := time.Now()
_, err := runResolutionScript(t, r, context.Background(), 150*time.Millisecond, script)
if !IsTimeoutError(err) || time.Since(started) > 500*time.Millisecond {
t.Fatalf("resolver work escaped allowance: elapsed=%s err=%v", time.Since(started), err)
}
})
}
}
func TestResolutionBudgetContinuationPreservesStallCancellation(t *testing.T) {
r := newFileDownloadTestRuntime(t, func(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: &resolutionDelayedBody{
ReadCloser: io.NopCloser(strings.NewReader("audio")), ctx: req.Context(), delay: time.Second,
}, Request: req}, nil
})
ctx, finish := r.beginResolutionBudget(context.Background(), 30*time.Millisecond)
defer finish()
req, err := http.NewRequestWithContext(ctx, "GET", "https://cdn.example.com/audio", nil)
if err != nil {
t.Fatal(err)
}
req, watchdog := bindStallWatchdog(req, 90*time.Millisecond)
defer watchdog.stop()
resp, err := r.doResolutionTransfer(r.httpClient, req, true)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
if !errors.Is(err, context.Canceled) || !watchdog.stalled.Load() || ctx.Err() != nil {
t.Fatalf("stall cancellation lost: read=%v stalled=%v resolution=%v", err, watchdog.stalled.Load(), ctx.Err())
}
}
func TestResolutionBudgetAllowsActiveNativeTransfers(t *testing.T) {
for _, kind := range []string{"plain", "chunked", "segments"} {
t.Run(kind, func(t *testing.T) {
+3 -2
View File
@@ -230,6 +230,7 @@ func (r *extensionRuntime) fileDownloadChunked(
lastCheckpointAt := time.Now()
attemptsUsed := probeAttempts
fullResponse := false
completedChunk := false
buffer := make([]byte, 64*1024)
for totalSize <= 0 || totalWritten < totalSize {
chunkStart := totalWritten
@@ -264,8 +265,7 @@ func (r *extensionRuntime) fileDownloadChunked(
request.Header.Set("If-Range", validator)
}
request, watchdog := bindStallWatchdog(request, downloadStallTimeout)
response, responseErr := client.Do(request)
r.trackResolutionTransfer(response)
response, responseErr := r.doResolutionTransfer(client, request, attempt == 1 && completedChunk)
if responseErr != nil {
stalled := watchdog.stalled.Load()
watchdog.stop()
@@ -446,6 +446,7 @@ func (r *extensionRuntime) fileDownloadChunked(
}
if readErr == nil && chunkWritten > 0 {
chunkComplete = true
completedChunk = true
fullResponse = response.StatusCode == http.StatusOK
break
}
+20 -12
View File
@@ -169,6 +169,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
tempPath string,
policy DownloadTransferPolicy,
received *atomic.Int64,
completed *atomic.Bool,
itemProgressReporter *ItemTransferProgressReporter,
) segmentTransferResult {
config := transferRetryConfig(policy)
@@ -218,8 +219,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
req.Header.Set("User-Agent", appUserAgent())
}
req, watchdog := bindStallWatchdog(req, downloadStallTimeout)
resp, err := client.Do(req)
r.trackResolutionTransfer(resp)
resp, err := r.doResolutionTransfer(client, req, attempt == 1 && completed.Load())
if err != nil {
stalled := watchdog.stalled.Load()
watchdog.stop()
@@ -244,7 +244,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
}
return segmentTransferResult{Index: spec.Index, Failure: &lastFailure}
}
if waitTransferRetry(ctx, retryDelay) != nil {
if r.waitResolutionRetry(ctx, retryDelay) != nil {
lastFailure.ErrorType = "cancelled"
lastFailure.Message = "download cancelled"
return segmentTransferResult{Index: spec.Index, Failure: &lastFailure}
@@ -273,7 +273,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
if retryAfter > 0 {
delay = time.Duration(retryAfter) * time.Second
}
if waitTransferRetry(ctx, delay) != nil {
if r.waitResolutionRetry(ctx, delay) != nil {
lastFailure.ErrorType = "cancelled"
lastFailure.Message = "download cancelled"
return segmentTransferResult{Index: spec.Index, Failure: &lastFailure}
@@ -319,6 +319,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
readErr = io.ErrUnexpectedEOF
}
if readErr == nil && size > 0 {
completed.Store(true)
return segmentTransferResult{
Index: spec.Index,
Path: tempPath,
@@ -349,7 +350,7 @@ func (r *extensionRuntime) fetchSegmentToTemp(
if attempt == policy.MaxAttempts {
return segmentTransferResult{Index: spec.Index, Failure: &lastFailure}
}
if waitTransferRetry(ctx, retryDelay) != nil {
if r.waitResolutionRetry(ctx, retryDelay) != nil {
lastFailure.ErrorType = "cancelled"
lastFailure.Message = "download cancelled"
return segmentTransferResult{Index: spec.Index, Failure: &lastFailure}
@@ -562,6 +563,7 @@ func (r *extensionRuntime) fileDownloadSegments(call goja.FunctionCall) goja.Val
jobs := make(chan segmentTransferSpec)
results := make(chan segmentTransferResult, policy.MaxParallelSegments)
var received atomic.Int64
var completed atomic.Bool
received.Store(totalWritten)
itemProgressReporter := NewItemTransferProgressReporter(activeItemID, totalWritten, 0)
var workers sync.WaitGroup
@@ -578,6 +580,7 @@ func (r *extensionRuntime) fileDownloadSegments(call goja.FunctionCall) goja.Val
segmentTempPath(stagedPath, spec.Index),
policy,
&received,
&completed,
itemProgressReporter,
)
select {
@@ -679,13 +682,18 @@ func (r *extensionRuntime) fileDownloadSegments(call goja.FunctionCall) goja.Val
)
}
if onProgress != nil {
_, _ = onProgress(
goja.Undefined(),
r.vm.ToValue(received.Load()),
r.vm.ToValue(int64(0)),
r.vm.ToValue(completedSegments),
r.vm.ToValue(len(segments)),
)
func() {
if b := r.currentResolutionBudget(); b != nil {
defer b.charge()()
}
_, _ = onProgress(
goja.Undefined(),
r.vm.ToValue(received.Load()),
r.vm.ToValue(int64(0)),
r.vm.ToValue(completedSegments),
r.vm.ToValue(len(segments)),
)
}()
}
}
}
+1 -2
View File
@@ -394,8 +394,7 @@ func (r *extensionRuntime) reliableFileDownload(
retryContext := req.Context()
req, watchdog := bindStallWatchdog(req, downloadStallTimeout)
resp, requestErr := client.Do(req)
r.trackResolutionTransfer(resp)
resp, requestErr := r.doResolutionTransfer(client, req, false)
if requestErr != nil {
stalled := watchdog.stalled.Load()
watchdog.stop()
+49
View File
@@ -1,6 +1,8 @@
package gobackend
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -8,6 +10,53 @@ import (
"testing"
)
func TestCompleteMetadataHintReturnsFileAccessErrors(t *testing.T) {
for _, format := range []string{"flac", "mp3", "m4a", "mp4", "aac", "opus", "ogg", "wav", "aiff", "aif", "aifc", "ape", "wv", "mpc"} {
t.Run(format, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "descriptor")
payload, err := ReadFileMetadataWithHint(path, "track."+format)
if !errors.Is(err, os.ErrNotExist) || payload != "" {
t.Fatalf("missing descriptor returned metadata=%s err=%v", payload, err)
}
if err := os.WriteFile(path, []byte("inaccessible"), 0000); err != nil {
t.Fatal(err)
}
if file, err := os.Open(path); err == nil {
file.Close()
t.Skip("host can bypass file permissions; missing-path check passed")
}
payload, err = ReadFileMetadataWithHint(path, "track."+format)
if !errors.Is(err, os.ErrPermission) || payload != "" {
t.Fatalf("unreadable descriptor returned metadata=%s err=%v", payload, err)
}
})
}
}
func TestCompleteMetadataHintAcceptsAudioWithoutTags(t *testing.T) {
for _, format := range []string{"wav", "aiff"} {
t.Run(format, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "descriptor")
if format == "wav" {
writeTestWAV(t, path)
} else {
writeTestAIFF(t, path)
}
payload, err := ReadFileMetadataWithHint(path, "track."+format)
if err != nil {
t.Fatal(err)
}
var metadata map[string]any
if err := json.Unmarshal([]byte(payload), &metadata); err != nil {
t.Fatal(err)
}
if metadata["title"] != "" || metadata["sample_rate"] != float64(44100) {
t.Fatalf("unexpected tagless audio metadata: %s", payload)
}
})
}
}
func TestCompleteMetadataHintMatchesNamedFileAndDescriptor(t *testing.T) {
for _, format := range []string{"mp3", "flac", "m4a", "wav"} {
t.Run(format, func(t *testing.T) {