chore(server): sidecars in asset_files (#21199)

* fix: sidecar check job

* feat: move sidecars to asset_files

* feat: combine with handleSidecarCheck

* fix(server): improved method signatures for stack and sidecar copying

* fix(server): improved method signatures for stack and sidecar copying

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Jonathan Jogenfors
2025-12-02 19:31:43 +01:00
committed by GitHub
parent 62628dfcfa
commit 1bcf28c062
26 changed files with 425 additions and 154 deletions
+23 -16
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,6 +30,7 @@ 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 { isFaceImportEnabled } from 'src/utils/misc';
import { upsertTags } from 'src/utils/tag';
@@ -359,17 +361,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;
}
@@ -394,7 +400,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,
@@ -414,18 +422,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);
@@ -456,14 +465,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,
]);