Skip to content

web: modernize the browser frontend and page the album/artist lists - #7020

Open
seitzbg wants to merge 12 commits into
beetbox:masterfrom
seitzbg:web-redesign
Open

seitzbg wants to merge 12 commits into
beetbox:masterfrom
seitzbg:web-redesign

Conversation

@seitzbg

@seitzbg seitzbg commented Sep 11, 2026 •

Copy link
Copy Markdown

Description

Rewrites the web frontend as a small ES-module app and drops the bundled jQuery/Backbone/Underscore. No build step or new dependency; Flask still serves static files, and the HTTP API is unchanged apart from the optional additions below.

The UI gains Songs/Albums/Artists browsing, album and track detail with cover art, playback with Media Session controls, light/dark theming, and hash routing so the back/forward buttons work. The track view still shows what the old one did, comments included.

Backward-compatible API additions (omit the params and responses are unchanged):

  • offset/limit on /album/, /artist/, /item/query/ and /album/query/, with an X-Total-Count header on paged lists and limit capped at 500. Query results are sliced after matching, so a limit can't hide a flexible-field match. /item/ is untouched.
  • /artist/ also returns an artist_art map (artist to an album id that has art) for the artist avatars.

Metadata is HTML-escaped client-side; the GHSA-3gxm-wfjx-m847 XSS test is updated accordingly. Album art needs #7015; without it, covers fall back to a gradient.

Screenshots

Captured against a small throwaway sample library (placeholder gradient cover art).

Before — the classic web UI:

Classic beets web UI

After — the redesign (dark, default):

Albums Artists
Albums grid Artists grid
Album view Track view
Album view Track view

The same Albums view in light theme (the UI follows the system light/dark preference):

Albums grid, light theme

The palette here is dark-first with a magenta accent, but that's easy to change — happy to adjust the theme if you'd prefer something more neutral or closer to an existing beets look.

To Do

  • Documentation.
  • Changelog.
  • Tests.

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

codecov Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.29%. Comparing base (103390f) to head (34c4d79).
⚠️ Report is 64 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/web/__init__.py 95.65% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7020      +/-   ##
==========================================
- Coverage   77.39%   77.29%   -0.11%     
==========================================
  Files         163      163              
  Lines       21841    21881      +40     
  Branches     3370     3375       +5     
==========================================
+ Hits        16904    16913       +9     
- Misses       4112     4149      +37     
+ Partials      825      819       -6     
Files with missing lines Coverage Δ
beetsplug/web/__init__.py 76.28% <95.65%> (+3.85%) ⬆️

... 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.

…avatar

The Albums and Artists browse headers put their filter box on the left
instead of the right. Artists have no artwork of their own, so /artist/
now also returns an artist_art map (each artist -> a random one of its
albums that has cover art) and the artist grid renders that cover as the
avatar, falling back to the gradient initial when an artist has no art.
item/query and album/query now accept the same optional offset/limit as
the list endpoints (threaded into lib.items/albums so beets stops building
objects past the page); no params still returns every match, unchanged.
The search box and album filter request one page (300) instead of every
match -- a broad query like 'love' dropped from ~29s/41MB to ~1.9s/0.6MB --
show a 'first 300, refine' hint when capped, and require 2+ characters.
- item/album query paging now slices matched results (post-filter) instead
  of pushing a SQL limit into the query, so a limited query never hides
  flexible-field matches beyond the first database rows
- paginated album list counts via the row count and builds only the
  requested page, instead of constructing every album on each scroll
- artist pages query albumartist exactly (albumartist:=) so "Air" no longer
  pulls in "Air Supply"; a slash in the name is escaped for the path parser
- detail views (track/album/artist) guard async callbacks with a render
  generation so a slow response can't overwrite a view navigated away from
- playback uses the context it was started from: Play-album and row play use
  the album's tracklist; Songs double-click uses the search results
- multi-disc albums order and queue by disc then track
- restore Media Session next/previous/play/pause handlers
- make the volume control functional and keyboard-accessible
- widen json_generator typing for the sliced iterator (mypy)
The classic UI displayed an item's comments field; restore it in the
redesigned track view (escaped, shown only when present) so the new UI
keeps feature parity. Add comments to the XSS regression test's guarded
fields.
@seitzbg seitzbg changed the title web: redesign the interface (modern buildless SPA) and add album/artist paging web: modernize the browser frontend and page the album/artist lists Sep 11, 2026

This branch has not been deployed

No deployments
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