Ants Projects Hub ← All projects

Beta

Album Builder

BetaLNXLatest: v0.5.2

✅ v0.5.2 — UX fix-pass: lyrics-pane fill + row-button play/pause toggle (2026-04-30)

User-reported UX gaps spotted on the v0.5.1 build:

  1. Lyrics pane was small. LyricsPanel called setFixedHeight(150) and NowPlayingPane finished its QVBoxLayout with addStretch(1) after the panel — together those two pinned the lyrics block to ~150 px and let empty space accumulate underneath. On a tall window the lyrics area looked stranded in the upper third of the right pane.
  2. Per-row ▶ button was load-only. Clicking the row's preview-play button on the currently-playing row reloaded the source from scratch (set_source + play), instead of toggling pause as the transport bar's main play/pause button does. The transport's play/pause button worked correctly — only the per-row button lacked the toggle.

Spec amendments (signed off via 2-pass cold-eyes review, 7 → 0 findings):

  • Spec 06 §user-visible-behavior — per-row preview-play button is now a load-or-toggle control with explicit four-state glyph mapping (PLAYING-on-active-source → Glyphs.PAUSE; PAUSED / STOPPED / ERROR / non-active → Glyphs.PLAY). Adds an ERROR-state same-row-click bullet (re-runs set_source + play()) and an Errors-table row for "active source's file removed between load and a same-row click."
  • Spec 06 test contract — TC-06-15 amended to flag that it supersedes the v0.4.0 signal-only assertion in test_library_pane_emits_preview_play_request; new TC-06-17 (active+playing → pause without reload), TC-06-18 (active+paused → resume), TC-06-19 (per-row glyph mapping with library-pane vs album-order-pane a11y bifurcation + dataChanged-row-range observable for the perf claim).
  • Spec 07 §Lyrics panel — panel now fills the available right-pane height below now-playing metadata, with a 150 px minimum (the pre-amendment fixed value) enforced via setMinimumHeight instead of setFixedHeight. New TC-07-16 with a measurable qtbot assertion (resize(420, 800)lyrics_panel.height() >= 300).

Shipped:

  • ✅ Step 3 — failing tests for TC-06-17/18/19 + TC-07-16 (10 of 11 red on the implementation-side, 1 already passing as the existing cross-row case).
  • ✅ Step 4 — implementation (lifted setFixedHeight(150)setMinimumHeight(150), dropped competing addStretch after the lyrics panel, added LibraryPane.set_active_play_state + AlbumOrderPane.set_active_play_state, routed Player.state_changed + source-swap into both panes from MainWindow, added load-or-toggle dispatch in _on_preview_play).
  • ✅ Step 5/6 — /audit + /indie-review in parallel (1 audit + 5 review findings, all L). Round 1 folded inline: pyright None-guard on album_order_pane.py:173, spec-06 PAUSED-vs-STOPPED split (PAUSED→toggle, STOPPED→fresh-load+restart), TC-06-15 marker disambiguation on the v0.4.0 test, and 2 new TC-06-19 tests for the album-order pane (set_album re-render preservation + set_active call-count observable). Round 2 convergence confirmed clean.
  • ✅ Step 8/9 — flipped status to ✅; commit pending; push pending user OK.

Convergence trace: spec amendments 1-pass cold-eyes review (7 → 0 findings), implementation, post-implementation /audit + /indie-review (6 → 0 findings). 471 → 484 passing tests (+13: 11 new TC-06-17/18/19 + TC-07-16 contracts and 2 round-1-fold-out additions). Ruff clean. Manual smoke-launch on the Tracks/ corpus completed without error.

v0.5.2 follow-up — click-to-preview-when-idle (2026-04-30)

Same release wave; user feedback after the v0.5.2 commit: the running ~/.local-installed app was still v0.4.1 (the install.sh deploy hadn't been re-run after the v0.5.2 source-tree commit), so the lyrics-pane fill + per-row pause weren't observed. Triaged that as a deployment gap (re-run install.sh). Same round, the user requested a new behaviour: clicking a track row (anywhere outside the play / toggle column) should populate the now-playing pane with that track's metadata + lyrics, but only when nothing is playing.

Spec amendments (signed off via 1-pass cold-eyes review, 10 → 0 findings):

  • Spec 06 §user-visible-behavior — new top bullet "Row body click previews-without-playing when idle" with sub-bullets for: hit-zones (library = non-_play / non-_toggle columns; album-order = label area only, drag-handle and play button excluded), keyboard-navigation parity (preview is mouse-click only — Enter routes through a separate _on_table_activated slot that handles only _play/_toggle), app-start state (STOPPED on launch, preview enabled from first click; restored last_played_track_path is not mutated by preview), Player.source() decoupling during preview, hover affordance (PointingHand cursor when STOPPED, Arrow otherwise — applies to both panes' viewports).
  • Spec 06 Errors table — new row for "late state_changed(STOPPED) arrives after preview populated the now-playing pane" (preview metadata wins; state_changed only repaints row glyphs, not the now-playing block).
  • Spec 06 test contract — TC-06-20 (STOPPED → preview), TC-06-21 (PLAYING/PAUSED/ERROR → no-op), TC-06-22 (album-order pane parity), TC-06-23 (last_played_track_path not mutated), TC-06-24 (late STOPPED clobber), TC-06-25 (keyboard nav inert), TC-06-26 (cursor mapping for all four states).

Code changes:

  • LibraryPane — new row_body_clicked signal; _on_table_clicked emits it for non-_play/_toggle columns; new _on_table_activated slot handles keyboard activation (no row-body branch); new set_row_body_cursor_for_state(stopped=) method.
  • _OrderRowWidget — new body_clicked signal; overridden mousePressEvent captures press position; overridden mouseReleaseEvent emits body_clicked only when the press → release delta is within QApplication.startDragDistance() (so genuine drags don't fire previews); label has WA_TransparentForMouseEvents so clicks fall through to the row widget.
  • AlbumOrderPane — new row_body_clicked signal; set_album connects each row's body_clicked to re-emit the row's path; new set_row_body_cursor_for_state(stopped=) mirroring the library pane.
  • MainWindow — new _on_row_body_clicked(path) handler gates on Player.state() == STOPPED, then set_track + _sync_lyrics_for_track; missing-track shows toast (mirroring _on_preview_play); _on_player_state_changed_for_rows flips both panes' cursors and is now also called once at end of __init__ so the construction-time cursor is correct without waiting for a state-change emission.

Audit/review trace: post-implementation /audit + /indie-review (12 → 0 findings, all H+M folded inline). 484 → 500 passing tests (+16: 9 initial TC-06-20..26 contracts + 7 round-1-fold-out additions covering ERROR-state no-op, PAUSED+ERROR cursor, real-mousePress plumbing, play-button-negative case, fresh-LRC lyrics population, Enter-doesn't-preview, construction-time cursor). Ruff clean. Manual smoke pending on ~/.local install (gated on the running v0.4.1 instance being closed first).


All releases on GitHub →

Album Builder

A small PyQt6 desktop app for curating albums from a folder of audio recordings, designed for Linux/KDE.

Status

v0.6.1 — WhisperX UX + artist-view report + post-feature debt sweep (shipped 2026-05-18) on top of v0.6.0 — Phase 5: Track Usage Indicator (shipped 2026-05-01). Phases 1-5 are feature-complete. The app scans Tracks/, curates albums via a per-row toggle column + drag-reorder pane, syncs lyrics during preview-play (WhisperX

  • wav2vec2 forced alignment, opt-in), shows a cross-album usage badge so tracks already on approved albums are visible at a glance, and on approve generates an M3U + numbered-symlink folder + PDF/HTML report (full report plus a stripped-down artist-view variant for sharing) under Albums/<slug>/. State persists across launches; library refreshes live when Tracks/ changes.

See ROADMAP.md for the full release log and docs/plans/ for per-phase implementation details.

Install (openSUSE Tumbleweed + KDE Plasma)

./install.sh

Then launch from the K Menu under Multimedia → Album Builder, or run album-builder from a terminal.

System dependencies

The installer assumes these are present:

  • Python 3.11+ (zypper install python311)
  • GStreamer audio plugins (zypper install gstreamer-plugins-good gstreamer-plugins-bad gstreamer-plugins-libav)
  • desktop-file-utils (for validation; optional)
  • Inkscape OR rsvg-convert OR cairosvg (for icon PNG generation; the installer falls back to cairosvg via pip if the others are missing)
  • WeasyPrint runtime libraries — Pango / Cairo / GDK-PixBuf, plus the standard fontconfig/freetype stack — for PDF report rendering (zypper install pango cairo gdk-pixbuf fontconfig; libffi is pulled in transitively by cairo/pango). On Debian / Ubuntu the equivalent set is libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 libharfbuzz-subset0. WeasyPrint's installation guide lists per-distro details if the import fails at runtime.

Uninstall

./uninstall.sh           # removes app, preserves user settings
./uninstall.sh --purge   # also removes ~/.config/album-builder and ~/.cache/album-builder

Develop

python3.11 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest
.venv/bin/python -m album_builder      # run from source

Layout

  • src/album_builder/ — application source
  • tests/ — pytest suite (domain + UI)
  • docs/specs/ — per-feature specifications
  • docs/plans/ — phased implementation plans
  • packaging/.desktop template
  • assets/ — icon
  • install.sh / uninstall.sh — per-user installer

License

MIT — see LICENSE.