From 7a1e8ce6d86d9c6923224ed225e92b51aff52005 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:36:29 +0200 Subject: [PATCH] chore: remove exif entity (#17499) --- server/src/database.ts | 4 +++ server/src/dtos/exif.dto.ts | 6 ++-- server/src/entities/asset.entity.ts | 9 ++--- server/src/entities/exif.entity.ts | 36 -------------------- server/src/repositories/media.repository.ts | 4 +-- server/src/services/asset.service.ts | 2 +- server/src/services/media.service.spec.ts | 22 ++++++------ server/src/services/metadata.service.spec.ts | 4 +-- server/test/fixtures/asset.stub.ts | 32 ++++++++--------- server/test/fixtures/shared-link.stub.ts | 1 - 10 files changed, 44 insertions(+), 76 deletions(-) delete mode 100644 server/src/entities/exif.entity.ts diff --git a/server/src/database.ts b/server/src/database.ts index 9ab89b96a5..e88705711e 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,3 +1,5 @@ +import { Selectable } from 'kysely'; +import { Exif as DatabaseExif } from 'src/db'; import { AlbumUserRole, AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; @@ -189,6 +191,8 @@ export type Session = { deviceType: string; }; +export type Exif = Omit, 'updatedAt' | 'updateId'>; + const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; export const columns = { diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 079891ae56..9fa61d93c8 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ExifEntity } from 'src/entities/exif.entity'; +import { Exif } from 'src/database'; export class ExifResponseDto { make?: string | null = null; @@ -28,7 +28,7 @@ export class ExifResponseDto { rating?: number | null = null; } -export function mapExif(entity: ExifEntity): ExifResponseDto { +export function mapExif(entity: Exif): ExifResponseDto { return { make: entity.make, model: entity.model, @@ -55,7 +55,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { }; } -export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto { +export function mapSanitizedExif(entity: Exif): ExifResponseDto { return { fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index def9a92ccd..55ad75d5c2 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,12 +1,11 @@ import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; -import { Tag, User } from 'src/database'; +import { Exif, Tag, User } from 'src/database'; import { DB } from 'src/db'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; @@ -47,7 +46,7 @@ export class AssetEntity { livePhotoVideoId!: string | null; originalFileName!: string; sidecarPath!: string | null; - exifInfo?: ExifEntity; + exifInfo?: Exif; tags?: Tag[]; sharedLinks!: SharedLinkEntity[]; albums?: AlbumEntity[]; @@ -65,7 +64,9 @@ export type AssetEntityPlaceholder = AssetEntity & { }; export function withExif(qb: SelectQueryBuilder) { - return qb.leftJoin('exif', 'assets.id', 'exif.assetId').select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')); + return qb + .leftJoin('exif', 'assets.id', 'exif.assetId') + .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); } export function withExifInner(qb: SelectQueryBuilder) { diff --git a/server/src/entities/exif.entity.ts b/server/src/entities/exif.entity.ts deleted file mode 100644 index 75064b7917..0000000000 --- a/server/src/entities/exif.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { AssetEntity } from 'src/entities/asset.entity'; - -export class ExifEntity { - asset?: AssetEntity; - assetId!: string; - updatedAt?: Date; - updateId?: string; - description!: string; // or caption - exifImageWidth!: number | null; - exifImageHeight!: number | null; - fileSizeInByte!: number | null; - orientation!: string | null; - dateTimeOriginal!: Date | null; - modifyDate!: Date | null; - timeZone!: string | null; - latitude!: number | null; - longitude!: number | null; - projectionType!: string | null; - city!: string | null; - livePhotoCID!: string | null; - autoStackId!: string | null; - state!: string | null; - country!: string | null; - make!: string | null; - model!: string | null; - lensModel!: string | null; - fNumber!: number | null; - focalLength!: number | null; - iso!: number | null; - exposureTime!: string | null; - profileDescription!: string | null; - colorspace!: string | null; - bitsPerSample!: number | null; - rating!: number | null; - fps?: number | null; -} diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index d9cac0b018..1e41dd6bb2 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -6,7 +6,7 @@ import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; import sharp from 'sharp'; import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants'; -import { ExifEntity } from 'src/entities/exif.entity'; +import { Exif } from 'src/database'; import { Colorspace, LogLevel } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { @@ -66,7 +66,7 @@ export class MediaRepository { return true; } - async writeExif(tags: Partial, output: string): Promise { + async writeExif(tags: Partial, output: string): Promise { try { const tagsToWrite: WriteTags = { ExifImageWidth: tags.exifImageWidth, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index d05bb023f2..51e54adf5b 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -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 AssetEntity, { auth })), + assets: assets.map((asset) => mapAsset(asset as unknown as AssetEntity, { auth })), }; }); } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index a754fc47d0..c55a277fae 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,8 +1,8 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; +import { Exif } from 'src/database'; import { AssetMediaSize } from 'src/dtos/asset-media.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; import { AssetFileType, AssetPathType, @@ -319,7 +319,7 @@ describe(MediaService.name, () => { it('should generate P3 thumbnails for a wide gamut image', async () => { mocks.asset.getById.mockResolvedValue({ ...assetStub.image, - exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity, + exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as Exif, }); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); @@ -2608,47 +2608,47 @@ describe(MediaService.name, () => { describe('isSRGB', () => { it('should return true for srgb colorspace', () => { - const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB' } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB' } as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); it('should return true for srgb profile description', () => { - const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB v1.31' } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB v1.31' } as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); it('should return true for 8-bit image with no colorspace metadata', () => { - const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 8 } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 8 } as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); it('should return true for image with no colorspace or bit depth metadata', () => { - const asset = { ...assetStub.image, exifInfo: {} as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: {} as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); it('should return false for non-srgb colorspace', () => { - const asset = { ...assetStub.image, exifInfo: { colorspace: 'Adobe RGB' } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { colorspace: 'Adobe RGB' } as Exif }; expect(sut.isSRGB(asset)).toEqual(false); }); it('should return false for non-srgb profile description', () => { - const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sP3C' } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sP3C' } as Exif }; expect(sut.isSRGB(asset)).toEqual(false); }); it('should return false for 16-bit image with no colorspace metadata', () => { - const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 16 } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 16 } as Exif }; expect(sut.isSRGB(asset)).toEqual(false); }); it('should return true for 16-bit image with sRGB colorspace', () => { - const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB', bitsPerSample: 16 } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB', bitsPerSample: 16 } as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); it('should return true for 16-bit image with sRGB profile', () => { - const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB', bitsPerSample: 16 } as ExifEntity }; + const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB', bitsPerSample: 16 } as Exif }; expect(sut.isSRGB(asset)).toEqual(true); }); }); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 9947d803a7..06337cdd43 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,8 +3,8 @@ import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import { defaults } from 'src/config'; +import { Exif } from 'src/database'; import { AssetEntity } from 'src/entities/asset.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; import { WithoutProperty } from 'src/repositories/asset.repository'; import { ImmichTags } from 'src/repositories/metadata.repository'; @@ -1190,7 +1190,7 @@ describe(MetadataService.name, () => { mocks.asset.getByIds.mockResolvedValue([ { ...assetStub.livePhotoStillAsset, - exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity, + exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as Exif, }, ]); mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index afdbaad8dd..669cf1b848 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,6 +1,6 @@ +import { Exif } from 'src/database'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { StorageAsset } from 'src/types'; @@ -128,7 +128,7 @@ export const assetStub = { isExternal: false, exifInfo: { fileSizeInByte: 123_000, - } as ExifEntity, + } as Exif, deletedAt: null, duplicateId: null, isOffline: false, @@ -202,7 +202,7 @@ export const assetStub = { fileSizeInByte: 5000, exifImageHeight: 1000, exifImageWidth: 1000, - } as ExifEntity, + } as Exif, stackId: 'stack-1', stack: stackStub('stack-1', [ { id: 'primary-asset-id' } as AssetEntity, @@ -247,7 +247,7 @@ export const assetStub = { fileSizeInByte: 5000, exifImageHeight: 3840, exifImageWidth: 2160, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -285,7 +285,7 @@ export const assetStub = { fileSizeInByte: 5000, exifImageHeight: 3840, exifImageWidth: 2160, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, status: AssetStatus.TRASHED, @@ -326,7 +326,7 @@ export const assetStub = { fileSizeInByte: 5000, exifImageHeight: 3840, exifImageWidth: 2160, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: true, }), @@ -364,7 +364,7 @@ export const assetStub = { fileSizeInByte: 5000, exifImageHeight: 3840, exifImageWidth: 2160, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -402,7 +402,7 @@ export const assetStub = { sidecarPath: null, exifInfo: { fileSizeInByte: 5000, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -439,7 +439,7 @@ export const assetStub = { sidecarPath: null, exifInfo: { fileSizeInByte: 5000, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -475,7 +475,7 @@ export const assetStub = { sidecarPath: null, exifInfo: { fileSizeInByte: 5000, - } as ExifEntity, + } as Exif, deletedAt: null, duplicateId: null, isOffline: false, @@ -514,7 +514,7 @@ export const assetStub = { fileSizeInByte: 100_000, exifImageHeight: 2160, exifImageWidth: 3840, - } as ExifEntity, + } as Exif, deletedAt: null, duplicateId: null, isOffline: false, @@ -605,7 +605,7 @@ export const assetStub = { city: 'test-city', state: 'test-state', country: 'test-country', - } as ExifEntity, + } as Exif, deletedAt: null, duplicateId: null, isOffline: false, @@ -710,7 +710,7 @@ export const assetStub = { sidecarPath: null, exifInfo: { fileSizeInByte: 100_000, - } as ExifEntity, + } as Exif, deletedAt: null, duplicateId: null, isOffline: false, @@ -749,7 +749,7 @@ export const assetStub = { sidecarPath: null, exifInfo: { fileSizeInByte: 5000, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -788,7 +788,7 @@ export const assetStub = { fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), @@ -827,7 +827,7 @@ export const assetStub = { fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, - } as ExifEntity, + } as Exif, duplicateId: null, isOffline: false, }), diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 4d2c509359..be69147e7a 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -232,7 +232,6 @@ export const sharedLinkStub = { iso: 100, exposureTime: '1/16', fps: 100, - asset: null as any, profileDescription: 'sRGB', bitsPerSample: 8, colorspace: 'sRGB',