import type { ProviderDescriptor } from './types'; /** * Today's date in ISO (YYYY-MM-DD) — used as the default for * `periodic_start_date` so new configs anchor to "today" rather than a * hardcoded date that gets further into the past on every release. */ const todayIso = (): string => new Date().toISOString().slice(0, 10); 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 = { 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: 10, 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, hint: 'hints.intervalDays' }, { key: 'periodic_start_date', label: 'trackingConfig.startDate', type: 'date', defaultValue: todayIso, hint: 'hints.periodicStartDate' }, { key: 'periodic_times', label: 'trackingConfig.times', type: 'time-list', defaultValue: '12:00', hint: 'hints.times', inlineHelp: 'trackingConfig.timesInlineHelp', validateFormat: true }, ], }, { key: 'scheduled', legend: 'trackingConfig.scheduledAssets', legendHint: 'hints.scheduledAssets', enabledField: 'scheduled_enabled', enabledDefault: false, fields: [ { key: 'scheduled_times', label: 'trackingConfig.times', type: 'time-list', defaultValue: '09:00', hint: 'hints.times', inlineHelp: 'trackingConfig.timesInlineHelp', validateFormat: true }, { key: 'scheduled_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'per_collection', hint: 'hints.scheduledAlbumMode' }, { 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' }, { key: 'scheduled_favorite_only', label: 'trackingConfig.favoritesOnly', type: 'toggle', defaultValue: false, hint: 'hints.favoritesOnly' }, ], }, { key: 'memory', legend: 'trackingConfig.memoryMode', legendHint: 'hints.memoryMode', enabledField: 'memory_enabled', enabledDefault: false, fields: [ { key: 'memory_times', label: 'trackingConfig.times', type: 'time-list', defaultValue: '09:00', hint: 'hints.times', inlineHelp: 'trackingConfig.timesInlineHelp', validateFormat: true }, { key: 'memory_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'combined', hint: 'hints.memoryAlbumMode' }, { key: 'memory_limit', label: 'trackingConfig.maxAssets', type: 'number', min: 1, max: 100, defaultValue: 10, hint: 'hints.maxAssets' }, { 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, hint: 'hints.minRating' }, { key: 'memory_favorite_only', label: 'trackingConfig.favoritesOnly', type: 'toggle', defaultValue: false, hint: 'hints.favoritesOnly' }, { key: 'memory_source', label: 'trackingConfig.memorySource', type: 'grid-select', gridItems: 'memorySourceItems', gridColumns: 2, defaultValue: 'albums', hint: 'hints.memorySource' }, ], }, { key: 'quietHours', legend: 'trackingConfig.quietHours', legendHint: 'hints.quietHours', enabledField: 'quiet_hours_enabled', enabledDefault: false, fields: [ { key: 'quiet_hours_start', label: 'trackingConfig.quietHoursStart', type: 'time', defaultValue: '22:00' }, { key: 'quiet_hours_end', label: 'trackingConfig.quietHoursEnd', type: 'time', defaultValue: '07:00' }, ], }, ], 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 }[] = []; // Run shared-link checks in parallel with a concurrency cap so a large // album set doesn't stall the save button for seconds. Cap of 6 keeps // the save dialog responsive for users with 50+ albums while staying // well under typical Immich per-IP rate limits. const CONCURRENCY = 6; async function checkOne(albumId: string): Promise { try { const links = await apiFn(`/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 */ } } const queue = [...newIds]; const workers: Promise[] = []; for (let i = 0; i < Math.min(CONCURRENCY, queue.length); i++) { workers.push((async () => { while (queue.length > 0) { const next = queue.shift(); if (next === undefined) return; await checkOne(next); } })()); } await Promise.all(workers); if (warnings.length > 0) return { warnings, proceed: false }; return { proceed: true }; }, };