fix(preview): Symphonia format sniff and ranged stream startup (#1006)

* fix(preview): Symphonia format sniff and cluster member stream URLs

Resolve preview container hints from HTTP headers, Subsonic suffix, and
magic-byte sniff after Symphonia 0.6. Route preview streams through
clusterBrowseServerId like main playback; guard CoverArtImage when the
preview cover ref is still loading.

* fix(preview): adapt Symphonia sniff branch for main without cluster routing

Keep formatSuffix and cover-ref guards from the cluster work; use
buildStreamUrl on the active server instead of clusterBrowseServerId.

* fix(preview): use ranged HTTP so preview starts without full-file download

Open preview via RangedHttpSource when the server supports byte ranges;
fall back to buffered download otherwise. Gate in-memory probe with
ProbeSeekGate so Symphonia 0.6 does not scan the entire file before audio.

* docs: CHANGELOG and credits for preview fix PR #1006

* chore: drop PR #1006 from settings credits (minor fix)

* fix(preview): allow clippy too_many_arguments on audio_preview_play

formatSuffix pushed the Tauri command to 8 args; matches other IPC commands.
This commit is contained in:
cucadmuh
2026-06-05 22:59:20 +03:00
committed by GitHub
parent f706336e58
commit a66d932afe
13 changed files with 395 additions and 73 deletions
+7 -1
View File
@@ -200,6 +200,12 @@ describe('previewStore — startPreview', () => {
expect(state.duration).toBe(30);
});
it('passes Subsonic suffix as formatSuffix for Symphonia container hints', async () => {
await usePreviewStore.getState().startPreview({ ...song('flac-1'), suffix: 'flac' }, 'albums');
const call = invokeMock.mock.calls.find(c => c[0] === 'audio_preview_play');
expect(call?.[1]).toMatchObject({ formatSuffix: 'flac' });
});
it('starts at 0 when the track is too short to need a mid-track seek', async () => {
// duration <= previewDuration * 1.5 → start at 0.
await usePreviewStore.getState().startPreview({ ...song(), duration: 30 }, 'suggestions');
@@ -277,7 +283,7 @@ describe('previewStore — startPreview', () => {
throw new Error('engine offline');
});
await expect(usePreviewStore.getState().startPreview(song('song-2'), 'suggestions')).rejects.toThrow(/engine offline/);
await usePreviewStore.getState().startPreview(song('song-2'), 'suggestions');
// Only rolls back when the rolled-back id is still the optimistic one.
const state = usePreviewStore.getState();
+33 -3
View File
@@ -1,4 +1,5 @@
import { buildStreamUrl } from '../api/subsonicStreamUrl';
import type { SubsonicSong } from '../api/subsonicTypes';
import type { TrackPreviewLocation } from './authStoreTypes';
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
@@ -14,6 +15,29 @@ export interface PreviewingTrack {
coverArt?: string;
}
export interface PreviewSongInput {
id: string;
title: string;
artist: string;
coverArt?: string;
duration?: number;
suffix?: string;
}
/** Map a browse/playlist song row into preview input (keeps Subsonic suffix for format hints). */
export function previewInputFromSong(
song: Pick<SubsonicSong, 'id' | 'title' | 'artist' | 'coverArt' | 'duration' | 'suffix'>,
): PreviewSongInput {
return {
id: song.id,
title: song.title,
artist: song.artist,
coverArt: song.coverArt,
duration: song.duration,
suffix: song.suffix,
};
}
interface PreviewState {
/** Subsonic song id of the active preview, or null when nothing previews. */
previewingId: string | null;
@@ -33,7 +57,7 @@ interface PreviewState {
*/
audioStarted: boolean;
startPreview: (song: { id: string; title: string; artist: string; coverArt?: string; duration?: number }, location: TrackPreviewLocation) => Promise<void>;
startPreview: (song: PreviewSongInput, location: TrackPreviewLocation) => Promise<void>;
stopPreview: () => Promise<void>;
/** Internal — called from the TauriEventBridge on `audio:preview-start`. */
@@ -100,7 +124,12 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
set({
previewingId: song.id,
previewingTrack: { id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt },
previewingTrack: {
id: song.id,
title: song.title,
artist: song.artist,
coverArt: song.coverArt,
},
elapsed: 0,
duration: previewDuration,
audioStarted: false,
@@ -113,13 +142,14 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
startSec,
durationSec: previewDuration,
volume: computePreviewVolume(),
formatSuffix: song.suffix ?? null,
});
} catch (e) {
// Roll back optimistic state on failure.
if (get().previewingId === song.id) {
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
}
throw e;
console.error('Preview playback failed', e);
}
},