embedart WriteError hides underlying FLAC write error
- Dominant language
- Python
- Stars
- 15.7k
- Forks
- 2.1k
- Avg merge
- 4d 21h
- Merged PRs (30d)
- 31
Description
I've run into an error that I think may actually warrant two separate issues because the first issue (this one) hides the second one. I'm not as sure if the second one is an actual issue though, since it's a further extension of the error reporting.
### The Surface-Level Problem
The first problem is that a WriteError during embedart is not being properly reported. When running with `beet -vv embedart Walkerworld` I get the following output:
```sh
aargonian@NytegearMusic ➜ beet -vv embedart Walkerworld
user configuration: /home/aargonian/.config/beets/config.yaml
data directory: /home/aargonian/.config/beets
plugin paths: []
Loading plugins: albumtypes, badfiles, chroma, convert, duplicates, edit, embedart, export, fetchart, filetote, fromfilename, info, inline, lastgenre, lyrics, mbsync, missing, musicbrainz, permissions, replaygain, unimported
fetchart: google: Disabling art source due to missing key
fetchart: lastfm: Disabling art source due to missing key
ImageMagick version check failed: [Errno 2] No such file or directory: 'magick'
artresizer: method is ImageMagick
lastgenre: Loading whitelist /music/beets/config/lastgenre-whitelist.txt
lastgenre: Loading canonicalization tree /music/beets/venv/lib/python3.11/site-packages/beetsplug/lastgenre/genres-tree.yaml
inline: adding item field album_folder
inline: adding item field disc_prefix
inline: adding item field track_file
Sending event: pluginload
library database: /music/beets/musiclibrary.db
library directory: /music/library/originals
Sending event: library_opened
Parsed query: AndQuery([OrQuery([SubstringQuery('album', 'Walkerworld', fast=True), SubstringQuery('albumartist', 'Walkerworld', fast=True), SubstringQuery('genres', 'Walkerworld', fast=True)])])
Parsed sort: NullSort()
Alan Walker - Walkerworld
Modify artwork for 1 album (Y/n)? Y
embedart: Resizing album art to 2000 pixels wide and encoding at quality level 90
artresizer: ImageMagick resizing /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/cover.png to /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
embedart: Embedding album art into Alan Walker - Walkerworld
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
embedart: embedding /tmp/beets/util_artresizer/resize_IM_liutzmqm.png
Sending event: write
error writing , >
Sending event: cli_exit
```
The `error writing , >` is of course also logged when not using `-vv`, which is how I noticed the problem for this album in the first place. I'm not sure what the error wrapping code currently does, but I did do some poking around in my virtualenv to see if I could surface more info myself. I'm not super proficient with python or familiar with beets's structure, so forgive me if it leads to the wrong conclusion here, but I found that the underlying error appears to be an `UnreadableFileError` created when trying to make the new version of the file with the embedded artwork.
In `beetsplug/_util/art.py`, `embed_item` calls `item.try_write`, but does not appear to wrap it in a try block, presumably because try_write should not usually throw if the write fails and the result should be checked? Although the current version of `embed_item` does not check the result at all and simply exits the function.
I tracked the `try_write` call itself to exist on the `Item` class in `models.py`, which in turn just calls `write`. The `write` method appears to properly catch the UnreadableFileError being thrown by `mediafile.save()` and re-raises it as `WriteError`, but the wrapping seems to have lost the details of the error, or they at least aren't being logged completely even in verbose mode.
That leads into the second problem, which is the real underlying cause of the error.
### The Underlying Problem
To find the second problem I temporarily threw a `print` statement on the `UnreadableFileError` in the `try_write` method before it gets wrapped to see what it says. This is what it reported as a sample from one of the files in that album:
```sh
aargonian@NytegearMusic ➜ beet embedart Walkerworld
Alan Walker - Walkerworld
Modify artwork for 1 album (Y/n)? Y
embedart: Embedding album art into Alan Walker - Walkerworld
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/01. Welcome to Walkerworld.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/03. Spectre 2.0.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/10. Darkride.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/05. Endless Summer.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/02. Heart Over Mind.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/08. Yesterday.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/06. Hero.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/07. Born to Ride.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/09. Land of the Heroes.flac: block is too long to write
error writing , >
Unreadable File Error: /music/library/originals/Alan Walker/Albums/Walkerworld - [2023]/04. Better Off (Alone, Pt. III).flac: block is too long to write
error writing , >
```
As the error occurred for every file in the album, I ran `flac --test` on one of the tracks to check their state:
```sh
aargonian@NytegearMusic ➜ flac --test ./01.\ Welcome\ to\ Walkerworld.flac
flac 1.4.2
Copyright (C) 2000-2009 Josh Coalson, 2011-2022 Xiph.Org Foundation
flac comes with ABSOLUTELY NO WARRANTY. This is free software, and you are
welcome to redistribute it under certain conditions. Type `flac' for details.
01. Welcome to Walkerworld.flac: WARNING, cannot check MD5 signature since it was unset in the STREAMINFO
ok
```
Aside from the warning, it was fine. The other thing I checked was the temporary cover image attempting to be embedded:
```sh
aargonian@NytegearMusic ➜ ls -al resize_IM_liutzmqm.png
-rw------- 1 aargonian aargonian 19894765 May 7 15:00 resize_IM_liutzmqm.png
```
As shown in the listing, the file is just around 19MiB in size. After doing a little digging, my understanding is that FLAC limits a single metadata block to 16,777,215 bytes. The metadata block header is 4 bytes: the first byte contains the last-metadata-block flag and block type, while the remaining 24 bits encode the metadata block length. Since embedded artwork is (I assume) stored in a PICTURE metadata block, this effectively caps a single embedded FLAC picture block at just under 16 MiB.
To test if this was truly the issue, I just changed my config for embedart for the `maxwidth` from 2000 to 1600. The issue promptly went away.
In addition to preserving the underlying exception text, it would be helpful if this specific failure could produce a more specific/actionable message. `block is too long to write` is technically accurate, but without knowing how FLAC metadata blocks are structured it is not obvious that the embedded artwork is too large and that lowering `maxwidth`, changing the resized image format, or reducing image quality could fix it. A more useful message would probably mention something about a limit on the size of the artwork being embedded and recommend tuning config files down where possible.
If this second issue needs another, separate tracking issue, please let me know. And also let me know if I can provide any more useful info. Thanks to everyone working on beets for the awesome software!
### Setup
* OS: Raspbian Bookworm 2026-03-11 (Linux 6.12.75+rpt-rpi-2712)
* Python version: 3.11.2
* beets version: 2.10.0
* Turning off plugins made problem go away (yes/no): no
My configuration (output of `beet config`) is:
```yaml
directory: /music/library/originals
# --------------- Main ---------------
library: /music/beets/musiclibrary.db
artist_credit: no
# --------------- Tagging ---------------
per_disc_numbering: yes
ignore_video_tracks: yes
match:
ignored_media:
- Data CD
- DVD
- DVD-Video
- Blu-ray
- HD-DVD
- VCD
- SVCD
- UMD
- VHS
replace:
'[\\/]': _
^\.: _
'[\x00-\x1f]': _
<: "\u2039"
'>': "\u203A"
':': "\uA789"
'"': "\u2033"
\?: "\uFF1F"
\*: "\u2217"
\|: "\u01C0"
\.$: _
\s+$: ''
^\s+: ''
^-: _
import:
move: yes
incremental: no
quiet_fallback: skip
languages: [en]
item_fields:
album_folder: "name = album or 'Unknown Album'\nif albumdisambig:\n name += f' ({albumdisambig})'\nif year:\n name += f' - [{year}]'\nreturn name\n"
disc_prefix: "total = int(disctotal or 1)\nif total <= 1:\n return ''\nfmt = media or 'Disc'\nreturn f'{fmt} {int(disc or 1)}/'\n"
track_file: 'total = int(disctotal or 1)
d = int(disc or 1)
t = int(track or 0)
prefix = f''{d:02d}-{t:02d}'' if total > 1 else f''{t:02d}''
return f''{prefix}. {title or "Untitled"}''
'
paths:
releasekind:soundtrack: OSTs/$album_folder%aunique{album year albumdisambig,albumartist albumtype,()}/$disc_prefix$track_file
releasekind:live: $albumartist/Live/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
releasekind:remix: $albumartist/Remixes/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
releasekind:single: $albumartist/Singles/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
releasekind:ep: $albumartist/EPs/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
releasekind:compilation: $albumartist/Anthology/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
albumtypes:soundtrack: OSTs/$album_folder%aunique{album year albumdisambig,albumartist albumtype,()}/$disc_prefix$track_file
albumtypes:live: $albumartist/Live/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
albumtypes:remix: $albumartist/Remixes/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
albumtypes:single: $albumartist/Singles/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
albumtypes:ep: $albumartist/EPs/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
albumtypes:compilation: $albumartist/Anthology/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
comp: $albumartist/Anthology/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
singleton: $artist/Uncategorized/$title%sunique{artist title year mb_trackid,trackdisambig,()}/$title
default: $albumartist/Albums/$album_folder%aunique{albumartist album year albumdisambig,albumtype,()}/$disc_prefix$track_file
badfiles:
check_on_import: yes
commands:
flac: flac --test --silent
mp3: mp3val
wav: /usr/local/bin/check-audio-decode
m4a: /usr/local/bin/check-audio-decode
mp4: /usr/local/bin/check-audio-decode
aac: /usr/local/bin/check-audio-decode
opus: /usr/local/bin/check-audio-decode
ogg: /usr/local/bin/check-audio-decode
musicbrainz:
search_limit: 10
data_source_mismatch_penalty: 0.5
search_query_ascii: no
genres: no
genres_tag: genre
external_ids:
discogs: no
bandcamp: no
spotify: no
deezer: no
tidal: no
extra_tags: []
chroma:
auto: yes
search_limit: 10
data_source_mismatch_penalty: 0.5
# --------------- Plugins ---------------
plugins:
- musicbrainz
- convert
- mbsync
- fromfilename
- badfiles
- replaygain
- missing
- info
- edit
- duplicates
- chroma
- export
- fetchart
- embedart
- filetote
- unimported
- lastgenre
- lyrics
- permissions
- inline
- albumtypes
albumtypes:
types:
- ep: EP
- single: Single
- soundtrack: OST
- live: Live
- compilation: Anthology
- remix: Remix
ignore_va: compilation
bracket: '[]'
replaygain:
backend: ffmpeg
auto: no
targetlevel: 89
r128_targetlevel: 89
r128: Opus
overwrite: no
threads: 4
parallel_on_import: no
per_disc: no
peak: 'true'
lastgenre:
auto: no
source: album
canonical: yes
prefer_specific: yes
count: 3
min_weight: 20
force: yes
keep_existing: no
title_case: yes
whitelist: /music/beets/config/lastgenre-whitelist.txt
fallback:
cleanup_existing: no
pretend: no
ignorelist: {}
lyrics:
auto: no
synced: yes
auto_ignore:
translate:
api_key: REDACTED
from_languages: []
to_language:
dist_thresh: 0.11
google_API_key: REDACTED
google_engine_ID: REDACTED
genius_api_key: REDACTED
fallback:
force: no
keep_synced: no
local: no
print: no
sources: [lrclib, google, genius]
fetchart:
auto: yes
store_source: yes
high_resolution: yes
maxwidth: 2000
quality: 90
cover_names:
- cover
- front
- art
- album
- folder
fallback:
minwidth: 0
max_filesize: 0
enforce_ratio: no
cautious: no
sources:
- filesystem
- coverart
- itunes
- amazon
- albumart
- cover_art_url
deinterlace: no
cover_format:
google_key: REDACTED
google_engine: REDACTED
lastfm_key: REDACTED
fanarttv_key: REDACTED
embedart:
auto: yes
compare_threshold: 0
maxwidth: 2000
quality: 90
ifempty: no
remove_art_file: no
clearart_on_import: no
filetote:
extensions: .*
print_ignored: yes
pairing:
enabled: yes
pairing_only: no
extensions: .*
paths:
default: $albumpath/$subpath$old_filename
exclude:
filenames:
- cover.jpg
- .DS_Store
- Thumbs.db
- desktop.ini
patterns:
macos_junk: [__MACOSX/, .AppleDouble/]
extensions: ''
session:
operation:
_beets_lib:
_library_path:
import_path:
filenames: ''
patterns: {}
duplicate_action: merge
ignore: []
ignore_hidden: no
convert:
format: opus
dest: /music/library/opus
never_convert_lossy_files: yes
write_metadata: yes
embed: yes
copy_album_art: yes
quiet: no
formats:
mp3:
command: /music/beets/scripts/gapless-convert.sh $source $dest
extension: mp3
opus:
command: ffmpeg -nostdin -hide_banner -loglevel error -i $source -y -vn -c:a libopus -b:a 160k -vbr on -application audio $dest
extension: opus
aac:
command: ffmpeg -i $source -y -vn -acodec aac -aq 1 $dest
extension: m4a
alac:
command: ffmpeg -i $source -y -vn -acodec alac $dest
extension: m4a
flac: ffmpeg -i $source -y -vn -acodec flac $dest
ogg: ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest
wma: ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest
pretend: no
link: no
hardlink: no
threads: 4
id3v23: inherit
max_bitrate:
auto: no
auto_keep: no
tmpdir:
paths: {}
no_convert: ''
album_art_maxwidth: 0
delete_originals: no
playlist:
permissions:
file: 664
dir: 775
disabled_plugins: []
missing:
count: no
total: no
album: no
release_types: [album]
edit:
albumfields: album albumartist
itemfields: track title artist album
ignore_fields: id path
duplicates:
album: no
checksum: ''
copy: ''
count: no
delete: no
format: ''
full: no
keys: []
merge: no
move: ''
path: no
tiebreak: {}
strict: no
tag: ''
remove: no
export:
default_format: json
json:
formatting:
ensure_ascii: no
indent: 4
separators: &id001 [',', ': ']
sort_keys: yes
jsonlines:
formatting:
ensure_ascii: no
separators: *id001
sort_keys: yes
csv:
formatting:
delimiter: ','
dialect: excel
xml:
formatting: {}
unimported:
ignore_extensions: []
ignore_subdirectories: []
pathfields: {}
album_fields: {}
```
Contributor guide
Assessment
This issue has not been assessed yet.