mirror of
https://github.com/immich-app/immich.git
synced 2025-02-04 01:09:14 -05:00
feat(server): auto-link live photos (#1761)
* feat(server): auto-link live photos * fix: video extraction and linking
This commit is contained in:
parent
7a25d359b7
commit
36197cca98
4 changed files with 73 additions and 3 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { AssetEntity, ExifEntity } from '@app/infra';
|
import { AssetEntity, AssetType, ExifEntity } from '@app/infra';
|
||||||
import {
|
import {
|
||||||
IExifExtractionProcessor,
|
IExifExtractionProcessor,
|
||||||
IReverseGeocodingProcessor,
|
IReverseGeocodingProcessor,
|
||||||
|
@ -18,7 +18,12 @@ import { Repository } from 'typeorm/repository/Repository';
|
||||||
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
||||||
import { getName } from 'i18n-iso-countries';
|
import { getName } from 'i18n-iso-countries';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { ExifDateTime, exiftool } from 'exiftool-vendored';
|
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
|
||||||
|
import { IsNull, Not } from 'typeorm';
|
||||||
|
|
||||||
|
interface ImmichTags extends Tags {
|
||||||
|
ContentIdentifier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function geocoderInit(init: InitOptions) {
|
function geocoderInit(init: InitOptions) {
|
||||||
return new Promise<void>(function (resolve) {
|
return new Promise<void>(function (resolve) {
|
||||||
|
@ -139,7 +144,7 @@ export class MetadataExtractionProcessor {
|
||||||
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
|
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
|
||||||
try {
|
try {
|
||||||
const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data;
|
const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data;
|
||||||
const exifData = await exiftool.read(asset.originalPath).catch((e) => {
|
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
|
||||||
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
|
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -177,12 +182,33 @@ export class MetadataExtractionProcessor {
|
||||||
newExif.iso = exifData?.ISO || null;
|
newExif.iso = exifData?.ISO || null;
|
||||||
newExif.latitude = exifData?.GPSLatitude || null;
|
newExif.latitude = exifData?.GPSLatitude || null;
|
||||||
newExif.longitude = exifData?.GPSLongitude || null;
|
newExif.longitude = exifData?.GPSLongitude || null;
|
||||||
|
newExif.livePhotoCID = exifData?.MediaGroupUUID || null;
|
||||||
|
|
||||||
await this.assetRepository.save({
|
await this.assetRepository.save({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
createdAt: createdAt?.toISOString(),
|
createdAt: createdAt?.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
|
||||||
|
const motionAsset = await this.assetRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: Not(asset.id),
|
||||||
|
type: AssetType.VIDEO,
|
||||||
|
exifInfo: {
|
||||||
|
livePhotoCID: newExif.livePhotoCID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (motionAsset) {
|
||||||
|
await this.assetRepository.update(asset.id, { livePhotoVideoId: motionAsset.id });
|
||||||
|
await this.assetRepository.update(motionAsset.id, { isVisible: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse Geocoding
|
* Reverse Geocoding
|
||||||
*
|
*
|
||||||
|
@ -266,6 +292,11 @@ export class MetadataExtractionProcessor {
|
||||||
createdAt = asset.createdAt;
|
createdAt = asset.createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
|
||||||
|
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const newExif = new ExifEntity();
|
const newExif = new ExifEntity();
|
||||||
newExif.assetId = asset.id;
|
newExif.assetId = asset.id;
|
||||||
newExif.description = '';
|
newExif.description = '';
|
||||||
|
@ -279,6 +310,25 @@ export class MetadataExtractionProcessor {
|
||||||
newExif.state = null;
|
newExif.state = null;
|
||||||
newExif.country = null;
|
newExif.country = null;
|
||||||
newExif.fps = null;
|
newExif.fps = null;
|
||||||
|
newExif.livePhotoCID = exifData?.ContentIdentifier || null;
|
||||||
|
|
||||||
|
if (newExif.livePhotoCID) {
|
||||||
|
const photoAsset = await this.assetRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: Not(asset.id),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
livePhotoVideoId: IsNull(),
|
||||||
|
exifInfo: {
|
||||||
|
livePhotoCID: newExif.livePhotoCID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (photoAsset) {
|
||||||
|
await this.assetRepository.update(photoAsset.id, { livePhotoVideoId: asset.id });
|
||||||
|
await this.assetRepository.update(asset.id, { isVisible: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (videoTags && videoTags['location']) {
|
if (videoTags && videoTags['location']) {
|
||||||
const location = videoTags['location'] as string;
|
const location = videoTags['location'] as string;
|
||||||
|
|
|
@ -378,6 +378,7 @@ export const sharedLinkStub = {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
exifInfo: {
|
exifInfo: {
|
||||||
|
livePhotoCID: null,
|
||||||
id: 1,
|
id: 1,
|
||||||
assetId: 'id_1',
|
assetId: 'id_1',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
|
|
@ -44,6 +44,10 @@ export class ExifEntity {
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
city!: string | null;
|
city!: string | null;
|
||||||
|
|
||||||
|
@Index('IDX_live_photo_cid')
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
livePhotoCID!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
state!: string | null;
|
state!: string | null;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AppleContentIdentifier1676437878377 implements MigrationInterface {
|
||||||
|
name = 'AppleContentIdentifier1676437878377';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" ADD "livePhotoCID" character varying`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_live_photo_cid" ON "exif" ("livePhotoCID") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_live_photo_cid"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "livePhotoCID"`);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue