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>
pull/24934/head^2
Rahul Kumar Saini 2025-12-29 16:55:06 +07:00 committed by GitHub
parent e87bfa548a
commit a16c5955d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 4 deletions

@ -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), '[]')

@ -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);

@ -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'],

@ -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<any>, options: RenderMetadata) {
const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options;
const { filename, extension, asset, albumName, albumStartDate, albumEndDate, make, model, lensModel } = options;
const substitutions: Record<string, string> = {
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;

@ -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 };

@ -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({

@ -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());

@ -24,10 +24,8 @@
</ul>
</div>
<div>
<p class="uppercase font-medium text-primary">{$t('other')}</p>
<p class="uppercase font-medium text-primary">{$t('album')}</p>
<ul>
<li>{`{{assetId}}`} - Asset ID</li>
<li>{`{{assetIdShort}}`} - Asset ID (last 12 characters)</li>
<li>{`{{album}}`} - Album Name</li>
<li>
{`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy).
@ -39,5 +37,20 @@
</li>
</ul>
</div>
<div>
<p class="uppercase font-medium text-primary">{$t('camera')}</p>
<ul>
<li>{`{{make}}`} - FUJIFILM</li>
<li>{`{{model}}`} - X-T50</li>
<li>{`{{lensModel}}`} - XF27mm F2.8 R WR</li>
</ul>
</div>
<div>
<p class="uppercase font-medium text-primary">{$t('other')}</p>
<ul>
<li>{`{{assetId}}`} - Asset ID</li>
<li>{`{{assetIdShort}}`} - Asset ID (last 12 characters)</li>
</ul>
</div>
</div>
</div>