feat: simplify gallery.yaml and add support for album name overrides

This commit is contained in:
ralksta
2026-03-05 16:52:01 +01:00
parent 9179a74f50
commit 2a16a9386f
4 changed files with 126 additions and 64 deletions

View File

@@ -9,23 +9,20 @@ hero:
# Standalone albums shown directly on the homepage # Standalone albums shown directly on the homepage
albums: albums:
- a89c6085-a105-46c2-baa4-338bc69cab93 - a89c6085-a105-46c2-baa4-338bc69cab93: "Wüstenzauber"
- 9aee9367-a239-4651-980d-3368bc70b7b6 - 9aee9367-a239-4651-980d-3368bc70b7b6: "Heimatliebe"
# Subpages group multiple albums under one URL path # Subpages group multiple albums under one URL path
subpages: subpages:
- name: Deutschland Deutschland:
albums: - 9aee9367-a239-4651-980d-3368bc70b7b6
- 9aee9367-a239-4651-980d-3368bc70b7b6 Dubai:
- a89c6085-a105-46c2-baa4-338bc69cab93: "Burj Khalifa & Co"
- name: Dubai # Support for subpages with passwords or custom grids in the map format
albums: Polen:
- a89c6085-a105-46c2-baa4-338bc69cab93
- name: Polen
password: clientpass123 password: clientpass123
albums: albums:
- 4ad0ed89-55de-4e69-937f-2d9988f63ad1 - 4ad0ed89-55de-4e69-937f-2d9988f63ad1: "Krakau & mehr"
# Footer — optional minimal footer with social links # Footer — optional minimal footer with social links
footer: footer:

View File

@@ -13,8 +13,9 @@ hero: 00000000-0000-0000-0000-000000000000
# - 22222222-2222-2222-2222-222222222222 # - 22222222-2222-2222-2222-222222222222
# Standalone albums — shown directly on the homepage as cards # Standalone albums — shown directly on the homepage as cards
# Use a string for the UUID, or a map for an inline name override.
albums: albums:
- 11111111-1111-1111-1111-111111111111 - 11111111-1111-1111-1111-111111111111: "Meine Favoriten" # inline override
- 22222222-2222-2222-2222-222222222222 - 22222222-2222-2222-2222-222222222222
# Show camera/lens EXIF on photo hover (default: true) # Show camera/lens EXIF on photo hover (default: true)
@@ -30,21 +31,17 @@ albums:
# aspectRatio: "1" # "1", "3/2", "auto", etc. default: "1" # aspectRatio: "1" # "1", "3/2", "auto", etc. default: "1"
# layout: masonry # "masonry" or "uniform", default: "masonry" # layout: masonry # "masonry" or "uniform", default: "masonry"
# Subpages — group albums into collections at /subpage-slug/album-slug # Subpages — group albums into collections at /subpage-slug/album-slug.
# You can use a simple map for concise definitions, or a full object for passwords/grids.
subpages: subpages:
- name: Japan Japan: # Simple map format
grid: # override global grid for this subpage - 33333333-3333-3333-3333-333333333333: "Tokyo Nights"
columns: 4 - 44444444-4444-4444-4444-444444444444
layout: uniform
aspectRatio: "3/2"
albums:
- 33333333-3333-3333-3333-333333333333 # Tokyo
- 44444444-4444-4444-4444-444444444444 # Kyoto
- name: Wedding – Smith Wedding: # Advanced object format (needed for passwords/grid overrides)
password: clientpass123 # visitors must enter this to view password: clientpass123
albums: albums:
- 55555555-5555-5555-5555-555555555555 - 55555555-5555-5555-5555-555555555555: "The Big Day"
# Footer — optional minimal footer with social links # Footer — optional minimal footer with social links
# footer: # footer:

View File

@@ -148,9 +148,11 @@ interface GalleryYaml {
email?: string; email?: string;
website?: string; website?: string;
}; };
subpages?: Array<{ subpages?:
| Record<string, string[] | Array<string | Record<string, string>>>
| Array<{
name: string; name: string;
albums: string[]; albums: Array<string | Record<string, string>>;
password?: string; password?: string;
grid?: { grid?: {
columns?: number; columns?: number;
@@ -239,6 +241,8 @@ export interface AppConfig {
footer: FooterConfig | null; footer: FooterConfig | null;
/** Show the /map page. Default: false. */ /** Show the /map page. Default: false. */
map: boolean; map: boolean;
/** Explicit album name overrides from gallery.yaml. */
albumOverrides: Record<string, string>;
cacheTtl: number; cacheTtl: number;
rateLimitRpm: number; rateLimitRpm: number;
} }
@@ -318,45 +322,101 @@ export function getConfig(): AppConfig {
// Resolve theme: string shorthand → preset, object → merged with preset base // Resolve theme: string shorthand → preset, object → merged with preset base
const theme = resolveTheme(gallery.theme); const theme = resolveTheme(gallery.theme);
const albumOverrides: Record<string, string> = {};
function processAlbumEntry(entry: string | Record<string, string>, context: string): string {
if (typeof entry === 'string') {
return validateUuid(entry, context);
}
// Map format: { "uuid": "Custom Name" }
const [uuid, name] = Object.entries(entry)[0];
const validatedUuid = validateUuid(uuid, context);
albumOverrides[validatedUuid] = name;
return validatedUuid;
}
// Parse standalone albums // Parse standalone albums
const standaloneAlbumIds = (gallery.albums ?? []).map((id) => const standaloneAlbumIds = (gallery.albums ?? []).map((entry) =>
validateUuid(id, 'gallery.yaml albums'), processAlbumEntry(entry, 'gallery.yaml albums'),
); );
if (standaloneAlbumIds.length === 0 && (!gallery.subpages || gallery.subpages.length === 0)) { if (standaloneAlbumIds.length === 0 && (!gallery.subpages || (Array.isArray(gallery.subpages) ? gallery.subpages.length === 0 : Object.keys(gallery.subpages).length === 0))) {
throw new Error('gallery.yaml must define at least one album or subpage'); throw new Error('gallery.yaml must define at least one album or subpage');
} }
// Parse subpages // Parse subpages
const subpages: SubpageConfig[] = (gallery.subpages ?? []).map((sp) => { let subpages: SubpageConfig[] = [];
if (!sp.name || !sp.albums || sp.albums.length === 0) {
throw new Error( if (Array.isArray(gallery.subpages)) {
`Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`, // Original list format
); subpages = gallery.subpages.map((sp) => {
} if (!sp.name || !sp.albums || sp.albums.length === 0) {
return { throw new Error(
name: sp.name, `Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`,
slug: slugify(sp.name), );
albumIds: sp.albums.map((id) => validateUuid(id, `subpage "${sp.name}"`)), }
password: sp.password, return {
...(sp.grid name: sp.name,
? { slug: slugify(sp.name),
grid: { albumIds: sp.albums.map((entry) => processAlbumEntry(entry, `subpage "${sp.name}"`)),
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}), password: sp.password,
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}), ...(sp.grid
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}), ? {
...(sp.grid.layout != null grid: {
? { ...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
layout: (VALID_LAYOUTS.includes(sp.grid.layout) ...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
? sp.grid.layout ...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
: 'masonry') as GridConfig['layout'], ...(sp.grid.layout != null
} ? {
: {}), layout: (VALID_LAYOUTS.includes(sp.grid.layout)
}, ? sp.grid.layout
} : 'masonry') as GridConfig['layout'],
: {}), }
}; : {}),
}); },
}
: {}),
};
});
} else if (gallery.subpages) {
// New map format: { "Subpage Name": ["uuid1", { "uuid2": "Name" }] } OR { "Subpage Name": { password: "...", albums: [...] } }
subpages = Object.entries(gallery.subpages).map(([name, value]) => {
// If it's an array, it's just albums
if (Array.isArray(value)) {
return {
name,
slug: slugify(name),
albumIds: value.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
};
}
// If it's an object, it might have password, grid, albums
const sp = value as any;
const albumEntries = sp.albums || [];
return {
name,
slug: slugify(name),
albumIds: albumEntries.map((entry: any) => processAlbumEntry(entry, `subpage "${name}"`)),
password: sp.password,
...(sp.grid
? {
grid: {
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
...(sp.grid.layout != null
? {
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
? sp.grid.layout
: 'masonry') as GridConfig['layout'],
}
: {}),
},
}
: {}),
};
});
}
// Collect ALL album IDs (standalone + subpage) — deduplicated // Collect ALL album IDs (standalone + subpage) — deduplicated
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds)); const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
@@ -396,6 +456,7 @@ export function getConfig(): AppConfig {
} }
: null, : null,
map: gallery.map === true, map: gallery.map === true,
albumOverrides,
cacheTtl: env.CACHE_TTL * 1000, cacheTtl: env.CACHE_TTL * 1000,
rateLimitRpm: env.RATE_LIMIT_RPM, rateLimitRpm: env.RATE_LIMIT_RPM,
}; };

View File

@@ -166,10 +166,14 @@ class ImmichClient {
const allowedIds = new Set(this.config.albums); const allowedIds = new Set(this.config.albums);
const filtered = all const filtered = all
.filter((album) => allowedIds.has(album.id)) .filter((album) => allowedIds.has(album.id))
.map((album) => ({ .map((album) => {
...album, const name = this.config.albumOverrides[album.id] ?? album.albumName;
slug: slugify(album.albumName), return {
})); ...album,
albumName: name,
slug: slugify(name),
};
});
// Log album summary on first load so admins can see what's published // Log album summary on first load so admins can see what's published
if (!this.hasLoggedAlbums) { if (!this.hasLoggedAlbums) {
@@ -277,7 +281,10 @@ class ImmichClient {
// Filter out trashed assets // Filter out trashed assets
album.assets = (album.assets || []).filter((a) => !a.isTrashed); album.assets = (album.assets || []).filter((a) => !a.isTrashed);
album.slug = slugify(album.albumName);
const name = this.config.albumOverrides[album.id] ?? album.albumName;
album.albumName = name;
album.slug = slugify(name);
cache.set(cacheKey, album, this.config.cacheTtl); cache.set(cacheKey, album, this.config.cacheTtl);
return album; return album;