mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor: asset media service queries (#25477)
This commit is contained in:
parent
f88f1265b6
commit
984fb12ada
5 changed files with 133 additions and 149 deletions
|
|
@ -585,3 +585,40 @@ where
|
||||||
and "libraryId" = $2::uuid
|
and "libraryId" = $2::uuid
|
||||||
and "isExternal" = $3
|
and "isExternal" = $3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
-- AssetRepository.getForOriginal
|
||||||
|
select
|
||||||
|
"originalFileName",
|
||||||
|
"asset_file"."path" as "editedPath",
|
||||||
|
"originalPath"
|
||||||
|
from
|
||||||
|
"asset"
|
||||||
|
left join "asset_file" on "asset"."id" = "asset_file"."assetId"
|
||||||
|
and "asset_file"."isEdited" = $1
|
||||||
|
and "asset_file"."type" = $2
|
||||||
|
where
|
||||||
|
"asset"."id" = $3
|
||||||
|
|
||||||
|
-- AssetRepository.getForThumbnail
|
||||||
|
select
|
||||||
|
"asset_file"."path",
|
||||||
|
"asset"."originalPath",
|
||||||
|
"asset"."originalFileName"
|
||||||
|
from
|
||||||
|
"asset_file"
|
||||||
|
right join "asset" on "asset"."id" = "asset_file"."assetId"
|
||||||
|
where
|
||||||
|
"asset_file"."assetId" = $1
|
||||||
|
and "asset_file"."type" = $2
|
||||||
|
order by
|
||||||
|
"asset_file"."isEdited" desc
|
||||||
|
|
||||||
|
-- AssetRepository.getForVideo
|
||||||
|
select
|
||||||
|
"asset"."encodedVideoPath",
|
||||||
|
"asset"."originalPath"
|
||||||
|
from
|
||||||
|
"asset"
|
||||||
|
where
|
||||||
|
"asset"."id" = $1
|
||||||
|
and "asset"."type" = $2
|
||||||
|
|
|
||||||
|
|
@ -1009,4 +1009,47 @@ export class AssetRepository {
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, true] })
|
||||||
|
async getForOriginal(id: string, isEdited: boolean) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset')
|
||||||
|
.select('originalFileName')
|
||||||
|
.where('asset.id', '=', id)
|
||||||
|
.$if(isEdited, (qb) =>
|
||||||
|
qb
|
||||||
|
.leftJoin('asset_file', (join) =>
|
||||||
|
join
|
||||||
|
.onRef('asset.id', '=', 'asset_file.assetId')
|
||||||
|
.on('asset_file.isEdited', '=', true)
|
||||||
|
.on('asset_file.type', '=', AssetFileType.FullSize),
|
||||||
|
)
|
||||||
|
.select('asset_file.path as editedPath'),
|
||||||
|
)
|
||||||
|
.select('originalPath')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, AssetFileType.Preview] })
|
||||||
|
async getForThumbnail(id: string, type: AssetFileType) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset_file')
|
||||||
|
.select('asset_file.path')
|
||||||
|
.where('asset_file.assetId', '=', id)
|
||||||
|
.where('asset_file.type', '=', type)
|
||||||
|
.rightJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_file.assetId'))
|
||||||
|
.select(['asset.originalPath', 'asset.originalFileName'])
|
||||||
|
.orderBy('asset_file.isEdited', 'desc')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async getForVideo(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset')
|
||||||
|
.select(['asset.encodedVideoPath', 'asset.originalPath'])
|
||||||
|
.where('asset.id', '=', id)
|
||||||
|
.where('asset.type', '=', AssetType.Video)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -500,17 +500,9 @@ describe(AssetMediaService.name, () => {
|
||||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the asset is not found', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
|
|
||||||
expect(mocks.asset.getById).toHaveBeenCalledWith('asset-1', { files: true, edits: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download a file', async () => {
|
it('should download a file', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
mocks.asset.getForOriginal.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).resolves.toEqual(
|
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
@ -536,7 +528,10 @@ describe(AssetMediaService.name, () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
mocks.asset.getById.mockResolvedValue(editedAsset);
|
mocks.asset.getForOriginal.mockResolvedValue({
|
||||||
|
...editedAsset,
|
||||||
|
editedPath: '/uploads/user-id/fullsize/edited.jpg',
|
||||||
|
});
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
@ -562,7 +557,10 @@ describe(AssetMediaService.name, () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
mocks.asset.getById.mockResolvedValue(editedAsset);
|
mocks.asset.getForOriginal.mockResolvedValue({
|
||||||
|
...editedAsset,
|
||||||
|
editedPath: '/uploads/user-id/fullsize/edited.jpg',
|
||||||
|
});
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
@ -588,7 +586,7 @@ describe(AssetMediaService.name, () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
mocks.asset.getById.mockResolvedValue(editedAsset);
|
mocks.asset.getForOriginal.mockResolvedValue(editedAsset);
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: false })).resolves.toEqual(
|
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: false })).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
@ -600,23 +598,9 @@ describe(AssetMediaService.name, () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should download original file when no edits exist', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
|
||||||
new ImmichFileResponse({
|
|
||||||
path: '/original/path.jpg',
|
|
||||||
fileName: 'asset-id.jpg',
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cacheControl: CacheControl.PrivateWithCache,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a not found when edits exist but no edited file available', async () => {
|
it('should throw a not found when edits exist but no edited file available', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.withCropEdit);
|
mocks.asset.getForOriginal.mockResolvedValue({ ...assetStub.withCropEdit, editedPath: null });
|
||||||
|
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).rejects.toBeInstanceOf(
|
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).rejects.toBeInstanceOf(
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
|
@ -633,54 +617,9 @@ describe(AssetMediaService.name, () => {
|
||||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the asset does not exist', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
|
||||||
).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if the requested thumbnail file does not exist', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
|
||||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image, files: [] });
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
|
||||||
).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if the requested preview file does not exist', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
|
||||||
mocks.asset.getById.mockResolvedValue({
|
|
||||||
...assetStub.image,
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
id: '42',
|
|
||||||
path: '/path/to/preview',
|
|
||||||
type: AssetFileType.Thumbnail,
|
|
||||||
isEdited: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await expect(
|
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
|
||||||
).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
mocks.asset.getById.mockResolvedValue({
|
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/path/to/preview.jpg' });
|
||||||
...assetStub.image,
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
id: '42',
|
|
||||||
path: '/path/to/preview.jpg',
|
|
||||||
type: AssetFileType.Preview,
|
|
||||||
isEdited: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||||
|
|
@ -696,7 +635,7 @@ describe(AssetMediaService.name, () => {
|
||||||
|
|
||||||
it('should get preview file', async () => {
|
it('should get preview file', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image });
|
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/thumbs/path.jpg' });
|
||||||
await expect(
|
await expect(
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
|
|
@ -711,7 +650,7 @@ describe(AssetMediaService.name, () => {
|
||||||
|
|
||||||
it('should get thumbnail file', async () => {
|
it('should get thumbnail file', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image });
|
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/webp/path.ext' });
|
||||||
await expect(
|
await expect(
|
||||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
|
|
@ -736,22 +675,15 @@ describe(AssetMediaService.name, () => {
|
||||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the asset does not exist', async () => {
|
it('should throw an error if the video asset could not be found', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
|
|
||||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
|
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the asset is not a video', async () => {
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the encoded video path if available', async () => {
|
it('should return the encoded video path if available', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.hasEncodedVideo);
|
mocks.asset.getForVideo.mockResolvedValue(assetStub.hasEncodedVideo);
|
||||||
|
|
||||||
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
|
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
@ -764,7 +696,7 @@ describe(AssetMediaService.name, () => {
|
||||||
|
|
||||||
it('should fall back to the original path', async () => {
|
it('should fall back to the original path', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.video);
|
mocks.asset.getForVideo.mockResolvedValue(assetStub.video);
|
||||||
|
|
||||||
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
|
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
|
||||||
new ImmichFileResponse({
|
new ImmichFileResponse({
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetType,
|
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
CacheControl,
|
CacheControl,
|
||||||
JobName,
|
JobName,
|
||||||
|
|
@ -36,7 +35,7 @@ import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { UploadFile, UploadRequest } from 'src/types';
|
import { UploadFile, UploadRequest } from 'src/types';
|
||||||
import { requireUploadAccess } from 'src/utils/access';
|
import { requireUploadAccess } from 'src/utils/access';
|
||||||
import { asUploadRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { asUploadRequest, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||||
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
@ -197,27 +196,21 @@ export class AssetMediaService extends BaseService {
|
||||||
async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise<ImmichFileResponse> {
|
async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise<ImmichFileResponse> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
const { originalPath, originalFileName, editedPath } = await this.assetRepository.getForOriginal(
|
||||||
|
id,
|
||||||
|
dto.edited ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
if (asset.edits!.length > 0 && (dto.edited ?? false)) {
|
if (dto.edited && !editedPath) {
|
||||||
const { editedFullsizeFile } = getAssetFiles(asset.files ?? []);
|
throw new NotFoundException('Edited asset media not found');
|
||||||
|
|
||||||
if (!editedFullsizeFile) {
|
|
||||||
throw new NotFoundException('Edited asset media not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ImmichFileResponse({
|
|
||||||
path: editedFullsizeFile.path,
|
|
||||||
fileName: getFileNameWithoutExtension(asset.originalFileName) + getFilenameExtension(editedFullsizeFile.path),
|
|
||||||
contentType: mimeTypes.lookup(editedFullsizeFile.path),
|
|
||||||
cacheControl: CacheControl.PrivateWithCache,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path = editedPath ?? originalPath!;
|
||||||
|
|
||||||
return new ImmichFileResponse({
|
return new ImmichFileResponse({
|
||||||
path: asset.originalPath,
|
path,
|
||||||
fileName: asset.originalFileName,
|
fileName: getFileNameWithoutExtension(originalFileName) + getFilenameExtension(path),
|
||||||
contentType: mimeTypes.lookup(asset.originalPath),
|
contentType: mimeTypes.lookup(path),
|
||||||
cacheControl: CacheControl.PrivateWithCache,
|
cacheControl: CacheControl.PrivateWithCache,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -229,45 +222,30 @@ export class AssetMediaService extends BaseService {
|
||||||
): Promise<ImmichFileResponse | AssetMediaRedirectResponse> {
|
): Promise<ImmichFileResponse | AssetMediaRedirectResponse> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
if (dto.size === AssetMediaSize.Original) {
|
||||||
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
throw new BadRequestException('May not request original file');
|
||||||
|
|
||||||
const files = getAssetFiles(asset.files ?? []);
|
|
||||||
|
|
||||||
const requestingEdited = (dto.edited ?? false) && asset.edits!.length > 0;
|
|
||||||
const { fullsizeFile, previewFile, thumbnailFile } = {
|
|
||||||
fullsizeFile: requestingEdited ? files.editedFullsizeFile : files.fullsizeFile,
|
|
||||||
previewFile: requestingEdited ? files.editedPreviewFile : files.previewFile,
|
|
||||||
thumbnailFile: requestingEdited ? files.editedThumbnailFile : files.thumbnailFile,
|
|
||||||
};
|
|
||||||
|
|
||||||
let filepath = previewFile?.path;
|
|
||||||
if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) {
|
|
||||||
filepath = thumbnailFile.path;
|
|
||||||
} else if (size === AssetMediaSize.FULLSIZE) {
|
|
||||||
if (mimeTypes.isWebSupportedImage(asset.originalPath) && !dto.edited) {
|
|
||||||
// use original file for web supported images
|
|
||||||
return { targetSize: 'original' };
|
|
||||||
}
|
|
||||||
if (!fullsizeFile) {
|
|
||||||
// downgrade to preview if fullsize is not available.
|
|
||||||
// e.g. disabled or not yet (re)generated
|
|
||||||
return { targetSize: AssetMediaSize.PREVIEW };
|
|
||||||
}
|
|
||||||
filepath = fullsizeFile.path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filepath) {
|
const size = (dto.size ?? AssetMediaSize.THUMBNAIL) as unknown as AssetFileType;
|
||||||
throw new NotFoundException('Asset media not found');
|
const { originalPath, originalFileName, path } = await this.assetRepository.getForThumbnail(id, size);
|
||||||
|
|
||||||
|
if (size === AssetFileType.FullSize && mimeTypes.isWebSupportedImage(originalPath) && !dto.edited) {
|
||||||
|
// use original file for web supported images
|
||||||
|
return { targetSize: 'original' };
|
||||||
}
|
}
|
||||||
let fileName = getFileNameWithoutExtension(asset.originalFileName);
|
|
||||||
fileName += `_${size}`;
|
if (dto.size === AssetMediaSize.FULLSIZE && !path) {
|
||||||
fileName += getFilenameExtension(filepath);
|
// downgrade to preview if fullsize is not available.
|
||||||
|
// e.g. disabled or not yet (re)generated
|
||||||
|
return { targetSize: AssetMediaSize.PREVIEW };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `${getFileNameWithoutExtension(originalFileName)}_${size}${getFilenameExtension(path)}`;
|
||||||
|
|
||||||
return new ImmichFileResponse({
|
return new ImmichFileResponse({
|
||||||
fileName,
|
fileName,
|
||||||
path: filepath,
|
path,
|
||||||
contentType: mimeTypes.lookup(filepath),
|
contentType: mimeTypes.lookup(path),
|
||||||
cacheControl: CacheControl.PrivateWithCache,
|
cacheControl: CacheControl.PrivateWithCache,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -275,10 +253,10 @@ export class AssetMediaService extends BaseService {
|
||||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
const asset = await this.assetRepository.getForVideo(id);
|
||||||
|
|
||||||
if (asset.type !== AssetType.Video) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset is not a video');
|
throw new NotFoundException('Asset not found or asset is not a video');
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = asset.encodedVideoPath || asset.originalPath;
|
const filepath = asset.encodedVideoPath || asset.originalPath;
|
||||||
|
|
@ -487,13 +465,4 @@ export class AssetMediaService extends BaseService {
|
||||||
throw new BadRequestException('Quota has been exceeded!');
|
throw new BadRequestException('Quota has been exceeded!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findOrFail(id: string) {
|
|
||||||
const asset = await this.assetRepository.getById(id, { files: true, edits: true });
|
|
||||||
if (!asset) {
|
|
||||||
throw new NotFoundException('Asset not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,5 +50,8 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
||||||
upsertBulkMetadata: vitest.fn(),
|
upsertBulkMetadata: vitest.fn(),
|
||||||
deleteMetadataByKey: vitest.fn(),
|
deleteMetadataByKey: vitest.fn(),
|
||||||
deleteBulkMetadata: vitest.fn(),
|
deleteBulkMetadata: vitest.fn(),
|
||||||
|
getForOriginal: vitest.fn(),
|
||||||
|
getForThumbnail: vitest.fn(),
|
||||||
|
getForVideo: vitest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue