Skip to content

fix: embed cover art when the cover is already on disk - #230

Open
Piero-93 wants to merge 1 commit into
LumePart:devfrom
Piero-93:fix/embed-cached-cover-art
Open

fix: embed cover art when the cover is already on disk#230
Piero-93 wants to merge 1 commit into
LumePart:devfrom
Piero-93:fix/embed-cached-cover-art

Conversation

@Piero-93

Copy link
Copy Markdown

The bug

With EMBED_COVER_ART=true, the cover is added as an ffmpeg input only inside the track.CoverPath == "" branch of saveVideo, that is, only when it still has to be downloaded:

if c.Cfg.EmbedCoverArt && track.CoverURL != "" {
    if track.CoverPath == "" {
        if _, track.CoverPath = util.DownloadCover(...); track.CoverPath != "" {
            streams = append(streams, ffmpeg.Input(track.CoverPath))
        }
    }

loadCustomTracks fills CoverPath from the playlist cache, so for any custom playlist imported through the web UI (Spotify, Apple Music, ListenBrainz link) the image is already on disk when the download starts. The branch is skipped, the image never reaches ffmpeg, and EMBED_COVER_ART silently does nothing. Signature in the logs: an ffmpeg command with a single -i.

The fix

Fetching the cover and handing it to ffmpeg are now separate steps, and how the image is embedded depends on what the output container can hold:

container how
mp3, flac, m4a, m4b, mp4 attached picture stream, re-encoded to mjpeg and flagged attached_pic
ogg, oga, opus, spx, wv base64 METADATA_BLOCK_PICTURE tag
anything else (wav, aac, ac3, aiff, mka, webm) no artwork

Three failure modes that currently cost the whole track, not just the artwork, are gone as well:

  • wav, aac and ac3 lose the track today. Those muxers cannot hold an image, so passing one fails the entire command (wav muxer does not support any stream of type video) and the download never lands.
  • DownloadCover returns its destination path even when the fetch failed — the return sits outside the status check — so a 404 or an empty body yields a non-existent input and a failed command. The file is now checked to exist and to be non-empty. I left the helper's signature alone, since the web UI depends on its current contract.
  • If ffmpeg refuses the image for any other reason, the file is written a second time without it. Artwork can no longer cost a track.

Two smaller corrections in passing:

  • ffmpeg.Input(input).Audio() replaces the explicit "map": "0:a", which was only set on the branch without cover art. With two inputs the library maps every input, so a video stream in the downloaded file used to survive into the output.
  • -metadata:s:v title/comment gives the picture type 3 ("front cover"); without them ffmpeg writes type 0 ("other").

On mp4 the image is currently stored as an h264 video stream, and on mp3 as a re-encoded png. #219 reports the mp4 half of this and fixes it inside the old branch; here it falls out of the table above. Happy to rebase on top of #219, or to drop the overlapping part, if you would rather merge that one first.

Verification

Files written and artwork read back with a tag library, on ffmpeg 7.0.2, 8.1.2 and 9.0.1, with identical results on all three: 96 combinations of 16 containers by 6 cover inputs (jpeg, png, webp, truncated, empty, missing). No track lost in any of them, and picture type 3 wherever the container allows one.

Three judgement calls worth flagging:

  • mjpeg rather than copy. The cached file is named .jpg but holds whatever the CDN served; copying a WebP into mp4 fails with Could not find tag for codec webp, and re-encoding a still image is cheap. On the Vorbis comment path a WebP is skipped instead, with a warning, since there is no stdlib decoder for it.
  • aiff, mka and webm are left out deliberately. ffmpeg accepts the command, but the artwork cannot be read back afterwards.
  • The METADATA_BLOCK_PICTURE tag travels as a command-line argument, which Linux caps at 128 KiB each, so covers whose encoded tag exceeds 100 KiB are skipped. The default COVER_ART_SIZE=250 stays an order of magnitude below that.

Cover art was only handed to ffmpeg inside the `track.CoverPath == ""`
branch, that is, only when it still had to be downloaded. Custom
playlists cache the cover when the playlist is created (loadCustomTracks
fills CoverPath from the playlist cache), so for a Spotify, Apple Music
or ListenBrainz playlist imported through the web UI the image is
already on disk, the branch is skipped, and EMBED_COVER_ART silently
does nothing.

Fetching the cover and handing it to ffmpeg are now separate steps, and
how the image is embedded depends on what the container can hold:

- an attached picture stream for mp3, flac, m4a, m4b and mp4, re-encoded
  to mjpeg and flagged attached_pic so it is stored as artwork rather
  than as a regular video stream (h264 in mp4, png in mp3)
- a base64 METADATA_BLOCK_PICTURE tag for the Ogg family and WavPack,
  which ffmpeg cannot build from an image input but does pass through
- nothing at all for containers that cannot carry a picture (wav, aac,
  ac3), where handing ffmpeg an image makes the command fail and the
  track is lost with it

Two other ways to lose a track are gone as well: DownloadCover returns
its destination path even when the fetch failed, so the file is now
checked to exist and to be non-empty, and if ffmpeg refuses the image
anyway the file is written again without it.

Verified by writing files and reading the artwork back with a tag
library, on ffmpeg 7.0.2, 8.1.2 and 9.0.1: 96 combinations of container
and cover format (jpeg, png, webp, truncated, empty, missing), no track
lost, and picture type 3 wherever the container allows one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant