Files
notify-bridge/frontend/src/lib/providers/immich.ts
T
alexei.dolgolyov 8cb836e16c refactor: provider descriptor registry — eliminate provider-specific hardcoding
Replace all if/else chains keyed on provider type strings with a
descriptor-driven architecture. Each provider type (immich, gitea,
planka, scheduler, nut, google_photos) has a descriptor in
frontend/src/lib/providers/ that declares config fields, event
tracking fields, collection metadata, validation, and hooks.

Components now use getDescriptor(type) and render dynamically.
Dashboard provider card shows provider name + type when global
filter is active. Grid-items derived from registry.
2026-03-24 12:40:33 +03:00

134 lines
6.5 KiB
TypeScript

import type { ProviderDescriptor } from './types';
export const immichDescriptor: ProviderDescriptor = {
type: 'immich',
defaultName: 'Immich',
icon: 'mdiImageMultiple',
hasUrl: true,
urlPlaceholder: undefined, // uses generic i18n placeholder
configFields: [
{
key: 'api_key', configKey: 'api_key',
label: 'providers.apiKey', editLabel: 'providers.apiKeyKeep',
type: 'password', required: 'create-only',
},
{
key: 'external_domain', configKey: 'external_domain',
label: 'providers.externalDomain',
type: 'text', optional: true, placeholder: 'https://photos.example.com',
},
],
buildConfig(form, editing) {
const config: Record<string, any> = { url: form.url };
if (form.api_key) config.api_key = form.api_key;
if (form.external_domain) config.external_domain = form.external_domain;
if (!editing) config.api_key = form.api_key;
return { config };
},
hasConfigChanged(form, existing) {
return form.url !== (existing.url || '') ||
!!form.api_key ||
form.external_domain !== (existing.external_domain || '');
},
eventFields: [
{ key: 'track_assets_added', label: 'trackingConfig.assetsAdded', default: true },
{ key: 'track_assets_removed', label: 'trackingConfig.assetsRemoved', default: false },
{ key: 'track_collection_renamed', label: 'trackingConfig.albumRenamed', default: true },
{ key: 'track_collection_deleted', label: 'trackingConfig.albumDeleted', default: true },
{ key: 'track_sharing_changed', label: 'trackingConfig.sharingChanged', default: false },
{ key: 'track_images', label: 'trackingConfig.trackImages', default: true },
{ key: 'track_videos', label: 'trackingConfig.trackVideos', default: true },
{ key: 'notify_favorites_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
{ key: 'include_tags', label: 'trackingConfig.includePeople', default: true },
{ key: 'include_asset_details', label: 'trackingConfig.includeDetails', default: false },
],
extraTrackingFields: [
{ key: 'max_assets_to_show', label: 'trackingConfig.maxAssets', type: 'number', min: 0, max: 50, defaultValue: 5, hint: 'hints.maxAssets' },
{ key: 'assets_order_by', label: 'trackingConfig.sortBy', type: 'grid-select', gridItems: 'sortByItems', gridColumns: 2, defaultValue: 'none' },
{ key: 'assets_order', label: 'trackingConfig.sortOrder', type: 'grid-select', gridItems: 'sortOrderItems', gridColumns: 2, defaultValue: 'descending' },
],
featureSections: [
{
key: 'periodic', legend: 'trackingConfig.periodicSummary', legendHint: 'hints.periodicSummary',
enabledField: 'periodic_enabled', enabledDefault: false,
fields: [
{ key: 'periodic_interval_days', label: 'trackingConfig.intervalDays', type: 'number', min: 1, defaultValue: 1 },
{ key: 'periodic_start_date', label: 'trackingConfig.startDate', type: 'number', defaultValue: '2025-01-01' }, // rendered as date input
{ key: 'periodic_times', label: 'trackingConfig.times', type: 'number', defaultValue: '12:00' }, // rendered as text input
],
},
{
key: 'scheduled', legend: 'trackingConfig.scheduledAssets', legendHint: 'hints.scheduledAssets',
enabledField: 'scheduled_enabled', enabledDefault: false,
fields: [
{ key: 'scheduled_times', label: 'trackingConfig.times', type: 'number', defaultValue: '09:00' },
{ key: 'scheduled_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'per_collection' },
{ key: 'scheduled_limit', label: 'trackingConfig.maxAssets', type: 'number', min: 1, max: 100, defaultValue: 10, hint: 'hints.maxAssets' },
{ key: 'scheduled_asset_type', label: 'trackingConfig.assetType', type: 'grid-select', gridItems: 'assetTypeItems', gridColumns: 3, defaultValue: 'all' },
{ key: 'scheduled_min_rating', label: 'trackingConfig.minRating', type: 'number', min: 0, max: 5, defaultValue: 0, hint: 'hints.minRating' },
],
checkboxes: [
{ key: 'scheduled_favorite_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
],
},
{
key: 'memory', legend: 'trackingConfig.memoryMode', legendHint: 'hints.memoryMode',
enabledField: 'memory_enabled', enabledDefault: false,
fields: [
{ key: 'memory_source', label: 'trackingConfig.memorySource', type: 'grid-select', gridItems: 'memorySourceItems', gridColumns: 2, defaultValue: 'albums' },
{ key: 'memory_times', label: 'trackingConfig.times', type: 'number', defaultValue: '09:00' },
{ key: 'memory_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'combined' },
{ key: 'memory_limit', label: 'trackingConfig.maxAssets', type: 'number', min: 1, max: 100, defaultValue: 10 },
{ key: 'memory_asset_type', label: 'trackingConfig.assetType', type: 'grid-select', gridItems: 'assetTypeItems', gridColumns: 3, defaultValue: 'all' },
{ key: 'memory_min_rating', label: 'trackingConfig.minRating', type: 'number', min: 0, max: 5, defaultValue: 0 },
],
checkboxes: [
{ key: 'memory_favorite_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
],
},
],
collectionMeta: {
label: 'notificationTracker.albums',
icon: 'mdiImageMultiple',
placeholder: 'notificationTracker.selectAlbums',
countLabel: 'notificationTracker.albums_count',
desc: (col) => `${col.assetCount ?? col.asset_count ?? 0} assets`,
},
async onBeforeSave({ form, previousCollectionIds, collections, api: apiFn }) {
const newIds = (form.collection_ids as string[]).filter(id => !previousCollectionIds.includes(id));
if (newIds.length === 0) return { proceed: true };
interface SharedLink { is_accessible: boolean; is_expired: boolean; has_password: boolean }
const warnings: { id: string; name: string; issue: string }[] = [];
for (const albumId of newIds) {
try {
const links = await apiFn<SharedLink[]>(`/providers/${form.provider_id}/albums/${albumId}/shared-links`);
const validLink = links.find((l) => l.is_accessible && !l.is_expired);
if (!validLink) {
const album = collections.find(c => c.id === albumId);
const problematic = links.find((l) => l.is_expired || l.has_password);
warnings.push({
id: albumId,
name: album?.albumName || album?.name || albumId,
issue: problematic
? (problematic.is_expired ? 'expired' : 'password-protected')
: 'missing',
});
}
} catch { /* shared-link check failed, proceed */ }
}
if (warnings.length > 0) return { warnings, proceed: false };
return { proceed: true };
},
};