From a16c5955d725a20e72e30e560ec2e12be79ff1b6 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Saini Date: Mon, 29 Dec 2025 16:55:06 -0500 Subject: [PATCH] feat(server): Support camera `make`, `model`, and `lensModel` in Storage Template (#24650) * add support for make, model, lensModel in storage template * no pkg lock * Apply suggestion from @danieldietzler Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> * query and formatting --------- Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- server/src/queries/asset.job.repository.sql | 6 ++++++ .../src/repositories/asset-job.repository.ts | 3 +++ .../services/storage-template.service.spec.ts | 1 + .../src/services/storage-template.service.ts | 15 ++++++++++++++- server/src/types.ts | 3 +++ server/test/fixtures/asset.stub.ts | 3 +++ .../StorageTemplateSettings.svelte | 3 +++ .../SupportedVariablesPanel.svelte | 19 ++++++++++++++++--- 8 files changed, 49 insertions(+), 4 deletions(-) diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index b736998e28..ae2b5110c2 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -493,6 +493,9 @@ select "asset"."fileCreatedAt", "asset_exif"."timeZone", "asset_exif"."fileSizeInByte", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", ( select coalesce(json_agg(agg), '[]') @@ -529,6 +532,9 @@ select "asset"."fileCreatedAt", "asset_exif"."timeZone", "asset_exif"."fileSizeInByte", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", ( select coalesce(json_agg(agg), '[]') diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 214d42747f..8beb053aac 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -324,6 +324,9 @@ export class AssetJobRepository { 'asset.fileCreatedAt', 'asset_exif.timeZone', 'asset_exif.fileSizeInByte', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.lensModel', ]) .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .where('asset.deletedAt', 'is', null); diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index d0d7ea3a3c..c1898f8f12 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -84,6 +84,7 @@ describe(StorageTemplateService.name, () => { '{{y}}/{{y}}-{{MM}}/{{assetId}}', '{{y}}/{{y}}-{{WW}}/{{assetId}}', '{{album}}/{{filename}}', + '{{make}}/{{model}}/{{lensModel}}/{{filename}}', ], secondOptions: ['s', 'ss', 'SSS'], weekOptions: ['W', 'WW'], diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 864207bf05..bbf5a8a6bf 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -53,6 +53,7 @@ const storagePresets = [ '{{y}}/{{y}}-{{MM}}/{{assetId}}', '{{y}}/{{y}}-{{WW}}/{{assetId}}', '{{album}}/{{filename}}', + '{{make}}/{{model}}/{{lensModel}}/{{filename}}', ]; export interface MoveAssetMetadata { @@ -67,6 +68,9 @@ interface RenderMetadata { albumName: string | null; albumStartDate: Date | null; albumEndDate: Date | null; + make: string | null; + model: string | null; + lensModel: string | null; } @Injectable() @@ -115,6 +119,9 @@ export class StorageTemplateService extends BaseService { albumName: 'album', albumStartDate: new Date(), albumEndDate: new Date(), + make: 'FUJIFILM', + model: 'X-T50', + lensModel: 'XF27mm F2.8 R WR', }); } catch (error) { this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); @@ -301,6 +308,9 @@ export class StorageTemplateService extends BaseService { albumName, albumStartDate, albumEndDate, + make: asset.make, + model: asset.model, + lensModel: asset.lensModel, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); let destination = `${fullPath}.${extension}`; @@ -365,7 +375,7 @@ export class StorageTemplateService extends BaseService { } private render(template: HandlebarsTemplateDelegate, options: RenderMetadata) { - const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options; + const { filename, extension, asset, albumName, albumStartDate, albumEndDate, make, model, lensModel } = options; const substitutions: Record = { filename, ext: extension, @@ -375,6 +385,9 @@ export class StorageTemplateService extends BaseService { assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', + make: make ?? '', + model: model ?? '', + lensModel: lensModel ?? '', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/server/src/types.ts b/server/src/types.ts index e404332fac..779de1ee37 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -472,6 +472,9 @@ export type StorageAsset = { originalFileName: string; fileSizeInByte: number | null; files: AssetFile[]; + make: string | null; + model: string | null; + lensModel: string | null; }; export type OnThisDayData = { year: number }; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index f5935d5d0e..6e4193c110 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -65,6 +65,9 @@ export const assetStub = { originalFileName: 'IMG_123.jpg', fileSizeInByte: 12_345, files: [], + make: 'FUJIFILM', + model: 'X-T50', + lensModel: 'XF27mm F2.8 R WR', ...asset, }), noResizePath: Object.freeze({ diff --git a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte index e119e8d8b0..e8514358ee 100644 --- a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte +++ b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte @@ -60,6 +60,9 @@ assetId: 'a8312960-e277-447d-b4ea-56717ccba856', assetIdShort: '56717ccba856', album: $t('album_name'), + make: 'FUJIFILM', + model: 'X-T50', + lensModel: 'XF27mm F2.8 R WR', }; const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString()); diff --git a/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte index 74a05b553f..4274ac70bc 100644 --- a/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte +++ b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte @@ -24,10 +24,8 @@
-

{$t('other')}

+

{$t('album')}

    -
  • {`{{assetId}}`} - Asset ID
  • -
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • {`{{album}}`} - Album Name
  • {`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy). @@ -39,5 +37,20 @@
+
+

{$t('camera')}

+
    +
  • {`{{make}}`} - FUJIFILM
  • +
  • {`{{model}}`} - X-T50
  • +
  • {`{{lensModel}}`} - XF27mm F2.8 R WR
  • +
+
+
+

{$t('other')}

+
    +
  • {`{{assetId}}`} - Asset ID
  • +
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • +
+