mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
bd2d669b992278c0cb573445e01555906459a2db
34 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd2d669b99 | refactor(ui): co-locate OverlayScrollArea as a shared primitive in src/ui | ||
|
|
7c724a642f |
chore(eslint): add ESLint toolchain and clean src to strict 0/0 (#1165)
* chore(eslint): add eslint toolchain and configs * fix(eslint): resolve gradual-config errors (rules-of-hooks, no-empty, prefer-const, …) * chore(eslint): clear unused vars in config, contexts, app and test * chore(eslint): clear unused vars in api and music-network * chore(eslint): clear unused vars in utils * chore(eslint): clear unused vars in store * chore(eslint): clear unused vars in cover and hooks * chore(eslint): clear unused vars in components * chore(eslint): clear unused vars in pages * chore(eslint): remove explicit any in src * chore(eslint): align react-hooks exhaustive-deps * chore(eslint): zero gradual config on src * chore(eslint): strict hook rules in store and utils * chore(eslint): strict hook rules in hooks and cover * chore(eslint): strict hook rules in components * chore(eslint): strict hook rules in pages and contexts * chore(eslint): document scripts ignore in eslint config * chore(eslint): add lint script and zero strict findings * chore(eslint): address review round 1 (gradual 0/0, per-site disable reasons) * chore(eslint): tighten two set-state disable comments (review round 2 LOW) * chore(nix): sync npmDepsHash with package-lock.json * docs(changelog): add Under the Hood entry for ESLint setup (PR #1165) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
1a82376f8c |
feat(music-network): unified scrobble & enrichment framework (replaces hard-wired Last.fm) (#1066)
* feat(music-network): core domain types and wire contracts
Foundation for the Music Network framework: provider-agnostic domain
types, capability model, typed errors, and account shapes under
src/music-network/core, plus the ScrobbleWire / EnrichmentWire /
PresetManifest / AuthStrategy contracts. No runtime wiring yet.
* feat(music-network): generic audioscrobbler/listenbrainz/maloja transports
Generalize the Rust remote layer for the Music Network framework. Add
provider-agnostic transports parameterized by base_url:
- audioscrobbler_request: Audioscrobbler v2 with caller-supplied endpoint
(Last.fm, Libre.fm, Rocksky, custom GNU FM, Maloja compat share it)
- listenbrainz_request: Token-header JSON (direct + Maloja LB compat)
- maloja_request: native /apis/mlj_1 JSON
lastfm_request stays as a thin transition delegate against the fixed
host; it is removed once the framework owns all call sites. Wiremock
tests cover audioscrobbler_request with a custom base_url and API-error
mapping.
* feat(music-network): audioscrobbler wire with last.fm + libre.fm presets
Add the Audioscrobbler v2 wire, the behavioural successor to the legacy
src/api/lastfm.ts, implementing the full EnrichmentWire surface (scrobble,
now playing, love/unlove, loved sync, similar artists, track/artist stats,
top lists, recent tracks, user profile, urls).
- client.ts: transport wrapper over audioscrobbler_request, classifying
failures into MusicNetworkError without touching any store
- sign.ts: TS mirror of the api_sig base-string ordering rule (unit-tested)
- auth/tokenPoll.ts: browser token-poll connect flow as a reusable strategy
- presets/lastfm.ts, presets/librefm.ts: bundled, enrichment-capable,
token-poll presets (both endpoints verified live)
Extends WireContext with profileBase and ConnectContext with authBase so
URL builders and connect flows need no preset lookup.
* feat(music-network): listenbrainz + maloja-native wires, paste-auth presets
Add the scrobble-destination wires and presets:
- ListenBrainz wire (scrobble + now playing via playing_now), backing both
the direct api.listenbrainz.org preset and the Maloja /apis/listenbrainz
compat preset (one wire, two presets, differing only by base URL)
- Maloja native wire (/apis/mlj_1/newscrobble, scrobble only — Maloja has
no now-playing endpoint)
- Shared api_key_only paste-auth strategy (token/key/session-key paste);
the Audioscrobbler wire now dispatches token-poll vs paste by preset
- Presets: listenbrainz, maloja_listenbrainz, maloja_native, rocksky
(scrobble-only, session-key paste, bundled keys — verified live), and
custom_gnufm (token-poll, user-supplied url/key/secret)
- Contracts: ConnectContext.authStrategy, PresetManifest.selfHostedApiSuffix
maloja_compat (the {url}/apis/audioscrobbler mode) is intentionally omitted:
its protocol cannot be verified and is almost certainly the legacy handshake,
not the 2.0 web API; Maloja is covered by the native and ListenBrainz modes.
* feat(music-network): registry, orchestrator, enrichment router + runtime facade
Wire the framework together behind a single facade:
- registry: wireRegistry (WireId -> wire), presetRegistry (the 7 built-in
presets), registerBuiltinWires (one-time side-effect registration)
- CapabilityProbe: wire probe overlaid by manifest staticCapabilities as the
final authority (lets two presets on one wire diverge, e.g. Rocksky's
nowPlaying:false over the Audioscrobbler wire's optimistic yes)
- ScrobbleOrchestrator: best-effort fan-out; flips the per-account
session-error flag on AUTH_SESSION_INVALID and clears it on next success
- EnrichmentRouter: resolves the single primary to its EnrichmentWire; the
type guard rejects non-enrichment wires (Maloja/ListenBrainz)
- MusicNetworkRuntime: the only app entry point — accounts, roles, fan-out,
enrichment, urls, probe. Reads/writes state through the MusicNetworkStore
port (Phase 5 backs it with the auth store) and a RuntimeHost for side effects
- getMusicNetworkRuntime singleton + index.ts public barrel
Tests cover fan-out, master toggle, capability gating, session-error
flip/clear, primary eligibility, and enrichment routing.
* feat(music-network): auth-store state + lossless legacy migration + runtime bridge
Add the persisted Music Network state to the auth store and wire the runtime,
all additively — nothing existing breaks yet.
- authStoreTypes: musicNetworkAccounts / enrichmentPrimaryId /
scrobblingMasterEnabled + actions; legacy lastfm* fields kept until Phase 6
- authMusicNetworkActions + defaults/wiring (synchronous localStorage)
- accountPersistence: migrateLegacyLastfm (lossless — preserves session key,
username and scrobbling preference; fills bundled Last.fm key from the preset;
sets the migrated account as enrichment primary) + sanitizeAccounts
- authStoreRehydrate: one-shot migration guarded by a sentinel so a later
disconnect cannot resurrect the account from still-present legacy fields
- musicNetworkBridge: backs the MusicNetworkStore port with the auth store and
the RuntimeHost with the Tauri shell; initialized in pre-React bootstrap
nowPlayingEnabled stays a global toggle (not a lastfm* field); the Phase 6
playback call-site will gate dispatchNowPlaying on it, preserving behaviour.
* feat(music-network): route playback, enrichment and love through the runtime
Migrate every Last.fm call-site onto the Music Network runtime, preserving
behaviour:
- playback (audioEventHandlers, playTrackAction): scrobble@50% and now-playing
via dispatchScrobble/dispatchNowPlaying; loved-fetch via isTrackLoved. Now-
playing follows scrobbling (as Last.fm did), Navidrome now-playing keeps the
nowPlayingEnabled gate
- enrichment: useArtistSimilarArtists, useNowPlayingFetchers, Statistics, and
the ArtistDetail similar-artists gate now use the runtime, gated on an
enrichment primary
- love: PlayerBar, PlayerTrackInfo, all context menus, useNowPlayingStarLove and
the startup loved-sync route through setTrackLoved / toggleNetworkLove
- player store: lastfmLoved/lastfmLovedCache -> networkLoved/networkLovedCache;
lastfmActions -> networkLoveActions; loved cache storage renamed with a
lossless legacy-key fallback
- getMusicNetworkRuntimeOrNull() for best-effort callers so they no-op (not
throw) before the runtime is initialized
src/api/lastfm.ts and the Integrations UI still use the legacy path; they are
migrated and removed in the next phase.
* feat(music-network): manifest-driven Integrations UI + scrobble batch format
Replace the Last.fm Integrations card with a manifest-driven Music Network
section, and fix Audioscrobbler scrobbling to the batch/array shape.
- settings/musicNetwork/: MusicNetworkSection (master toggle, destination
cards, enrichment-primary picker, Maloja proxy warning, add-a-service list)
driven entirely off the preset registry; icon map from PresetManifest.icon
- IntegrationsTab delegates to MusicNetworkSection (Discord/Bandsintown/
Navidrome now-playing unchanged)
- i18n: musicNetwork.* across all 9 locales, incl. a per-field help hint for
Rocksky's CLI session-key flow (rocksky login)
- scrobble now uses the documented array form (artist[0]/track[0]/…); the bare
single form is only tolerated by Last.fm, Rocksky requires the indexed form
- auth-error detection keys off the response message (not the ambiguous numeric
code) so a Rocksky server-500 no longer flips the account to a reconnect state
- PresetManifest.PresetField gains an optional helpKey
Rocksky's server rejects some non-ASCII track metadata with a 500 — a Rocksky
backend bug; the client call is correct (verified).
* fix(music-network): clearer Integrations layout + scrobble batch fix
Address UI feedback on the Music Network section:
- per-account scrobble toggle moves inside its account block (was a loose
row between cards — unclear which account it belonged to)
- master toggle and the primary-service picker are now boxed blocks at the top,
not bare rows
- primary-service copy reworked: 'Primary service' + a line spelling out that
liked tracks/similar artists/stats come from it while scrobbling still goes
to all enabled services
- distinct zones separated by dividers (master/primary · connected · add)
Also folds in the verified scrobble fixes: documented array form
(artist[0]/track[0]/…) so Rocksky accepts scrobbles, and auth-error detection
by message rather than the ambiguous numeric code (a Rocksky server-500 no
longer flips the account to a reconnect state). Rocksky session-key field gains
a CLI help hint (rocksky login), all 9 locales.
* feat(music-network): indicator + remove legacy lastfm path
Phase 7b/7c — finish the cutover and delete the old Last.fm path.
- LastfmIndicator -> MusicNetworkIndicator (shows the enrichment primary's
status, click -> Integrations)
- delete src/api/lastfm.ts; remove Rust lastfm_request (remote.rs + lib.rs)
- remove legacy authStore lastfm* fields, actions and types; delete
authLastfmActions.ts; rehydrate migration reads the legacy blob via a cast
- migrate the remaining NowPlaying call-sites (NowPlaying.tsx + the now-playing
fetchers/prewarm/star-love hooks) off lastfmSessionKey/lastfmUsername onto the
enrichment primary (gate + cache key)
- type imports LastfmTrackInfo/LastfmArtistStats -> music-network TrackStats/
ArtistStats; drop 8 stale api/lastfm test mocks and the obsolete Last.fm auth
tests; update settingsTabs + src/CLAUDE.md
No lastfm imports remain outside src/music-network/; lastfm_request removed
(acceptance §12). tsc clean, 1947 frontend tests + remote rust tests green.
* test(music-network): cover scrobble shape + error classification, drop dead i18n keys
Remove the 14 unused legacy scrobble/connection i18n keys across all 9
locales (settings.lfm*/scrobble*, connection.lastfm*); the live love,
profile-link and now-playing keys stay.
Add regression tests for the parity-critical transport logic: the indexed
batch/array scrobble body, the auth-vs-network error classification
(numeric codes collide across providers), and the manifest-overrides-probe
capability merge.
* feat(music-network): provider-agnostic UI + Maloja Audioscrobbler & Koito presets
- de-hardcode the single provider name across every enrichment surface
(love labels, now-playing badge, stats title); derive it from the
enrichment primary and interpolate via {{provider}} i18n params
- surface a toast when a paste-auth connect probe fails — a static
'supported' capability flag no longer masks a runtime probe error
- add the Maloja Audioscrobbler (GNU FM) preset (the third Maloja wire
mode) and a Koito preset (ListenBrainz-compatible), both data-only
- generalise PRIVACY.md and the scrobbling help entry to the framework
- rename residual lfm* identifiers to network*; strip legacy flat
lastfm* fields from the persisted blob; neutral transport error prefix
- tests: error classification, scrobble body, capability probe, registry
* fix(music-network): validate paste-auth keys on connect + UI polish
- AudioscrobblerWire.probe now validates an api_key_only session with a
signed call and reports scrobble:'error' only on a genuine auth failure
(a scrobble-only service that rejects user.getInfo is not a bad key), so
an invalid Maloja Audioscrobbler / Rocksky key surfaces a connect toast
instead of failing silently; WireContext carries the preset authStrategy
- drop the unreachable Statistics empty-state branch and its dead
lfmNotConnected i18n key; use the useEnrichmentPrimaryLabel hook there
- drive the love-button glyph from the enrichment primary's manifest icon;
neutral Music Network section icon; remove a dead LastfmIcon import
- tests: paste-auth vs token-poll probe behaviour
* chore(music-network): rename showLastfmSimilar → showNetworkSimilar, refresh stale comments
Post-parity polish: the similar-artists toggle now sources from the generic
enrichment runtime, so rename the lingering lastfm-flavoured identifier; drop
stale 'Mirrors today's LastfmX' doc comments referencing the removed legacy
types, and generalise the scrobble-point comment.
* docs(changelog): Music Network entry (PR #1066)
Co-Authored-By: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
* fix(music-network): bound request timeout on the provider transports
audioscrobbler_request / listenbrainz_request / maloja_request built a
reqwest client with no timeout, so a hung provider left scrobble / probe /
loved-sync promises unresolved. Add a shared provider_http_client() with a
15s timeout, matching the sibling fetch_* commands. Addresses review C3.
* refactor(music-network): dedupe wire transport + no-enrichment helpers
The three provider clients repeated the same invoke -> classify-error ->
MusicNetworkError boilerplate, and the three probe() bodies repeated the
"mark every enrichment capability no" loop. Extract
wires/shared/invokeTransport() (each wire keeps its own arg shape + auth
rule) and markNoEnrichment() in core/capabilities.ts. Addresses review C4.
* refactor(music-network): drop write-only malojaWireMode dead state
malojaWireMode was written on connect but never read — the wire is resolved
by wireId and the Maloja base URL by the preset's selfHostedApiSuffix.
Remove the field, the MalojaWireMode type, the malojaWireModeFor helper,
the AccountPatch entry, the PresetField union member, and the export.
Addresses review C1.
* refactor(music-network): one useEnrichmentPrimary hook, drop lastfm fallback
The enrichment-primary lookup (accounts.find by enrichmentPrimaryId) was
duplicated across two hooks and inlined in the indicator and both
context-menu builders, two of them with a hardcoded 'lastfm' icon fallback.
Add one music-network/ui/useEnrichmentPrimary() returning
{account,label,icon}|null; useEnrichmentPrimaryLabel/Icon delegate to it and
the indicator + context menus consume it directly. Icon fallback is the
neutral 'custom' glyph, never a provider (provider-agnostic, §7.3).
Addresses review C2.
---------
Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
|
||
|
|
a151cf5deb |
fix(now-playing): keep metadata cards when the track plays from local cache (#1042)
* fix(now-playing): keep metadata cards when the track plays from local cache The Now Playing metadata fetches (album, artist info, discography, top songs) gated on shouldAttemptSubsonicForServer(serverId, trackId). That guard returns false when the current track's audio resolves to a psysonic-local:// URL (hot cache / offline bytes), which is correct for playback-byte calls but wrongly suppressed the metadata calls. On track change the prefetched next track plays from the hot cache, so every Subsonic-backed card blanked and only the Last.fm bio fallback and Bandsintown tour (both non-Subsonic) remained. Drop the trackId from the four Now Playing metadata gates so metadata is fetched whenever the server is reachable, regardless of where the audio bytes come from. True-offline is still handled by the online / reachability checks in the guard. The guard and all other call sites are unchanged. * docs: add CHANGELOG entry for PR #1042 * refactor(now-playing): single Subsonic network gate owned by the fetchers hook The metadata reachability guard (shouldAttemptSubsonicForServer) ran 2-3 times per Now Playing render/effect cycle: callers folded it into fetchEnabled in NowPlaying.tsx and useNowPlayingPrewarm on top of the checks already inside useNowPlayingFetchers and prewarmNowPlayingFetchers. Callers now pass fetchEnabled as intent only ("we have a playback server id"); the single reachability decision lives in the hook / prewarm function. Behaviour is unchanged — only the duplicated guard calls go. * test(now-playing): cover local-playback metadata behaviour, not only call shape Add a guard test proving the metadata gate (no trackId) bypasses the psysonic-local:// skip while a byte-style call stays blocked, and a hook test that fetches album/discography/top songs with a trackId-sensitive guard — so a reintroduced trackId at the gate fails the suite instead of silently blanking the cards. |
||
|
|
2d3c723a6e |
feat(offline): unify local playback, offline library, and favorites sync (#1008)
* feat(local-playback): LP-1 media layout and download_track_local
Add library-index-backed path builder in psysonic-core and a unified
Tauri download command that writes under media/{cache|library}/ with
layout fingerprints; legacy hot/offline commands unchanged for now.
* feat(local-playback): LP-2 localPlaybackStore and media tier Rust helpers
Add unified Zustand index with legacy offline/hot-cache import, media_layout
TS mirror, and Rust commands for tier size/purge/delete/promote.
* feat(local-playback): LP-3 wire prefetch and playback to unified index
Route downloads through download_track_local, delegate hot/offline shims
to localPlaybackStore, and update resolve/promote/prefetch plus key rewrite.
* feat(local-playback): LP-4–LP-6 offline UI, invalidation, and mediaDir
Offline Library loads pinned groups via library index; sync-idle invalidates
stale paths; Settings uses a single mediaDir with cache/library tier sizes.
* feat(local-playback): migrate legacy offline files to media/library layout
Move flat psysonic-offline downloads into nested media/library paths using
library index metadata, with retry on sync-idle when tracks are not yet indexed.
* feat(local-playback): simplify offline disk migration and restore Offline Library UI
Scan psysonic-offline on disk and relocate by library track id; restore pinSource
and cover art for migrated pins; add find_live_by_id for segment/key resolution.
* feat(local-playback): disk-first offline reconcile and fast library tier discovery
Reconcile library-tier index against on-disk files using candidate track IDs
instead of scanning the full catalog. Refresh Offline Library from disk on
open and focus so deleted folders drop out of the UI. Add Rust discover/prune
helpers and wire album/server reconcile through the unified path.
* fix(offline): resolve local playback URLs across server index-key variants
Offline Library play failed when library-tier files were indexed under a
host key while playback looked up only the active profile UUID. Use
findLocalPlaybackEntry for URL resolution, pin queueServerId to the card
server, and build play queues from tracks that still have on-disk bytes.
* fix(offline): playlist cards, playback from Offline Library, and local URL routing
Show playlists with name and quad/custom cover instead of the first track's
album artist. Build play queues with library-batch fallback and offline-only
server switch. Prefer library-tier URLs in playTrack; add playback-unavailable
toast and missing trackToSong import.
* fix(cache): ephemeral disk reconcile, empty-dir prune, and Storage UI
Sweep media/cache after eviction (orphan files, stale index, empty folders).
Settings: split media folder from cover cache; in-browser image cache lives
under Cover art cache with aligned columns; clear only IndexedDB images.
* feat(offline): show library disk usage in Offline Library header
Query media/library tier size on reconcile and display it in a right-aligned
stat block beside the page title and album count.
* fix(cache): defer unindexed hot-cache eviction; drop legacy offline size cap
Reconcile ephemeral cache without deleting files from other app instances;
evict unindexed hot-cache files oldest-first only when over hotCacheMaxMb.
Remove the hidden maxCacheMb gate and offline-full banner on album pages.
* feat(offline): play-all cache card and stable Offline Library grid rows
Add a shuffle-and-play card for all on-disk library pins plus hot-cache
tracks when buffering is enabled. Fix virtual row height for offline cards
and reserve the year line so grid rows no longer overlap.
* feat(offline): queue-cache grid card limited to media/cache
Replace the full-width play-all banner with a playlist-style grid tile.
Shuffle/enqueue only ephemeral hot-cache tracks when buffering is enabled,
not offline library pins.
* chore(licenses): regenerate bundled OSS list for 1.48.0-dev
Set GPL-3.0-or-later on workspace crates and extend cargo-about accepted
licenses so generate-licenses.mjs runs; refresh src/data/licenses.json.
* feat(favorites): auto-sync starred tracks into separate media/favorites tier
Keep manual Offline Library in media/library/ and favorites offline in
favorite-auto/index + media/favorites/ so toggling sync cannot purge
user-pinned bytes; playback resolves library before favorites.
* feat(favorites): compact offline toggle with disk icon and sync semaphore
Move control to the page header (disk + switch, tooltips); show red/yellow/green
LED when enabled instead of the full-width save-offline card.
* fix(favorites): trigger offline sync on star/unstar from anywhere
Hook star/unstar API so favorites offline reconcile runs globally (songs,
albums, artists); optimistic unstar removes local bytes; drop Favorites-page-only sync.
* fix(cache): skip hot-cache prefetch when favorites or library bytes exist
Treat favorite-auto tier like offline library for prefetch, stream promote,
and same-track replay so synced favorites are not duplicated in media/cache.
* fix(favorites): reconcile offline files on merged track union only
Dedupe artist/album/song stars into one target set per track id; drop eager
unstar deletes so overlapping favorites do not remove bytes still needed.
* feat(offline): add Favorites card to Offline Library
Mirror queue-cache card for favorite-auto tier with play, enqueue, and
navigation to Favorites on card click.
* feat(offline): show library+favorites disk total with icon breakdown
Sum media/library and media/favorites in the On disk widget and open an
icon popover on hover with per-tier sizes for screen readers and sighted users.
* feat(favorites): enable offline Favorites tab when auto-save is on
Keep Favorites in the sidebar when disconnected, land on /favorites without
manual pins, and load starred rows from the local library index.
* feat(favorites): cross-server offline browse with per-server covers
When auto-save is on, Favorites merges starred items from every indexed
server and syncs each server independently. Detail links carry ?server=
for offline album/artist pages; cover art resolves disk cache by entity
serverId instead of the active server only.
* feat(playback): mixed-server queue scope and cross-server favorites sync
Per-ref server identity for playback (URL index key in queue refs, profile
UUID for API): trackServerScope, playbackServer helpers, gapless/scrobble/covers
by playing ref. Remove cross-server enqueue block; remap queueItems on URL
remigration.
Favorites: star/unstar and favorite-auto sync target the owning serverId
(not only active); queueSongStar passes server through pending sync.
* fix(offline): suppress Subsonic calls during favorites and local playback
Add reachability guards so offline favorites browse, album detail, queue
sync, scrobble, and Now Playing metadata skip network when the server is
down or the track plays from psysonic-local. Load starred albums/tracks
from the library index only (not the full artist table), refresh favorites
from index first, and pass server scope in favorites navigation.
* fix(queue): export share and playlist save for active server only
Mixed-server queues now filter queue refs by the browsed server profile
before copying a share link or saving/updating a playlist from the toolbar.
* fix(offline): complete album pins and resume interrupted downloads
Prefer full getAlbum track lists when online so partial library index
does not truncate offline pins; refresh songs before pin from album
detail. Resume incomplete persisted pins after reconcile and reconnect,
cancel in-flight work on delete, and chunk library batch fetches past
100 refs.
* fix(offline): pin queue, queued UI, and re-pin after remove
Serialize album and playlist offline pins so parallel enqueue no longer
drops in-flight work. Show an explicit queued state on album and playlist
actions with dequeue on repeat click, sidebar tooltips for long labels,
and clear stale cancel flags so Make available offline works after remove.
* fix(offline): remove Offline Library cards without full page reload
Optimistically drop the deleted card from local grid state, show the
loading spinner only on first visit, and ignore stale disk refreshes so
pin updates no longer flash the whole library view.
* fix(offline): artist discography pin state and queue handling
Detect cached/queued/downloading from persisted album pins instead of
ephemeral bulkProgress, skip already offline or in-flight albums when
enqueueing discography, and show the correct hero button after revisit.
* fix(local-playback): address LP-1 review handoff (B1, M1–M7)
Harden media path sanitization and tier containment, align Rust/TS layout
fingerprints, serialize per-track downloads, and fix favorites re-enable,
multi-server debounce, prev-track promote key, now-playing reachability,
and ephemeral prefetch soft-skip for unindexed tracks.
* fix(offline): cancel in-flight favorites downloads on unstar
Abort Rust streams with the real favorites downloadId when sync is
rescheduled or disabled, and drop completed bytes that no longer belong
in the starred set so unstar does not leave orphan files on disk.
* fix(build): resolve TypeScript errors blocking prod nix build
Align mediaLayout with LibraryTrackDto camelCase, extend analysis-sync
reasons, fix OfflineLibrary grid cover typing, and tighten vitest mocks
so `tsc && vite build` passes under the flake beforeBuildCommand.
* fix(rust): satisfy clippy too_many_arguments for CI
Bundle offline-library analysis and local path/migration helpers into
parameter structs so `cargo clippy -D warnings` passes on the branch.
* fix(settings): show correct hot-cache track count in Buffering section
Count ephemeral localPlayback rows instead of prefix-matching index keys,
which always missed host:port server segments and showed zero tracks.
* fix(media-layout): align truncation threshold on code points (M1)
Rust sanitize_and_truncate_segment now uses char count like TS so long
non-ASCII metadata does not diverge layout fingerprints; add Cyrillic
parity tests and clarify ephemeral cold-miss doc on download_track_local.
* fix(test): use numeric cachedAt in hotCacheStore count test
Align test fixture with LocalPlaybackEntry type so tsc passes in CI.
* docs: add CHANGELOG and credits for offline experience PR #1008
* feat(offline): auto-sync manually cached playlists when track list changes
Re-download new tracks and prune removed ones for playlist pins only, triggered
from updatePlaylist, playlist detail load, smart-playlist polling, and reconnect.
* docs: note cached-playlist sync in CHANGELOG and credits for PR #1008
* fix(offline): exclude smart playlists from manual offline cache and sync
Hide cache-offline for psy-smart-* playlists, block download/sync paths, and
document the distinction in CHANGELOG.
* feat(offline): auto-sync cached albums and artist discographies
Generalize pinned playlist reconcile into pinnedOfflineSync so manually
pinned albums and artist discographies re-download added tracks and prune
removed ones on reopen, reconnect, and catalog changes.
* feat(offline): split pinned sync triggers by pin kind
Album and artist pins reconcile after library index sync and reconnect;
regular playlists reconcile hourly and on in-app playlist edits only.
Remove reconcile-on-open for album, artist, and playlist detail views.
* fix(offline): address PR #1008 review (N1, tests, pin queue)
Scope playlist reconcile to the owning server via getPlaylistForServer.
Add artist discography and mixed-server playlist tests; dedupe pending
sync jobs; skip pinTasks overwrite during active downloads.
|
||
|
|
4c70408bd6 |
fix(now-playing): split artist links and About the Artist tabs (#960)
* fix(now-playing): split artist links and About the Artist tabs OpenSubsonic artists[] now drives per-artist navigation on Now Playing hero and the queue current-track row (matching player bar). About the Artist loads bio for each performer via tabs when a track has multiple artist ids; queue Info uses the primary ref for bio/tour fetch. Reported by zunoz on the Psysonic Discord (v1.47.0-rc.3). * docs: note Now Playing multi-artist fix in CHANGELOG (PR #960) |
||
|
|
df3533bb5a |
fix(cover): Windows thumbnails, tier fallback, PNG decode, coverArt id (#878)
* fix(cover): tier fallback for sparse surfaces and Windows asset URLs Sparse UI (player bar, queue) now reads disk covers via the same tier ladder as dense grids, so a warm 800.webp satisfies a 128px request. Reject non-asset convertFileSrc results on Windows, widen Tauri asset scope, and seed ladder keys on cover:tier-ready. applyDiskPath uses seedGridDiskSrcCache only to avoid notify/subscriber infinite loops. * fix(artist): top-track thumb uses album coverArt already warm in grid Song coverArt ids often differ from album cover ids (e.g. Octastorium in the grid vs empty track thumb). Prefer the album row's coverArt on artist pages and ensure high priority for 32px dense cells. * fix(cover): albumId for playback/queue; no broken img until disk URL ready Prefer albumId over track-id coverArt (Navidrome). Wire queue to CoverArtImage with playback scope. CoverArtImage renders a placeholder div until asset src exists to avoid the browser broken-image icon. * fix(test): add song id to resolveArtistPageSongCoverArtId fixture Pick<SubsonicSong, …> requires id; fixes tsc in CI/build. * fix(cover): resolve albumId for Now Playing and artist top tracks Prefer albumId when album.coverArt echoes track id; use sparse surface on artist suggestion thumbs; apply resolveSubsonicSongCoverArtId across playback surfaces (Now Playing, fullscreen, mobile, mini). * fix(cover): decode PNG from Subsonic before WebP tier encode Enable `png` in the image crate — some servers return PNG cover art; failed decode left `.fetch-failed` and empty thumbs for those albums. * refactor(cover): consolidate cover id resolution and align tests Move resolveSubsonicSongCoverArtId helpers to src/cover/resolveCoverArtId.ts with resolvePlaybackTrackCoverArtId for player surfaces; co-locate tests; fix FullscreenPlayer expectations for albumId-first resolution. * docs: CHANGELOG and credits for PR #878 * fix(cover): keep per-track coverArt when distinct from song id Address PR #878 review (b): albumId only when coverArt is missing or echoes track id; pin case with unit test; comment isRawFsPath symmetry. * chore(cover): address PR #878 review nits (scope, tests, rename) Narrow asset scope to cover-cache dirs only; add diskSrcCache Windows-path tests; rename ArtistTopTrackCover; CHANGELOG symptom-first wording. * fix(cover): restore asset scope to app data dirs (Windows regression) $APPDATA/cover-cache/** did not match Tauri scope resolution — covers were blocked after load. Use $APPDATA/** and $APPLOCALDATA/** (no $DATA). * fix(cover): Windows asset URLs — restore DATA scope, path normalize Regression after review nits: dropped $DATA/** and strict isAssetProtocolUrl blocked valid http://asset.localhost URLs on Windows. Normalize C:/ paths before convertFileSrc; CoverArtImage/Hero hide broken img on load error. * fix(cover): disk peek fallbacks when cache folder id differs Small surfaces resolve albumId while cover-cache often stores WebP under track id or album.coverArt from the grid. Peek batch now tries legacy ids; playback scope resolves server index key by URL key, not UUID-only lookup. * fix(cover): Navidrome al-* vs mf-* disk id mismatch UI used mf-* coverArtId while library backfill only cached al-* folders. Prefer album id for display/peek when coverArt is mf-*; backfill now queues both distinct album_id and cover_art_id values. * fix(cover): mf→al disk peek when mf folder missing in cache Navidrome Subsonic often returns mf-* coverArtId while backfill only creates al-* folders. Peek mf first, then al-* from hints; load albumId from library when Subsonic omits it; ensure fallback uses al-* id. * feat(cover): CoverArtRef, segment disk layout, library-index backfill Normalize cover caching around stable entity ids from the local library and Navidrome fetch ids. Disk paths live in psysonic_core::cover_cache_layout (album/<entityId>/); UI uses CoverArtRef with cacheEntityId + fetchCoverArtId. - Remove SQLite/mf peek helpers (diskPeekIds, peekCoverOnDisk, mergeDiskIdHints) - Backfill reads album/artist rows from library SQLite (bare Navidrome ids ok) - Use stored cover_art_id for HTTP; per-disc dirs only when discs differ - Migrate call sites to albumCoverRef / albumCoverRefForPlayback * feat(cover): central CoverEntry resolver (artist, album, track) Add resolveEntry.ts and Rust CoverEntry helpers as the single source of truth for cache_entity_id vs fetch_cover_art_id. ref.ts delegates to them; resolveCoverArtId becomes a thin compatibility shim. * feat(cover): resolve cover entries from local library index Add library_resolve_cover_entry IPC and cover_resolve.rs so album, artist, and track covers use SQLite cover_art_id + disc detection. TypeScript helpers in resolveEntryLibrary.ts prefer the index over live API fields when rows exist. * feat(cover): library-first hooks for grids and playback UI Add useAlbumCoverRef, useArtistCoverRef, useTrackCoverRef, and usePlaybackTrackCoverRef — sync fallback then SQLite index upgrade. Wire album/artist cards, album header, song card, and all player surfaces to resolve covers from the local library when indexed. * feat(cover): complete library-first migration across all UI surfaces Add Album/Artist/TrackCoverArtImage, useLibraryCoverPrefetch, and batch resolve helpers. Migrate grids, search, home, playback sidecars, warm peek, playlists, and share flows to hooks that upgrade from SQLite. Backfill normalizes album rows through cover_resolve; document paths in COVER_PATHS.md. Radio remains a deliberate non-library exception. * fix(cover): stop render loop from unstable serverScope in library hooks Default param `{ kind: 'active' }` created a new object every render, so every grid cell re-ran library_resolve IPC and setState in a loop. Use COVER_SCOPE_ACTIVE singleton, coverScopeKey deps, and guarded sync updates. * chore(cover): remove COVER_PATHS.md from app tree (lives in workdocs) Audit doc is team spec — see workdocs 2026-05-cover-art-pipeline/cover-paths-audit.md. * fix(cover): unstick library backfill after route changes (PR #870 regression) useCoverNavigationPriority cleanup called beginNavigation instead of end, leaking navigationHoldDepth so ui_priority_hold never released and backfill never downloaded. Also skip disk check after cover_resolve normalization. * fix(cover): segment progress, cap backfill CPU, include artists in catalog Progress and disk size now scan album/ and artist/ segments (canonical 800.webp). Prune legacy flat server/al-* dirs on startup and backfill pass. Backfill: max 2 concurrent ensures; JPEG decode and WebP encode run on the blocking pool behind a shared 2-permit semaphore so Tokio workers stay cool. Artists were missing because the catalog only read the empty artist table; add distinct artist_id from track and album rows. Paginate with a composite (kind, id) cursor so album and artist rows are not skipped. * fix(cover): drop legacy prune; backfill per-disc and artist catalog Remove prune_legacy_* and cover_cache_catalog_entry — layout is only cover_dir (album|artist segments); stale flat dirs clear on LAYOUT_STAMP change. Backfill: artists from track/album artist_id; expand albums to per-CD mf-* slots when discs differ; fix resolve_album_cover_entry when album row is missing. * fix(cover): reduce library IPC storms and fix multi-disc player art Skip per-row library_resolve on live search and artist album grids; warm grids from API coverArt after mount instead of blocking layout. Dedupe and cap concurrent library_resolve calls. Restore per-disc cache keys in the player and queue when track mf-* art differs from the album bucket. * fix(cover): skip library resolve on advanced and full search rows Use API coverArt for album/artist rails and lazy viewport artwork so result pages do not fire hundreds of library_resolve IPC calls at once. * fix(cover): default libraryResolve off for browse grids and rails Skip per-card library_resolve on album/artist/song browse UI by default; keep it on album/artist headers, playback queue rows, and orbit approval. * fix(cover): split UI/backfill CPU pools and restore mainstage hero carousel Library backfill no longer shares the 2-permit JPEG/WebP semaphore with visible cover ensures. Hero initializes albums from props, re-binds scroll visibility after mount, updates backdrop on slide change, and uses library resolve for correct cover art on the banner. * fix(analysis): resume full-library scan after candidates phase Reset the SQL cursor when entering full-library mode so tracks with partial analysis are not skipped. Tighten TS backfill completion and CPU queue watermarking; align cover-cache key tests with album-scoped storage keys. * fix(library): remove useless map_err in cover_resolve (clippy) CI treats clippy::useless-conversion as error on rusqlite optional() chains. * fix(cover): satisfy clippy on cover_cache_ensure IPC args Pass CoverCacheEnsureArgs as a single Tauri parameter instead of nine positional fields; align frontend invoke payload with { args }. |
||
|
|
418b25914a |
feat(cover): unify cover pipeline and stabilize mainstage/now-playing (#870)
* chore(cover): scaffold cover module and rust cover_cache stub Wave 0: src/cover/ skeleton per contracts.md §12, stub IPC commands in cover_cache/mod.rs (no-op returns until phase B). * feat(cover): add unified cover module and tier resolver (phase A) Wave 1A: tiers, storage keys, resolveJs with cold/sibling races, useCoverArt, CoverArtImage, layoutSizes, playback scope helpers, coverSiblings tier ladder, deprecated shims on subsonicStreamUrl. * feat(cover): rust disk cache and tier-ready events (phase B) Wave 1B: cover_cache module with WebP tier encode, HTTP canonical 800 fetch, cover_cache_* commands, cover:tier-ready / cover:evicted events, disk layout tests. * feat(cover): prefetch hook, tier-ready handoff, library backfill IPC (phase B/C) Wave 2: useCoverArtPrefetch, cover:tier-ready/evicted bridge, one-time IDB cover key clear, prefetch registry drain, MainApp wiring. * feat(cover): migrate dense grids to CoverArtImage and prefetch (phase D) Wave 3A: dense surfaces use layout-native displayCssPx, surface=dense, coverPrefetchRegister on Home/Albums/search; AlbumCard cell width from grid. * feat(cover): migrate sparse surfaces and integrations (phase E sparse) Wave 3B: sparse CoverArtImage/useCoverArt, lightbox tier 2000, ArtistHeroCover, MPRIS/Discord/export integrations, playback chrome and detail heroes. * feat(cover): revalidation scheduler and disk pressure gate (phase E+) Wave 4: coverCacheMaxMb settings (en/ru), StorageTab disk usage, cover_cache_configure, useCoverRevalidateScheduler, playbackServer uses cover fetchUrl; pressure watermarks. * docs: CHANGELOG and credits for cover art pipeline PR #869 * fix(cover): stop webview getCoverArt storm on dense grids (429) Dense surfaces no longer put rotating getCoverArt URLs in img src; load disk via Rust ensure + convertFileSrc. Tier-ready notifies listeners instead of invalidating IDB. Throttle background prefetch and cap Home registry. * fix(cover): omit empty img src until cover URL is ready React 19 warns on src=""; CoverArtImage uses undefined until disk/IDB resolves; queue current track shows placeholder when src is still empty. * fix(cover): disk cache by host index key, parallel ensure, asset protocol Bind cover storage to serverIndexKey (library host), rename cover IPC/events, fix REST base URL and Tauri flat args, enable protocol-asset for disk paths, add prioritized ensure queue, and wipe legacy profile-UUID cache once. Limit Vite dep scan to index.html so research/target HTML is ignored. * fix(cover): WebP tiers, disk peek, home cache, asset URLs for mainstage Encode lossy WebP (~82), write only missing tiers, library cover backfill, and cover_cache_peek_batch for fast paint from disk. diskSrcCache + CSP asset protocol; no IDB fallback when server is up. Session Home feed cache with warm peek on return; BecauseYouLike deduped cover hook and high prefetch. * feat(cover): per-server cache strategy and native library backfill Move cover disk cache settings to Offline & cache with Lazy/Aggressive per server, per-server clear, and no size cap. Run full-catalog backfill on the Rust runtime (sync-idle wake, bounded HTTP, bulk 800px writes without flooding the webview). Drop global prefetch limits from auth store and waveform clear from the offline storage block. * fix(build): CSP connect-src for Subsonic API; quieter prod nix build Prod webview blocked axios ping after cover CSP (missing connect-src). Drop cargo tauri -v in flake build, raise Vite chunk limit, ignore tsbuildinfo. * fix(cover): complete WebP ladder in library bulk backfill Aggressive backfill now writes all derived tiers (128–800), skips IDs only when the full ladder exists (not 800 alone), avoids fetch-failed markers on bulk HTTP errors, and stops the pass when the active server changes. * fix(cover,home): navigation-priority backfill and Because You Like UX Pause library cover backfill while navigating; split peek/ensure traffic so grids and rails win over bulk work. Disk src lookup, grid warm hooks, and non-blocking mainstage prime for faster visible covers. Because You Like: session snapshot, staggered horizontal skeleton row, text hidden until cover is ready, and layout aligned with loaded cards. * feat(random-albums,library): local-first album fetch + cover art pipeline Random Albums теперь запрашивает локальный SQLite-индекс (ORDER BY RANDOM() LIMIT N) вместо сетевого запроса к серверу. При готовом индексе спиннер исчезает практически мгновенно; сеть используется только как фолбэк. - advanced_search.rs: добавляет `("random", _) => RANDOM()` в allowlist сортировок - browseTextSearch.ts: runLocalRandomAlbums — SQLite-рандом для Albums - RandomAlbums.tsx: doFetchRandomAlbums local-first для обоих путей (без жанра и с жанром через runLocalAlbumsByGenres + JS-shuffle); speculative reserve прогревает следующий батч в фоне после каждого Refresh Также: обновление пайплайна обложек (coverTraffic, peekQueue, ensureQueue, diskSrcLookup, warmDiskPeek, prefetchRegistry, useCoverArt, useWarmGridCovers, useCoverNavigationPriority, resolveIntersectionScrollRoot и сопутствующие компоненты/хуки). * fix(random-albums): prevent double-load on Zustand rehydration useEffect([selectedGenres, load]) fired twice on every visit: first with default store values, then again ~50 ms later when Zustand rehydrated mixMinRatingFilterEnabled/minAlbum/minArtist from localStorage. Previously this was invisible because the first network fetch took ~1.5 s, so loadingRef.current was still true on the second fire. With the new local-first SQLite path the first load completes in ~50 ms, leaving the guard cleared before rehydration triggers a second random batch. Fix: ref-pattern — keep loadRef.current fresh on every render, effect depends only on selectedGenres. Manual Refresh and genre-filter changes still call the latest closure correctly. * fix(random-albums): stop warmCoverDiskSrcBatch in fillReserve from causing visual flash fillReserve вызывал warmCoverDiskSrcBatch для обложек резервного батча, что вызывало bumpDiskSrcCache() для каждой новой обложки (~30+ вызовов). Это будило всех подписчиков useCoverArt на текущей странице, провоцируя видимую перерисовку примерно через ~1.5 с после загрузки (когда filterAlbumsByMixRatings делает сетевые запросы к рейтингам артистов). - fillReserve: убран warmCoverDiskSrcBatch — обложки прогреваются лениво при consume резерва через primeAlbumCoversForDisplay - reserve-путь в load(): добавлен primeAlbumCoversForDisplay перед setAlbums (аналогично non-reserve пути; при уже прогретом кэше — мгновенно) * feat(because-you-like): reserve-first pattern — instant display on return visits Каждый визит на Mainstage после первого теперь отдаёт готовую заготовку мгновенно, вместо spinner → сетевые запросы → контент. Архитектура: - resolvePicks / fetchBecauseYouLike вынесены на уровень модуля (выход из замыкания useEffect); читают текущий localStorage, возвращают { anchor, recs, nextAnchorHistory, nextPicksHistory } - fillBecauseReserve — fire-and-forget фоновая функция: запускается сразу после отображения результата, кладёт следующий батч в _becauseReserve. Covers намеренно не прогреваются (bumpDiskSrcCache на текущей странице не нужен); они прогреваются через primeAlbumCoversForDisplay при consume. - useLayoutEffect: если reserve готов — не сбрасывает стейт в skeleton (контент появляется без мигания) - useEffect: reserve-first path — consume → primeCovers → setState → fill; full-fetch path сохранён как fallback при первом визите или промахе Поведение: - Визит 1: full fetch (как раньше) → показ → fillReserve R1 - Визит 2+: consume R1 → мгновенный показ → fillReserve R2 - При сетевом сбое: restore из session cache (как раньше) * fix(because-you-like): initialise state from reserve — no skeleton flash on remount При ремаунте компонент стартовал с refreshing=true/anchor=null/recs=[] и показывал skeleton на один тик до того как useEffect отработает. Теперь useState() использует lazy initializers, которые читают _becauseReserve прямо в первом рендере: если reserve валиден — state сразу refreshing=false, anchor=X, recs=[...] и skeleton не показывается вообще. Covers уже в diskSrcCache (из предыдущего показа) и появляются без дополнительных запросов. useLayoutEffect упрощён: вызывает hasValidReserve() и сбрасывает в skeleton только если reserve отсутствует (для случая navigation без ремаунта). * fix(because-you-like): apply reserve in useLayoutEffect to handle async pool arrival Lazy initializers не могли применить reserve при первом рендере, потому что mostPlayed/recentlyPlayed/starred приходят из Home.tsx асинхронно — pool=[] на первом рендере, poolKey не совпадает с reserve. useLayoutEffect теперь активно ставит стейт из reserve (а не просто не сбрасывает): когда pool обновляется до реальных данных, useLayoutEffect срабатывает синхронно до paint, проверяет reserve и сразу применяет anchor/recs/refreshing=false. При отсутствии reserve — сбрасывает в skeleton как прежде. * fix(because-you-like): reserve > cache > skeleton — eliminate skeleton flash on mount Корневая причина: Home.tsx загружает mostPlayed асинхронно через useEffect, поэтому на первом рендере pool=[], poolKey=''. Reserve хранится с реальным poolKey → mismatch → lazy initializers запускали skeleton. Теперь двухуровневый fallback без зависимости от poolKey: 1. reserve (serverId + poolKey совпадают) → мгновенный новый батч 2. becauseYouLikeCache (только serverId) → stale-while-revalidate, контент доступен сразу с mount, обновляется тихо в фоне 3. skeleton → только при полном отсутствии данных (первый визит) Применяется одинаково в lazy useState initializers, useLayoutEffect и full-fetch path useEffect (не сбрасывать в skeleton пока есть cached контент). * fix(because-you-like): key reserve by serverId only; guard useEffect on empty pool Проблема: reserve хранился с poolKey, но на первом рендере pool=[] → poolKey='' → mismatch → показывался кэш (предыдущий набор) ~500ms пока Home.tsx не загружал mostPlayed. Исправления: - BecauseReserve: убран poolKey — reserve валиден для любого pool-состояния на том же сервере. Pool (топ-артисты) меняется медленно; один раз показать reserve с чуть устаревшим anchor лучше чем показывать предыдущий набор 500ms - hasValidReserve: проверяет только serverId - fillBecauseReserve: убран poolKey из сигнатуры и хранилища - useEffect: guard pool.length === 0 → возврат без fetch/consume; effect перезапустится когда pool заполнится (реальные deps изменятся) → reserve применяется из useLayoutEffect ещё до pool, без стале-флэша Итоговый порядок: reserve (instant, serverId) > cache (stale-while-revalidate) > skeleton (только первый визит) * fix(home): remove mix-rating deps from feed useEffect — prevent Zustand rehydration double-fetch Корень: useAuthStore(mixMinRatingFilterEnabled/Album/Artist) были в deps useEffect. Zustand persist реhydrates асинхронно — сначала activeServerId, потом mix-rating значения. Это вызывало двойной запуск эффекта: - Первый запуск: homeFeedCache hit → показывает набор предыдущего просмотра - Второй запуск (после rehydration): cache miss или повторный fetch с реальными mix-настройками → ~500ms → новый набор Итог: Hero, AlbumRow, BecauseYouLikeRail показывали предыдущий набор первые ~500ms при каждом возврате на Mainstage. Fix: убраны mixMinRatingFilterEnabled/Album/Artist из deps. getMixMinRatingsConfigFromAuth() читается внутри эффекта через getState() — всегда актуальные значения без пересоздания замыкания. Mix-настройки по-прежнему применяются при fetch, но не вызывают двойной запуск при rehydration. * feat(home): local-first discover songs via SQLite ORDER BY RANDOM() Добавлена runLocalRandomSongs (аналог runLocalRandomAlbums для треков) в browseTextSearch.ts — использует libraryAdvancedSearch с sort random, field уже поддерживается Rust-кодом через wildcarded ("random", _) ветку. В Home.tsx: discoverSongs теперь сначала пробует локальный индекс, и только при недоступности (индекс не готов, ошибка) падает обратно на getRandomSongs.view. Ускоряет первую загрузку Mainstage — треки берутся из SSD вместо сети. * fix(home): pre-populate state from cache at mount — eliminate empty-state flash on return visits Причина: Home.tsx размонтируется при навигации. При возврате первый рендер всегда с пустыми массивами (heroAlbums=[], mostPlayed=[] и т.д.), потом useEffect читает homeFeedCache и заполняет state. Даже один кадр с пустым состоянием вызывает перерисовку Hero и BecauseYouLikeRail (pool=[]). Решение: getInitialHomeFeed() читает homeFeedCache синхронно через useAuthStore.getState() (не hook) в lazy useState initializers. К моменту повторного визита store уже rehydrated — все state получают кэшированные данные до первого рендера. Дополнительно: wasPrePopulated предотвращает повторный applyFeedSnapshot в useEffect когда state уже заполнен — иначе новые ссылки на массивы вызывали бы ненужные ре-рендеры дочерних компонентов с теми же данными. * fix(mainstage): keep refresh without return flicker Keep Home and Because You Like visually stable during a single visit while still refreshing data for the next re-enter. Improve mainstage cover warmup by ensuring and pre-decoding above-the-fold artwork so hero and top rails appear instantly after navigation. * fix(mainstage): stabilize because rail and hero background framing Measure Because You Like layout before first paint to avoid width snap flicker, and render hero background as centered cover-fit images so the frame no longer jumps from top to middle on mount. * fix(now-playing): prewarm track data and prevent stale carry-over Warm Now Playing fetch caches and playback cover art on track change so entering the page no longer waits on first-load requests. Gate key-based sections (top songs, tour, Last.fm) by the active track/artist keys to avoid briefly rendering values from the previous track. * fix(cover,test): refresh playback scope and default tauri cover mocks Recompute playback cover scope when queue/server context changes so now-playing art resolves against the correct server after handoffs. Add default cover-cache invoke handlers to the shared Tauri test harness to prevent unhandled rejections in suites that mount cover-aware UI. * fix(cover,now-playing,test): align prewarm scopes and tighten tauri mocks Make cover-cache invoke defaults opt-in for tests, align radio prewarm scope with active rendering scope, and add targeted hook tests for prewarm + playback-scope reactivity. Also harden Rust cover URL building to avoid panic on malformed base URLs. * test(cover): hoist mocked useCoverArt and clean EOF whitespace Fix the new playback-scope hook test to use a hoisted vi.mock-safe stub and keep branch-wide diff checks clean by removing an accidental trailing blank line. * fix(cover): align playback ensure auth and harden backfill retry flow Use playback-server credentials for playback-scoped cover ensures, persist fetch-failed markers for bulk library backfill failures, and avoid advancing backfill cursor when UI-priority hold interrupts a batch. * fix(ci): resolve clippy lint and update frontend node runtime Move fetch helper before the test module to satisfy clippy's items-after-test-module rule, and modernize frontend CI to setup-node v6 with lts/* instead of pinned Node 20. * chore(settings): simplify cover and analytics strategy copy Move strategy summaries below tables, simplify Lazy/Aggressive wording, keep analytics warning always visible, and localize Russian texts to plain language without technical jargon. |
||
|
|
db98d30a78 |
fix(playback): cross-server browse, Lucky Mix, and Now Playing (#768)
* fix(playback): keep browsed server when queue plays elsewhere Lucky Mix on a non-playback server now clears the old queue and pins the active server before building. Now Playing metadata uses apiForServer against the queue server instead of forcing ensurePlaybackServerActive on every activeServerId change. * docs: note PR #768 in CHANGELOG and settings credits * fix(now-playing): gate AudioMuse similar artists on playback server Match getArtistInfoForServer credentials: when browsing another server while the queue plays elsewhere, similar-artist count follows the queue server's AudioMuse flag, not the browsed server. |
||
|
|
45e0e1206f |
fix(playback): pin queue playback to source server when browsing another library (#717)
* fix(playback): pin queue streams, cover art, and library links to queue server When the active server changes while a queue from another server is playing, keep streams and UI on queueServerId; switch back for artist/album links and queue or player-bar context menus. * fix(playback): switch to queue server when opening Now Playing Ensure active server matches queueServerId before Subsonic fetches on the Now Playing page, mobile player route, and queue info panel; scope caches by server id. * docs(credits): mention Now Playing in PR #717 contribution line * fix(playback): route scrobble and queue sync to queue server Address PR review: apiForServer for scrobble/now-playing/savePlayQueue, clear queueServerId on server removal, mini-player queueServerId sync, block cross-server enqueue with toast, and regression tests. |
||
|
|
7a7a9f5e6b |
refactor(utils): group utils/ files into topic folders (Phase L, part 1) (#689)
111 of 122 top-level src/utils/ files move into 16 topic folders (audio, cache, cover, share, server, playback, playlist, deviceSync, waveform, mix, format, export, changelog, ui, perf, componentHelpers). True singletons with no cluster stay at the utils/ root. Pure file-move: a path-aware codemod rewrote 539 relative-import specifiers across 275 files; no logic touched. The hot-path coverage gate list (.github/frontend-hot-path-files.txt) is updated to the new paths for the 11 gated utils files — a mechanical consequence of the move, not a CI change. tsc is green. |
||
|
|
6f8dd73448 |
refactor(now-playing): G.73 — extract TourCard + DiscographyCard + useNowPlayingFetchers + useNowPlayingStarLove (cluster) (#640)
Four-cut cluster closing out the NowPlaying decomposition. 715 → 391 LOC (−324). TourCard — Bandsintown tour-dates card with the privacy-prompt gate (shown before the user opts in), loading state, empty state, 5-item show-more list with date / venue / place, and the "Tour data via Bandsintown" credit footer. DiscographyCard — chronological album grid (10×2 = 20 tiles initial, show-more for the rest), each tile a cached cover-art thumbnail with a tooltip and click-through to the album page. useNowPlayingFetchers — the eight cached fetch effects that drove the page: song meta / artist info / album / top songs / Bandsintown tour events (+ loading state) / discography / Last.fm track stats / Last.fm artist stats. Each effect follows the same pattern (cache hit → seed state, cache miss → fetch + setCache + setState, cancellation flag on cleanup). All eight makeCache<…> instances move into the hook module too. useNowPlayingStarLove — local starred + lfmLoved booleans seeded from songMeta.starred and lfmTrack.userLoved respectively, plus toggleStar (star/unstar Subsonic API) and toggleLfmLove (lastfmLove/Unlove with the session key). NowPlaying drops the now-unused imports: star/unstar (Subsonic), getArtist/getArtistInfo/getTopSongs/getSong/getAlbum, the entire '../api/lastfm' line, fetchBandsintownEvents + BandsintownEvent, makeCache (was passing through to the page only for cache instantiation), plus SubsonicAlbum (only the type is still used inside the hook). The radio + dashboard JSX in the body still references everything via the hook returns. Pure code move otherwise. |
||
|
|
2ddc2a2345 |
refactor(now-playing): G.72 — extract Hero + ArtistCard + AlbumCard + TopSongsCard + CreditsCard (cluster) (#639)
Five-cut cluster pulling the dashboard subcards out of NowPlaying.tsx. 1119 → 711 LOC (−408). Each card was already a top-level React.memo'd block with a clean prop interface, so this is straight file-per-card extraction with no behaviour change. Hero — full hero strip: cover image, title, artist/album/year/age sub, format/codec/sample-rate/bit-depth/duration badges with the Hi-Res chip, the favourite + Last.fm-love + lyrics action buttons, play-count line, and the Last.fm track + artist stats rows (with their per-row `you played N×` highlight). renderStars (5-star inline indicator) moves inline as a private helper since only Hero calls it. ArtistCard — about-this-artist card: optional hero image (filtered through isRealArtistImage so we don't render the well-known "2a96…" placeholder), name, sanitized + clamp-with-Read-more bio, similar-artist chip row. Owns its bioExpanded / bioOverflows state and the useLayoutEffect that measures overflow. AlbumCard — from-this-album card: meta line (year / track position / total duration / play count), sliding-window tracklist anchored on the current track (top 10 by default, or the 10 ending at the running track if it's beyond position 10), and the show-all toggle. TopSongsCard — top-songs-by-artist card: rank-ordered list (max 8), each row clicks into playerStore via the onPlay callback. CreditsCard — contributor / song-info card: role-grouped list with i18n role label lookup (falls back to the raw role string). NowPlaying drops the now-unused imports (useLayoutEffect, LastfmIcon, Star, MicVocal, Heart, Headphones, TrendingUp from lucide, sanitizeHtml + isRealArtistImage + formatTotalDuration from nowPlayingHelpers) plus the inline renderStars helper. Pure code move otherwise. |
||
|
|
1c34cc04c7 |
refactor(now-playing): G.71 — extract helpers + cache + NpCardWrap + NpColumnEl + RadioView (cluster) (#638)
Five-cut cluster opening the NowPlaying refactor. 1384 → 1119 LOC (−265). nowPlayingHelpers — eight pure helpers + ContributorRow type: formatTime, formatCompact, formatTotalDuration, sanitizeHtml (strip dangerous attributes + trailing Last.fm "Read more" link), isoToParts (date formatting for Bandsintown), buildContributorRows (dedupes contributor list, hides redundant "Artist = main artist" row), isRealArtistImage (filter the Last.fm "2a96…" placeholder MD5 that aggregating Subsonic backends still emit). nowPlayingCache — module-level TTL cache used by all subcomponents: CACHE_TTL_MS (5 min), CacheEntry type, makeCache() factory. NowPlaying still instantiates eight `makeCache<…>()` typed caches inline at module scope; only the factory + TTL constant move. NpCardWrap — drag-source wrapper around each dashboard card, participates in the psyDnD drag stream via useDragSource. NpColumnEl — drop-target column. Owns the document mousemove listener that determines which wrapper the dragged card would land before (x-axis decides column, y-axis bisects wrapper rects to compute insert index). No-op when no card is being dragged. RadioView — full radio-playing layout: hero card with stream name + current artist/title/album + AzuraCast progress bar + listeners badge, "Up Next" card, recently-played list. Subscribes to nothing on its own; takes the radioMeta tuple + currentRadio + resolvedCover from the parent. NonNullStoreField type alias moves with it. NowPlaying drops the inline definitions; renderStars + the eight typed cache instances stay in the page for now (renderStars is used by Hero + TopSongsCard, both of which still live inline; the typed caches are consumed by NowPlaying's load effects). Pure code move otherwise. |
||
|
|
9606a99efb |
refactor(api): F.50 — extract 7 small domain modules (#615)
Seven domain-eng splits peel ~200 LOC of read endpoints out of `api/subsonic.ts`: - `subsonicStreamUrl.ts` — `buildStreamUrl`, `coverArtCacheKey`, `buildCoverArtUrl`, `buildDownloadUrl` (token-signed URL builders for the four /rest endpoints we hand to the browser). - `subsonicStarRating.ts` — `getStarred`, `star`, `unstar`, `setRating`, `probeEntityRatingSupport`. `setRating` still triggers the lazy `navidromeBrowse` cache invalidation; the same-folder lazy import path is preserved. - `subsonicSearch.ts` — `search`, `searchSongsPaged`. - `subsonicScrobble.ts` — `scrobbleSong`, `reportNowPlaying`, `getNowPlaying`. - `subsonicAlbumInfo.ts` — `getAlbumInfo2`. - `subsonicLyrics.ts` — `getLyricsBySongId`. - `subsonicGenres.ts` — `getGenres`, `getAlbumsByGenre`. 63 external call sites migrated to direct imports. Four `vi.mock` targets in the store-level tests pointed at `../api/subsonic` and were updated to the new module paths. Pure code-move. subsonic.ts: 762 → 561 LOC (−201). |
||
|
|
006635de4b |
refactor(api): F.49 — extract library + artists + ratings (#614)
Three domain-eng modules peel ~316 LOC of fetch/mapping out of `api/subsonic.ts`: - `subsonicLibrary.ts` — browse + random + per-song fetch (`getMusicDirectory`, `getMusicIndexes`, `getMusicFolders`, `getRandomAlbums`, `getAlbumList`, `getRandomSongs`, `getRandomSongsFiltered`, `getSong`, `getAlbum`, `filterSongsToActiveLibrary`, `similarSongsRequestCount`, plus the private `albumIdsInActiveLibraryScope` cache). - `subsonicArtists.ts` — artist endpoints (`getArtists`, `getArtist`, `getArtistInfo`, `getTopSongs`, `getSimilarSongs2`, `getSimilarSongs`). Uses Library's `filterSongsToActiveLibrary` and `similarSongsRequestCount` for the per-library scoping fallback. - `subsonicRatings.ts` — `parseSubsonicEntityStarRating` parser plus `prefetchArtistUserRatings` and `prefetchAlbumUserRatings` workers with the shared 7-min cache. Calls back into Library/Artists for the per-id fetch. 51 external call sites migrated to direct imports. No re-export shims in `subsonic.ts`. Statistics endpoints still in `subsonic.ts` keep their `RATING_CACHE_TTL` constant locally (same 7-min window). subsonic.ts: 1078 → 762 LOC (−316). |
||
|
|
72030f17fd |
refactor(api): F.48 — extract subsonic types + client primitives (#613)
First Phase F slice. Splits the 1333-LOC `api/subsonic.ts` along its two most obvious axes: - `subsonicTypes.ts` — all ~24 exported interfaces + type aliases (album/song/artist/playlist/directory/genre/now-playing/radio, random-songs filters, three statistics shapes, search + starred results, AlbumInfo, structured-lyrics types, etc.) plus the `RADIO_PAGE_SIZE` constant. - `subsonicClient.ts` — token-auth + `getClient` + `api<T>()` + `libraryFilterParams` + `secureRandomSalt` / `getAuthParams` / `SUBSONIC_CLIENT`. The credential-bearing API helpers (`pingWithCredentials`, `apiWithCredentials`, `restBaseFromUrl`, `probeInstantMixWithCredentials`) stay in `subsonic.ts` for now — they could move into the client module in a follow-up. 66 external call sites migrated to direct imports from the new modules (no re-export shims in `subsonic.ts`). Pure code-move; contract test stays green. subsonic.ts: 1333 → 1078 LOC (−255). |
||
|
|
9fac6eb490 |
refactor(player): E.42 — migrate playerStore re-exports to direct imports (#607)
Migrates ~74 call sites away from the playerStore re-export shims that were kept during M0–E.41 to avoid touching 30+ imports per PR. Now that the bigger refactor work is done, each helper goes back to its real home: - `initAudioListeners`, `installQueueUndoHotkey`, `flushPlayQueuePosition` → from their own store modules - `getPlaybackProgressSnapshot`, `subscribePlaybackProgress`, `PlaybackProgressSnapshot` → from `playbackProgress` - `resolveReplayGainDb`, `shuffleArray`, `songToTrack` → from `utils/*` - `_resetQueueUndoStacksForTest`, `consumePendingQueueListScrollTop`, `registerQueueListScrollTopReader` → from `queueUndo` - `PlayerState`, `Track` types → from `playerStoreTypes` Drops the corresponding 13 re-export stubs from `playerStore.ts` and the now-unused imports. Also drops dead section banners + per-wrapper comments above one-line action delegates. Trims one stale "(separate PR)" note in `transportLightActions.ts` since that follow-up landed in E.39. `playerStore.ts`: 180 → 112 LOC (−68). Down from Phase E's starting 3732 LOC. `bootstrap.test.ts` mock target updated from `../store/playerStore` to `../store/queueUndoHotkey` to keep the spy reachable after the import change. |
||
|
|
9ad0f8af6d |
feat(ui): UI refinements — sidebar indicators, adaptive header, and interaction polish (#397)
* feat(ui): unify queue toggle handle behavior Show the queue toggle in the header when the queue is collapsed and use a seam-aligned drag handle when it is open. Hide the seam handle while the main content is actively scrolling to reduce accidental interactions. * feat(ui): add adaptive header search collapse behavior Collapse header search to a magnifier when top controls get crowded and expand it as an overlay only while active. Use measured header space with hysteresis to avoid flicker and keep neighboring controls stable. * chore(ui): remove leftover search prototype artifacts Drop an unused icon import from live search and remove an unused header container-type rule left from an earlier layout experiment. * feat(ui): persist sidebar and queue visibility state Save left sidebar collapse and right queue open/closed visibility in local storage helpers so both panel modes are restored after app restart. * feat(ui): unify player overflow menu behavior Use a single overflow menu for click and wheel interactions, with a volume-only mode that keeps the same layout and volume controls as the full menu. * feat(ui): add wheel seek controls to waveform Apply 10-second wheel seek steps with trailing 1-second debounce and keep the waveform preview stable so the playhead moves smoothly during rapid scroll input. * fix(now-playing): stabilize narrow dashboard layout Switch now-playing responsiveness to container-based breakpoints and prevent stacked widgets from overlapping when width is constrained. * fix(search): reduce collapse jitter and avoid header overlap Add a short collapse-state cooldown to prevent threshold flicker and hide conflicting header controls while collapsed search expands as an overlay. * fix(i18n): localize player overflow controls across locales Replace hardcoded player overflow labels with translation keys and add the missing keys for all shipped locale files. * fix(search): keep advanced control clickable in collapsed mode Prevent focus loss on the advanced search button in collapsed overlay mode so its click handler consistently runs. * fix(i18n): restore queue translation in offline library Use the existing queue.appendToQueue key for the offline enqueue button tooltip and label instead of a missing key and hardcoded English text. * fix(ui): apply overlay scrollbar to right-panel text tabs Switch now-playing content, lyrics, and info panes to OverlayScrollArea and harden tour-item layout so long concert metadata stays within panel bounds. * fix(ui): add unread indicator for new releases and guard sidebar drag clicks Track unread new-release IDs per server/library scope and clear the badge when opening the New Releases page. Also prevent click-through navigation after sidebar drag release and keep related i18n/responsive sidebar-adjacent refinements in this snapshot. * fix(ui): stabilize live dropdown layering and unread reset flow Render the topbar Live dropdown via a portal so it consistently overlays sidebar layers. Rework new-releases unread tracking to handle library scope baselines, ignore stale refresh races, and mark items as seen after a 5-second stay on the New Releases page. * feat(ui): add localized New badges for recently added albums Show a theme-consistent New badge on album cards and album detail for albums created within the last 48 hours. Localize the badge label across all supported locales and centralize recency logic in a shared utility to avoid duplication. * fix(album): prevent tracklist jump when entering multiselect Move bulk selection actions from the tracklist body into the album toolbar next to the track filter. Keep selection controls stable in the header area so enabling multiselect no longer shifts the tracklist content downward. * fix(tray): add playback-state badge and finalize queue handle tooltip Show play/pause/stop icons in the Linux tray now-playing entry and persist state safely in Tauri managed state. Also switch the queue-resize handle tooltip to the dedicated localized key across all locales. * fix(header): prioritize search collapse before Live/Orbit labels Make topbar compression deterministic by collapsing search first and compacting Live/Orbit labels only in sustained low-space mode. Add sticky hysteresis-based header compact state to prevent oscillation while resizing. * fix(ui): stabilize header compaction and show tray state icons Prevent topbar flicker in the narrow-width range by tightening compact-mode thresholds, gating on real overflow, and removing width transitions from live search. Also include playback state icons in tray tooltip text across platforms while preserving tooltip length limits. * fix(tray): keep tooltip iconization Windows-only Revert Linux tray tooltip/title fallback attempts and keep state icons only in Windows tray tooltips, while Linux continues to show playback state in the now-playing menu entry. * fix(ui): restore queue resize response after overlay scroll interactions Hide the queue handle while scrolling on both the main route viewport and the now-playing viewport, and clear stale thumb-drag state before starting queue resize. Also ignore inactive/faded overlay thumbs in resizer suppression so horizontal pointer transitions no longer leave the queue seam unresponsive. * docs(changelog): summarize ui-refinements branch features Document the branch-level feature additions in 1.45.0 as separate changelog sections and group remaining branch-local fixes under a single polish entry. * docs(changelog): add PR #397 references for ui-refinements Attach PR metadata to the new 1.45.0 ui-refinement sections and the polish entry so release notes map directly to the merged branch discussion. |
||
|
|
db72ed9e5a |
feat(now-playing): draggable widget cards with per-user layout (#267)
Each dashboard card (Album, Top Songs, Credits, Artist, Discography, On Tour) is now a widget the user grabs and drops anywhere — reorder within a column or move across columns. Layout persists per-install via the new nowPlayingLayoutStore (Zustand + localStorage, same shape as sidebarStore with an onRehydrateStorage guard for unknown IDs). Drag uses psyDnD (useDragSource / psy-drop event, the mouse-event based system from DragDropContext) — HTML5 native DnD is a no-go on WebKitGTK Linux where it always shows a forbidden cursor. The whole card is the drag handle; the column listens via a document-level mousemove for the drop indicator and via a global psy-drop listener that reads the latest hovered column from a ref, so drops below the last card still land correctly regardless of column height. Visibility is toggled from a layout menu in the top-right of the dashboard — two sections (visible / hidden) plus a reset-to-defaults button. No hide buttons on the cards themselves (keeps the card UI uncluttered). An equal-height grid (align-items: stretch + min-height on columns) keeps the drop zone active for the full vertical span of either column. Co-authored-by: Psychotoxical <dev@psysonic.app> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b4f31e0954 |
feat(now-playing): redesign page as info dashboard (#266)
Replace the flat Now Playing page with a two-column dashboard: hero (cover, metadata, badges, Last.fm stats + love button, release-age tagline), and a grid of themed cards (Album tracklist with sliding window, most-played-by-artist, credits, about-the-artist with Last.fm bio fallback, compact discography contact-sheet with expand, upcoming tours via Bandsintown). Adds lastfmGetTrackInfo + lastfmGetArtistStats for global listener counts + userplaycount, plus Last.fm love/unlove wired to the new hero button. Module-level TTL caches per entity keep same-artist track switches instant. Artist-image guard filters the well-known Last.fm no-image placeholder so the card collapses cleanly when no real image exists. Co-authored-by: Psychotoxical <dev@psysonic.app> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ccdb899833 |
fix(theme): improve Latte and GTA theme readability
Latte: fix server dropdown dark background (--bg-card instead of hardcoded #1e1e2e), add back-button white text over album cover overlay, fix NowPlaying white-on-white text (artist/album, badges, bio, tracklist, star/heart icons use semantic vars). GTA: bump --text-muted from #484848 to #707070 and --text-primary from #e8e8e8 to #d8d8d8 — sidebar section labels, Settings descriptions and album detail info were nearly invisible on the near-black background. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
69c0c5a907 | merge: integrate origin/main into feat/audiomuse-navidrome | ||
|
|
36f3d42dbe |
feat(discovery): Navidrome AudioMuse-AI client integration and library scoping
Add per-server toggle for AudioMuse-style discovery: Instant Mix via getSimilarSongs, server similar artists instead of Last.fm on artist pages, and higher similar-artist count in Now Playing when enabled. When browsing a single music folder, filter similar/top song results by album ids from that scope (Navidrome does not apply musicFolderId to those endpoints). Request larger similar batches under a narrow scope. Keep radio-from-track as two blocks (shuffled top songs, then shuffled similar-by-artist) so it differs from Instant Mix. Show Alpha badge beside the setting. Add research/ to .gitignore. |
||
|
|
46cefb5712 |
feat: add ICY metadata and AzuraCast radio streaming support
Agent-Logs-Url: https://github.com/nisrael/psysonic/sessions/88faada5-28bb-446f-b53b-46a0efef387e Co-authored-by: GitHub Copilot <198982749+copilot@users.noreply.github.com> Signed-off-by: Nils Israel <nils@sxda.io> |
||
|
|
1624880c2a |
feat(ratings): mix cutoff filter for random flows and home, i18n
Apply the same per-axis star cutoff to random mix (multi-batch fetch until full), random albums, Hero, and Home hero/discover rows. Prefetch artist and album ratings via Subsonic when list payloads omit them. Exclude items when 0 < rating ≤ threshold; 0 or missing counts as unrated. Settings: rating filter description interpolates sidebar menu labels; Russian strings for custom title bar; short axis labels aligned across locales. |
||
|
|
7263d93d42 |
feat: v1.29.0 — Radio fast-start, seek fix, OGG, error toasts, contributors
Incorporates PRs #38 (nisarg-78), #42 #43 #44 (JulianNymark) with fixes: Audio / Playback - Artist Radio starts immediately from fast getTopSongs (local library); getSimilarSongs2 (Last.fm) enriches queue in background — no more wait - Fix seek audio glitch: EqualPowerFadeIn only resets to zero-gain on seeks to track start (<100 ms); all other seeks resume at unity gain - OGG/Vorbis container support via symphonia-format-ogg (PR #42) - Human-readable audio error messages in SizedDecoder (PR #44) - Audio playback errors shown as themed toast notifications (PR #43) Queue / Radio - Infinite Queue: proactive load of 5 tracks (was 25) when ≤2 remain - Radio: proactive reload at ≤2 remaining tracks, independent of Infinite Queue setting — radio no longer stops at last track - Fix: clicking Start Radio multiple times no longer stacks duplicates - Fix: Start Radio on artist keeps current song playing - Manual tracks always appear before Radio, Radio before Auto-added - Queue separators: "— Radio —" and "— Auto —" dividers - Fix: radio proactive load now works even when songs lack artistId (uses currentRadioArtistId module var as fallback) UI / UX - Click synced lyrics lines to seek (PR #38) - Volume scroll wheel on volume slider (PR #38) - Lyrics: active / completed / upcoming visual states (PR #38) - Shared toast utility (src/utils/toast.ts) replaces inline toast fn - Auto-updater: relaunch_after_update Rust command exits first to release single-instance lock before spawning new process About / Credits - nisarg-78 and JulianNymark added to contributors (since v1.29.0) - netherguy4 added as Special Thanks for feature ideas and feedback - i18n: aboutSpecialThanksLabel in all 5 languages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
361e9cfdb3 |
feat: v1.14.0 — Critical Buffer Fix, Gapless/Crossfade stable, UX polish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
d927ef2082 |
feat: v1.12.0 — Lyrics, 15 new themes, Last.fm stable, Crossfade stable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
867c5fbd3e |
feat: v1.11.0 — Movies themes, Settings polish, Gapless stable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
e550340565 |
feat: v1.10.0 — new streaming themes, favourite toggle fix, home page improvements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
57b70e6154 |
feat: v1.9.0 — new themes, keybindings, font picker, home play behavior
Themes: - Add Neon Drift (midnight blue / electric cyan synthwave) - Add Cupertino Light + Cupertino Dark (macOS Ventura-inspired, frosted glass) - Add Betriebssysteme group (Cupertino, Aero Glass, Luna Teal) - Rename all trademarked theme IDs to original names (WnAmp, Navy Jukebox, Cobalt Media, Onyx Cinema, Aero Glass, Luna Teal) - Reorder ThemePicker: Psysonic Themes first, then Mediaplayer, Betriebssysteme Keybindings: - New keybindingsStore with 10 bindable actions (play/pause, next, prev, volume, seek ±10s, queue, fullscreen, native fullscreen) - Settings UI for rebinding; defaults: Space=play-pause, F11=native-fullscreen Font picker: - New fontStore; 10 UI fonts selectable in Settings → Appearance - Applied via data-font attribute on <html> Home page: - AlbumCard: Details button → Play button via playAlbum() utility - Hero: Play Album starts playback with 700ms fade-out instead of navigating - playAlbum.ts: fade, store-only volume restore, playTrack handoff Now Playing: - 3-column hero layout; EQ bars truly centred (flex:1 on both outer columns) - Background brightness 0.25→0.55, overlay 0.55→0.38 (art now visible) - Better text contrast for track times, card links, section titles Linux audio: - Set PIPEWIRE_LATENCY + PULSE_LATENCY_MSEC before stream creation to reduce ALSA snd_pcm_recover underrun frequency on PipeWire Remove butterchurn visualizer (VisualizerCanvas, butterchurn.d.ts) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
2ba7845c79 |
feat: Last.fm beta, Similar Artists, Statistics Last.fm stats, TooltipPortal, CustomSelect, Psychowave theme (v1.7.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
59115a09d2 |
feat: 10-band EQ, connection indicator, new icon (v1.5.0)
### 10-Band Graphic Equalizer - Full EQ in Rust audio engine via EqSource<S> biquad peak filters - 10 built-in presets + custom preset save/delete - EqSource::try_seek() implemented — also fixes waveform seek which broke silently when EQ was introduced (rodio returned SeekError::NotSupported) - EQ state persisted in localStorage, synced to Rust on startup ### Connection Indicator - LED in header (green/red/pulsing) with server name and LAN/WAN label - Offline overlay with retry button when server is unreachable - useConnectionStatus hook ### New App Icon - New logo (logo-psysonic.png) applied across Login, Sidebar, Settings, README and all Tauri platform icons (Windows, macOS, Linux, Android, iOS) ### Now Playing Page - New /now-playing route added ### Fixes - WaveformSeek: mousemove/mouseup on window to fix drag outside canvas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |