fix(server): extraction of Samsung Motionphoto videos (#6337)

* Fix extraction of samsung motionphoto videos

* Refactor binary tag extraction to the repository to consolidate exiftool usage

* format

* fix linting and swap argument orders

* Fix tag name and conditional order

* Add unit test

* Update server test assets submodule

* Remove old motion photo video assets when a new one is extracted

* delete first, then write

* Include motion photo asset uuid's in the filename

If the filenames are not uniquified, then we can't delete old/corrupt ones

* Fix formatting and fix/add tests

* chore: only use new uuid

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Aram Akhavan
2024-01-22 10:04:45 -08:00
committed by GitHub
parent 7b314f9435
commit a972dd4060
10 changed files with 193 additions and 63 deletions
+43 -12
View File
@@ -354,7 +354,7 @@ export class MetadataService {
}
private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) {
if (asset.type !== AssetType.IMAGE || asset.livePhotoVideoId) {
if (asset.type !== AssetType.IMAGE) {
return;
}
@@ -362,6 +362,8 @@ export class MetadataService {
const isMotionPhoto = tags.MotionPhoto;
const isMicroVideo = tags.MicroVideo;
const videoOffset = tags.MicroVideoOffset;
const hasMotionPhotoVideo = tags.MotionPhotoVideo;
const hasEmbeddedVideoFile = tags.EmbeddedVideoType === 'MotionPhoto_Data' && tags.EmbeddedVideoFile;
const directory = Array.isArray(rawDirectory) ? (rawDirectory as DirectoryEntry[]) : null;
let length = 0;
@@ -381,7 +383,7 @@ export class MetadataService {
length = videoOffset;
}
if (!length) {
if (!length && !hasEmbeddedVideoFile && !hasMotionPhotoVideo) {
return;
}
@@ -390,20 +392,35 @@ export class MetadataService {
try {
const stat = await this.storageRepository.stat(asset.originalPath);
const position = stat.size - length - padding;
const video = await this.storageRepository.readFile(asset.originalPath, {
buffer: Buffer.alloc(length),
position,
length,
});
let video: Buffer;
// Samsung MotionPhoto video extraction
// HEIC-encoded
if (hasMotionPhotoVideo) {
video = await this.repository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
}
// JPEG-encoded; HEIC also contains these tags, so this conditional must come second
else if (hasEmbeddedVideoFile) {
video = await this.repository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
}
// Default video extraction
else {
video = await this.storageRepository.readFile(asset.originalPath, {
buffer: Buffer.alloc(length),
position,
length,
});
}
const checksum = this.cryptoRepository.hashSha1(video);
const motionPath = StorageCore.getAndroidMotionPath(asset);
this.storageCore.ensureFolders(motionPath);
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
if (!motionAsset) {
// We create a UUID in advance so that each extracted video can have a unique filename
// (allowing us to delete old ones if necessary)
const motionAssetId = this.cryptoRepository.randomUUID();
const motionPath = StorageCore.getAndroidMotionPath(asset, motionAssetId);
const createdAt = asset.fileCreatedAt ?? asset.createdAt;
motionAsset = await this.assetRepository.create({
id: motionAssetId,
libraryId: asset.libraryId,
type: AssetType.VIDEO,
fileCreatedAt: createdAt,
@@ -419,11 +436,25 @@ export class MetadataService {
deviceId: 'NONE',
});
this.storageCore.ensureFolders(motionPath);
await this.storageRepository.writeFile(motionAsset.originalPath, video);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
}
await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
// If the asset already had an associated livePhotoVideo, delete it, because
// its checksum doesn't match the checksum of the motionAsset we just extracted
// (if it did, getByChecksum() would've returned non-null)
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);
}
} else {
this.logger.debug(
`Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
'base64',
)} already exists in the repository`,
);
}
this.logger.debug(`Finished motion photo video extraction (${asset.id})`);
} catch (error: Error | any) {