124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
/**
|
|
* Typed environment configuration with validation.
|
|
* Fails fast at startup if required variables are missing.
|
|
*/
|
|
|
|
function requireEnv(name: string): string {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
throw new Error(`Missing required environment variable: ${name}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/** A subpage groups multiple Immich albums under one URL path. */
|
|
export interface SubpageConfig {
|
|
name: string;
|
|
slug: string;
|
|
albumIds: string[];
|
|
}
|
|
|
|
/**
|
|
* Extract a UUID from a string that may be "Label:uuid" or just "uuid".
|
|
*/
|
|
function extractUuid(entry: string): string | null {
|
|
const trimmed = entry.trim();
|
|
if (!trimmed) return null;
|
|
const uuidMatch = trimmed.match(
|
|
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
);
|
|
return uuidMatch ? uuidMatch[1] : trimmed;
|
|
}
|
|
|
|
/**
|
|
* Parse LIGHTBOX_SUBPAGES env var.
|
|
* Format: "Japan > uuid1, uuid2; Italy > uuid3, uuid4"
|
|
* Each subpage separated by ";", name separated from album IDs by ">".
|
|
*/
|
|
function parseSubpages(raw: string | undefined): SubpageConfig[] {
|
|
if (!raw || !raw.trim()) return [];
|
|
|
|
// Split on semicolons OR newlines — supports both single-line and multiline formats
|
|
return raw
|
|
.split(/[;\n]/)
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean)
|
|
.map((segment) => {
|
|
const parts = segment.split('>');
|
|
if (parts.length < 2) {
|
|
console.warn(`[Config] Skipping malformed subpage entry: "${segment}"`);
|
|
return null;
|
|
}
|
|
const name = parts[0].trim();
|
|
const albumIds = parts
|
|
.slice(1)
|
|
.join('>') // rejoin in case name contained ">"
|
|
.split(',')
|
|
.map((id) => extractUuid(id))
|
|
.filter((id): id is string => id !== null);
|
|
|
|
if (!name || albumIds.length === 0) {
|
|
console.warn(`[Config] Skipping subpage with missing name or albums: "${segment}"`);
|
|
return null;
|
|
}
|
|
|
|
return { name, slug: slugify(name), albumIds };
|
|
})
|
|
.filter((sp): sp is SubpageConfig => sp !== null);
|
|
}
|
|
|
|
export function getConfig() {
|
|
const apiUrl = requireEnv('IMMICH_API_URL').replace(/\/+$/, '');
|
|
const apiKey = requireEnv('IMMICH_API_KEY');
|
|
|
|
// Parse standalone albums from LIGHTBOX_ALBUMS
|
|
const standaloneAlbumIds = requireEnv('LIGHTBOX_ALBUMS')
|
|
.split(',')
|
|
.map((entry) => extractUuid(entry))
|
|
.filter((id): id is string => id !== null);
|
|
|
|
if (standaloneAlbumIds.length === 0) {
|
|
throw new Error('LIGHTBOX_ALBUMS must contain at least one album ID');
|
|
}
|
|
|
|
// Parse subpages (optional)
|
|
const subpages = parseSubpages(process.env.LIGHTBOX_SUBPAGES);
|
|
|
|
// Collect ALL album IDs (standalone + subpage) — deduplicated
|
|
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
|
|
const allAlbumIds = [...new Set([...standaloneAlbumIds, ...subpageAlbumIds])];
|
|
|
|
return {
|
|
immich: {
|
|
apiUrl: `${apiUrl}/api`,
|
|
apiKey,
|
|
},
|
|
/** All album IDs (standalone + subpage) for allowlist validation */
|
|
albums: allAlbumIds,
|
|
/** Album IDs that appear only in LIGHTBOX_ALBUMS and NOT in any subpage */
|
|
standaloneAlbums: standaloneAlbumIds.filter((id) => !subpageAlbumIds.has(id)),
|
|
/** Subpage definitions */
|
|
subpages,
|
|
siteTitle: process.env.SITE_TITLE || 'Gallery',
|
|
siteSubtitle: process.env.SITE_SUBTITLE || '',
|
|
/** Immich asset ID for the homepage hero image */
|
|
heroImage: process.env.HERO_IMAGE || '',
|
|
cacheTtl: parseInt(process.env.CACHE_TTL || '300', 10) * 1000, // convert to ms
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert an album name into a URL-friendly slug.
|
|
* "Kloster Chorin" → "kloster-chorin"
|
|
*/
|
|
export function slugify(name: string): string {
|
|
return name
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '') // strip diacritics (ü→u, é→e)
|
|
.replace(/[^a-z0-9]+/g, '-') // non-alphanumeric → hyphens
|
|
.replace(/^-+|-+$/g, ''); // trim leading/trailing hyphens
|
|
}
|
|
|
|
export type AppConfig = ReturnType<typeof getConfig>;
|