mirror of
https://github.com/immich-app/immich.git
synced 2025-12-24 17:24:56 +03:00
refactor: remove album entity, update types (#17450)
This commit is contained in:
@@ -6,15 +6,15 @@ import {
|
||||
AlbumStatisticsResponseDto,
|
||||
CreateAlbumDto,
|
||||
GetAlbumsDto,
|
||||
UpdateAlbumDto,
|
||||
UpdateAlbumUserDto,
|
||||
mapAlbum,
|
||||
MapAlbumDto,
|
||||
mapAlbumWithAssets,
|
||||
mapAlbumWithoutAssets,
|
||||
UpdateAlbumDto,
|
||||
UpdateAlbumUserDto,
|
||||
} from 'src/dtos/album.dto';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -39,7 +39,7 @@ export class AlbumService extends BaseService {
|
||||
async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
|
||||
let albums: AlbumEntity[];
|
||||
let albums: MapAlbumDto[];
|
||||
if (assetId) {
|
||||
albums = await this.albumRepository.getByAssetId(ownerId, assetId);
|
||||
} else if (shared === true) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Stats } from 'node:fs';
|
||||
import { AssetFile } from 'src/database';
|
||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
@@ -173,7 +174,7 @@ const assetEntity = Object.freeze({
|
||||
},
|
||||
livePhotoVideoId: null,
|
||||
sidecarPath: null,
|
||||
}) as AssetEntity;
|
||||
} as MapAsset);
|
||||
|
||||
const existingAsset = Object.freeze({
|
||||
...assetEntity,
|
||||
@@ -182,18 +183,18 @@ const existingAsset = Object.freeze({
|
||||
checksum: Buffer.from('_getExistingAsset', 'utf8'),
|
||||
libraryId: 'libraryId',
|
||||
originalFileName: 'existing-filename.jpeg',
|
||||
}) as AssetEntity;
|
||||
}) as MapAsset;
|
||||
|
||||
const sidecarAsset = Object.freeze({
|
||||
...existingAsset,
|
||||
sidecarPath: 'sidecar-path',
|
||||
checksum: Buffer.from('_getExistingAssetWithSideCar', 'utf8'),
|
||||
}) as AssetEntity;
|
||||
}) as MapAsset;
|
||||
|
||||
const copiedAsset = Object.freeze({
|
||||
id: 'copied-asset',
|
||||
originalPath: 'copied-path',
|
||||
}) as AssetEntity;
|
||||
}) as MapAsset;
|
||||
|
||||
describe(AssetMediaService.name, () => {
|
||||
let sut: AssetMediaService;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFound
|
||||
import { extname } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Asset } from 'src/database';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetMediaResponseDto,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -212,7 +213,7 @@ export class AssetMediaService extends BaseService {
|
||||
const asset = await this.findOrFail(id);
|
||||
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
||||
|
||||
const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files);
|
||||
const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files ?? []);
|
||||
let filepath = previewFile?.path;
|
||||
if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) {
|
||||
filepath = thumbnailFile.path;
|
||||
@@ -375,7 +376,7 @@ export class AssetMediaService extends BaseService {
|
||||
* Uses only vital properties excluding things like: stacks, faces, smart search info, etc,
|
||||
* and then queues a METADATA_EXTRACTION job.
|
||||
*/
|
||||
private async createCopy(asset: AssetEntity): Promise<AssetEntity> {
|
||||
private async createCopy(asset: Omit<Asset, 'id'>) {
|
||||
const created = await this.assetRepository.create({
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: asset.originalPath,
|
||||
@@ -398,12 +399,7 @@ export class AssetMediaService extends BaseService {
|
||||
return created;
|
||||
}
|
||||
|
||||
private async create(
|
||||
ownerId: string,
|
||||
dto: AssetMediaCreateDto,
|
||||
file: UploadFile,
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetEntity> {
|
||||
private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) {
|
||||
const asset = await this.assetRepository.create({
|
||||
ownerId,
|
||||
libraryId: null,
|
||||
@@ -444,7 +440,7 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async findOrFail(id: string): Promise<AssetEntity> {
|
||||
private async findOrFail(id: string) {
|
||||
const asset = await this.assetRepository.getById(id, { files: true });
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset not found');
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
@@ -35,7 +34,7 @@ describe(AssetService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
const mockGetById = (assets: AssetEntity[]) => {
|
||||
const mockGetById = (assets: MapAsset[]) => {
|
||||
mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId)));
|
||||
};
|
||||
|
||||
@@ -608,7 +607,7 @@ describe(AssetService.name, () => {
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...assetStub.primaryImage,
|
||||
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
||||
} as AssetEntity);
|
||||
});
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import {
|
||||
AssetResponseDto,
|
||||
MapAsset,
|
||||
MemoryLaneResponseDto,
|
||||
SanitizedAssetResponseDto,
|
||||
mapAsset,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
||||
@@ -43,7 +43,7 @@ export class AssetService extends BaseService {
|
||||
yearsAgo,
|
||||
// TODO move this to clients
|
||||
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`,
|
||||
assets: assets.map((asset) => mapAsset(asset as unknown as AssetEntity, { auth })),
|
||||
assets: assets.map((asset) => mapAsset(asset, { auth })),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class AssetService extends BaseService {
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||
|
||||
let previousMotion: AssetEntity | null = null;
|
||||
let previousMotion: MapAsset | null = null;
|
||||
if (rest.livePhotoVideoId) {
|
||||
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
|
||||
} else if (rest.livePhotoVideoId === null) {
|
||||
@@ -233,7 +233,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files);
|
||||
const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files ?? []);
|
||||
const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath];
|
||||
|
||||
if (deleteOnDisk) {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DuplicateService extends BaseService {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
|
||||
const previewFile = getAssetFile(asset.files || [], AssetFileType.PREVIEW);
|
||||
if (!previewFile) {
|
||||
this.logger.warn(`Asset ${id} is missing preview image`);
|
||||
return JobStatus.FAILED;
|
||||
|
||||
@@ -285,9 +285,9 @@ describe(JobService.name, () => {
|
||||
it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => {
|
||||
if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') {
|
||||
if (item.data.id === 'asset-live-image') {
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoStillAsset as any]);
|
||||
} else {
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoMotionAsset as any]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ export class JobService extends BaseService {
|
||||
|
||||
case JobName.METADATA_EXTRACTION: {
|
||||
if (item.data.source === 'sidecar-write') {
|
||||
const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
|
||||
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([item.data.id]);
|
||||
if (asset) {
|
||||
this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset));
|
||||
}
|
||||
@@ -284,7 +284,7 @@ export class JobService extends BaseService {
|
||||
break;
|
||||
}
|
||||
|
||||
const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
|
||||
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([item.data.id]);
|
||||
if (!asset) {
|
||||
this.logger.warn(`Could not find asset ${item.data.id} after generating thumbnails`);
|
||||
break;
|
||||
|
||||
@@ -350,7 +350,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -371,7 +371,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -392,7 +392,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -410,7 +410,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -431,7 +431,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -451,7 +451,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -471,7 +471,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -489,7 +489,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
@@ -518,7 +518,7 @@ describe(LibraryService.name, () => {
|
||||
|
||||
const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1);
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ValidateLibraryImportPathResponseDto,
|
||||
ValidateLibraryResponseDto,
|
||||
} from 'src/dtos/library.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { AssetSyncResult } from 'src/repositories/library.repository';
|
||||
@@ -467,7 +466,7 @@ export class LibraryService extends BaseService {
|
||||
|
||||
@OnJob({ name: JobName.LIBRARY_SYNC_ASSETS, queue: QueueName.LIBRARY })
|
||||
async handleSyncAssets(job: JobOf<JobName.LIBRARY_SYNC_ASSETS>): Promise<JobStatus> {
|
||||
const assets = await this.assetRepository.getByIds(job.assetIds);
|
||||
const assets = await this.assetJobRepository.getForSyncAssets(job.assetIds);
|
||||
|
||||
const assetIdsToOffline: string[] = [];
|
||||
const trashedAssetIdsToOffline: string[] = [];
|
||||
@@ -561,7 +560,16 @@ export class LibraryService extends BaseService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
private checkExistingAsset(asset: AssetEntity, stat: Stats | null): AssetSyncResult {
|
||||
private checkExistingAsset(
|
||||
asset: {
|
||||
isOffline: boolean;
|
||||
libraryId: string | null;
|
||||
originalPath: string;
|
||||
status: AssetStatus;
|
||||
fileModifiedAt: Date;
|
||||
},
|
||||
stat: Stats | null,
|
||||
): AssetSyncResult {
|
||||
if (!stat) {
|
||||
// File not found on disk or permission error
|
||||
if (asset.isOffline) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto';
|
||||
import { Stats } from 'node:fs';
|
||||
import { constants } from 'node:fs/promises';
|
||||
import { defaults } from 'src/config';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
|
||||
import { WithoutProperty } from 'src/repositories/asset.repository';
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
@@ -549,7 +549,6 @@ describe(MetadataService.name, () => {
|
||||
livePhotoVideoId: null,
|
||||
libraryId: null,
|
||||
});
|
||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
@@ -719,7 +718,7 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockImplementation(
|
||||
(asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise<AssetEntity>,
|
||||
(asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise<MapAsset>,
|
||||
);
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
@@ -1394,7 +1393,7 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should set sidecar path if exists (sidecar named photo.xmp)', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt]);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt as any]);
|
||||
mocks.storage.checkFileExists.mockResolvedValueOnce(false);
|
||||
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
||||
|
||||
@@ -1446,7 +1445,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
describe('handleSidecarDiscovery', () => {
|
||||
it('should skip hidden assets', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset as any]);
|
||||
await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id });
|
||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -271,7 +271,7 @@ export class MetadataService extends BaseService {
|
||||
];
|
||||
|
||||
if (this.isMotionPhoto(asset, exifTags)) {
|
||||
promises.push(this.applyMotionPhotos(asset as unknown as Asset, exifTags, dates, stats));
|
||||
promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats));
|
||||
}
|
||||
|
||||
if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { Person } from 'src/database';
|
||||
import { AssetFaces, FaceSearch } from 'src/db';
|
||||
import { Chunked, OnJob } from 'src/decorators';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -315,6 +316,7 @@ export class PersonService extends BaseService {
|
||||
const facesToAdd: (Insertable<AssetFaces> & { id: string })[] = [];
|
||||
const embeddings: FaceSearch[] = [];
|
||||
const mlFaceIds = new Set<string>();
|
||||
|
||||
for (const face of asset.faces) {
|
||||
if (face.sourceType === SourceType.MACHINE_LEARNING) {
|
||||
mlFaceIds.add(face.id);
|
||||
@@ -477,7 +479,7 @@ export class PersonService extends BaseService {
|
||||
embedding: face.faceSearch.embedding,
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: machineLearning.facialRecognition.minFaces,
|
||||
minBirthDate: face.asset.fileCreatedAt,
|
||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
||||
});
|
||||
|
||||
// `matches` also includes the face itself
|
||||
@@ -503,7 +505,7 @@ export class PersonService extends BaseService {
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: 1,
|
||||
hasPerson: true,
|
||||
minBirthDate: face.asset.fileCreatedAt,
|
||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
||||
});
|
||||
|
||||
if (matchWithPerson.length > 0) {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe(SearchService.name, () => {
|
||||
fieldName: 'exifInfo.city',
|
||||
items: [{ value: 'test-city', data: assetStub.withLocation.id }],
|
||||
});
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.withLocation]);
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]);
|
||||
const expectedResponse = [
|
||||
{ fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetMapOptions, AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import {
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
SearchSuggestionType,
|
||||
SmartSearchDto,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
import { SearchExploreItem } from 'src/repositories/search.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -36,7 +35,7 @@ export class SearchService extends BaseService {
|
||||
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
|
||||
const options = { maxFields: 12, minAssetsPerField: 5 };
|
||||
const cities = await this.assetRepository.getAssetIdByCity(auth.user.id, options);
|
||||
const assets = await this.assetRepository.getByIdsWithAllRelations(cities.items.map(({ data }) => data));
|
||||
const assets = await this.assetRepository.getByIdsWithAllRelationsButStacks(cities.items.map(({ data }) => data));
|
||||
const items = assets.map((asset) => ({ value: asset.exifInfo!.city!, data: mapAsset(asset, { auth }) }));
|
||||
return [{ fieldName: cities.fieldName, items }];
|
||||
}
|
||||
@@ -139,7 +138,7 @@ export class SearchService extends BaseService {
|
||||
return [auth.user.id, ...partnerIds];
|
||||
}
|
||||
|
||||
private mapResponse(assets: AssetEntity[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto {
|
||||
private mapResponse(assets: MapAsset[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto {
|
||||
return {
|
||||
albums: { total: 0, count: 0, items: [], facets: [] },
|
||||
assets: {
|
||||
|
||||
@@ -244,7 +244,7 @@ describe(SharedLinkService.name, () => {
|
||||
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
|
||||
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
|
||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -333,8 +333,7 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should return metadata tags with a default image path if the asset id is not set', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
|
||||
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: null, assets: [] });
|
||||
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||
description: '0 shared photos & videos',
|
||||
imageUrl: `https://my.immich.app/feature-panel.png`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { SharedLink } from 'src/database';
|
||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkSearchDto,
|
||||
} from 'src/dtos/shared-link.dto';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { Permission, SharedLinkType } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
|
||||
@@ -98,7 +98,7 @@ export class SharedLinkService extends BaseService {
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
const sharedLink = await this.findOrFail(auth.user.id, id);
|
||||
await this.sharedLinkRepository.remove(sharedLink);
|
||||
await this.sharedLinkRepository.remove(sharedLink.id);
|
||||
}
|
||||
|
||||
// TODO: replace `userId` with permissions and access control checks
|
||||
@@ -182,7 +182,7 @@ export class SharedLinkService extends BaseService {
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
|
||||
const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets?.length || 0;
|
||||
const imagePath = assetId
|
||||
? `/api/assets/${assetId}/thumbnail?key=${sharedLink.key.toString('base64url')}`
|
||||
: '/feature-panel.png';
|
||||
@@ -194,11 +194,11 @@ export class SharedLinkService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||
private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) {
|
||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
||||
}
|
||||
|
||||
private validateAndRefreshToken(sharedLink: SharedLinkEntity, dto: SharedLinkPasswordDto): string {
|
||||
private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string {
|
||||
const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`);
|
||||
const sharedLinkTokens = dto.token?.split(',') || [];
|
||||
if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
@@ -63,7 +62,7 @@ describe(SyncService.name, () => {
|
||||
it('should return a response requiring a full sync when there are too many changes', async () => {
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(
|
||||
Array.from<AssetEntity>({ length: 10_000 }).fill(assetStub.image),
|
||||
Array.from<typeof assetStub.image>({ length: 10_000 }).fill(assetStub.image),
|
||||
);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -12,6 +11,6 @@ export class ViewService extends BaseService {
|
||||
|
||||
async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise<AssetResponseDto[]> {
|
||||
const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path);
|
||||
return assets.map((asset) => mapAsset(asset as unknown as AssetEntity, { auth }));
|
||||
return assets.map((asset) => mapAsset(asset, { auth }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user