mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: add album start and end dates for storage template (#17188)
This commit is contained in:
parent
c70140e707
commit
fe8c5e8107
@ -69,6 +69,7 @@ describe(StorageTemplateService.name, () => {
|
||||
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||
'{{y}}/{{MM}}/{{filename}}',
|
||||
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
|
||||
'{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}',
|
||||
'{{y}}/{{MMM}}/{{filename}}',
|
||||
'{{y}}/{{MMMM}}/{{filename}}',
|
||||
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||
@ -182,6 +183,63 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle album startDate', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const album = albumStub.oneAsset;
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template =
|
||||
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
|
||||
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset);
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValueOnce([
|
||||
{
|
||||
startDate: asset.fileCreatedAt,
|
||||
endDate: asset.fileCreatedAt,
|
||||
albumId: album.id,
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`,
|
||||
oldPath: asset.originalPath,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle else condition from album startDate', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template =
|
||||
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
|
||||
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`,
|
||||
oldPath: asset.originalPath,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should migrate previously failed move from original path when it still exists', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
|
||||
|
@ -28,6 +28,7 @@ const storagePresets = [
|
||||
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||
'{{y}}/{{MM}}/{{filename}}',
|
||||
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
|
||||
'{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}',
|
||||
'{{y}}/{{MMM}}/{{filename}}',
|
||||
'{{y}}/{{MMMM}}/{{filename}}',
|
||||
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||
@ -54,6 +55,8 @@ interface RenderMetadata {
|
||||
filename: string;
|
||||
extension: string;
|
||||
albumName: string | null;
|
||||
albumStartDate: Date | null;
|
||||
albumEndDate: Date | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -62,6 +65,7 @@ export class StorageTemplateService extends BaseService {
|
||||
compiled: HandlebarsTemplateDelegate<any>;
|
||||
raw: string;
|
||||
needsAlbum: boolean;
|
||||
needsAlbumMetadata: boolean;
|
||||
} | null = null;
|
||||
|
||||
private get template() {
|
||||
@ -99,6 +103,8 @@ export class StorageTemplateService extends BaseService {
|
||||
filename: 'IMG_123',
|
||||
extension: 'jpg',
|
||||
albumName: 'album',
|
||||
albumStartDate: new Date(),
|
||||
albumEndDate: new Date(),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`);
|
||||
@ -255,9 +261,20 @@ export class StorageTemplateService extends BaseService {
|
||||
}
|
||||
|
||||
let albumName = null;
|
||||
let albumStartDate = null;
|
||||
let albumEndDate = null;
|
||||
if (this.template.needsAlbum) {
|
||||
const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id);
|
||||
albumName = albums?.[0]?.albumName || null;
|
||||
const album = albums?.[0];
|
||||
if (album) {
|
||||
albumName = album.albumName || null;
|
||||
|
||||
if (this.template.needsAlbumMetadata) {
|
||||
const [metadata] = await this.albumRepository.getMetadataForIds([album.id]);
|
||||
albumStartDate = metadata?.startDate || null;
|
||||
albumEndDate = metadata?.endDate || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const storagePath = this.render(this.template.compiled, {
|
||||
@ -265,6 +282,8 @@ export class StorageTemplateService extends BaseService {
|
||||
filename: sanitized,
|
||||
extension,
|
||||
albumName,
|
||||
albumStartDate,
|
||||
albumEndDate,
|
||||
});
|
||||
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||
let destination = `${fullPath}.${extension}`;
|
||||
@ -323,12 +342,13 @@ export class StorageTemplateService extends BaseService {
|
||||
return {
|
||||
raw: template,
|
||||
compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }),
|
||||
needsAlbum: template.includes('{{album}}'),
|
||||
needsAlbum: template.includes('album'),
|
||||
needsAlbumMetadata: template.includes('album-startDate') || template.includes('album-endDate'),
|
||||
};
|
||||
}
|
||||
|
||||
private render(template: HandlebarsTemplateDelegate<any>, options: RenderMetadata) {
|
||||
const { filename, extension, asset, albumName } = options;
|
||||
const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options;
|
||||
const substitutions: Record<string, string> = {
|
||||
filename,
|
||||
ext: extension,
|
||||
@ -346,6 +366,15 @@ export class StorageTemplateService extends BaseService {
|
||||
|
||||
for (const token of Object.values(storageTokens).flat()) {
|
||||
substitutions[token] = dt.toFormat(token);
|
||||
if (albumName) {
|
||||
// Use system time zone for album dates to ensure all assets get the exact same date.
|
||||
substitutions['album-startDate-' + token] = albumStartDate
|
||||
? DateTime.fromJSDate(albumStartDate, { zone: systemTimeZone }).toFormat(token)
|
||||
: '';
|
||||
substitutions['album-endDate-' + token] = albumEndDate
|
||||
? DateTime.fromJSDate(albumEndDate, { zone: systemTimeZone }).toFormat(token)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
return template(substitutions).replaceAll(/\/{2,}/gm, '/');
|
||||
|
@ -78,6 +78,8 @@
|
||||
};
|
||||
|
||||
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
|
||||
const albumStartTime = luxon.DateTime.fromISO(new Date('2021-12-31T05:32:41.750').toISOString());
|
||||
const albumEndTime = luxon.DateTime.fromISO(new Date('2023-05-06T09:15:17.100').toISOString());
|
||||
|
||||
const dateTokens = [
|
||||
...templateOptions.yearOptions,
|
||||
@ -91,6 +93,8 @@
|
||||
|
||||
for (const token of dateTokens) {
|
||||
substitutions[token] = dt.toFormat(token);
|
||||
substitutions['album-startDate-' + token] = albumStartTime.toFormat(token);
|
||||
substitutions['album-endDate-' + token] = albumEndTime.toFormat(token);
|
||||
}
|
||||
|
||||
return template(substitutions);
|
||||
|
@ -29,6 +29,14 @@
|
||||
<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).
|
||||
{$t('admin.storage_template_date_time_sample', { values: { date: '2021-12-31T05:32:41.750' } })}
|
||||
</li>
|
||||
<li>
|
||||
{`{{album-endDate-x}}`} - Album End Date and Time (e.g. album-endDate-MM).
|
||||
{$t('admin.storage_template_date_time_sample', { values: { date: '2023-05-06T09:15:17.100' } })}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user