feat(server): add isVideo/isImage storage template vars, UI support, and unit tests

pull/28511/head
tn801534 2026-05-20 20:37:31 +08:00
parent 815ff677fc
commit 78ed4c0b65
4 changed files with 59 additions and 4 deletions

View File

@ -86,6 +86,7 @@ describe(StorageTemplateService.name, () => {
'{{y}}/{{y}}-{{WW}}/{{assetId}}',
'{{album}}/{{filename}}',
'{{make}}/{{model}}/{{lensModel}}/{{filename}}',
'{{#if isVideo}}videos{{/if}}/{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
],
secondOptions: ['s', 'ss', 'SSS'],
weekOptions: ['W', 'WW'],
@ -260,6 +261,53 @@ describe(StorageTemplateService.name, () => {
});
});
it('should use handlebar if condition for isVideo (video asset)', async () => {
const user = UserFactory.create();
const asset = AssetFactory.from({ type: AssetType.Video }).owner(user).exif().build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{#if isVideo}}videos{{/if}}/{{y}}/{{filename}}';
sut.onConfigInit({ newConfig: config });
mocks.user.get.mockResolvedValue(user);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath: expect.stringContaining(
`/data/library/${user.id}/videos/${asset.fileCreatedAt.getFullYear()}/${asset.originalFileName}`,
),
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
});
});
it('should use handlebar if condition for isVideo (image asset)', async () => {
const user = UserFactory.create();
const asset = AssetFactory.from({ type: AssetType.Image }).owner(user).exif().build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{#if isVideo}}videos{{/if}}/{{y}}/{{filename}}';
sut.onConfigInit({ newConfig: config });
mocks.user.get.mockResolvedValue(user);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: asset.id,
newPath: expect.not.stringContaining('videos/') &&
expect.stringContaining(
`/data/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${asset.originalFileName}`,
),
oldPath: asset.originalPath,
pathType: AssetPathType.Original,
});
});
it('should handle album startDate', async () => {
const user = UserFactory.create();
const asset = AssetFactory.from().owner(user).exif().build();

View File

@ -54,6 +54,7 @@ const storagePresets = [
'{{y}}/{{y}}-{{WW}}/{{assetId}}',
'{{album}}/{{filename}}',
'{{make}}/{{model}}/{{lensModel}}/{{filename}}',
'{{#if isVideo}}videos{{/if}}/{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
];
export interface MoveAssetMetadata {
@ -404,6 +405,8 @@ export class StorageTemplateService extends BaseService {
ext: extension,
filetype: asset.type == AssetType.Image ? 'IMG' : 'VID',
filetypefull: asset.type == AssetType.Image ? 'IMAGE' : 'VIDEO',
isVideo: asset.type == AssetType.Video ? 'true' : '',
isImage: asset.type == AssetType.Image ? 'true' : '',
assetId: asset.id,
assetIdShort: asset.id.slice(-12),
//just throw into the root if it doesn't belong to an album

View File

@ -53,10 +53,12 @@
});
const substitutions: Record<string, string> = {
filename: 'IMAGE_56437',
ext: 'jpg',
filetype: 'IMG',
filetypefull: 'IMAGE',
filename: 'VIDEO_12345',
ext: 'mp4',
filetype: 'VID',
filetypefull: 'VIDEO',
isVideo: 'true',
isImage: '',
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
assetIdShort: '56717ccba856',
album: $t('album_name'),

View File

@ -21,6 +21,8 @@
<ul>
<li>{`{{filetype}}`} - VID or IMG</li>
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
<li>{`{{isVideo}}`} — 'true' if video, otherwise empty</li>
<li>{`{{isImage}}`} — 'true' if image, otherwise empty</li>
</ul>
</div>