Three related improvements to the now-playing media player and the
"add to wishlist" / "download missing" modals.
1. Play buttons across track-list modals
Every track row in the download-missing modals (Spotify, Tidal,
YouTube, services, artist album, wishlist download-missing) and
the add-to-wishlist modal now carries a play button. Click runs
playTrackFromLibraryOrStream:
- If the track has a local file_path → playLibraryTrack
- Else POST /api/stats/resolve-track to find it in the library
by title + artist → playLibraryTrack
- Else fall back to _gsPlayTrack streaming
Backend ownership response gains track_id / title / file_path so
the wishlist modal's owned tracks can hand the right metadata
to the player without an extra round trip.
The add-to-wishlist modal previously showed the play button only
on owned tracks; now the button is unconditional so the streaming
fallback can take over for unowned ones (matches the standard
pattern from the rest of the app).
2. Clean media-player display titles
YouTube / Tidal / Qobuz / torrent / usenet plugins encode their
source-side identifier into the filename field as
<source_id>||<display> so download() can recover it later. The
media player's track-title renderer never knew about this
convention and showed strings like
"wvgFsXoGFnQ||Sometimes I Cry When I'm Alone" verbatim in the
now-playing UI. extractTrackTitle and setTrackInfo now strip the
<id>|| prefix defensively so any path into the player gets a
clean display.
Local library playback also fetches canonical metadata from
/api/stats/resolve-track when track.id is present so title /
artist / album / album art come straight from the SoulSync DB
instead of whatever the caller passed in. Falls back silently
to caller values on any error so playback never blocks on the
metadata fetch.
3. Lyrics panel + View Artist close
New collapsed lyrics panel between the playback controls and
queue panel. POST /api/lyrics/fetch (new backend endpoint)
prefers the local .lrc / .txt sidecar files SoulSync writes
during post-processing so downloaded tracks resolve lyrics with
zero network hits; falls back to LRClib exact-match (when album
+ duration are available) then to LRClib search.
Synced LRC results are parsed (handles multi-stamp lines for
repeated choruses), and the active line highlights + smooth-
scrolls into the middle of the viewport on every audio
timeupdate. Plain-text results render without highlighting.
Per-track cache prevents re-fetching when the user revisits the
same track. Lyrics fetch is fire-and-forget — failure shows
"No lyrics found" without ever blocking playback.
View Artist on the expanded player now calls
closeNowPlayingModal before navigating; the modal was previously
sitting open over the artist page, hiding it. Handler is bound
once and is a no-op when no artist_id is attached.
CSS additions are additive (new .modal-track-play-btn and
.np-lyrics-* rules); no existing styles touched. Backend endpoint
returns 200-with-success-false on any miss so callers can render
"no lyrics" without treating it as an error.
WHATS_NEW updated under 2.5.9 with two entries (lyrics + View
Artist close).
|
||
|---|---|---|
| .. | ||
| src | ||
| static | ||
| tests | ||
| .gitignore | ||
| .oxfmtrc.json | ||
| .oxlintrc.json | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
WebUI Hybrid Rendering
SoulSync's web UI is in a transition phase:
- most pages still render through the legacy vanilla JS shell
/issuesis rendered by the new React app- a small shell bridge keeps both runtimes aware of the active page, profile context, and navigation state
How It Fits Together
flowchart LR
Browser["Browser parses /webui/index.html"]
Legacy["Legacy shell scripts\n(core.js -> ... -> init.js)"]
Bridge["shell-bridge.js\nwindow.SoulSyncWebShellBridge"]
React["Vite React app\nsrc/app/main.tsx"]
Router["TanStack Router\nwindow.SoulSyncWebRouter"]
Browser --> Legacy
Browser --> React
Legacy --> Bridge
React --> Router
Router --> Bridge
Bridge --> Legacy
Runtime Roles
-
webui/static/init.js- boots the legacy shell
- selects the active profile
- handles the legacy page loading flow
-
webui/static/shell-bridge.js- owns the browser-side bridge object
- exposes
window.SoulSyncWebShellBridge - owns the shared page chrome and route handoff helpers
-
webui/src/app/main.tsx- mounts the React app
- binds
window.SoulSyncWebRouter
-
webui/src/platform/shell/route-controllers.tsx- listens for bridge readiness
- keeps React pages aligned with the shell
Load Order
The current order in index.html matters:
- legacy shell scripts load first
init.jssets up the shell runtimeshell-bridge.jspublishes the bridge and shared chrome helpers after the shell state exists- the Vite React app is injected through
{{ vite_assets('body') }}and boots as a module after parsing
That order avoids load-time references to missing globals and keeps the React side able to react to bridge readiness events. The React entry can start fetching early, but the shell bridge and legacy globals are already available by the time the React runtime starts acting on them.
Notes
- The bridge is intentionally small and browser-only.
- This is the start of the migration, not a full replacement of the legacy shell.
- When adding another React page, check whether it needs:
- a route entry in
webui/src/platform/shell/route-manifest.ts - bridge typings in
webui/src/platform/shell/globals.d.ts - a legacy fallback path in
webui/static/init.js - bridge glue or handoff logic in
webui/static/shell-bridge.js
- a route entry in
Folder Layout
The React webui uses a small set of predictable folders so route slices stay easy to extend, test, and understand.
webui/src/
app/ React bootstrap, router, query client, shared API client
components/ Shared UI primitives
platform/ Shell bridge and browser/platform integration
routes/ Route-local code and TanStack Router pages
test/ Shared test utilities and setup helpers
Route Slices
- Keep route-specific code inside
webui/src/routes/<route>/. - Put the routing entry in
route.tsx. - Put route-local UI in a
-ui/folder. - Prefix non-routing files with
-so TanStack Router ignores them. - Keep the route slice small and cohesive.
- Prefer a few files with clear responsibilities over many tiny files with overlapping names.
Example:
webui/src/routes/issues/
route.tsx
-issues.types.ts
-issues.api.ts
-issues.helpers.ts
-issues.api.test.ts
-issues.helpers.test.ts
-ui/
issues-page.tsx
issue-detail-modal.tsx
issue-domain-host.tsx
The initial issues slice is the model to follow:
-issues.api.tsholds request code and query options-issues.helpers.tsholds pure normalization and formatting-issues.types.tsholds shared types-ui/holds the page, modal, and legacy handoff UI
Shared Code
- Put reusable UI in
webui/src/components/. - Put shell integration in
webui/src/platform/. - Put bootstrap and app-wide wiring in
webui/src/app/. - Move code up a level only when it is genuinely shared.
- Avoid creating new conventions that overlap with existing ones.
Testing Choices
We have a lot of testing tools available, but we do not need all of them for every feature.
- Use plain unit tests for pure functions and small transforms.
- Use React component or route tests when the behavior lives in the UI or router.
- Use MSW-backed tests when request shape, response handling, or error handling matters.
- Use Playwright when the behavior is best proven end-to-end with the server and browser together.
- Prefer the smallest test setup that still proves the thing that can regress.
Development
The repo root now owns the full local-dev instructions. Start there for the portable launcher and backend/frontend setup:
- README.md for the end-to-end dev flow
npm run checkandnpm run fixfor React-side linting and formatting