mirror of
https://github.com/immich-app/immich.git
synced 2025-01-07 00:50:23 -05:00
feat(server): Handle sidecars in external libraries (#14800)
* handle sidecars in external libraries * don't add separate source
This commit is contained in:
parent
6080e6e827
commit
4bc2aa5451
5 changed files with 355 additions and 83 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
||||||
import { cpSync, existsSync } from 'node:fs';
|
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
|
@ -406,65 +406,93 @@ describe('/libraries', () => {
|
||||||
it('should reimport a modified file', async () => {
|
it('should reimport a modified file', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp/reimport`],
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.post(`/libraries/${library.id}/scan`)
|
.post(`/libraries/${library.id}/scan`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ refreshModifiedFiles: true });
|
.send();
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
model: 'NIKON D750',
|
|
||||||
});
|
});
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
expect(assets.count).toEqual(1);
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
|
expect(asset).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'asset.jpg',
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
model: 'NIKON D750',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not reimport unmodified files', async () => {
|
it('should not reimport unmodified files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp/reimport`],
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.post(`/libraries/${library.id}/scan`)
|
.post(`/libraries/${library.id}/scan`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ refreshModifiedFiles: true });
|
.send();
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
model: 'NIKON D750',
|
|
||||||
});
|
});
|
||||||
expect(assets.count).toBe(0);
|
|
||||||
|
expect(assets.count).toEqual(1);
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
|
expect(asset).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'asset.jpg',
|
||||||
|
exifInfo: expect.not.objectContaining({
|
||||||
|
model: 'NIKON D750',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set an asset offline if its file is missing', async () => {
|
it('should set an asset offline if its file is missing', async () => {
|
||||||
|
@ -601,6 +629,298 @@ describe('/libraries', () => {
|
||||||
|
|
||||||
expect(assets).toEqual(assetsBefore);
|
expect(assets).toEqual(assetsBefore);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('xmp metadata', async () => {
|
||||||
|
it('should import metadata from file.xmp', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import metadata from file.ext.xmp', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file.ext.xmp to file metadata', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch from using file.xmp to file metadata', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
||||||
|
});
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
|
await scan(admin.accessToken, library.id);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
|
expect(newAssets.items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'glarus.nef',
|
||||||
|
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /libraries/:id/validate', () => {
|
describe('POST /libraries/:id/validate', () => {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 99544a200412d553103cc7b8f1a28f339c7cffd9
|
Subproject commit 9e3b964b080dca6f035b29b86e66454ae8aeda78
|
|
@ -414,7 +414,6 @@ describe(LibraryService.name, () => {
|
||||||
localDateTime: expect.any(Date),
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: 'photo.jpg',
|
originalFileName: 'photo.jpg',
|
||||||
sidecarPath: null,
|
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -423,57 +422,9 @@ describe(LibraryService.name, () => {
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.SIDECAR_DISCOVERY,
|
||||||
data: {
|
data: {
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
source: 'upload',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should import a new asset with sidecar', async () => {
|
|
||||||
const mockLibraryJob: ILibraryFileJob = {
|
|
||||||
id: libraryStub.externalLibrary1.id,
|
|
||||||
ownerId: mockUser.id,
|
|
||||||
assetPath: '/data/user1/photo.jpg',
|
|
||||||
};
|
|
||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
||||||
|
|
||||||
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
|
||||||
|
|
||||||
expect(assetMock.create.mock.calls).toEqual([
|
|
||||||
[
|
|
||||||
{
|
|
||||||
ownerId: mockUser.id,
|
|
||||||
libraryId: libraryStub.externalLibrary1.id,
|
|
||||||
checksum: expect.any(Buffer),
|
|
||||||
originalPath: '/data/user1/photo.jpg',
|
|
||||||
deviceAssetId: expect.any(String),
|
|
||||||
deviceId: 'Library Import',
|
|
||||||
fileCreatedAt: expect.any(Date),
|
|
||||||
fileModifiedAt: expect.any(Date),
|
|
||||||
localDateTime: expect.any(Date),
|
|
||||||
type: AssetType.IMAGE,
|
|
||||||
originalFileName: 'photo.jpg',
|
|
||||||
sidecarPath: '/data/user1/photo.jpg.xmp',
|
|
||||||
isExternal: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: JobName.METADATA_EXTRACTION,
|
|
||||||
data: {
|
|
||||||
id: assetStub.image.id,
|
|
||||||
source: 'upload',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -507,7 +458,6 @@ describe(LibraryService.name, () => {
|
||||||
localDateTime: expect.any(Date),
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
originalFileName: 'video.mp4',
|
originalFileName: 'video.mp4',
|
||||||
sidecarPath: null,
|
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -516,10 +466,9 @@ describe(LibraryService.name, () => {
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.SIDECAR_DISCOVERY,
|
||||||
data: {
|
data: {
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
source: 'upload',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -396,12 +396,6 @@ export class LibraryService extends BaseService {
|
||||||
|
|
||||||
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
|
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
|
||||||
|
|
||||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
|
||||||
let sidecarPath: string | null = null;
|
|
||||||
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
|
|
||||||
sidecarPath = `${assetPath}.xmp`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
|
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
|
||||||
|
|
||||||
const mtime = stat.mtime;
|
const mtime = stat.mtime;
|
||||||
|
@ -418,8 +412,6 @@ export class LibraryService extends BaseService {
|
||||||
localDateTime: mtime,
|
localDateTime: mtime,
|
||||||
type: assetType,
|
type: assetType,
|
||||||
originalFileName: parse(assetPath).base,
|
originalFileName: parse(assetPath).base,
|
||||||
|
|
||||||
sidecarPath,
|
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -431,7 +423,11 @@ export class LibraryService extends BaseService {
|
||||||
async queuePostSyncJobs(asset: AssetEntity) {
|
async queuePostSyncJobs(asset: AssetEntity) {
|
||||||
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
|
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
|
// We queue a sidecar discovery which, in turn, queues metadata extraction
|
||||||
|
await this.jobRepository.queue({
|
||||||
|
name: JobName.SIDECAR_DISCOVERY,
|
||||||
|
data: { id: asset.id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueScan(id: string) {
|
async queueScan(id: string) {
|
||||||
|
|
|
@ -698,7 +698,7 @@ export class MetadataService extends BaseService {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSync && (!asset.isVisible || asset.sidecarPath)) {
|
if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -720,6 +720,13 @@ export class MetadataService extends BaseService {
|
||||||
sidecarPath = sidecarPathWithoutExt;
|
sidecarPath = sidecarPathWithoutExt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asset.isExternal) {
|
||||||
|
if (sidecarPath !== asset.sidecarPath) {
|
||||||
|
await this.assetRepository.update({ id: asset.id, sidecarPath });
|
||||||
|
}
|
||||||
|
return JobStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
if (sidecarPath) {
|
if (sidecarPath) {
|
||||||
await this.assetRepository.update({ id: asset.id, sidecarPath });
|
await this.assetRepository.update({ id: asset.id, sidecarPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
|
|
Loading…
Reference in a new issue