refactor: asset media endpoints (#9831)

* refactor: asset media endpoints

* refactor: mobile upload livePhoto as separate request

* refactor: change mobile backup flow to use new asset upload endpoints

* chore: format and analyze dart code

* feat: mark motion as hidden when linked

* feat: upload video portion of live photo before image portion

* fix: incorrect assetApi calls in mobile code

* fix: download asset

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
Jason Rasmussen 2024-05-31 13:44:04 -04:00 committed by GitHub
parent 66fced40e7
commit 69d2fcb43e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 1932 additions and 2456 deletions

View file

@ -44,7 +44,7 @@ describe('AlbumCard component', () => {
await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
expect(albumImgElement).toHaveAttribute('alt', album.albumName);
expect(sdkMock.getAssetThumbnail).not.toHaveBeenCalled();
expect(sdkMock.viewAsset).not.toHaveBeenCalled();
expect(albumNameElement).toHaveTextContent(album.albumName);
expect(albumDetailsElement).toHaveTextContent(new RegExp(detailsText));

View file

@ -1,15 +1,13 @@
<script lang="ts">
import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
import { getAssetThumbnailUrl } from '$lib/utils';
import { type AlbumResponseDto } from '@immich/sdk';
export let album: AlbumResponseDto | undefined;
export let preload = false;
export let css = '';
$: thumbnailUrl =
album && album.albumThumbnailAssetId
? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp)
: null;
album && album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
</script>
<div class="relative aspect-square">

View file

@ -9,7 +9,6 @@
import { isTenMinutesApart } from '$lib/utils/timesince';
import {
ReactionType,
ThumbnailFormat,
createActivity,
deleteActivity,
getActivities,
@ -182,7 +181,7 @@
<a class="aspect-square w-[75px] h-[75px]" href="{AppRoute.ALBUMS}/{albumId}/photos/{reaction.assetId}">
<img
class="rounded-lg w-[75px] h-[75px] object-cover"
src={getAssetThumbnailUrl(reaction.assetId, ThumbnailFormat.Webp)}
src={getAssetThumbnailUrl(reaction.assetId)}
alt="Profile picture of {reaction.user.name}, who commented on this asset"
/>
</a>
@ -235,7 +234,7 @@
>
<img
class="rounded-lg w-[75px] h-[75px] object-cover"
src={getAssetThumbnailUrl(reaction.assetId, ThumbnailFormat.Webp)}
src={getAssetThumbnailUrl(reaction.assetId)}
alt="Profile picture of {reaction.user.name}, who liked this asset"
/>
</a>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { getAssetThumbnailUrl } from '$lib/utils';
import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
import { type AlbumResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { normalizeSearchString } from '$lib/utils/string-utils.js';
import AlbumListItemDetails from './album-list-item-details.svelte';
@ -35,7 +35,7 @@
<span class="h-12 w-12 shrink-0 rounded-xl bg-slate-300">
{#if album.albumThumbnailAssetId}
<img
src={getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp)}
src={getAssetThumbnailUrl(album.albumThumbnailAssetId)}
alt={album.albumName}
class="z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg"
data-testid="album-image"

View file

@ -11,7 +11,7 @@
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
import { delay, isFlipped } from '$lib/utils/asset-utils';
import {
ThumbnailFormat,
AssetMediaSize,
getAssetInfo,
updateAsset,
type AlbumResponseDto,
@ -474,7 +474,7 @@
alt={album.albumName}
class="h-[50px] w-[50px] rounded object-cover"
src={album.albumThumbnailAssetId &&
getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Jpeg)}
getAssetThumbnailUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
draggable="false"
/>
</div>

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { serveFile, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { getAssetOriginalUrl, getKey } from '$lib/utils';
import { AssetMediaSize, AssetTypeEnum, viewAsset, type AssetResponseDto } from '@immich/sdk';
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { getAssetFileUrl, getKey } from '$lib/utils';
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
const photoSphereConfigs =
@ -20,9 +20,9 @@
const loadAssetData = async () => {
if (asset.type === AssetTypeEnum.Video) {
return { source: getAssetFileUrl(asset.id, false, false) };
return { source: getAssetOriginalUrl(asset.id) };
}
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
const data = await viewAsset({ id: asset.id, size: AssetMediaSize.Preview, key: getKey() });
const url = URL.createObjectURL(data);
return url;
};

View file

@ -44,7 +44,7 @@ describe('PhotoViewer component', () => {
expect(downloadRequestMock).toBeCalledWith(
expect.objectContaining({
url: `/api/asset/file/${asset.id}?isThumb=false&isWeb=true&c=${asset.checksum}`,
url: `/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.checksum}`,
}),
);
await waitFor(() => expect(screen.getByRole('img')).toBeInTheDocument());
@ -61,7 +61,7 @@ describe('PhotoViewer component', () => {
await waitFor(() => expect(screen.getByRole('img')).toHaveAttribute('src', 'url-two'));
expect(downloadRequestMock).toBeCalledWith(
expect.objectContaining({
url: `/api/asset/file/${asset.id}?isThumb=false&isWeb=false&c=${asset.checksum}`,
url: `/api/assets/${asset.id}/original?c=${asset.checksum}`,
}),
);
});
@ -76,7 +76,7 @@ describe('PhotoViewer component', () => {
await waitFor(() => expect(screen.getByRole('img')).toHaveAttribute('src', 'url-two'));
expect(downloadRequestMock).toBeCalledWith(
expect.objectContaining({
url: `/api/asset/file/${asset.id}?isThumb=false&isWeb=true&c=new-checksum`,
url: `/api/assets/${asset.id}/thumbnail?size=preview&c=new-checksum`,
}),
);
});

View file

@ -3,11 +3,11 @@
import { boundingBoxesArray } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { downloadRequest, getAssetFileUrl, handlePromiseError } from '$lib/utils';
import { downloadRequest, getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
import { shortcuts } from '$lib/actions/shortcut';
import { type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { type AssetResponseDto, AssetTypeEnum, AssetMediaSize } from '@immich/sdk';
import { useZoomImageWheel } from '@zoom-image/svelte';
import { onDestroy, onMount } from 'svelte';
import { fade } from 'svelte/transition';
@ -62,7 +62,9 @@
// TODO: Use sdk once it supports signals
const res = await downloadRequest({
url: getAssetFileUrl(asset.id, !loadOriginal, false, checksum),
url: loadOriginal
? getAssetOriginalUrl({ id: asset.id, checksum })
: getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Preview, checksum }),
signal: abortController.signal,
});
@ -76,7 +78,9 @@
for (const preloadAsset of preloadAssets) {
if (preloadAsset.type === AssetTypeEnum.Image) {
await downloadRequest({
url: getAssetFileUrl(preloadAsset.id, !loadOriginal, false),
url: loadOriginal
? getAssetOriginalUrl(preloadAsset.id)
: getAssetThumbnailUrl({ id: preloadAsset.id, size: AssetMediaSize.Preview }),
signal: abortController.signal,
});
}

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { loopVideo as loopVideoPreference, videoViewerVolume, videoViewerMuted } from '$lib/stores/preferences.store';
import { getAssetFileUrl, getAssetThumbnailUrl } from '$lib/utils';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { ThumbnailFormat } from '@immich/sdk';
import { AssetMediaSize } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let assetId: string;
export let loopVideo: boolean;
@ -16,7 +16,7 @@
let assetFileUrl: string;
$: {
const next = getAssetFileUrl(assetId, false, true, checksum);
const next = getAssetPlaybackUrl({ id: assetId, checksum });
if (assetFileUrl !== next) {
assetFileUrl = next;
element && element.load();
@ -54,7 +54,7 @@
on:ended={() => dispatch('onVideoEnded')}
bind:muted={$videoViewerMuted}
bind:volume={$videoViewerVolume}
poster={getAssetThumbnailUrl(assetId, ThumbnailFormat.Jpeg, checksum)}
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, checksum })}
>
<source src={assetFileUrl} type="video/mp4" />
<track kind="captions" />

View file

@ -2,11 +2,12 @@
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetFileUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetTypeEnum, ThumbnailFormat, type AssetResponseDto } from '@immich/sdk';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl } from '$lib/utils';
import {
mdiArchiveArrowDownOutline,
mdiCameraBurst,
@ -33,7 +34,6 @@
export let thumbnailSize: number | undefined = undefined;
export let thumbnailWidth: number | undefined = undefined;
export let thumbnailHeight: number | undefined = undefined;
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
export let selected = false;
export let selectionCandidate = false;
export let disabled = false;
@ -181,7 +181,7 @@
{#if asset.resized}
<ImageThumbnail
url={getAssetThumbnailUrl(asset.id, format, asset.checksum)}
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, checksum: asset.checksum })}
altText={getAltText(asset)}
widthStyle="{width}px"
heightStyle="{height}px"
@ -197,7 +197,7 @@
{#if asset.type === AssetTypeEnum.Video}
<div class="absolute top-0 h-full w-full">
<VideoThumbnail
url={getAssetFileUrl(asset.id, false, true, asset.checksum)}
url={getAssetPlaybackUrl({ id: asset.id, checksum: asset.checksum })}
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
curve={selected}
durationInSeconds={timeToSeconds(asset.duration)}
@ -209,7 +209,7 @@
{#if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
<div class="absolute top-0 h-full w-full">
<VideoThumbnail
url={getAssetFileUrl(asset.livePhotoVideoId, false, true, asset.checksum)}
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, checksum: asset.checksum })}
pauseIcon={mdiMotionPauseOutline}
playIcon={mdiMotionPlayOutline}
showTime={false}

View file

@ -3,7 +3,7 @@
import { photoViewer } from '$lib/stores/assets.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import { AssetTypeEnum, ThumbnailFormat, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { linear } from 'svelte/easing';
@ -43,7 +43,7 @@
if (assetType === AssetTypeEnum.Image) {
image = $photoViewer;
} else if (assetType === AssetTypeEnum.Video) {
const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
const data = getAssetThumbnailUrl(assetId);
const img: HTMLImageElement = new Image();
img.src = data;

View file

@ -1,27 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { shortcuts } from '$lib/actions/shortcut';
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import { AppRoute, QueryParameter } from '$lib/constants';
import { type Viewport } from '$lib/stores/assets.store';
import { memoryStore } from '$lib/stores/memory.store';
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { shortcuts } from '$lib/actions/shortcut';
import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { ThumbnailFormat, getMemoryLane, type AssetResponseDto } from '@immich/sdk';
import { AssetMediaSize, getMemoryLane, type AssetResponseDto } from '@immich/sdk';
import {
mdiChevronDown,
mdiChevronLeft,
@ -243,7 +243,7 @@
{#if previousMemory}
<img
class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl(previousMemory.assets[0].id, ThumbnailFormat.Jpeg)}
src={getAssetThumbnailUrl({ id: previousMemory.assets[0].id, size: AssetMediaSize.Preview })}
alt="Previous memory"
draggable="false"
/>
@ -275,7 +275,7 @@
<img
transition:fade
class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl(currentAsset.id, ThumbnailFormat.Jpeg)}
src={getAssetThumbnailUrl({ id: currentAsset.id, size: AssetMediaSize.Preview })}
alt={currentAsset.exifInfo?.description}
draggable="false"
/>
@ -321,7 +321,7 @@
{#if nextMemory}
<img
class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl(nextMemory.assets[0].id, ThumbnailFormat.Jpeg)}
src={getAssetThumbnailUrl({ id: nextMemory.assets[0].id, size: AssetMediaSize.Preview })}
alt="Next memory"
draggable="false"
/>

View file

@ -4,7 +4,7 @@
import { memoryStore } from '$lib/stores/memory.store';
import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
import { getMemoryLane } from '@immich/sdk';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
@ -75,7 +75,7 @@
>
<img
class="h-full w-full rounded-xl object-cover"
src={getAssetThumbnailUrl(memory.assets[0].id, ThumbnailFormat.Webp)}
src={getAssetThumbnailUrl(memory.assets[0].id)}
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
draggable="false"
/>

View file

@ -183,7 +183,7 @@
/>
{:else}
<img
src={getAssetThumbnailUrl(feature.properties?.id, undefined)}
src={getAssetThumbnailUrl(feature.properties?.id)}
class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
alt={feature.properties?.city && feature.properties.country
? `Map marker for images taken in ${feature.properties.city}, ${feature.properties.country}`

View file

@ -2,7 +2,7 @@
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { getAssetThumbnailUrl } from '$lib/utils';
import { ThumbnailFormat, type AssetResponseDto, type DuplicateResponseDto, getAllAlbums } from '@immich/sdk';
import { type AssetResponseDto, type DuplicateResponseDto, getAllAlbums } from '@immich/sdk';
import { mdiCheck, mdiTrashCanOutline } from '@mdi/js';
import { onMount } from 'svelte';
import { s } from '$lib/utils';
@ -56,7 +56,7 @@
<button type="button" on:click={() => onSelectAsset(asset)} class="block relative">
<!-- THUMBNAIL-->
<img
src={getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
src={getAssetThumbnailUrl(asset.id)}
alt={asset.id}
title={`${assetData}`}
class={`w-[250px] h-[250px] object-cover rounded-t-xl border-t-[4px] border-l-[4px] border-r-[4px] border-gray-300 ${isSelected ? 'border-immich-primary dark:border-immich-dark-primary' : 'dark:border-gray-800'} transition-all`}