mirror of
https://github.com/immich-app/immich.git
synced 2026-06-12 19:11:52 -07:00
fix: handle removing motion part if asset is not motion photo anymore
This commit is contained in:
@@ -82,6 +82,9 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
||||
|
||||
[[tools."github:webassembly/binaryen"]]
|
||||
version = "version_124"
|
||||
backend = "github:webassembly/binaryen"
|
||||
|
||||
@@ -532,4 +532,5 @@ export const lockableProperties = [
|
||||
'rating',
|
||||
'timeZone',
|
||||
'tags',
|
||||
] as const;
|
||||
'livePhotoCID',
|
||||
] as const satisfies Array<keyof AssetExifTable>;
|
||||
|
||||
@@ -181,7 +181,7 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||
await this.assetRepository.upsertExif({
|
||||
exif: { assetId: asset.id, fileSizeInByte: file.size },
|
||||
exif: { assetId: asset.id, fileSizeInByte: file.size, livePhotoCID: dto.livePhotoVideoId },
|
||||
lockedPropertiesBehavior: 'override',
|
||||
});
|
||||
|
||||
|
||||
@@ -396,6 +396,23 @@ export class MetadataService extends BaseService {
|
||||
tasks.push(() => this.applyMotionPhotos(asset, exifTags, dates, stats));
|
||||
}
|
||||
|
||||
if (!this.isMotionPhoto(asset, exifTags) && !exifData.livePhotoCID && asset.livePhotoVideoId) {
|
||||
// delete the motion part if the asset gets changed to not be a live photo anymore
|
||||
tasks.push(async () => {
|
||||
if (!asset.livePhotoVideoId) {
|
||||
throw new Error('asset.livePhotoVideoId should not have been reset');
|
||||
}
|
||||
await this.assetRepository.update({ id: asset.id, livePhotoVideoId: null });
|
||||
const count = await this.assetRepository.getLivePhotoCount(asset.livePhotoVideoId);
|
||||
if (count === 0) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id: asset.livePhotoVideoId, deleteOnDisk: true },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) {
|
||||
tasks.push(() => this.applyTaggedFaces(asset, exifTags));
|
||||
}
|
||||
@@ -500,7 +517,7 @@ export class MetadataService extends BaseService {
|
||||
const { sidecarFile } = getAssetFiles(asset.files);
|
||||
const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`;
|
||||
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone, livePhotoCID } = _.pick(
|
||||
{
|
||||
description: asset.exifInfo.description,
|
||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal,
|
||||
@@ -509,6 +526,7 @@ export class MetadataService extends BaseService {
|
||||
rating: asset.exifInfo.rating ?? 0,
|
||||
tags: asset.exifInfo.tags,
|
||||
timeZone: asset.exifInfo.timeZone,
|
||||
livePhotoCID: asset.exifInfo.livePhotoCID,
|
||||
},
|
||||
lockedProperties,
|
||||
);
|
||||
@@ -522,6 +540,7 @@ export class MetadataService extends BaseService {
|
||||
GPSLongitude: longitude,
|
||||
Rating: rating,
|
||||
TagsList: tags,
|
||||
ContentIdentifier: livePhotoCID,
|
||||
},
|
||||
_.isUndefined,
|
||||
);
|
||||
|
||||
@@ -3,10 +3,12 @@ import { Stats } from 'node:fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { AssetType, JobName } from 'src/enum';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
@@ -41,7 +43,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||
SystemMetadataRepository,
|
||||
TagRepository,
|
||||
],
|
||||
mock: [EventRepository, StorageRepository, LoggingRepository],
|
||||
mock: [EventRepository, JobRepository, StorageRepository, LoggingRepository],
|
||||
});
|
||||
|
||||
ctx.getMock(StorageRepository).stat.mockResolvedValue({
|
||||
@@ -152,4 +154,24 @@ describe(MetadataService.name, () => {
|
||||
).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') });
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove motion asset if asset is updated to not be a motion photo anymore', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset: motionAsset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, livePhotoVideoId: motionAsset.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: '' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual(
|
||||
expect.objectContaining({ livePhotoVideoId: null }),
|
||||
);
|
||||
expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id: motionAsset.id, deleteOnDisk: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user