diff --git a/e2e/src/api/specs/jobs.e2e-spec.ts b/e2e/src/api/specs/jobs.e2e-spec.ts new file mode 100644 index 0000000000..4b8126c941 --- /dev/null +++ b/e2e/src/api/specs/jobs.e2e-spec.ts @@ -0,0 +1,86 @@ +import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk'; +import { readFile } from 'node:fs/promises'; +import { basename } from 'node:path'; +import { errorDto } from 'src/responses'; +import { app, testAssetDir, utils } from 'src/utils'; +import request from 'supertest'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; + +describe('/jobs', () => { + let admin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + }); + + describe('PUT /jobs', () => { + afterEach(async () => { + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Resume, + force: false, + }); + }); + + it('should require authentication', async () => { + const { status, body } = await request(app).put('/jobs/metadataExtraction'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should queue metadata extraction for missing assets', async () => { + const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`; + const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`; + + await utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(path1), filename: basename(path1) }, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Pause, + force: false, + }); + + const { id } = await utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(path2), filename: basename(path2) }, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + { + const asset = await utils.getAssetInfo(admin.accessToken, id); + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo?.make).toBeNull(); + } + + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Empty, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Resume, + force: false, + }); + + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Start, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + { + const asset = await utils.getAssetInfo(admin.accessToken, id); + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo?.make).toBe('NIKON CORPORATION'); + } + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index efd9ce76b9..d303b71eaa 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -6,6 +6,8 @@ import { CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, + JobCommandDto, + JobName, MetadataSearchDto, Permission, PersonCreateDto, @@ -29,6 +31,7 @@ import { getConfigDefaults, login, searchAssets, + sendJobCommand, setBaseUrl, signUpAdmin, tagAssets, @@ -475,6 +478,9 @@ export const utils = { tagAssets: (accessToken: string, tagId: string, assetIds: string[]) => tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }), + jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) => + sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }), + setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') => await context.addCookies([ { diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 1f52b9c71a..201f295d49 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -467,8 +467,8 @@ export class AssetRepository implements IAssetRepository { ) .$if(property === WithoutProperty.EXIF, (qb) => qb - .innerJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId') - .where('job_status.metadataExtractedAt', 'is', null) + .leftJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId') + .where((eb) => eb.or([eb('job_status.metadataExtractedAt', 'is', null), eb('assetId', 'is', null)])) .where('assets.isVisible', '=', true), ) .$if(property === WithoutProperty.FACES, (qb) =>