feat(web): use thumbhash as a cache key (#16106)

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2025-02-15 22:34:13 -05:00 committed by GitHub
parent c524fcf084
commit f386b4d377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 41 additions and 38 deletions

View File

@ -43,10 +43,10 @@
import DetailPanel from './detail-panel.svelte'; import DetailPanel from './detail-panel.svelte';
import CropArea from './editor/crop-tool/crop-area.svelte'; import CropArea from './editor/crop-tool/crop-area.svelte';
import EditorPanel from './editor/editor-panel.svelte'; import EditorPanel from './editor/editor-panel.svelte';
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import SlideshowBar from './slideshow-bar.svelte'; import SlideshowBar from './slideshow-bar.svelte';
import VideoViewer from './video-wrapper-viewer.svelte'; import VideoViewer from './video-wrapper-viewer.svelte';
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
type HasAsset = boolean; type HasAsset = boolean;
@ -190,7 +190,7 @@
} }
}; };
const onAssetUpdate = (assetUpdate: AssetResponseDto) => { const onAssetUpdate = ({ asset: assetUpdate }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => {
if (assetUpdate.id === asset.id) { if (assetUpdate.id === asset.id) {
asset = assetUpdate; asset = assetUpdate;
} }
@ -198,8 +198,8 @@
onMount(async () => { onMount(async () => {
unsubscribes.push( unsubscribes.push(
websocketEvents.on('on_upload_success', onAssetUpdate), websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
websocketEvents.on('on_asset_update', onAssetUpdate), websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
); );
slideshowStateUnsubscribe = slideshowState.subscribe((value) => { slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
@ -377,6 +377,7 @@
case AssetAction.KEEP_THIS_DELETE_OTHERS: case AssetAction.KEEP_THIS_DELETE_OTHERS:
case AssetAction.UNSTACK: { case AssetAction.UNSTACK: {
closeViewer(); closeViewer();
break;
} }
} }
@ -483,7 +484,7 @@
{:else} {:else}
<VideoViewer <VideoViewer
assetId={previewStackedAsset.id} assetId={previewStackedAsset.id}
checksum={previewStackedAsset.checksum} cacheKey={previewStackedAsset.thumbhash}
projectionType={previewStackedAsset.exifInfo?.projectionType} projectionType={previewStackedAsset.exifInfo?.projectionType}
loopVideo={true} loopVideo={true}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
@ -500,7 +501,7 @@
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer <VideoViewer
assetId={asset.livePhotoVideoId} assetId={asset.livePhotoVideoId}
checksum={asset.checksum} cacheKey={asset.thumbhash}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
@ -529,7 +530,7 @@
{:else} {:else}
<VideoViewer <VideoViewer
assetId={asset.id} assetId={asset.id}
checksum={asset.checksum} cacheKey={asset.thumbhash}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}

View File

@ -50,7 +50,7 @@
img = new Image(); img = new Image();
await tick(); await tick();
img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum }); img.src = getAssetOriginalUrl({ id: asset.id, cacheKey: asset.thumbhash });
img.addEventListener('load', () => onImageLoad(true)); img.addEventListener('load', () => onImageLoad(true));
img.addEventListener('error', (error) => { img.addEventListener('error', (error) => {

View File

@ -40,7 +40,7 @@ describe('PhotoViewer component', () => {
expect(getAssetThumbnailUrlSpy).toBeCalledWith({ expect(getAssetThumbnailUrlSpy).toBeCalledWith({
id: asset.id, id: asset.id,
size: AssetMediaSize.Preview, size: AssetMediaSize.Preview,
checksum: asset.checksum, cacheKey: asset.thumbhash,
}); });
expect(getAssetOriginalUrlSpy).not.toBeCalled(); expect(getAssetOriginalUrlSpy).not.toBeCalled();
}); });
@ -50,7 +50,7 @@ describe('PhotoViewer component', () => {
render(PhotoViewer, { asset }); render(PhotoViewer, { asset });
expect(getAssetThumbnailUrlSpy).not.toBeCalled(); expect(getAssetThumbnailUrlSpy).not.toBeCalled();
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, checksum: asset.checksum }); expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
}); });
it('loads original for shared link when download permission is true and showMetadata permission is true', () => { it('loads original for shared link when download permission is true and showMetadata permission is true', () => {
@ -59,7 +59,7 @@ describe('PhotoViewer component', () => {
render(PhotoViewer, { asset, sharedLink }); render(PhotoViewer, { asset, sharedLink });
expect(getAssetThumbnailUrlSpy).not.toBeCalled(); expect(getAssetThumbnailUrlSpy).not.toBeCalled();
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, checksum: asset.checksum }); expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
}); });
it('not loads original image when shared link download permission is false', () => { it('not loads original image when shared link download permission is false', () => {
@ -70,7 +70,7 @@ describe('PhotoViewer component', () => {
expect(getAssetThumbnailUrlSpy).toBeCalledWith({ expect(getAssetThumbnailUrlSpy).toBeCalledWith({
id: asset.id, id: asset.id,
size: AssetMediaSize.Preview, size: AssetMediaSize.Preview,
checksum: asset.checksum, cacheKey: asset.thumbhash,
}); });
expect(getAssetOriginalUrlSpy).not.toBeCalled(); expect(getAssetOriginalUrlSpy).not.toBeCalled();
@ -84,7 +84,7 @@ describe('PhotoViewer component', () => {
expect(getAssetThumbnailUrlSpy).toBeCalledWith({ expect(getAssetThumbnailUrlSpy).toBeCalledWith({
id: asset.id, id: asset.id,
size: AssetMediaSize.Preview, size: AssetMediaSize.Preview,
checksum: asset.checksum, cacheKey: asset.thumbhash,
}); });
expect(getAssetOriginalUrlSpy).not.toBeCalled(); expect(getAssetOriginalUrlSpy).not.toBeCalled();

View File

@ -70,19 +70,19 @@
for (const preloadAsset of preloadAssets || []) { for (const preloadAsset of preloadAssets || []) {
if (preloadAsset.type === AssetTypeEnum.Image) { if (preloadAsset.type === AssetTypeEnum.Image) {
let img = new Image(); let img = new Image();
img.src = getAssetUrl(preloadAsset.id, useOriginal, preloadAsset.checksum); img.src = getAssetUrl(preloadAsset.id, useOriginal, preloadAsset.thumbhash);
} }
} }
}; };
const getAssetUrl = (id: string, useOriginal: boolean, checksum: string) => { const getAssetUrl = (id: string, useOriginal: boolean, cacheKey: string | null) => {
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) { if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, checksum }); return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
} }
return useOriginal return useOriginal
? getAssetOriginalUrl({ id, checksum }) ? getAssetOriginalUrl({ id, cacheKey })
: getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, checksum }); : getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
}; };
copyImage = async () => { copyImage = async () => {
@ -158,7 +158,7 @@
preload(useOriginalImage, preloadAssets); preload(useOriginalImage, preloadAssets);
}); });
let imageLoaderUrl = $derived(getAssetUrl(asset.id, useOriginalImage, asset.checksum)); let imageLoaderUrl = $derived(getAssetUrl(asset.id, useOriginalImage, asset.thumbhash));
</script> </script>
<svelte:window <svelte:window

View File

@ -13,7 +13,7 @@
interface Props { interface Props {
assetId: string; assetId: string;
loopVideo: boolean; loopVideo: boolean;
checksum: string; cacheKey: string | null;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
onNextAsset?: () => void; onNextAsset?: () => void;
onVideoEnded?: () => void; onVideoEnded?: () => void;
@ -24,7 +24,7 @@
let { let {
assetId, assetId,
loopVideo, loopVideo,
checksum, cacheKey,
onPreviousAsset = () => {}, onPreviousAsset = () => {},
onNextAsset = () => {}, onNextAsset = () => {},
onVideoEnded = () => {}, onVideoEnded = () => {},
@ -39,7 +39,7 @@
onMount(() => { onMount(() => {
if (videoPlayer) { if (videoPlayer) {
assetFileUrl = getAssetPlaybackUrl({ id: assetId, checksum }); assetFileUrl = getAssetPlaybackUrl({ id: assetId, cacheKey });
forceMuted = false; forceMuted = false;
videoPlayer.load(); videoPlayer.load();
} }
@ -106,7 +106,7 @@
onclose={() => onClose()} onclose={() => onClose()}
muted={forceMuted || $videoViewerMuted} muted={forceMuted || $videoViewerMuted}
bind:volume={$videoViewerVolume} bind:volume={$videoViewerVolume}
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, checksum })} poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
src={assetFileUrl} src={assetFileUrl}
> >
</video> </video>

View File

@ -6,7 +6,7 @@
interface Props { interface Props {
assetId: string; assetId: string;
projectionType: string | null | undefined; projectionType: string | null | undefined;
checksum: string; cacheKey: string | null;
loopVideo: boolean; loopVideo: boolean;
onClose?: () => void; onClose?: () => void;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
@ -18,7 +18,7 @@
let { let {
assetId, assetId,
projectionType, projectionType,
checksum, cacheKey,
loopVideo, loopVideo,
onPreviousAsset, onPreviousAsset,
onClose, onClose,
@ -33,7 +33,7 @@
{:else} {:else}
<VideoNativeViewer <VideoNativeViewer
{loopVideo} {loopVideo}
{checksum} {cacheKey}
{assetId} {assetId}
{onPreviousAsset} {onPreviousAsset}
{onNextAsset} {onNextAsset}

View File

@ -327,7 +327,7 @@
{/if} {/if}
<ImageThumbnail <ImageThumbnail
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, checksum: asset.checksum })} url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
altText={$getAltText(asset)} altText={$getAltText(asset)}
widthStyle="{width}px" widthStyle="{width}px"
heightStyle="{height}px" heightStyle="{height}px"
@ -339,7 +339,7 @@
<div class="absolute top-0 h-full w-full"> <div class="absolute top-0 h-full w-full">
<VideoThumbnail <VideoThumbnail
{assetStore} {assetStore}
url={getAssetPlaybackUrl({ id: asset.id, checksum: asset.checksum })} url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
enablePlayback={mouseOver && $playVideoThumbnailOnHover} enablePlayback={mouseOver && $playVideoThumbnailOnHover}
curve={selected} curve={selected}
durationInSeconds={timeToSeconds(asset.duration)} durationInSeconds={timeToSeconds(asset.duration)}
@ -352,7 +352,7 @@
<div class="absolute top-0 h-full w-full"> <div class="absolute top-0 h-full w-full">
<VideoThumbnail <VideoThumbnail
{assetStore} {assetStore}
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, checksum: asset.checksum })} url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
pauseIcon={mdiMotionPauseOutline} pauseIcon={mdiMotionPauseOutline}
playIcon={mdiMotionPlayOutline} playIcon={mdiMotionPlayOutline}
showTime={false} showTime={false}

View File

@ -180,28 +180,30 @@ const createUrl = (path: string, parameters?: Record<string, unknown>) => {
return getBaseUrl() + url.pathname + url.search + url.hash; return getBaseUrl() + url.pathname + url.search + url.hash;
}; };
export const getAssetOriginalUrl = (options: string | { id: string; checksum?: string }) => { type AssetUrlOptions = { id: string; cacheKey?: string | null };
export const getAssetOriginalUrl = (options: string | AssetUrlOptions) => {
if (typeof options === 'string') { if (typeof options === 'string') {
options = { id: options }; options = { id: options };
} }
const { id, checksum } = options; const { id, cacheKey } = options;
return createUrl(getAssetOriginalPath(id), { key: getKey(), c: checksum }); return createUrl(getAssetOriginalPath(id), { key: getKey(), c: cacheKey });
}; };
export const getAssetThumbnailUrl = (options: string | { id: string; size?: AssetMediaSize; checksum?: string }) => { export const getAssetThumbnailUrl = (options: string | (AssetUrlOptions & { size?: AssetMediaSize })) => {
if (typeof options === 'string') { if (typeof options === 'string') {
options = { id: options }; options = { id: options };
} }
const { id, size, checksum } = options; const { id, size, cacheKey } = options;
return createUrl(getAssetThumbnailPath(id), { size, key: getKey(), c: checksum }); return createUrl(getAssetThumbnailPath(id), { size, key: getKey(), c: cacheKey });
}; };
export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: string }) => { export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
if (typeof options === 'string') { if (typeof options === 'string') {
options = { id: options }; options = { id: options };
} }
const { id, checksum } = options; const { id, cacheKey } = options;
return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum }); return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: cacheKey });
}; };
export const getProfileImageUrl = (user: UserResponseDto) => export const getProfileImageUrl = (user: UserResponseDto) =>