fix(artwork): validate ffmpeg pipe before returning in cover art fallback

ffmpeg.ExtractImage returns a pipe-based reader immediately, before ffmpeg
finishes processing. When the audio file has no embedded image stream (e.g.
a plain MP3), ffmpeg exits with an error that closes the pipe asynchronously.
The selectImageReader function saw the non-nil reader as a success and
returned it instead of falling through to the next source in the chain
(album art). This caused getCoverArt to return an error response for tracks
on albums where the disc artwork reader was invoked but no embedded art
existed.

Fixed by reading one byte from the pipe to validate the stream delivers
data before returning it. If the read fails, the reader is closed and nil
is returned, allowing the fallback chain to continue to album artwork.

Closes #5265
This commit is contained in:
Deluan
2026-03-30 07:01:38 -04:00
parent 9fe9cf3ff6
commit 420d2c8e5a
+16 -1
View File
@@ -130,8 +130,23 @@ func fromFFmpegTag(ctx context.Context, ffmpeg ffmpeg.FFmpeg, path string) sourc
if err != nil {
return nil, "", err
}
return r, path, nil
// Validate that the stream actually contains image data by reading the first byte.
// ffmpeg.ExtractImage returns a pipe reader that may fail asynchronously if the
// file has no video/image stream (e.g., an MP3 without embedded art).
buf := make([]byte, 1)
n, err := r.Read(buf)
if n == 0 || err != nil {
r.Close()
return nil, "", fmt.Errorf("ffmpeg produced no image data for %s: %w", path, err)
}
return readCloser{Reader: io.MultiReader(bytes.NewReader(buf[:n]), r), Closer: r}, path, nil
}
}
// readCloser combines a Reader and a Closer into an io.ReadCloser.
type readCloser struct {
io.Reader
io.Closer
}
func fromAlbum(ctx context.Context, a *artwork, id model.ArtworkID) sourceFunc {