From 7e0356e22797000352026de7a26a0f2f82d20fb6 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:34:32 +0100 Subject: [PATCH] refactor: more small tests (#26159) --- .../src/services/asset-media.service.spec.ts | 48 ++- server/src/services/asset.service.spec.ts | 148 ++++---- server/src/services/duplicate.service.spec.ts | 20 +- server/src/services/job.service.spec.ts | 12 +- server/src/services/library.service.spec.ts | 83 ++--- server/src/services/media.service.spec.ts | 287 ++++++++------- server/src/services/metadata.service.spec.ts | 331 +++++++++--------- server/src/services/sync.service.spec.ts | 16 +- server/test/factories/asset.factory.ts | 2 +- server/test/fixtures/asset.stub.ts | 298 +--------------- 10 files changed, 463 insertions(+), 782 deletions(-) diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 52e4559760..918ea65793 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -429,45 +429,40 @@ describe(AssetMediaService.name, () => { }); it('should handle a live photo', async () => { - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(authStub.user1.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should hide the linked motion asset', async () => { - mocks.asset.getById.mockResolvedValueOnce({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build(); + const asset = AssetFactory.create(); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: 'live-photo-motion-asset', + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); }); @@ -777,12 +772,13 @@ describe(AssetMediaService.name, () => { }); it('should fall back to the original path', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id])); - mocks.asset.getForVideo.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForVideo.mockResolvedValue(asset); - await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( + await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.video.originalPath, + path: asset.originalPath, cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', }), diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 0b1b78d41a..9d2f130d86 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,6 +1,5 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; @@ -10,7 +9,7 @@ import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { factory } from 'test/small.factory'; +import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; const stats: AssetStats = { @@ -34,14 +33,8 @@ describe(AssetService.name, () => { expect(sut).toBeDefined(); }); - const mockGetById = (assets: MapAsset[]) => { - mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId))); - }; - beforeEach(() => { ({ sut, mocks } = newTestService(AssetService)); - - mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]); }); describe('getStatistics', () => { @@ -254,74 +247,79 @@ describe(AssetService.name, () => { it('should fail linking a live video if the motion part could not be found', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); await expect( - sut.update(auth, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: 'unknown', }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: 'unknown', }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: 'unknown', visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: 'unknown', userId: auth.user.id, }); }); it('should fail linking a live video if the motion part is not a video', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from().owner(auth.user).build(); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(authStub.admin, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); it('should fail linking a live video if the motion part has a different owner', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(motionAsset); await expect( - sut.update(auth, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); @@ -351,36 +349,40 @@ describe(AssetService.name, () => { it('should unlink a live video', async () => { const auth = AuthFactory.create(); - const asset = AssetFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.update.mockResolvedValueOnce(asset); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(auth.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + const unlinkedAsset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.update.mockResolvedValueOnce(unlinkedAsset); - await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }); + await sut.update(auth, asset.id, { livePhotoVideoId: null }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, livePhotoVideoId: null, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, - visibility: assetStub.livePhotoStillAsset.visibility, + id: motionAsset.id, + visibility: asset.visibility, }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); it('should fail unlinking a live video if the asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - // eslint-disable-next-line unicorn/no-useless-undefined - mocks.asset.getById.mockResolvedValueOnce(undefined); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(void 0); - await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(authStub.admin, asset.id, { livePhotoVideoId: null })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); @@ -600,63 +602,31 @@ describe(AssetService.name, () => { }); it('should delete a live photo', async () => { - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); mocks.asset.getLivePhotoCount.mockResolvedValue(0); await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, deleteOnDisk: true, }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.AssetDelete, - data: { - id: assetStub.livePhotoMotionAsset.id, - deleteOnDisk: true, - }, - }, - ], - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.AssetDelete, data: { id: motionAsset.id, deleteOnDisk: true } }], + [{ name: JobName.FileDelete, data: { files: [asset.originalPath] } }], ]); }); it('should not delete a live motion part if it is being used by another asset', async () => { + const asset = AssetFactory.create({ livePhotoVideoId: newUuid() }); mocks.asset.getLivePhotoCount.mockResolvedValue(2); - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); - await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, - deleteOnDisk: true, - }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.FileDelete, data: { files: [`/data/library/IMG_${asset.id}.jpg`] } }], ]); }); diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index 57e40cc3f6..16c54fc15e 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -4,6 +4,7 @@ import { SearchService } from 'src/services/search.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; +import { newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; @@ -151,9 +152,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -168,9 +167,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -197,16 +194,13 @@ describe(SearchService.name, () => { }); it('should skip if asset is not visible', async () => { - const id = assetStub.livePhotoMotionAsset.id; - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ - ...hasEmbedding, - visibility: AssetVisibility.Hidden, - }); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, ...asset }); - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Skipped); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is not visible, skipping`); }); it('should fail if asset is missing embedding', async () => { diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 00460e3cc0..a464c9e174 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,8 +1,8 @@ -import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetType, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { JobService } from 'src/services/job.service'; import { JobItem } from 'src/types'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(JobService.name, () => { @@ -56,22 +56,22 @@ describe(JobService.name, () => { { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [AssetFactory.create({ id: 'asset-id' })], + stub: [AssetFactory.create({ id: 'asset-1' })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr], - stub: [assetStub.livePhotoStillAsset], + stub: [AssetFactory.create({ id: 'asset-1', livePhotoVideoId: newUuid() })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr, JobName.AssetEncodeVideo], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.SmartSearch, data: { id: 'asset-1' } }, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 67eea0fe3f..d0c2d0a785 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -7,11 +7,10 @@ import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeMockWatcher } from 'test/repositories/storage.repository.mock'; -import { factory, newUuid } from 'test/small.factory'; +import { factory, newDate, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { vitest } from 'vitest'; @@ -307,13 +306,13 @@ describe(LibraryService.name, () => { it('should queue asset sync', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); mocks.storage.walk.mockImplementation(async function* generator() {}); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); + mocks.library.streamAssetIds.mockReturnValue(makeStream([asset])); mocks.asset.getLibraryAssetCount.mockResolvedValue(1); mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n }); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); const response = await sut.handleQueueSyncAssets({ id: library.id }); @@ -323,7 +322,7 @@ describe(LibraryService.name, () => { libraryId: library.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns, - assetIds: [assetStub.external.id], + assetIds: [asset.id], progressCounter: 1, totalAssets: 1, }, @@ -344,8 +343,9 @@ describe(LibraryService.name, () => { describe('handleSyncAssets', () => { it('should offline assets no longer on disk', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -353,20 +353,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should set assets deleted from disk as offline', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -374,20 +375,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should do nothing with offline assets deleted from disk', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -395,7 +397,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -404,8 +406,9 @@ describe(LibraryService.name, () => { }); it('should un-trash an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: [], @@ -413,20 +416,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: false, deletedAt: null, }); }); it('should do nothing with offline asset if covered by exclusion pattern', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: ['**/path.jpg'], @@ -434,8 +438,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -445,8 +449,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with offline asset if not in import path', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/import/'], exclusionPatterns: [], @@ -454,8 +459,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -465,8 +470,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with unchanged online assets', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -474,8 +480,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: asset.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -483,8 +489,9 @@ describe(LibraryService.name, () => { }); it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -492,13 +499,13 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith( - [assetStub.trashedOffline.id], + [asset.id], expect.not.objectContaining({ fileCreatedAt: expect.anything(), }), @@ -506,8 +513,9 @@ describe(LibraryService.name, () => { }); it('should update with online assets that have changed', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -515,13 +523,9 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - if (assetStub.external.fileModifiedAt == null) { - throw new Error('fileModifiedAt is null'); - } + const mtime = new Date(asset.fileModifiedAt.getDate() + 1); - const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1); - - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockResolvedValue({ mtime } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -530,7 +534,7 @@ describe(LibraryService.name, () => { { name: JobName.SidecarCheck, data: { - id: assetStub.external.id, + id: asset.id, source: 'upload', }, }, @@ -1023,9 +1027,10 @@ describe(LibraryService.name, () => { it('should handle an error event', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset); mocks.library.getAll.mockResolvedValue([library]); mocks.storage.watch.mockImplementation( makeMockWatcher({ diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index a15211c6c3..4e0f4e246d 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -5,7 +5,9 @@ import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetPathType, + AssetStatus, AssetType, + AssetVisibility, AudioCodec, Colorspace, ExifOrientation, @@ -44,6 +46,8 @@ describe(MediaService.name, () => { expect(sut).toBeDefined(); }); + // TODO these should all become medium tests of either the service or the repository. + // The entire logic of what to queue lives in the SQL query now describe('handleQueueGenerateThumbnails', () => { it('should queue all assets', async () => { const asset = AssetFactory.create(); @@ -71,7 +75,8 @@ describe(MediaService.name, () => { }); it('should queue trashed assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ status: AssetStatus.Trashed, deletedAt: new Date() }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -80,13 +85,14 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.trashed.id }, + data: { id: asset.id }, }, ]); }); it('should queue archived assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ visibility: AssetVisibility.Archive }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -95,7 +101,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.archived.id }, + data: { id: asset.id }, }, ]); }); @@ -361,17 +367,19 @@ describe(MediaService.name, () => { }); it('should skip video thumbnail generation if no video stream', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toThrowError(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip invisible assets', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.livePhotoMotionAsset); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); @@ -470,9 +478,10 @@ describe(MediaService.name, () => { }); it('should generate a thumbnail for a video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -492,14 +501,14 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), isEdited: false, isProgressive: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), isEdited: false, @@ -509,9 +518,10 @@ describe(MediaService.name, () => { }); it('should tonemap thumbnail for hdr video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -531,14 +541,14 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), isEdited: false, isProgressive: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), isEdited: false, @@ -548,12 +558,13 @@ describe(MediaService.name, () => { }); it('should always generate video thumbnail in one pass', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '5000k' }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -573,9 +584,10 @@ describe(MediaService.name, () => { }); it('should not skip intra frames for MTS file', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamMTS); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -590,9 +602,10 @@ describe(MediaService.name, () => { }); it('should override reserved color metadata', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamReserved); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -609,10 +622,11 @@ describe(MediaService.name, () => { }); it('should use scaling divisible by 2 even when using quick sync', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -800,13 +814,14 @@ describe(MediaService.name, () => { }); it('should never set isProgressive for videos', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { progressive: true }, thumbnail: { progressive: true } }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ expect.objectContaining({ @@ -1272,9 +1287,10 @@ describe(MediaService.name, () => { }); it('should skip videos', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + const asset = AssetFactory.from({ type: AssetType.Video }).exif().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await expect(sut.handleAssetEditThumbnailGeneration({ id: assetStub.video.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); @@ -1802,7 +1818,8 @@ describe(MediaService.name, () => { describe('handleQueueVideoConversion', () => { it('should queue all video assets', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueVideoConversion({ force: true }); @@ -1811,13 +1828,14 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); it('should queue all video assets without encoded videos', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); await sut.handleQueueVideoConversion({}); @@ -1825,7 +1843,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); @@ -1833,13 +1851,14 @@ describe(MediaService.name, () => { describe('handleVideoConversion', () => { beforeEach(() => { - mocks.assetJob.getForVideoConversion.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ id: 'video-id', type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); sut.videoInterfaces = { dri: ['renderD128'], mali: true }; }); it('should skip transcoding if asset not found', async () => { mocks.assetJob.getForVideoConversion.mockResolvedValue(void 0); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1848,7 +1867,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1868,7 +1887,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleAudioStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1886,13 +1905,13 @@ describe(MediaService.name, () => { it('should skip a video without any streams', async () => { mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should skip a video without any height', async () => { mocks.media.probe.mockResolvedValue(probeStub.noHeight); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1900,7 +1919,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1911,14 +1930,14 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video')); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.Failed); + await expect(sut.handleVideoConversion({ id: 'video-id' })).resolves.toBe(JobStatus.Failed); expect(mocks.media.transcode).toHaveBeenCalledTimes(1); }); it('should transcode when set to all', async () => { mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1933,7 +1952,7 @@ describe(MediaService.name, () => { it('should transcode when optimal and too big', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1948,7 +1967,7 @@ describe(MediaService.name, () => { it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '30M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1963,7 +1982,7 @@ describe(MediaService.name, () => { it('should transcode when max bitrate is not a number', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: 'foo' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1980,7 +1999,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1995,7 +2014,7 @@ describe(MediaService.name, () => { it('should scale horizontally when video is horizontal', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2010,7 +2029,7 @@ describe(MediaService.name, () => { it('should scale vertically when video is vertical', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2027,7 +2046,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2044,7 +2063,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2061,7 +2080,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Hevc, acceptedAudioCodecs: [AudioCodec.Aac] }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2082,7 +2101,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2103,7 +2122,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2118,7 +2137,7 @@ describe(MediaService.name, () => { it('should copy audio stream when audio matches target', async () => { mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2132,7 +2151,7 @@ describe(MediaService.name, () => { it('should remux when input is not an accepted container', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamAvi); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2148,28 +2167,28 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not remux when input is not an accepted container and transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if target codec is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2191,7 +2210,7 @@ describe(MediaService.name, () => { it('should set max bitrate if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2206,7 +2225,7 @@ describe(MediaService.name, () => { it('should default max bitrate to kbps if no unit is provided', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2221,7 +2240,7 @@ describe(MediaService.name, () => { it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2236,7 +2255,7 @@ describe(MediaService.name, () => { it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2257,7 +2276,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2278,7 +2297,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2293,7 +2312,7 @@ describe(MediaService.name, () => { it('should configure preset for vp9', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, preset: 'slow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2308,7 +2327,7 @@ describe(MediaService.name, () => { it('should not configure preset for vp9 if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.Vp9 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2323,7 +2342,7 @@ describe(MediaService.name, () => { it('should configure threads if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, threads: 2 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2338,7 +2357,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for h264 if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2353,7 +2372,7 @@ describe(MediaService.name, () => { it('should omit thread flags for h264 if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2368,7 +2387,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for hevc if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2383,7 +2402,7 @@ describe(MediaService.name, () => { it('should omit thread flags for hevc if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2398,7 +2417,7 @@ describe(MediaService.name, () => { it('should use av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2423,7 +2442,7 @@ describe(MediaService.name, () => { it('should map `veryslow` preset to 4 for av1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, preset: 'veryslow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2438,7 +2457,7 @@ describe(MediaService.name, () => { it('should set max bitrate for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, maxBitrate: '2M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2453,7 +2472,7 @@ describe(MediaService.name, () => { it('should set threads for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2470,7 +2489,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4, maxBitrate: '2M' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2491,7 +2510,7 @@ describe(MediaService.name, () => { targetResolution: '1080p', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2500,21 +2519,21 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, targetVideoCodec: VideoCodec.Vp9 }, }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should fail if hwaccel option is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should set options for nvenc', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2551,7 +2570,7 @@ describe(MediaService.name, () => { twoPass: true, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2568,7 +2587,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2585,7 +2604,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2602,7 +2621,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2617,7 +2636,7 @@ describe(MediaService.name, () => { it('should ignore two pass for nvenc if max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2634,7 +2653,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2656,7 +2675,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2677,7 +2696,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2694,7 +2713,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2734,7 +2753,7 @@ describe(MediaService.name, () => { preferredHwDevice: '/dev/dri/renderD128', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2754,7 +2773,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2774,7 +2793,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, targetVideoCodec: VideoCodec.Vp9 }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2794,7 +2813,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2803,7 +2822,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2824,7 +2843,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2850,7 +2869,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2879,7 +2898,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2897,7 +2916,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2918,7 +2937,7 @@ describe(MediaService.name, () => { it('should set options for vaapi', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2950,7 +2969,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2974,7 +2993,7 @@ describe(MediaService.name, () => { it('should set cq options for vaapi when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3000,7 +3019,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3019,7 +3038,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3040,7 +3059,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preferredHwDevice: '/dev/dri/renderD128' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3061,7 +3080,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3086,7 +3105,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3109,7 +3128,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3129,7 +3148,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3147,7 +3166,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3170,7 +3189,7 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(3); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3187,7 +3206,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3204,7 +3223,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: [], mali: true }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -3213,7 +3232,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3253,7 +3272,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Hevc, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3270,7 +3289,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3287,7 +3306,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3309,7 +3328,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3328,7 +3347,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: false, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3350,7 +3369,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3369,7 +3388,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is required and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3388,7 +3407,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is optimal and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3407,7 +3426,7 @@ describe(MediaService.name, () => { it('should transcode when policy is required and video is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3422,7 +3441,7 @@ describe(MediaService.name, () => { it('should convert to yuv420p when scaling without tone-mapping', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3438,10 +3457,10 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.logger.isLevelEnabled.mockReturnValue(true); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true }); - expect(mocks.media.transcode).toHaveBeenCalledWith(assetStub.video.originalPath, expect.any(String), { + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: true }); + expect(mocks.media.transcode).toHaveBeenCalledWith('/original/path.ext', expect.any(String), { inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, @@ -3455,19 +3474,23 @@ describe(MediaService.name, () => { it('should not count frames for progress when log level is not debug', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.logger.isLevelEnabled.mockReturnValue(false); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false }); + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); }); it('should process unknown audio stream', async () => { + const asset = AssetFactory.create({ + type: AssetType.Video, + originalPath: '/original/path.ext', + }); mocks.media.probe.mockResolvedValue(probeStub.audioStreamUnknown); - mocks.asset.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); + mocks.asset.getByIds.mockResolvedValue([asset]); + await sut.handleVideoConversion({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( - '/original/path.ext', - '/data/encoded-video/user-id/as/se/asset-id.mp4', + asset.originalPath, + expect.stringContaining('video-id.mp4'), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:a copy']), diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index b20bc8b46f..d60a9b8487 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { defaults } from 'src/config'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetFileType, AssetType, @@ -18,7 +17,6 @@ import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { assetStub } from 'test/fixtures/asset.stub'; -import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; import { personStub } from 'test/fixtures/person.stub'; import { tagStub } from 'test/fixtures/tag.stub'; @@ -604,14 +602,12 @@ describe(MetadataService.name, () => { }); it('should not apply motion photos if asset is video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); @@ -632,13 +628,14 @@ describe(MetadataService.name, () => { }); it('should extract the correct video orientation', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mockReadTags({}); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }), { lockedPropertiesBehavior: 'skip' }, @@ -646,16 +643,14 @@ describe(MetadataService.name, () => { }); it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -667,57 +662,52 @@ describe(MetadataService.name, () => { EmbeddedVideoType: 'MotionPhoto_Data', }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'MotionPhotoVideo', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'MotionPhotoVideo'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', EmbeddedVideoFile: new BinaryField(0, ''), @@ -725,56 +715,51 @@ describe(MetadataService.name, () => { MotionPhoto: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'EmbeddedVideoFile', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'EmbeddedVideoFile'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the motion photo video from the XMP directory entry ', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -783,47 +768,46 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); - expect(mocks.storage.readFile).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - expect.any(Object), - ); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object)); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should delete old motion photo video assets if they do not match what is extracted', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoWithOriginalFileName); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -831,21 +815,21 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockImplementation( - (asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise, - ); + mocks.asset.create.mockResolvedValue(AssetFactory.create({ type: AssetType.Video })); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.job.queue).toHaveBeenNthCalledWith(1, { name: JobName.AssetDelete, - data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true }, + data: { id: asset.livePhotoVideoId, deleteOnDisk: true }, }); }); it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -853,12 +837,12 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); mocks.storage.checkFileExists.mockResolvedValue(true); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); // The still asset gets saved by handleMetadataExtraction, but not the video @@ -867,10 +851,9 @@ describe(MetadataService.name, () => { }); it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -878,31 +861,26 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(4); }); it('should not update storage usage if motion photo is external', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - isExternal: true, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ isExternal: true }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -910,11 +888,11 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.create.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); }); @@ -1026,7 +1004,8 @@ describe(MetadataService.name, () => { }); it('should extract duration', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1035,13 +1014,13 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.video.id, + id: asset.id, duration: '00:00:06.210', }), ); @@ -1070,7 +1049,8 @@ describe(MetadataService.name, () => { }); it('should omit duration of zero', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1079,20 +1059,21 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.video.id, + id: asset.id, duration: null, }), ); }); it('should a handle duration of 1 week', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1101,13 +1082,13 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.video.id, + id: asset.id, duration: '168:00:00.000', }), ); @@ -1148,7 +1129,8 @@ describe(MetadataService.name, () => { }); it('should ignore Duration from exif for videos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, {}); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, @@ -1158,7 +1140,7 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' })); @@ -1487,17 +1469,18 @@ describe(MetadataService.name, () => { }); it('should handle not finding a match', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ livePhotoCID: 'CID', - ownerId: assetStub.livePhotoMotionAsset.ownerId, - otherAssetId: assetStub.livePhotoMotionAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, libraryId: null, type: AssetType.Image, }); @@ -1508,65 +1491,67 @@ describe(MetadataService.name, () => { }); it('should link photo and video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + libraryId: null, livePhotoCID: 'CID', - ownerId: assetStub.livePhotoStillAsset.ownerId, - otherAssetId: assetStub.livePhotoStillAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, type: AssetType.Video, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); - expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); + expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([motionAsset.id]); }); it('should notify clients on live photo link', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { - userId: assetStub.livePhotoMotionAsset.ownerId, - assetId: assetStub.livePhotoMotionAsset.id, + userId: motionAsset.ownerId, + assetId: motionAsset.id, }); }); it('should search by libraryId', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - libraryId: 'library-id', - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' }); + const asset = AssetFactory.create({ libraryId: 'library-id' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', { - assetId: assetStub.livePhotoStillAsset.id, - userId: assetStub.livePhotoStillAsset.ownerId, + assetId: asset.id, + userId: asset.ownerId, }); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - ownerId: 'user-id', - otherAssetId: 'live-photo-still-asset', + ownerId: asset.ownerId, + otherAssetId: asset.id, livePhotoCID: 'CID', libraryId: 'library-id', - type: 'VIDEO', + type: AssetType.Video, }); }); diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index 47438cd059..395ff86099 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -1,7 +1,6 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { SyncService } from 'src/services/sync.service'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -23,10 +22,14 @@ describe(SyncService.name, () => { describe('getAllAssetsForUserFullSync', () => { it('should return a list of all assets owned by the user', async () => { - mocks.asset.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]); + const [asset1, asset2] = [ + AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(), + AssetFactory.from().owner(authStub.user1.user).build(), + ]; + mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]); await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ - mapAsset(assetStub.external, mapAssetOpts), - mapAsset(assetStub.hasEncodedVideo, mapAssetOpts), + mapAsset(asset1, mapAssetOpts), + mapAsset(asset2, mapAssetOpts), ]); expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({ ownerId: authStub.user1.user.id, @@ -73,15 +76,16 @@ describe(SyncService.name, () => { it('should return a response with changes and deletions', async () => { const asset = AssetFactory.create({ ownerId: authStub.user1.user.id }); + const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); mocks.partner.getAll.mockResolvedValue([]); mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]); - mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]); + mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: false, upserted: [mapAsset(asset, mapAssetOpts)], - deleted: [assetStub.external.id], + deleted: [deletedAsset.id], }); expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1); diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts index 8c9a9a5840..f8ed7dffc7 100644 --- a/server/test/factories/asset.factory.ts +++ b/server/test/factories/asset.factory.ts @@ -29,7 +29,7 @@ export class AssetFactory { static from(dto: AssetLike = {}) { const id = dto.id ?? newUuid(); - const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`; + const originalFileName = dto.originalFileName ?? (dto.type === AssetType.Video ? `MOV_${id}.mp4` : `IMG_${id}.jpg`); return new AssetFactory({ id, diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index fd0c6cf002..e4b2a168eb 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,10 +1,8 @@ -import { AssetFace, AssetFile, Exif } from 'src/database'; +import { Exif } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { StorageAsset } from 'src/types'; -import { authStub } from 'test/fixtures/auth.stub'; -import { fileStub } from 'test/fixtures/file.stub'; import { userStub } from 'test/fixtures/user.stub'; import { factory } from 'test/small.factory'; @@ -105,300 +103,6 @@ export const assetStub = { isEdited: false, }), - trashed: Object.freeze({ - id: 'asset-id', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - status: AssetStatus.Trashed, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - trashedOffline: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - libraryId: 'library-id', - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: true, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - archived: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - external: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/user1/photo.jpg', - checksum: Buffer.from('path hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - libraryId: 'library-id', - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - stackId: null, - stack: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - video: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'asset-id.ext', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Video, - files: [previewFile], - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: { - fileSizeInByte: 100_000, - exifImageHeight: 2160, - exifImageWidth: 3840, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - livePhotoMotionAsset: Object.freeze({ - status: AssetStatus.Active, - id: fileStub.livePhotoMotion.uuid, - originalPath: fileStub.livePhotoMotion.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Video, - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 100_000, - timeZone: `America/New_York`, - }, - files: [], - libraryId: null, - visibility: AssetVisibility.Hidden, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as unknown as MapAsset & { - faces: AssetFace[]; - files: (AssetFile & { isProgressive: boolean })[]; - exifInfo: Exif; - edits: AssetEditActionItem[]; - }), - - livePhotoStillAsset: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as unknown as MapAsset & { - faces: AssetFace[]; - files: (AssetFile & { isProgressive: boolean })[]; - edits: AssetEditActionItem[]; - }), - - livePhotoWithOriginalFileName: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - originalFileName: fileStub.livePhotoStill.originalName, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files: [] as AssetFile[], - libraryId: null, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }), - withLocation: Object.freeze({ id: 'asset-with-favorite-id', status: AssetStatus.Active,