Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75d1d21cc6 |
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
@@ -25,8 +24,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
useAppSettingsState(AppSettingsEnum.syncAlbums);
|
useAppSettingsState(AppSettingsEnum.syncAlbums);
|
||||||
final isDarkTheme = context.isDarkTheme;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
final albums = ref.watch(backupProvider).availableAlbums;
|
final albums = ref.watch(backupProvider).availableAlbums;
|
||||||
final searchQuery = useState('');
|
|
||||||
final formFocus = useFocusNode();
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -36,10 +33,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
final filteredAlbums = albums.where((album) {
|
|
||||||
return album.name.toLowerCase().contains(searchQuery.value.toLowerCase());
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
buildAlbumSelectionList() {
|
buildAlbumSelectionList() {
|
||||||
if (albums.isEmpty) {
|
if (albums.isEmpty) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
@@ -55,10 +48,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
((context, index) {
|
((context, index) {
|
||||||
return AlbumInfoListTile(
|
return AlbumInfoListTile(
|
||||||
album: filteredAlbums[index],
|
album: albums[index],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
childCount: filteredAlbums.length,
|
childCount: albums.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -81,10 +74,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: 12,
|
||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
),
|
),
|
||||||
itemCount: filteredAlbums.length,
|
itemCount: albums.length,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
return AlbumInfoCard(
|
return AlbumInfoCard(
|
||||||
album: filteredAlbums[index],
|
album: albums[index],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -254,9 +247,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius:
|
borderRadius: BorderRadius.circular(10),
|
||||||
BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -285,56 +277,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// buildSearchBar(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 4),
|
|
||||||
child: TextField(
|
|
||||||
focusNode: formFocus,
|
|
||||||
onChanged: (value) => searchQuery.value = value,
|
|
||||||
onTapOutside: (_) => formFocus.unfocus(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.only(left: 24),
|
|
||||||
filled: true,
|
|
||||||
fillColor: context.primaryColor.withValues(alpha: 0.1),
|
|
||||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
disabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.colorScheme.primary.withAlpha(150),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.search_rounded,
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
hintText: 'search_albums'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverLayoutBuilder(
|
SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
if (constraints.crossAxisExtent > 600) {
|
if (constraints.crossAxisExtent > 600) {
|
||||||
|
|||||||
Vendored
+1
@@ -143,6 +143,7 @@ export interface Assets {
|
|||||||
isFavorite: Generated<boolean>;
|
isFavorite: Generated<boolean>;
|
||||||
isOffline: Generated<boolean>;
|
isOffline: Generated<boolean>;
|
||||||
isVisible: Generated<boolean>;
|
isVisible: Generated<boolean>;
|
||||||
|
isDirty: Generated<boolean>;
|
||||||
libraryId: string | null;
|
libraryId: string | null;
|
||||||
livePhotoVideoId: string | null;
|
livePhotoVideoId: string | null;
|
||||||
localDateTime: Timestamp | null;
|
localDateTime: Timestamp | null;
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export class AssetEntity {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isOffline!: boolean;
|
isOffline!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isDirty!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'bytea' })
|
@Column({ type: 'bytea' })
|
||||||
@Index()
|
@Index()
|
||||||
checksum!: Buffer; // sha1 checksum
|
checksum!: Buffer; // sha1 checksum
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddAssetIsDirty1742127949957 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" ADD "isDirty" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isDirty"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -264,7 +264,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true, isDirty: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the exif description', async () => {
|
it('should update the exif description', async () => {
|
||||||
@@ -371,6 +371,7 @@ describe(AssetService.name, () => {
|
|||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||||
|
isDirty: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,6 +393,7 @@ describe(AssetService.name, () => {
|
|||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
|
isDirty: true,
|
||||||
});
|
});
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
|
||||||
@@ -429,7 +431,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||||
|
|
||||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true, isDirty: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class AssetService extends BaseService {
|
|||||||
|
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||||
|
|
||||||
const asset = await this.assetRepository.update({ id, ...rest });
|
const asset = await this.assetRepository.update({ id, isDirty: true, ...rest });
|
||||||
|
|
||||||
if (previousMotion) {
|
if (previousMotion) {
|
||||||
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
|
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
|
||||||
@@ -144,7 +144,7 @@ export class AssetService extends BaseService {
|
|||||||
options.duplicateId != undefined ||
|
options.duplicateId != undefined ||
|
||||||
options.rating != undefined
|
options.rating != undefined
|
||||||
) {
|
) {
|
||||||
await this.assetRepository.updateAll(ids, options);
|
await this.assetRepository.updateAll(ids, { isDirty: true, ...options });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should handle an asset that could not be found', async () => {
|
it('should handle an asset that could not be found', async () => {
|
||||||
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).not.toHaveBeenCalled();
|
expect(mocks.asset.upsertExif).not.toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -125,11 +125,11 @@ describe(MetadataService.name, () => {
|
|||||||
it('should handle a date in a sidecar file', async () => {
|
it('should handle a date in a sidecar file', async () => {
|
||||||
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.sidecar.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -144,14 +144,14 @@ describe(MetadataService.name, () => {
|
|||||||
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
||||||
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
FileCreateDate: fileCreatedAt.toISOString(),
|
FileCreateDate: fileCreatedAt.toISOString(),
|
||||||
FileModifyDate: fileModifiedAt.toISOString(),
|
FileModifyDate: fileModifiedAt.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ dateTimeOriginal: fileModifiedAt }),
|
expect.objectContaining({ dateTimeOriginal: fileModifiedAt }),
|
||||||
);
|
);
|
||||||
@@ -167,14 +167,14 @@ describe(MetadataService.name, () => {
|
|||||||
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
||||||
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
FileCreateDate: fileCreatedAt.toISOString(),
|
FileCreateDate: fileCreatedAt.toISOString(),
|
||||||
FileModifyDate: fileModifiedAt.toISOString(),
|
FileModifyDate: fileModifiedAt.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileCreatedAt }));
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileCreatedAt }));
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
@@ -187,7 +187,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should account for the server being in a non-UTC timezone', async () => {
|
it('should account for the server being in a non-UTC timezone', async () => {
|
||||||
process.env.TZ = 'America/Los_Angeles';
|
process.env.TZ = 'America/Los_Angeles';
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -205,7 +205,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle lists of numbers', async () => {
|
it('should handle lists of numbers', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
ISO: [160],
|
ISO: [160],
|
||||||
FileCreateDate: assetStub.image.fileCreatedAt.toISOString(),
|
FileCreateDate: assetStub.image.fileCreatedAt.toISOString(),
|
||||||
@@ -213,7 +213,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
@@ -225,7 +225,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should apply reverse geocoding', async () => {
|
it('should apply reverse geocoding', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
|
mocks.asset.getById.mockResolvedValue(assetStub.withLocation);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
@@ -236,7 +236,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
||||||
);
|
);
|
||||||
@@ -250,19 +250,19 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should discard latitude and longitude on null island', async () => {
|
it('should discard latitude and longitude on null island', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
|
mocks.asset.getById.mockResolvedValue(assetStub.withLocation);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
GPSLatitude: 0,
|
GPSLatitude: 0,
|
||||||
GPSLongitude: 0,
|
GPSLongitude: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null }));
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from TagsList', async () => {
|
it('should extract tags from TagsList', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ TagsList: ['Parent'] });
|
mockReadTags({ TagsList: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchy from TagsList', async () => {
|
it('should extract hierarchy from TagsList', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||||
@@ -292,7 +292,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a string', async () => {
|
it('should extract tags from Keywords as a string', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Keywords: 'Parent' });
|
mockReadTags({ Keywords: 'Parent' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a list', async () => {
|
it('should extract tags from Keywords as a list', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Keywords: ['Parent'] });
|
mockReadTags({ Keywords: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a list with a number', async () => {
|
it('should extract tags from Keywords as a list with a number', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Keywords: ['Parent', 2024] });
|
mockReadTags({ Keywords: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchal tags from Keywords', async () => {
|
it('should extract hierarchal tags from Keywords', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Keywords: 'Parent/Child' });
|
mockReadTags({ Keywords: 'Parent/Child' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -342,7 +342,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore Keywords when TagsList is present', async () => {
|
it('should ignore Keywords when TagsList is present', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchy from HierarchicalSubject', async () => {
|
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||||
@@ -382,7 +382,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove existing tags', async () => {
|
it('should remove existing tags', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({});
|
mockReadTags({});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -435,11 +435,11 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not apply motion photos if asset is video', async () => {
|
it('should not apply motion photos if asset is video', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||||
@@ -451,7 +451,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an invalid Directory Item', async () => {
|
it('should handle an invalid Directory Item', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
ContainerDirectory: [{ Foo: 100 }],
|
ContainerDirectory: [{ Foo: 100 }],
|
||||||
@@ -461,20 +461,20 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the correct video orientation', async () => {
|
it('should extract the correct video orientation', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
mocks.asset.getById.mockResolvedValue(assetStub.video);
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
mockReadTags({});
|
mockReadTags({});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.video.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }),
|
expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null });
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -497,7 +497,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetStub.livePhotoWithOriginalFileName.originalPath,
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||||
'MotionPhotoVideo',
|
'MotionPhotoVideo',
|
||||||
);
|
);
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||||
@@ -525,7 +525,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
|
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null });
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
EmbeddedVideoFile: new BinaryField(0, ''),
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
||||||
@@ -545,7 +545,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetStub.livePhotoWithOriginalFileName.originalPath,
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||||
'EmbeddedVideoFile',
|
'EmbeddedVideoFile',
|
||||||
);
|
);
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||||
@@ -573,7 +573,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null });
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -589,7 +589,7 @@ describe(MetadataService.name, () => {
|
|||||||
mocks.storage.readFile.mockResolvedValue(video);
|
mocks.storage.readFile.mockResolvedValue(video);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.storage.readFile).toHaveBeenCalledWith(
|
expect(mocks.storage.readFile).toHaveBeenCalledWith(
|
||||||
@@ -621,7 +621,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoWithOriginalFileName]);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoWithOriginalFileName);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -647,7 +647,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -669,7 +669,7 @@ 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 () => {
|
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null });
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -694,9 +694,11 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not update storage usage if motion photo is external', async () => {
|
it('should not update storage usage if motion photo is external', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([
|
mocks.asset.getById.mockResolvedValue({
|
||||||
{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null, isExternal: true },
|
...assetStub.livePhotoStillAsset,
|
||||||
]);
|
livePhotoVideoId: null,
|
||||||
|
isExternal: true,
|
||||||
|
});
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -738,11 +740,11 @@ describe(MetadataService.name, () => {
|
|||||||
tz: 'UTC-11:30',
|
tz: 'UTC-11:30',
|
||||||
Rating: 3,
|
Rating: 3,
|
||||||
};
|
};
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags(tags);
|
mockReadTags(tags);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith({
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith({
|
||||||
assetId: assetStub.image.id,
|
assetId: assetStub.image.id,
|
||||||
bitsPerSample: expect.any(Number),
|
bitsPerSample: expect.any(Number),
|
||||||
@@ -798,11 +800,11 @@ describe(MetadataService.name, () => {
|
|||||||
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
||||||
tz: undefined,
|
tz: undefined,
|
||||||
};
|
};
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags(tags);
|
mockReadTags(tags);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
timeZone: 'UTC+0',
|
timeZone: 'UTC+0',
|
||||||
@@ -811,7 +813,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract duration', async () => {
|
it('should extract duration', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.video });
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -822,7 +824,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.video.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -833,7 +835,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only extract duration for videos', async () => {
|
it('should only extract duration for videos', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.image });
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -843,7 +845,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -854,7 +856,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should omit duration of zero', async () => {
|
it('should omit duration of zero', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.video });
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -865,7 +867,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.video.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -876,7 +878,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should a handle duration of 1 week', async () => {
|
it('should a handle duration of 1 week', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
mocks.asset.getById.mockResolvedValue({ ...assetStub.video });
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -887,7 +889,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.video.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -898,7 +900,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore duration from exif data', async () => {
|
it('should ignore duration from exif data', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({}, { Duration: { Value: 123 } });
|
mockReadTags({}, { Duration: { Value: 123 } });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -906,7 +908,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should trim whitespace from description', async () => {
|
it('should trim whitespace from description', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Description: '\t \v \f \n \r' });
|
mockReadTags({ Description: '\t \v \f \n \r' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -926,7 +928,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a numeric description', async () => {
|
it('should handle a numeric description', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Description: 1000 });
|
mockReadTags({ Description: 1000 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -938,7 +940,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip importing metadata when the feature is disabled', async () => {
|
it('should skip importing metadata when the feature is disabled', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
||||||
mockReadTags(metadataStub.withFace);
|
mockReadTags(metadataStub.withFace);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -946,7 +948,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(metadataStub.empty);
|
mockReadTags(metadataStub.empty);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -954,7 +956,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip importing faces without name', async () => {
|
it('should skip importing faces without name', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(metadataStub.withFaceNoName);
|
mockReadTags(metadataStub.withFaceNoName);
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -966,7 +968,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip importing faces with empty name', async () => {
|
it('should skip importing faces with empty name', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(metadataStub.withFaceEmptyName);
|
mockReadTags(metadataStub.withFaceEmptyName);
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -978,14 +980,14 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should apply metadata face tags creating new persons', async () => {
|
it('should apply metadata face tags creating new persons', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(metadataStub.withFace);
|
mockReadTags(metadataStub.withFace);
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
||||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.primaryImage.id, { faces: { person: false } });
|
||||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
||||||
expect.objectContaining({ name: personStub.withName.name }),
|
expect.objectContaining({ name: personStub.withName.name }),
|
||||||
@@ -1019,14 +1021,14 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should assign metadata face tags to existing persons', async () => {
|
it('should assign metadata face tags to existing persons', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(metadataStub.withFace);
|
mockReadTags(metadataStub.withFace);
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
||||||
mocks.person.createAll.mockResolvedValue([]);
|
mocks.person.createAll.mockResolvedValue([]);
|
||||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.primaryImage.id, { faces: { person: false } });
|
||||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
||||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||||
@@ -1051,7 +1053,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid modify date', async () => {
|
it('should handle invalid modify date', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ ModifyDate: '00:00:00.000' });
|
mockReadTags({ ModifyDate: '00:00:00.000' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -1063,7 +1065,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid rating value', async () => {
|
it('should handle invalid rating value', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Rating: 6 });
|
mockReadTags({ Rating: 6 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -1075,7 +1077,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle valid rating value', async () => {
|
it('should handle valid rating value', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Rating: 5 });
|
mockReadTags({ Rating: 5 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -1087,7 +1089,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle valid negative rating value', async () => {
|
it('should handle valid negative rating value', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({ Rating: -1 });
|
mockReadTags({ Rating: -1 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -1099,11 +1101,11 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle livePhotoCID not set', async () => {
|
it('should handle livePhotoCID not set', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.image.id, { faces: { person: false } });
|
||||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
||||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||||
@@ -1111,14 +1113,14 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle not finding a match', async () => {
|
it('should handle not finding a match', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
||||||
JobStatus.SUCCESS,
|
JobStatus.SUCCESS,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||||
@@ -1132,7 +1134,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should link photo and video', async () => {
|
it('should link photo and video', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
@@ -1140,7 +1142,7 @@ describe(MetadataService.name, () => {
|
|||||||
JobStatus.SUCCESS,
|
JobStatus.SUCCESS,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], {
|
expect(mocks.asset.getById).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id, {
|
||||||
faces: { person: false },
|
faces: { person: false },
|
||||||
});
|
});
|
||||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||||
@@ -1158,12 +1160,10 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should notify clients on live photo link', async () => {
|
it('should notify clients on live photo link', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([
|
mocks.asset.getById.mockResolvedValue({
|
||||||
{
|
...assetStub.livePhotoStillAsset,
|
||||||
...assetStub.livePhotoStillAsset,
|
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
||||||
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
@@ -1178,12 +1178,10 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should search by libraryId', async () => {
|
it('should search by libraryId', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([
|
mocks.asset.getById.mockResolvedValue({
|
||||||
{
|
...assetStub.livePhotoStillAsset,
|
||||||
...assetStub.livePhotoStillAsset,
|
libraryId: 'library-id',
|
||||||
libraryId: 'library-id',
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||||
@@ -1204,7 +1202,7 @@ describe(MetadataService.name, () => {
|
|||||||
{ Device: { Manufacturer: '1', ModelName: '2' }, AndroidMake: '3', AndroidModel: '4' },
|
{ Device: { Manufacturer: '1', ModelName: '2' }, AndroidMake: '3', AndroidModel: '4' },
|
||||||
{ AndroidMake: '1', AndroidModel: '2' },
|
{ AndroidMake: '1', AndroidModel: '2' },
|
||||||
])('should read camera make and model correct place %s', async (metaData) => {
|
])('should read camera make and model correct place %s', async (metaData) => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags(metaData);
|
mockReadTags(metaData);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
@@ -1251,19 +1249,19 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('handleSidecarSync', () => {
|
describe('handleSidecarSync', () => {
|
||||||
it('should do nothing if asset could not be found', async () => {
|
it('should do nothing if asset could not be found', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([]);
|
mocks.asset.getById.mockResolvedValue(void 0);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if asset has no sidecar path', async () => {
|
it('should do nothing if asset has no sidecar path', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(true);
|
mocks.storage.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
@@ -1278,7 +1276,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set sidecar path if exists (sidecar named photo.xmp)', async () => {
|
it('should set sidecar path if exists (sidecar named photo.xmp)', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecarWithoutExt);
|
||||||
mocks.storage.checkFileExists.mockResolvedValueOnce(false);
|
mocks.storage.checkFileExists.mockResolvedValueOnce(false);
|
||||||
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
@@ -1295,7 +1293,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set sidecar path if exists (two sidecars named photo.ext.xmp and photo.xmp, should pick photo.ext.xmp)', async () => {
|
it('should set sidecar path if exists (two sidecars named photo.ext.xmp and photo.xmp, should pick photo.ext.xmp)', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
||||||
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
@@ -1313,7 +1311,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should unset sidecar path if file does not exist anymore', async () => {
|
it('should unset sidecar path if file does not exist anymore', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(false);
|
mocks.storage.checkFileExists.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
@@ -1330,26 +1328,26 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('handleSidecarDiscovery', () => {
|
describe('handleSidecarDiscovery', () => {
|
||||||
it('should skip hidden assets', async () => {
|
it('should skip hidden assets', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id });
|
||||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip assets with a sidecar path', async () => {
|
it('should skip assets with a sidecar path', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.sidecar.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.sidecar.id });
|
||||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing when a sidecar is not found ', async () => {
|
it('should do nothing when a sidecar is not found ', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(false);
|
mocks.storage.checkFileExists.mockResolvedValue(false);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update a image asset when a sidecar is found', async () => {
|
it('should update a image asset when a sidecar is found', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(true);
|
mocks.storage.checkFileExists.mockResolvedValue(true);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
||||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
|
expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
|
||||||
@@ -1360,7 +1358,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update a video asset when a sidecar is found', async () => {
|
it('should update a video asset when a sidecar is found', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
mocks.asset.getById.mockResolvedValue(assetStub.video);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(true);
|
mocks.storage.checkFileExists.mockResolvedValue(true);
|
||||||
await sut.handleSidecarDiscovery({ id: assetStub.video.id });
|
await sut.handleSidecarDiscovery({ id: assetStub.video.id });
|
||||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
|
expect(mocks.storage.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
|
||||||
@@ -1373,13 +1371,13 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('handleSidecarWrite', () => {
|
describe('handleSidecarWrite', () => {
|
||||||
it('should skip assets that do not exist anymore', async () => {
|
it('should skip assets that do not exist anymore', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([]);
|
mocks.asset.getById.mockResolvedValue(void 0);
|
||||||
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip jobs with not metadata', async () => {
|
it('should skip jobs with not metadata', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED);
|
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1389,7 +1387,7 @@ describe(MetadataService.name, () => {
|
|||||||
const gps = 12;
|
const gps = 12;
|
||||||
const date = '2023-11-22T04:56:12.196Z';
|
const date = '2023-11-22T04:56:12.196Z';
|
||||||
|
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
mocks.asset.getById.mockResolvedValue(assetStub.sidecar);
|
||||||
await expect(
|
await expect(
|
||||||
sut.handleSidecarWrite({
|
sut.handleSidecarWrite({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { ReverseGeocodeResult } from 'src/repositories/map.repository';
|
|||||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { JobOf } from 'src/types';
|
import { JobOf } from 'src/types';
|
||||||
|
import { getSidecarPath } from 'src/utils/asset.util';
|
||||||
import { isFaceImportEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { upsertTags } from 'src/utils/tag';
|
import { upsertTags } from 'src/utils/tag';
|
||||||
@@ -162,15 +163,29 @@ export class MetadataService extends BaseService {
|
|||||||
|
|
||||||
@OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
@OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
||||||
async handleMetadataExtraction(data: JobOf<JobName.METADATA_EXTRACTION>): Promise<JobStatus> {
|
async handleMetadataExtraction(data: JobOf<JobName.METADATA_EXTRACTION>): Promise<JobStatus> {
|
||||||
const [{ metadata, reverseGeocoding }, [asset]] = await Promise.all([
|
const [{ metadata, reverseGeocoding }, asset] = await Promise.all([
|
||||||
this.getConfig({ withCache: true }),
|
this.getConfig({ withCache: true }),
|
||||||
this.assetRepository.getByIds([data.id], { faces: { person: false } }),
|
this.assetRepository.getById(data.id, { faces: { person: false } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asset.isDirty) {
|
||||||
|
const { exifInfo } = (await this.assetRepository.getById(asset.id, { exifInfo: true })) || {};
|
||||||
|
await this.handleSidecarWrite({
|
||||||
|
id: asset.id,
|
||||||
|
description: exifInfo?.description,
|
||||||
|
dateTimeOriginal: exifInfo?.dateTimeOriginal?.toISOString(),
|
||||||
|
latitude: exifInfo?.latitude ?? undefined,
|
||||||
|
longitude: exifInfo?.longitude ?? undefined,
|
||||||
|
rating: exifInfo?.rating ?? undefined,
|
||||||
|
tags: true,
|
||||||
|
});
|
||||||
|
asset.sidecarPath = asset.sidecarPath || getSidecarPath(asset);
|
||||||
|
}
|
||||||
|
|
||||||
const exifTags = await this.getExifTags(asset);
|
const exifTags = await this.getExifTags(asset);
|
||||||
if (!exifTags.FileCreateDate || !exifTags.FileModifyDate || exifTags.FileSize === undefined) {
|
if (!exifTags.FileCreateDate || !exifTags.FileModifyDate || exifTags.FileSize === undefined) {
|
||||||
this.logger.warn(`Missing file creation or modification date for asset ${asset.id}: ${asset.originalPath}`);
|
this.logger.warn(`Missing file creation or modification date for asset ${asset.id}: ${asset.originalPath}`);
|
||||||
@@ -314,14 +329,14 @@ export class MetadataService extends BaseService {
|
|||||||
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
||||||
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
const asset = await this.assetRepository.getById(id, { tags: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsList = (asset.tags || []).map((tag) => tag.value);
|
const tagsList = (asset.tags || []).map((tag) => tag.value);
|
||||||
|
|
||||||
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
|
const sidecarPath = asset.sidecarPath || getSidecarPath(asset);
|
||||||
const exif = _.omitBy(
|
const exif = _.omitBy(
|
||||||
<Tags>{
|
<Tags>{
|
||||||
Description: description,
|
Description: description,
|
||||||
@@ -342,7 +357,7 @@ export class MetadataService extends BaseService {
|
|||||||
await this.metadataRepository.writeTags(sidecarPath, exif);
|
await this.metadataRepository.writeTags(sidecarPath, exif);
|
||||||
|
|
||||||
if (!asset.sidecarPath) {
|
if (!asset.sidecarPath) {
|
||||||
await this.assetRepository.update({ id, sidecarPath });
|
await this.assetRepository.update({ id, sidecarPath, isDirty: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@@ -754,7 +769,7 @@ export class MetadataService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {
|
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const asset = await this.assetRepository.getById(id);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class TagService extends BaseService {
|
|||||||
|
|
||||||
const results = await this.tagRepository.upsertAssetIds(items);
|
const results = await this.tagRepository.upsertAssetIds(items);
|
||||||
for (const assetId of new Set(results.map((item) => item.assetsId))) {
|
for (const assetId of new Set(results.map((item) => item.assetsId))) {
|
||||||
|
await this.assetRepository.update({ id: assetId, isDirty: true });
|
||||||
await this.eventRepository.emit('asset.tag', { assetId });
|
await this.eventRepository.emit('asset.tag', { assetId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ export class TagService extends BaseService {
|
|||||||
|
|
||||||
for (const { id: assetId, success } of results) {
|
for (const { id: assetId, success } of results) {
|
||||||
if (success) {
|
if (success) {
|
||||||
|
await this.assetRepository.update({ id: assetId, isDirty: true });
|
||||||
await this.eventRepository.emit('asset.tag', { assetId });
|
await this.eventRepository.emit('asset.tag', { assetId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,6 +127,7 @@ export class TagService extends BaseService {
|
|||||||
|
|
||||||
for (const { id: assetId, success } of results) {
|
for (const { id: assetId, success } of results) {
|
||||||
if (success) {
|
if (success) {
|
||||||
|
await this.assetRepository.update({ id: assetId, isDirty: true });
|
||||||
await this.eventRepository.emit('asset.untag', { assetId });
|
await this.eventRepository.emit('asset.untag', { assetId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.respons
|
|||||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||||
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetFileType, AssetType, Permission } from 'src/enum';
|
import { AssetFileType, AssetType, Permission } from 'src/enum';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
@@ -17,6 +18,8 @@ const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType
|
|||||||
return (files || []).find((file) => file.type === type);
|
return (files || []).find((file) => file.type === type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSidecarPath = (asset: AssetEntity) => `${asset.originalPath}.xmp`;
|
||||||
|
|
||||||
export const getAssetFiles = (files?: AssetFileEntity[]) => ({
|
export const getAssetFiles = (files?: AssetFileEntity[]) => ({
|
||||||
previewFile: getFileByType(files, AssetFileType.PREVIEW),
|
previewFile: getFileByType(files, AssetFileType.PREVIEW),
|
||||||
thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL),
|
thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL),
|
||||||
|
|||||||
Vendored
+20
@@ -88,6 +88,7 @@ export const assetStub = {
|
|||||||
isExternal: false,
|
isExternal: false,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noWebpPath: Object.freeze<AssetEntity>({
|
noWebpPath: Object.freeze<AssetEntity>({
|
||||||
@@ -126,6 +127,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noThumbhash: Object.freeze<AssetEntity>({
|
noThumbhash: Object.freeze<AssetEntity>({
|
||||||
@@ -161,6 +163,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
primaryImage: Object.freeze<AssetEntity>({
|
primaryImage: Object.freeze<AssetEntity>({
|
||||||
@@ -207,6 +210,7 @@ export const assetStub = {
|
|||||||
]),
|
]),
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image: Object.freeze<AssetEntity>({
|
image: Object.freeze<AssetEntity>({
|
||||||
@@ -247,6 +251,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashed: Object.freeze<AssetEntity>({
|
trashed: Object.freeze<AssetEntity>({
|
||||||
@@ -287,6 +292,7 @@ export const assetStub = {
|
|||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
status: AssetStatus.TRASHED,
|
status: AssetStatus.TRASHED,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashedOffline: Object.freeze<AssetEntity>({
|
trashedOffline: Object.freeze<AssetEntity>({
|
||||||
@@ -328,6 +334,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: true,
|
isOffline: true,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
archived: Object.freeze<AssetEntity>({
|
archived: Object.freeze<AssetEntity>({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
@@ -367,6 +374,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
external: Object.freeze<AssetEntity>({
|
external: Object.freeze<AssetEntity>({
|
||||||
@@ -406,6 +414,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image1: Object.freeze<AssetEntity>({
|
image1: Object.freeze<AssetEntity>({
|
||||||
@@ -444,6 +453,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageFrom2015: Object.freeze<AssetEntity>({
|
imageFrom2015: Object.freeze<AssetEntity>({
|
||||||
@@ -482,6 +492,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
video: Object.freeze<AssetEntity>({
|
video: Object.freeze<AssetEntity>({
|
||||||
@@ -522,6 +533,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
livePhotoMotionAsset: Object.freeze({
|
livePhotoMotionAsset: Object.freeze({
|
||||||
@@ -613,6 +625,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecar: Object.freeze<AssetEntity>({
|
sidecar: Object.freeze<AssetEntity>({
|
||||||
@@ -648,6 +661,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecarWithoutExt: Object.freeze<AssetEntity>({
|
sidecarWithoutExt: Object.freeze<AssetEntity>({
|
||||||
@@ -683,6 +697,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasEncodedVideo: Object.freeze<AssetEntity>({
|
hasEncodedVideo: Object.freeze<AssetEntity>({
|
||||||
@@ -721,6 +736,7 @@ export const assetStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasFileExtension: Object.freeze<AssetEntity>({
|
hasFileExtension: Object.freeze<AssetEntity>({
|
||||||
@@ -760,6 +776,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageDng: Object.freeze<AssetEntity>({
|
imageDng: Object.freeze<AssetEntity>({
|
||||||
@@ -800,6 +817,7 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasEmbedding: Object.freeze<AssetEntity>({
|
hasEmbedding: Object.freeze<AssetEntity>({
|
||||||
@@ -842,6 +860,7 @@ export const assetStub = {
|
|||||||
embedding: '[1, 2, 3, 4]',
|
embedding: '[1, 2, 3, 4]',
|
||||||
},
|
},
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasDupe: Object.freeze<AssetEntity>({
|
hasDupe: Object.freeze<AssetEntity>({
|
||||||
@@ -884,5 +903,6 @@ export const assetStub = {
|
|||||||
embedding: '[1, 2, 3, 4]',
|
embedding: '[1, 2, 3, 4]',
|
||||||
},
|
},
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
isDirty: false,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
+1
@@ -247,6 +247,7 @@ export const sharedLinkStub = {
|
|||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
|
isDirty: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ describe(MetadataService.name, () => {
|
|||||||
process.env.TZ = serverTimeZone ?? undefined;
|
process.env.TZ = serverTimeZone ?? undefined;
|
||||||
|
|
||||||
const { filePath } = await createTestFile(exifData);
|
const { filePath } = await createTestFile(exifData);
|
||||||
mocks.asset.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
|
mocks.asset.getById.mockResolvedValue({ id: 'asset-1', originalPath: filePath } as AssetEntity);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const assetFactory = (asset: Partial<Asset> = {}) => ({
|
|||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
|
isDirty: false,
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
localDateTime: newDate(),
|
localDateTime: newDate(),
|
||||||
|
|||||||
Reference in New Issue
Block a user