Skip to content

web: resolve relative album artpath before serving art - #7015

Open
seitzbg wants to merge 2 commits into
beetbox:masterfrom
seitzbg:web-artpath-fix
Open

web: resolve relative album artpath before serving art#7015
seitzbg wants to merge 2 commits into
beetbox:masterfrom
seitzbg:web-artpath-fix

Conversation

@seitzbg

@seitzbg seitzbg commented Sep 11, 2026

Copy link
Copy Markdown

Description

GET /album/<id>/art returns a 500 when an album's artpath is stored relative to the library directory.

album_art() passes album.artpath straight to flask.send_file(). beets stores artpath relative to the music directory and only resolves it lazily, through the music_dir ContextVar, in the thread that set it. The web server runs with app.run(threaded=True), so worker threads never set that ContextVar; there the path stays relative and send_file() resolves it against the app root (beetsplug/web/), raising FileNotFoundError, so Flask returns a 500. Absolute artpaths are unaffected.

Fix

Resolve a relative artpath against g.lib.directory before serving it, through util.syspath():

if not os.path.isabs(artpath):
    artpath = os.path.join(g.lib.directory, artpath)
return flask.send_file(util.syspath(artpath))

Testing

Added a regression test that reproduces the failure from a separate thread (which, like the threaded=True workers, doesn't inherit the ContextVar) against an on-disk library, and asserts a 200 with the file's bytes. It fails without the fix and passes with it.

To Do

  • Changelog.
  • Tests.

@seitzbg
seitzbg requested a review from a team as a code owner September 11, 2026 03:29
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.25%. Comparing base (103390f) to head (c9105ee).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/web/__init__.py 60.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7015      +/-   ##
==========================================
- Coverage   77.39%   77.25%   -0.14%     
==========================================
  Files         163      163              
  Lines       21841    21844       +3     
  Branches     3370     3371       +1     
==========================================
- Hits        16904    16876      -28     
- Misses       4112     4146      +34     
+ Partials      825      822       -3     
Files with missing lines Coverage Δ
beetsplug/web/__init__.py 73.09% <60.00%> (+0.66%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Serene-Arc Serene-Arc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for the PR. Overall, it looks good except for one thing. We're currently in the process of moving everything that we can to the more modern pathlib library in Python. If you could change the path stuff to use pathlib, that would be great.

Comment thread beetsplug/web/__init__.py
# to an absolute path so send_file doesn't look under the app root.
if not os.path.isabs(artpath):
artpath = os.path.join(g.lib.directory, artpath)
return flask.send_file(util.syspath(artpath))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are moving to pathlib instead of os functions where possible. These can be done with pathlib functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to pathlib here in c9105ee. Using the album.art_filepath property now and joining the relative case against the library dir with /. Kept os.fsdecode since that's the bytes to str step art_filepath itself does; pathlib won't take bytes and a plain .decode() would choke on non-utf8 filenames.

Comment thread test/plugins/test_web.py Outdated
Comment on lines +706 to +708
rel = os.path.join(b"rel_art_dir", b"cover.png")
abspath = os.path.join(lib.directory, rel)
os.makedirs(os.path.dirname(syspath(abspath)), exist_ok=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More places where pathlib can be used instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same commit. os.makedirs/os.path.dirname are now abspath.parent.mkdir(parents=True, exist_ok=True), and the joins use /.

Address review on beetbox#7015: replace os.path.isabs/os.path.join (and the
os.makedirs/os.path.dirname/open in the test) with pathlib, reusing the
Album.art_filepath helper. os.fsdecode stays as the bytes->str bridge,
matching beets' own model idiom.
@seitzbg

seitzbg commented Sep 12, 2026

Copy link
Copy Markdown
Author

Pushed the pathlib conversion in c9105ee, both the route and the test. The only os call left is os.fsdecode, which is the bytes to str bridge beets already uses in Album.art_filepath (pathlib doesn't accept bytes). Should be good for another look.

@seitzbg
seitzbg requested a review from Serene-Arc September 12, 2026 02:12
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.

2 participants