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
+2 -2
View File
@@ -8,7 +8,7 @@ import type { Track } from '../../store/playerStoreTypes';
import { songToTrack } from '../../utils/playback/songToTrack';
import { useSelectionStore } from '../../store/selectionStore';
import { useThemeStore } from '../../store/themeStore';
import { usePreviewStore } from '../../store/previewStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import StarRating from '../StarRating';
import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrackListHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
@@ -116,7 +116,7 @@ export const TrackRow = React.memo(function TrackRow({
className={`playlist-suggestion-preview-btn${isPreviewing ? ' is-previewing' : ''}${isPreviewAudioStarted ? ' audio-started' : ''}`}
onClick={e => {
e.stopPropagation();
usePreviewStore.getState().startPreview({ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration }, 'albums');
usePreviewStore.getState().startPreview(previewInputFromSong(song), 'albums');
}}
data-tooltip={isPreviewing ? t('playlists.previewStop') : t('playlists.preview')}
aria-label={isPreviewing ? t('playlists.previewStop') : t('playlists.preview')}
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { AudioLines, ChevronRight, Play, Square } from 'lucide-react';
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
import { usePreviewStore } from '../../store/previewStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatTrackTime } from '../../utils/format/formatDuration';
@@ -82,7 +82,7 @@ export default function ArtistDetailTopTracks({
<button
type="button"
className={`playlist-suggestion-preview-btn${previewingId === song.id ? ' is-previewing' : ''}${previewingId === song.id && previewAudioStarted ? ' audio-started' : ''}`}
onClick={e => { e.stopPropagation(); usePreviewStore.getState().startPreview({ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration }, 'artist'); }}
onClick={e => { e.stopPropagation(); usePreviewStore.getState().startPreview(previewInputFromSong(song), 'artist'); }}
data-tooltip={previewingId === song.id ? t('playlists.previewStop') : t('playlists.preview')}
aria-label={previewingId === song.id ? t('playlists.previewStop') : t('playlists.preview')}
>
@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
import type { ColDef } from '../../utils/useTracklistColumns';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
import { usePreviewStore } from '../../store/previewStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useSelectionStore } from '../../store/selectionStore';
import { useThemeStore } from '../../store/themeStore';
import { useDragDrop } from '../../contexts/DragDropContext';
@@ -122,7 +122,7 @@ export default function FavoritesSongsTracklist({
L.playTrack(L.visibleTracks[index], L.visibleTracks);
},
startPreview: (song) => usePreviewStore.getState().startPreview(
{ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration },
previewInputFromSong(song),
'favorites',
),
rate: (songId, r) => latest.current.handleRate(songId, r),
+5 -2
View File
@@ -60,7 +60,10 @@ export function PlayerTrackInfo({
const previewCoverRef = useAlbumCoverRef(
showPreviewMeta ? coverArtId : null,
showPreviewMeta ? coverArtId : null,
undefined,
showPreviewMeta ? { libraryResolve: false } : undefined,
);
const activeCoverRef = showPreviewMeta ? previewCoverRef : playbackCoverRef;
const layoutItems = usePlayerBarLayoutStore(s => s.items);
const isLayoutVisible = (id: PlayerBarLayoutItemId) =>
layoutItems.find(i => i.id === id)?.visible !== false;
@@ -86,10 +89,10 @@ export function PlayerTrackInfo({
<Cast size={20} />
</div>
)
) : !isRadio && (showPreviewMeta ? coverArtId : playbackCoverRef) ? (
) : !isRadio && activeCoverRef ? (
<CoverArtImage
className="player-album-art"
coverRef={showPreviewMeta ? previewCoverRef! : playbackCoverRef!}
coverRef={activeCoverRef}
displayCssPx={128}
surface="sparse"
ensurePriority="high"
@@ -13,7 +13,7 @@ import type { ColDef } from '../../utils/useTracklistColumns';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { usePlayerStore } from '../../store/playerStore';
import { usePreviewStore } from '../../store/previewStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useThemeStore } from '../../store/themeStore';
import { useDragDrop } from '../../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
@@ -149,7 +149,7 @@ export default function PlaylistTracklist({
L.playTrack(L.displayedTracks[index], L.displayedTracks);
},
startPreview: (song) => usePreviewStore.getState().startPreview(
{ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration },
previewInputFromSong(song),
'playlists',
),
toggleStar: (song, e) => latest.current.handleToggleStar(song, e),
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { AudioLines, ChevronRight, Heart, Play, Square } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { Track } from '../../store/playerStoreTypes';
import { usePreviewStore } from '../../store/previewStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useDragDrop } from '../../contexts/DragDropContext';
import { formatRandomMixDuration } from '../../utils/componentHelpers/randomMixHelpers';
@@ -110,7 +110,7 @@ export default function RandomMixTrackRow({
onClick={e => {
e.stopPropagation();
usePreviewStore.getState().startPreview(
{ id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt, duration: song.duration },
previewInputFromSong(song),
'randomMix',
);
}}