merge: remote-tracking branch 'immich/main' into feat/integrity-checks-izzy

This commit is contained in:
izzy
2025-12-17 15:09:39 +00:00
parent 08e532170f
commit 0e75f38e4a
310 changed files with 18246 additions and 7046 deletions
+26 -18
View File
@@ -8,9 +8,10 @@ import { constants } from 'node:fs/promises';
import { join, parse } from 'node:path';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { Asset, AssetFace } from 'src/database';
import { Asset, AssetFace, AssetFile } from 'src/database';
import { OnEvent, OnJob } from 'src/decorators';
import {
AssetFileType,
AssetType,
AssetVisibility,
DatabaseLock,
@@ -29,7 +30,9 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { isAssetChecksumConstraint } from 'src/utils/database';
import { mimeTypes } from 'src/utils/mime-types';
import { isFaceImportEnabled } from 'src/utils/misc';
import { upsertTags } from 'src/utils/tag';
@@ -37,7 +40,6 @@ import { upsertTags } from 'src/utils/tag';
const EXIF_DATE_TAGS: Array<keyof ImmichTags> = [
'SubSecDateTimeOriginal',
'SubSecCreateDate',
'SubSecMediaCreateDate',
'DateTimeOriginal',
'CreationDate',
'CreateDate',
@@ -360,17 +362,21 @@ export class MetadataService extends BaseService {
break;
}
const isChanged = sidecarPath !== asset.sidecarPath;
const { sidecarFile } = getAssetFiles(asset.files);
const isChanged = sidecarPath !== sidecarFile?.path;
this.logger.debug(
`Sidecar check found old=${asset.sidecarPath}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`,
`Sidecar check found old=${sidecarFile?.path}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`,
);
if (!isChanged) {
return JobStatus.Skipped;
}
await this.assetRepository.update({ id: asset.id, sidecarPath });
await (sidecarPath === null
? this.assetRepository.deleteFile({ assetId: asset.id, type: AssetFileType.Sidecar })
: this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath }));
return JobStatus.Success;
}
@@ -395,7 +401,9 @@ export class MetadataService extends BaseService {
const tagsList = (asset.tags || []).map((tag) => tag.value);
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
const { sidecarFile } = getAssetFiles(asset.files);
const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`;
const exif = _.omitBy(
<Tags>{
Description: description,
@@ -415,18 +423,19 @@ export class MetadataService extends BaseService {
await this.metadataRepository.writeTags(sidecarPath, exif);
if (!asset.sidecarPath) {
await this.assetRepository.update({ id, sidecarPath });
if (asset.files.length === 0) {
await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: sidecarPath });
}
return JobStatus.Success;
}
private getSidecarCandidates({ sidecarPath, originalPath }: { sidecarPath: string | null; originalPath: string }) {
private getSidecarCandidates({ files, originalPath }: { files: AssetFile[]; originalPath: string }) {
const candidates: string[] = [];
if (sidecarPath) {
candidates.push(sidecarPath);
const { sidecarFile } = getAssetFiles(files);
if (sidecarFile?.path) {
candidates.push(sidecarFile.path);
}
const assetPath = parse(originalPath);
@@ -457,14 +466,12 @@ export class MetadataService extends BaseService {
return { width, height };
}
private async getExifTags(asset: {
originalPath: string;
sidecarPath: string | null;
type: AssetType;
}): Promise<ImmichTags> {
private async getExifTags(asset: { originalPath: string; files: AssetFile[]; type: AssetType }): Promise<ImmichTags> {
const { sidecarFile } = getAssetFiles(asset.files);
const [mediaTags, sidecarTags, videoTags] = await Promise.all([
this.metadataRepository.readTags(asset.originalPath),
asset.sidecarPath ? this.metadataRepository.readTags(asset.sidecarPath) : null,
sidecarFile ? this.metadataRepository.readTags(sidecarFile.path) : null,
asset.type === AssetType.Video ? this.getVideoTags(asset.originalPath) : null,
]);
@@ -480,7 +487,8 @@ export class MetadataService extends BaseService {
}
// prefer duration from video tags
if (videoTags) {
// don't save duration if asset is definitely not an animated image (see e.g. CR3 with Duration: 1s)
if (videoTags || !mimeTypes.isPossiblyAnimatedImage(asset.originalPath)) {
delete mediaTags.Duration;
}