Compare commits

...

1 Commits

Author SHA1 Message Date
Daniel Dietzler 144ee8e01b fix: handle removing motion part if asset is not motion photo anymore 2026-06-11 15:46:45 +02:00
5 changed files with 49 additions and 4 deletions
+3
View File
@@ -82,6 +82,9 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
version = "7.1.3-6" version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg" backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
[[tools."github:webassembly/binaryen"]] [[tools."github:webassembly/binaryen"]]
version = "version_124" version = "version_124"
backend = "github:webassembly/binaryen" backend = "github:webassembly/binaryen"
+2 -1
View File
@@ -532,4 +532,5 @@ export const lockableProperties = [
'rating', 'rating',
'timeZone', 'timeZone',
'tags', 'tags',
] as const; 'livePhotoCID',
] as const satisfies Array<keyof AssetExifTable>;
+1 -1
View File
@@ -181,7 +181,7 @@ export class AssetMediaService extends BaseService {
} }
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
await this.assetRepository.upsertExif({ await this.assetRepository.upsertExif({
exif: { assetId: asset.id, fileSizeInByte: file.size }, exif: { assetId: asset.id, fileSizeInByte: file.size, livePhotoCID: dto.livePhotoVideoId },
lockedPropertiesBehavior: 'override', lockedPropertiesBehavior: 'override',
}); });
+20 -1
View File
@@ -396,6 +396,23 @@ export class MetadataService extends BaseService {
tasks.push(() => this.applyMotionPhotos(asset, exifTags, dates, stats)); 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)) { if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) {
tasks.push(() => this.applyTaggedFaces(asset, exifTags)); tasks.push(() => this.applyTaggedFaces(asset, exifTags));
} }
@@ -500,7 +517,7 @@ export class MetadataService extends BaseService {
const { sidecarFile } = getAssetFiles(asset.files); const { sidecarFile } = getAssetFiles(asset.files);
const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; 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, description: asset.exifInfo.description,
dateTimeOriginal: asset.exifInfo.dateTimeOriginal, dateTimeOriginal: asset.exifInfo.dateTimeOriginal,
@@ -509,6 +526,7 @@ export class MetadataService extends BaseService {
rating: asset.exifInfo.rating ?? 0, rating: asset.exifInfo.rating ?? 0,
tags: asset.exifInfo.tags, tags: asset.exifInfo.tags,
timeZone: asset.exifInfo.timeZone, timeZone: asset.exifInfo.timeZone,
livePhotoCID: asset.exifInfo.livePhotoCID,
}, },
lockedProperties, lockedProperties,
); );
@@ -522,6 +540,7 @@ export class MetadataService extends BaseService {
GPSLongitude: longitude, GPSLongitude: longitude,
Rating: rating, Rating: rating,
TagsList: tags, TagsList: tags,
ContentIdentifier: livePhotoCID,
}, },
_.isUndefined, _.isUndefined,
); );
@@ -3,10 +3,12 @@ import { Stats } from 'node:fs';
import { writeFile } from 'node:fs/promises'; import { writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { AssetType, JobName } from 'src/enum';
import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository'; import { AssetRepository } from 'src/repositories/asset.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository'; import { EventRepository } from 'src/repositories/event.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository';
import { StorageRepository } from 'src/repositories/storage.repository'; import { StorageRepository } from 'src/repositories/storage.repository';
@@ -41,7 +43,7 @@ const setup = (db?: Kysely<DB>) => {
SystemMetadataRepository, SystemMetadataRepository,
TagRepository, TagRepository,
], ],
mock: [EventRepository, StorageRepository, LoggingRepository], mock: [EventRepository, JobRepository, StorageRepository, LoggingRepository],
}); });
ctx.getMock(StorageRepository).stat.mockResolvedValue({ ctx.getMock(StorageRepository).stat.mockResolvedValue({
@@ -152,4 +154,24 @@ describe(MetadataService.name, () => {
).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') }); ).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 },
});
});
}); });