[Bug]: Covers not loading for m4b audiobooks #2406

Open
opened 2026-04-25 00:06:48 +02:00 by adam · 11 comments
Owner

Originally created by @sandreas on GitHub (Dec 8, 2024).

What happened?

I have a library of many m4b's and after upgrading audiobookshelf to a newer version, the covers of newly added m4b files don't show up, although I ensured that the files contain a valid cover.

What did you expect to happen?

I would have expected the covers to show up as normal.

Steps to reproduce the issue

Add the attached file to your audio book folder and wait for it to be scanned.
input.zip

This problem might be related to #1063 and #2885 - but is very specific to m4b files with embedded cover. I found out that the problem is possibly a wrong ffmpeg extraction command in:

https://github.com/advplyr/audiobookshelf/blob/190a1000d9b5909b5bcd953f32f39fa8f261ecb9/server/utils/ffmpegHelpers.js#L56

    var ffmpeg = Ffmpeg(filepath)
    ffmpeg.addOption(['-map 0:v:0', '-frames:v 1'])
    ffmpeg.output(outputpath)

which results in

ffmpeg -i input.m4b -map 0:v:0 -frames:v 1 cover.jpg

This command extracts a FULLY BLACK cover for m4b files.

Possible solution
The corrected command that works and extracts the cover as expected would be:

ffmpeg -i input.m4b -map 0:v -map -0:V -c copy -frames:v 1 cover.jpg

or in code

    var ffmpeg = Ffmpeg(filepath)
    ffmpeg.addOption(['-map 0:v', '-map 0:V', '-c copy', '-frames:v 1'])
    ffmpeg.output(outputpath)

but I'm not sure, that this is the place the problem occurs.

Audiobookshelf version

2.17.4

How are you running audiobookshelf?

Docker

What OS is your Audiobookshelf server hosted from?

Linux

If the issue is being seen in the UI, what browsers are you seeing the problem on?

Chrome

Logs

No errors in log files

Additional Notes

Suggested solution should fix the problem.

Originally created by @sandreas on GitHub (Dec 8, 2024). ### What happened? I have a library of many m4b's and after upgrading audiobookshelf to a newer version, the covers of newly added m4b files don't show up, although I ensured that the files contain a valid cover. ### What did you expect to happen? I would have expected the covers to show up as normal. ### Steps to reproduce the issue Add the attached file to your audio book folder and wait for it to be scanned. [input.zip](https://github.com/user-attachments/files/18054321/input.zip) This problem might be related to #1063 and #2885 - but is very specific to `m4b` files with embedded cover. I found out that the problem is possibly a wrong `ffmpeg` extraction command in: https://github.com/advplyr/audiobookshelf/blob/190a1000d9b5909b5bcd953f32f39fa8f261ecb9/server/utils/ffmpegHelpers.js#L56 ```js var ffmpeg = Ffmpeg(filepath) ffmpeg.addOption(['-map 0:v:0', '-frames:v 1']) ffmpeg.output(outputpath) ``` which results in ```bash ffmpeg -i input.m4b -map 0:v:0 -frames:v 1 cover.jpg ``` This command extracts a FULLY BLACK cover for `m4b` files. **Possible solution** The corrected command that works and extracts the cover as expected would be: ```bash ffmpeg -i input.m4b -map 0:v -map -0:V -c copy -frames:v 1 cover.jpg ``` or in code ```js var ffmpeg = Ffmpeg(filepath) ffmpeg.addOption(['-map 0:v', '-map 0:V', '-c copy', '-frames:v 1']) ffmpeg.output(outputpath) ``` but I'm not sure, that this is the place the problem occurs. ### Audiobookshelf version 2.17.4 ### How are you running audiobookshelf? Docker ### What OS is your Audiobookshelf server hosted from? Linux ### If the issue is being seen in the UI, what browsers are you seeing the problem on? Chrome ### Logs ```shell No errors in log files ``` ### Additional Notes Suggested solution should fix the problem.
adam added the bug label 2026-04-25 00:06:48 +02:00
Author
Owner

@sandreas commented on GitHub (Dec 13, 2024):

Reproduced and fixed the problem on my system - the fix can be used as described above (use other ffmpeg params). I try to submit a PR in the next days.

@sandreas commented on GitHub (Dec 13, 2024): Reproduced and fixed the problem on my system - the fix can be used as described above (use other `ffmpeg` params). I try to submit a PR in the next days.
Author
Owner

@advplyr commented on GitHub (Dec 13, 2024):

Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529

That update was needed in my testing for multiple embedded covers

@advplyr commented on GitHub (Dec 13, 2024): Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529 That update was needed in my testing for multiple embedded covers
Author
Owner

@sandreas commented on GitHub (Dec 14, 2024):

Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529

I'm going to check this in the next days.

That update was needed in my testing for multiple embedded covers

Yeah, I know that this might cause a regression for other formats not extracting the covers any more. I try to find a generic solution that works for all formats, otherwise I think we must provide a format paramter to switch the parameters depending on the format.

@sandreas commented on GitHub (Dec 14, 2024): > Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529 I'm going to check this in the next days. > That update was needed in my testing for multiple embedded covers Yeah, I know that this might cause a regression for other formats not extracting the covers any more. I try to find a generic solution that works for all formats, otherwise I think we must provide a `format` paramter to switch the parameters depending on the format.
Author
Owner

@sandreas commented on GitHub (Dec 20, 2024):

@advplyr

Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529

Yes, before this change it worked. Re-checked this also manually via:

# works, cover is extracted as expected
ffmpeg -i input.m4b -map 0:v -frames:v 1 cover.jpg

vs

# creates all black cover
ffmpeg -i input.m4b -map 0:v -frames:v 1 cover.jpg

What does this mean? I think that my version also might work with multi cover files:

ffmpeg -i input.m4b -map 0:v -map -0:V -c copy -frames:v 1 cover.jpg

EDIT: but to be sure I would at least setup some regression tests...

@sandreas commented on GitHub (Dec 20, 2024): @advplyr > Did this PR break it for you? https://github.com/advplyr/audiobookshelf/pull/3529 Yes, before this change it worked. Re-checked this also manually via: ``` # works, cover is extracted as expected ffmpeg -i input.m4b -map 0:v -frames:v 1 cover.jpg ``` vs ``` # creates all black cover ffmpeg -i input.m4b -map 0:v -frames:v 1 cover.jpg ``` What does this mean? I think that my version also might work with multi cover files: ``` ffmpeg -i input.m4b -map 0:v -map -0:V -c copy -frames:v 1 cover.jpg ``` EDIT: but to be sure I would at least setup some regression tests...
Author
Owner

@sandreas commented on GitHub (Jan 27, 2025):

@advplyr Any news on this? I would really like to see this fixed...

@sandreas commented on GitHub (Jan 27, 2025): @advplyr Any news on this? I would really like to see this fixed...
Author
Owner

@sandreas commented on GitHub (Aug 26, 2025):

@advplyr Update: Still not fixed in 2.29.0 - had to replace ffmpegHelpers.js and re-scan files to make this work

@sandreas commented on GitHub (Aug 26, 2025): @advplyr Update: Still not fixed in 2.29.0 - had to replace ffmpegHelpers.js and re-scan files to make this work
Author
Owner

@lieut-data commented on GitHub (Jan 16, 2026):

I've run into this issue for a large number of my audiobooks, many of which have multiple cover art "video streams". I think the solution is just to pick the largest one, ignoring (in my case) often 1x1 cover arts. Here's an example from one of my audio books:

Stream #0:1 (PNG):

  • Resolution: 1x1 pixels (essentially a placeholder)
  • Format: PNG
  • Marked as "(timed thumbnails)"

Stream #0:3 (MJPEG):

  • Resolution: 400x400 pixels (actual cover art size)
  • Format: MJPEG
  • Marked as "(attached pic)"
@lieut-data commented on GitHub (Jan 16, 2026): I've run into this issue for a large number of my audiobooks, many of which have multiple cover art "video streams". I think the solution is just to pick the largest one, ignoring (in my case) often 1x1 cover arts. Here's an example from one of my audio books: Stream #0:1 (PNG): - Resolution: 1x1 pixels (essentially a placeholder) - Format: PNG - Marked as "(timed thumbnails)" Stream #0:3 (MJPEG): - Resolution: 400x400 pixels (actual cover art size) - Format: MJPEG - Marked as "(attached pic)"
Author
Owner

@sandreas commented on GitHub (Jan 16, 2026):

@lieut-data

I fixed this manually using a dirty hack in ffmpeg_helper.js. Because the call handling is promise based (not async), this is really a bad way of solving this but for me it works (most of the time).

I did this long time ago just for myself and re-checked on every update I made, so it may now fail with the newest version. The Idea is to retry extracting with another ffmpeg command as long as the cover file is < 750 bytes (1x1px). All you have to do is just to adjust a ffmpegHelpers.js file.

A better way would probably be to extract ALL existing covers with:

ffmpeg -i input_audio -map 0:v -c copy cover_%04d.jpg

And then iterate the extracted files with the 750 bytes file size check. This might be go really wrong on video files - I never checked.

If you are using docker, you can copy the original, make your adjustments and mount the new file over the old one:

 "/my/fixes/ffmpegHelpers_2.32.1.js:/server/utils/ffmpegHelpers.js"

Here is the dirty hack:

async function extractCoverArt(filepath, outputpath) {
  var dirname = Path.dirname(outputpath)
  await fs.ensureDir(dirname)
  // ffmpeg -i file.mp3 -an -c:v copy file.jpg
  var optionSets = [
    ['-map 0:v', '-map 0:V', '-c copy', '-frames:v 1'],
    ['-map 0:v:0', '-frames:v 1'],
    ['-an', '-c:v', 'copy', 'frames:v 1']
  ];
  return new Promise((resolve) => {
    extractCoverArtHelper(filepath, outputpath, optionSets[0])
      .then((result) => {
        if(result !== false) {
          resolve(result)
        } else {
          extractCoverArtHelper(filepath, outputpath, optionSets[1])
            .then((result) => {
              if(result !== false) {
                resolve(result)
              } else {
                extractCoverArtHelper(filepath, outputpath, optionSets[2])
                  .then((result) => {
                    resolve(result)
                  });
              }
            });
        }
      });
  });
}

async function extractCoverArtHelper(filepath, outputpath, options) {
  return new Promise((resolve) => {
    /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
    var ffmpeg = Ffmpeg(filepath)
    ffmpeg.addOption(options)
    ffmpeg.output(outputpath)

    ffmpeg.on('start', (cmd) => {
        Logger.debug(`[FfmpegHelpers] Extract Cover Cmd: ${cmd}`)
    })
    ffmpeg.on('error', (err, stdout, stderr) => {
        Logger.error(`[FfmpegHelpers] Extract Cover Error ${err}`)
        resolve(false)
    })
    ffmpeg.on('end', () => {
        Logger.debug(`[FfmpegHelpers] Cover Art Extracted Successfully`)
        // 750 is chosen for 1x1px black (https://github.com/Zeugma440/atldotnet/blob/main/ATL/Resources/1px_jpg.jpg)
        var extractionSuccessful;
        try {
          extractionSuccessful = fs.pathExistsSync(outputpath) && fso.statSyncAttempt(outputpath).size > 750
        } catch(e) {
          extractionSuccessful = false
        }
        if(extractionSuccessful) {
          resolve(outputpath)
        } else {
          resolve(false)
        }
    })
    ffmpeg.run()
  });
}


module.exports.extractCoverArt = extractCoverArt
@sandreas commented on GitHub (Jan 16, 2026): @lieut-data I fixed this manually using a dirty hack in [`ffmpeg_helper.js`](https://github.com/advplyr/audiobookshelf/blob/122fc34a75a6730f99736c3ae01186871b3d90ef/server/utils/ffmpegHelpers.js#L50). Because the call handling is promise based (not async), this is really a bad way of solving this but for me it works (most of the time). I did this long time ago just for myself and re-checked on every update I made, so it may now fail with the newest version. The Idea is to retry extracting with another ffmpeg command as long as the cover file is < 750 bytes (1x1px). All you have to do is just to adjust a `ffmpegHelpers.js` file. A better way would probably be to extract ALL existing covers with: ```sh ffmpeg -i input_audio -map 0:v -c copy cover_%04d.jpg ``` And then iterate the extracted files with the 750 bytes file size check. This might be go really wrong on video files - I never checked. If you are using docker, you can copy the original, make your adjustments and mount the new file over the old one: ``` "/my/fixes/ffmpegHelpers_2.32.1.js:/server/utils/ffmpegHelpers.js" ``` Here is the dirty hack: ```js async function extractCoverArt(filepath, outputpath) { var dirname = Path.dirname(outputpath) await fs.ensureDir(dirname) // ffmpeg -i file.mp3 -an -c:v copy file.jpg var optionSets = [ ['-map 0:v', '-map 0:V', '-c copy', '-frames:v 1'], ['-map 0:v:0', '-frames:v 1'], ['-an', '-c:v', 'copy', 'frames:v 1'] ]; return new Promise((resolve) => { extractCoverArtHelper(filepath, outputpath, optionSets[0]) .then((result) => { if(result !== false) { resolve(result) } else { extractCoverArtHelper(filepath, outputpath, optionSets[1]) .then((result) => { if(result !== false) { resolve(result) } else { extractCoverArtHelper(filepath, outputpath, optionSets[2]) .then((result) => { resolve(result) }); } }); } }); }); } async function extractCoverArtHelper(filepath, outputpath, options) { return new Promise((resolve) => { /** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */ var ffmpeg = Ffmpeg(filepath) ffmpeg.addOption(options) ffmpeg.output(outputpath) ffmpeg.on('start', (cmd) => { Logger.debug(`[FfmpegHelpers] Extract Cover Cmd: ${cmd}`) }) ffmpeg.on('error', (err, stdout, stderr) => { Logger.error(`[FfmpegHelpers] Extract Cover Error ${err}`) resolve(false) }) ffmpeg.on('end', () => { Logger.debug(`[FfmpegHelpers] Cover Art Extracted Successfully`) // 750 is chosen for 1x1px black (https://github.com/Zeugma440/atldotnet/blob/main/ATL/Resources/1px_jpg.jpg) var extractionSuccessful; try { extractionSuccessful = fs.pathExistsSync(outputpath) && fso.statSyncAttempt(outputpath).size > 750 } catch(e) { extractionSuccessful = false } if(extractionSuccessful) { resolve(outputpath) } else { resolve(false) } }) ffmpeg.run() }); } module.exports.extractCoverArt = extractCoverArt ```
Author
Owner

@lieut-data commented on GitHub (Jan 16, 2026):

Thanks, @sandreas! I submitted a PR to address this in a different way -- see https://github.com/advplyr/audiobookshelf/pull/4988.

In that, I'm using ffprobe to examine the video tracks, filtering out any with a dimension < 1x1, and also picking the biggest one. You can see my screenshots showing the results on my own library, and with the added unit tests, I'm hoping this passes muster for review with team here -- happy to iterate if any changes are required!

@lieut-data commented on GitHub (Jan 16, 2026): Thanks, @sandreas! I submitted a PR to address this in a different way -- see https://github.com/advplyr/audiobookshelf/pull/4988. In that, I'm using ffprobe to examine the video tracks, filtering out any with a dimension < 1x1, and also picking the biggest one. You can see my screenshots showing the results on my own library, and with the added unit tests, I'm hoping this passes muster for review with team here -- happy to iterate if any changes are required!
Author
Owner

@sandreas commented on GitHub (Jan 16, 2026):

@lieut-data looks great. I'm going to integrate this in my patch ;-) thanks

@sandreas commented on GitHub (Jan 16, 2026): @lieut-data looks great. I'm going to integrate this in my patch ;-) thanks
Author
Owner

@lieut-data commented on GitHub (Mar 23, 2026):

@advplyr, would you be open to assigning this to me? I have (and am running) a fix with https://github.com/advplyr/audiobookshelf/pull/4988. Would love to get this upstreamed :)

@lieut-data commented on GitHub (Mar 23, 2026): @advplyr, would you be open to assigning this to me? I have (and am running) a fix with https://github.com/advplyr/audiobookshelf/pull/4988. Would love to get this upstreamed :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf#2406