Files
Psychotoxical-psysonic/src/cover/CoverArtImage.tsx
T
cucadmuh a66d932afe 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.
2026-06-05 22:59:20 +03:00

199 lines
5.6 KiB
TypeScript

import type { ImgHTMLAttributes } from 'react';
import type React from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { DEFAULT_CACHED_IMAGE_PREPARE_MARGIN } from '../components/CachedImage';
import { resolveIntersectionScrollRoot } from '../utils/ui/resolveIntersectionScrollRoot';
import { coverEnsureQueued, coverEnsureReprioritize } from './ensureQueue';
import { coverPrefetchBumpPriority } from './prefetchRegistry';
import { coverServerReachable } from './reachability';
import { coverStorageKeyFromRef } from './storageKeys';
import { resolveCoverDisplayTier } from './tiers';
import { coverImgSrc } from './imgSrc';
import { useCoverArt } from './useCoverArt';
import type { CoverArtRef, CoverPrefetchPriority, CoverSurfaceKind } from './types';
export type CoverArtImageProps = {
coverRef: CoverArtRef | null | undefined;
displayCssPx: number;
surface?: CoverSurfaceKind;
fullRes?: boolean;
className?: string;
alt?: string;
fetchQueueBias?: number;
observeRootMargin?: string;
observeScrollRootId?: string;
ensurePriority?: CoverPrefetchPriority;
} & Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'>;
export function CoverArtImage({
coverRef,
displayCssPx,
surface,
fullRes,
className,
alt,
fetchQueueBias: _fetchQueueBias,
observeRootMargin = DEFAULT_CACHED_IMAGE_PREPARE_MARGIN,
observeScrollRootId,
ensurePriority: ensurePriorityProp,
onError: restOnError,
...rest
}: CoverArtImageProps) {
if (!coverRef) {
return (
<div
className={className}
data-cover-provisional="true"
role="img"
aria-label={alt ?? ''}
{...(rest as React.HTMLAttributes<HTMLDivElement>)}
/>
);
}
const pinnedHigh = ensurePriorityProp === 'high';
const [ensurePriority, setEnsurePriority] = useState<CoverPrefetchPriority>(
ensurePriorityProp ?? 'middle',
);
const [seenViewport, setSeenViewport] = useState(false);
const seenViewportRef = useRef(false);
const imgRef = useRef<HTMLImageElement>(null);
const [imgLoadFailed, setImgLoadFailed] = useState(false);
useEffect(() => {
if (ensurePriorityProp) setEnsurePriority(ensurePriorityProp);
}, [ensurePriorityProp]);
useEffect(() => {
seenViewportRef.current = seenViewport;
}, [seenViewport]);
useEffect(() => {
setImgLoadFailed(false);
}, [coverRef.cacheEntityId, coverRef.cacheKind, coverRef.fetchCoverArtId, displayCssPx, surface, fullRes]);
useLayoutEffect(() => {
const el = imgRef.current;
if (!el) return;
const root =
(observeScrollRootId
? (document.getElementById(observeScrollRootId) as Element | null)
: null) ?? resolveIntersectionScrollRoot(el);
const tier = resolveCoverDisplayTier(displayCssPx, { surface, fullRes });
const storageKey = coverStorageKeyFromRef(coverRef, tier);
const reachable = coverServerReachable(coverRef.serverScope);
const queueEnsure = (priority: CoverPrefetchPriority) => {
if (!reachable) return;
void coverEnsureQueued(storageKey, coverRef, tier, priority);
};
const applyIntersecting = () => {
seenViewportRef.current = true;
setSeenViewport(true);
setEnsurePriority('high');
coverPrefetchBumpPriority(coverRef, 'high');
coverEnsureReprioritize(storageKey, 'high');
queueEnsure('high');
};
const applyLeftViewport = () => {
if (!seenViewportRef.current || pinnedHigh) return;
setEnsurePriority('middle');
coverEnsureReprioritize(storageKey, 'middle');
queueEnsure('middle');
};
const applyEntry = (entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) applyIntersecting();
else applyLeftViewport();
};
const observer = new IntersectionObserver(
entries => {
for (const entry of entries) applyEntry(entry);
},
{
root: root ?? undefined,
rootMargin: observeRootMargin,
threshold: [0, 0.05, 0.15],
},
);
observer.observe(el);
const drainRecords = () => {
for (const entry of observer.takeRecords()) applyEntry(entry);
};
drainRecords();
let cancelled = false;
const raf1 = requestAnimationFrame(() => {
if (cancelled) return;
drainRecords();
requestAnimationFrame(() => {
if (cancelled) return;
drainRecords();
});
});
return () => {
cancelled = true;
cancelAnimationFrame(raf1);
observer.disconnect();
};
}, [
coverRef,
displayCssPx,
surface,
fullRes,
observeRootMargin,
observeScrollRootId,
pinnedHigh,
]);
const { src, provisional, onImgError } = useCoverArt(coverRef, displayCssPx, {
surface,
fullRes,
ensurePriority,
seenViewport,
alt,
});
const imgSrc = coverImgSrc(src);
if (!imgSrc || imgLoadFailed) {
return (
<div
ref={imgRef as React.RefObject<HTMLDivElement | null>}
className={className}
data-cover-provisional="true"
data-observe-root-margin={observeRootMargin}
data-observe-scroll-root={observeScrollRootId}
role="img"
aria-label={alt ?? ''}
{...(rest as React.HTMLAttributes<HTMLDivElement>)}
/>
);
}
return (
<img
ref={imgRef}
src={imgSrc}
className={className}
alt={alt ?? ''}
data-cover-provisional={provisional ? 'true' : undefined}
data-observe-root-margin={observeRootMargin}
data-observe-scroll-root={observeScrollRootId}
{...rest}
onError={e => {
setImgLoadFailed(true);
onImgError?.();
restOnError?.(e);
}}
/>
);
}