mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
0b84a199e41e3cd5e645ed6c59f9ac84bd839585
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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). |
||
|
|
4d564e5016 |
refactor(auth): E.43 — extract authStore types + defaults + helpers (#608)
First slice of the authStore split. Pure code-move: no behaviour change. - `authStoreTypes.ts` — `ServerProfile`, `AuthState`, plus all union types (`SeekbarStyle`, `LoggingMode`, `NormalizationEngine`, `DiscordCoverSource`, `LoudnessLufsPreset`, `LyricsSourceId`, `LyricsSourceConfig`, `TrackPreviewLocation`, `TrackPreviewLocations`). - `authStoreDefaults.ts` — `LOUDNESS_LUFS_PRESETS`, `DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB`, `TRACK_PREVIEW_LOCATIONS`, `DEFAULT_TRACK_PREVIEW_LOCATIONS`, `DEFAULT_LYRICS_SOURCES`, `MIX_MIN_RATING_FILTER_MAX_STARS`, `RANDOM_MIX_SIZE_OPTIONS`. - `authStoreHelpers.ts` — `generateId`, `sanitizeLoudnessLufsPreset`, `sanitizeLoudnessPreAnalysisFromStorage`, `clampMixFilterMinStars`, `clampRandomMixSize`, `clampSkipStarThreshold`, `skipStarCountStorageKey`, `sanitizeSkipStarCounts`. 12 external call sites migrated to direct imports from the new modules (no re-export shims left in authStore.ts — applies the [feedback_prevent_god_modules] rule 5: avoid re-export debt). authStore.ts: 889 → 518 LOC (−371). |
||
|
|
af1b9661f5 |
fix(orbit): three interlocking guest playback bugs (#525)
* fix(orbit): guest short-circuits queue-exhaustion fallback paths
When a guest's local queue runs out (single-track queue from `syncToHost`
empties on `audio:ended`), the player walks the standard fallback chain
in `next()`: radio top-up → infinite-queue → stop. The infinite-queue
branch builds a 6-track queue and calls `playTrack`, which trips
`orbitBulkGuard` and pops a "Add 6 tracks to the Orbit queue?" modal.
Hitting Cancel leaves playback frozen; "Add them all" injects unrelated
tracks into the host's shared queue.
In an active Orbit guest session the host owns the queue. Skip the
fallback paths entirely and just stop — the next `useOrbitGuest` pull
tick will sync to whatever the host advanced to.
Bonus side-effect: kills the deferred-promise race where a
`buildInfiniteQueueCandidates().then(...)` from a guest's track end
could resolve *after* a Catch Up replaced the queue and pop the modal
a second time against the now-current 1-track queue.
* fix(orbit): treat natural track-end as not-diverged in guest sync
When a guest's track ended naturally before the host advanced, the
divergence-detection branch read `player.isPlaying === false` and
classified it as the user manually paused — so it refused to load the
host's next track. The guest sat silent until they clicked Catch Up.
`handleAudioEnded` keeps `currentTrack` pinned to the just-ended track
and resets `currentTime` to 0, while a real manual pause leaves
`currentTime` somewhere mid-track. Use the 0-position discriminator to
classify natural-end as not-diverged so the host's new track loads.
Confirmed via the captured guest log buffer:
18:43:08.598 [track-change] host: VJkV5… → 6i6RP… BUT guest diverged
(player.isPlaying=false ≠ last.isPlaying=true)
— guest stuck for ~33s until Catch Up was pressed.
* fix(orbit): Catch Up polls until engine is ready before seeking
The 400 ms blind setTimeout in `onCatchUp` was too short for an
HTTP-streamed cold-start on high-latency links. If the audio engine
wasn't ready by then, `seek(fraction)` silently no-oped and playback
started at 0:00, making Catch Up effectively useless on exactly the
slow links where it's needed. Captured log shows a Catch Up bringing
the guest to posSec=30, then 5 s later the guest was at posSec=6
(playback restarted from the head).
Replace with the same poll-until-ready pattern `syncToHost` already
uses: check every 100 ms, fire the seek as soon as the engine reports
playing, fall back to a blind apply at the 4 s deadline.
* docs(changelog): add orbit guest playback fixes entry
* fix(orbit): debounce Catch Up button + match bar item height
Two follow-on UX fixes after PR #525's three primary bugs landed:
1. **Debounce visibility.** Drift is computed from an asymmetric signal:
guest's `currentTime` updates in coarse ~5 s chunks, while host's
position is extrapolated linearly via `(nowMs - posAt)`. Even on a
perfectly-synced session the diff swings ±5 s every tick, so the
button flickered in and out continuously. Show only after drift has
stayed over the 3 s threshold for ≥ 3 s of wall clock — measurement
noise is filtered out, real sustained drift still surfaces in time.
2. **Match neighbour height.** The button was 32 px tall against 26 px
for the other action buttons (.orbit-bar__settings) so every flicker
shifted the entire bar height. Set `height: 26 px` and tighten the
padding/font so the layout is stable regardless of visibility.
* fix(orbit): tighten queue-extension lockout + reliable initial-sync seek
Two follow-on fixes after the 4-bug umbrella:
**1. Local queue-extension paths fully off during Orbit.**
Phase check broadened from `active` to cover `starting` / `joining` /
`active` so a fetch-then-join race can't pop the bulk-add modal *after*
the join. The proactive infinite-queue topper inside `next()` (which
fires when ≤ 2 auto-tracks remain ahead) is now also gated, plus each
async `.then()` callback in the radio + infinite-queue paths re-checks
at resolution time. A `playTrack(... 6-track queue ...)` after the user
joined Orbit was the path that re-triggered the "Add 5 tracks?" modal
on a freshly-joined guest.
**2. `syncToHost` only seeks once the engine reports playing.**
The previous 2 s deadline-fallback applied the seek even when the
engine hadn't started, where the seek silently no-ops and the track
plays from 0:00. Symptom: clicking Catch Up makes the song "jump 50 %
forward" — that's the seek finally landing because the engine is now
ready, the initial-sync seek had already failed silently. New deadline
is 5 s, and on timeout we return `false` so the outer pull tick keeps
`lastAppliedRef` null and the 500 ms fast-poll retries.
* fix(orbit): double-click play button + hide preview during session
Two cucadmuh-flagged gaps:
**1. Double-click on the inline play button now reaches the orbit-add
path.** The album-track row's onDoubleClick already routes to
`addTrackToOrbit` when in Orbit, but the inline play button stopped
propagation on click — so clicking it twice just fired the "double-
click to add" hint toast and never touched the orbit queue. Add an
onDoubleClick on the button itself that delegates to the parent's
`onDoubleClickSong`.
**2. Track preview is suppressed during an Orbit session.** Preview
shares the Rust audio engine with the shared playback, so starting
one as a guest yanks the host's track out from under everyone. A new
`[data-orbit-active]` attribute on `<html>` (set whenever role is
host/guest and phase is starting/joining/active) hides every
preview button via a single CSS rule, and `previewStore.startPreview`
short-circuits as a defensive guard for keyboard shortcuts and any
programmatic callers.
|
||
|
|
d7ff1d3113 |
fix(preview): keep preview sink volume in sync with player slider (#498) (#502)
* fix(preview): keep preview sink volume in sync with player slider (#498) The Rust preview sink had its volume set once at audio_preview_play and then never updated. audio_set_volume only ramps the main sink, so slider movements during a preview had zero effect on the preview level. With the default loudness normalization (-4.5 dB pre-analysis attenuation) applied at start, even a 100% slider gives 1.0 × 0.596 × MASTER_HEADROOM ≈ 53% — matching the user-visible "fixed at around 50%" symptom. - Add audio_preview_set_volume Rust command that updates the preview sink if one is active (clamp + master headroom mirror the path used in audio_preview_play). - Extract the preview-volume calculation in previewStore into computePreviewVolume() so startPreview and the new sync path share one formula (slider value, plus the LUFS pre-analysis attenuation the engine already applies to the main sink). - Subscribe to playerStore at module level: when volume changes and a preview is active, push the recomputed value to Rust. Auth / normalization tweaks during preview are intentionally not synced — preview is short and that case is rare. Reported by netherguy4. * docs: changelog entry for PR #502 Logs the preview-volume-slider sync fix in v1.46.0 "## Fixed". |
||
|
|
e44e6dcdf4 |
fix: restore audio refactor + features lost in #419 squash-merge (#429)
The squash-merge of PR #419 was performed against an outdated PR base that predated several main-side refactors and features. The resulting squash inadvertently re-introduced files that had already been removed (`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx` animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc). This commit: * Restores all collateral-damage files to their pre-#419 main state ( |
||
|
|
18b4a982ef |
feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing
* deleted endsAt and showDuration strings, changed eta update to 30s
* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon
* feat(queue): fold ETA into existing total/remaining toggle as third mode
The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.
ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.
i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.
* docs(changelog): add #419 queue UX improvements entry
Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
|
||
|
|
297c9f1125 |
fix(preview): sync audio start, ring animation, and download timeout (#423)
* fix(preview): sync audio start, ring animation, and download timeout Three coupled fixes for the track-preview engine: 1. Audio sync. `Sink::try_seek` was running on a worker thread after `sink.append(source)`, so the sink began playing position 0 while the seek was still iterating to the mid-track target. With the 30 s `take_duration` cap counting wall-clock from append, audio could only become audible ~25% into the preview window. The seek now runs on the bare source before append, then `take_duration` wraps it — playback starts at the seek position with the cap measured from there. 2. Ring animation gating. The CSS progress-ring animation was bound to `is-previewing` (set on click), so the ring sprinted ahead of any download/decode/seek warmup and didn't reset cleanly when switching from one preview to another. Added an `audioStarted` flag in `previewStore` that flips on `audio:preview-start` from the engine; CSS animation is now gated on `audio-started` instead. `is-previewing` still drives tooltip/icon for instant click feedback. Same SVG is reused for a 25%-arc rotating loading spinner while waiting for audio, with a 150 ms delay so cached/short previews don't flash. 3. Download timeout. The shared `audio_http_client` caps at 30 s, which aborts mid-download on multi-hundred-MB uncompressed files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now builds a dedicated client with a 5 min timeout for the bytes fetch. Watchdog still bounds the playback window at 30 s once the audio actually starts. Touches `audio/preview.rs`, `previewStore.ts`, `components.css` plus the eight tracklist/player-bar components that render the preview button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changelog): add preview audio sync fix for PR #423 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
20a083a9a6 |
feat(player): preview indicator in player bar + smart stop semantics (#394)
* feat(player): preview-active state on play button (ring + stop icon) Checkpoint: play button mirrors the inline preview button from tracklists during preview playback — hollow circle, accent ring depleting over the preview duration, Square (stop) icon. Click still resumes main playback, which the Rust audio engine cancels the preview for. i18n key player.previewActive in all 8 locales for tooltip + aria-label. * feat(player): show preview track in player bar + smart stop semantics The player-bar info cell (cover, title, artist) now mirrors the previewing track during preview playback, with a small accent "Preview" pill above the title and an accent top-border on the bar. Rating, fullscreen hint and album/artist link clicks are suppressed while previewing — they target the queued track, not the preview. Stop semantics for the two transport buttons during preview: - Big play button (Square+ring visual): stops preview, main auto-resumes if it was playing before. Matches the tracklist preview-button behaviour. - Small Stop button: new audio_preview_stop_silent Rust command — stops preview AND leaves main paused, so "Stop = silence" actually goes silent. previewStore now stores the full PreviewingTrack (id, title, artist, coverArt) — the seven startPreview call sites pass it through. i18n key player.previewLabel in all 8 locales. |
||
|
|
a14dba8167 |
feat(audio): rust track preview engine + inline play/preview buttons (#392)
* feat(audio): rust preview engine with secondary sink Adds a parallel rodio Sink on the existing OutputStream for 30s mid-track previews. Two new Tauri commands (audio_preview_play, audio_preview_stop) plus three events (audio:preview-start / -progress / -end). The main sink is paused with Sink::pause() and auto-resumed on preview end iff it was playing beforehand. * feat(playlists): migrate suggestion preview to rust audio engine Replaces the HTML5 <audio> path with the new rust preview engine. previewStore mirrors the engine's start/progress/end events so any tracklist row can render preview UI from a single source of truth. Spacebar redirects to stopPreview while a preview plays, hardware mediakeys are silently dropped (Q5), and tray clicks cancel the preview before forwarding the original action. * feat(albums): inline play + preview buttons in tracklist rows Track number stays static on hover instead of swapping to a play icon — the dedicated Play and Preview buttons in the title cell take over click-to-play and click-to-preview. Active+playing rows keep the eq-bars (also on hover), active+paused rows fall back to the static accent-coloured number. Pilot for the wider rollout to other tracklists. * feat(tracklists): roll out inline play + preview buttons Mirrors the AlbumTrackList pilot across the remaining track-row based lists: PlaylistDetail main tracks, Favorites, ArtistDetail top tracks, RandomMix (both genre-mix and filtered-songs lists). Track number stays static, the dedicated Play + Preview buttons in the title cell take over click-to-play and click-to-preview. * feat(settings): track preview toggle + configurable position and duration Adds an opt-out switch and two sliders to Settings → Audio: start position (0-90 % of track length, default 33 %) and preview duration (5-60 s, default 30 s). The progress-ring animation follows the duration via a CSS variable so the visual matches the engine's auto-stop. Disabling the feature hides every inline preview button via a single root-level data attribute, no per-row conditional rendering required. i18n keys added in all 8 locales. * fix(audio): cancel preview when main playback (re)starts audio_play, audio_play_radio, audio_resume and audio_stop did not know about the parallel preview sink, so clicking Play on a track that was currently being previewed left the preview running on top of the freshly started main playback. New helper clears the resume flag, bumps the preview generation, drops the sink and emits an 'interrupted' end event before any of those commands touches the main sink. * feat(settings): per-location track preview toggles Splits the single trackPreviewsEnabled toggle into a master + 6 per-location sub-toggles (suggestions, albums, playlists, favorites, artist, randomMix). Master remains the kill switch; sub-toggles are only honoured when master is on. Each tracklist container is marked with `data-preview-loc="<id>"` and hidden via scoped CSS when the matching root attribute is "off". startPreview now takes a location argument so the store can guard logic too. i18n added in all 8 locales. * fix(contextmenu): use ChevronsRight for Play Next to distinguish from preview |