From 4babaddd87c4a02066ba8e8ad355cdf8cc2739d5 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 19 Mar 2026 21:11:38 +0300 Subject: [PATCH] Replace video_warning with target_type + has_videos/has_photos Major template system improvements: - Remove video_warning field from TemplateConfig model - Add target_type, has_videos, has_photos to template context - Templates use {% if target_type == "telegram" and has_videos %} for conditional Telegram warnings instead of a separate field - date_format moved from "Telegram" to "Settings" group - Add target type selector (Telegram/Webhook) in template editor to preview how templates render for each target type - All template slots now use JinjaEditor (not plain ) - Preview endpoint accepts target_type parameter - Clean up TemplateConfigCreate schema (remove stale fields) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/i18n/en.json | 8 +++-- frontend/src/lib/i18n/ru.json | 8 +++-- .../src/routes/template-configs/+page.svelte | 36 +++++++++++++------ .../api/template_configs.py | 25 +++++-------- .../api/template_vars.py | 6 ++-- .../immich_watcher_server/database/models.py | 11 +++--- .../services/notifier.py | 13 ++++--- .../templates/en/assets_added.jinja2 | 4 +-- .../templates/ru/assets_added.jinja2 | 4 +-- 9 files changed, 66 insertions(+), 49 deletions(-) diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index cd8bfa1..c902196 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -263,8 +263,8 @@ "periodicAlbum": "Per-album item", "scheduledAssets": "Scheduled assets", "memoryMode": "Memory mode", - "telegramSettings": "Telegram", - "videoWarning": "Video warning", + "settings": "Settings", + "previewAs": "Preview as", "preview": "Preview", "variables": "Variables", "assetFields": "Asset fields (in {% for asset in added_assets %})", @@ -289,7 +289,9 @@ "added_assets": "List of asset dicts (use {% for asset in added_assets %})", "removed_assets": "List of removed asset IDs (strings)", "shared": "Whether album is shared (boolean)", - "video_warning": "Video size warning (from template config, only if videos present)", + "target_type": "Target type: 'telegram' or 'webhook'", + "has_videos": "Whether added assets contain videos (boolean)", + "has_photos": "Whether added assets contain photos (boolean)", "old_name": "Previous album name (rename events)", "new_name": "New album name (rename events)", "old_shared": "Was album shared before rename (boolean)", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index f116815..f1899a4 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -263,8 +263,8 @@ "periodicAlbum": "Элемент альбома", "scheduledAssets": "Запланированные фото", "memoryMode": "Воспоминания", - "telegramSettings": "Telegram", - "videoWarning": "Предупреждение о видео", + "settings": "Настройки", + "previewAs": "Предпросмотр как", "preview": "Предпросмотр", "variables": "Переменные", "assetFields": "Поля файла (в {% for asset in added_assets %})", @@ -289,7 +289,9 @@ "added_assets": "Список файлов ({% for asset in added_assets %})", "removed_assets": "Список ID удалённых файлов (строки)", "shared": "Общий альбом (boolean)", - "video_warning": "Предупреждение о видео (из конфига шаблона, если есть видео)", + "target_type": "Тип получателя: 'telegram' или 'webhook'", + "has_videos": "Содержат ли добавленные файлы видео (boolean)", + "has_photos": "Содержат ли добавленные файлы фото (boolean)", "old_name": "Прежнее название альбома (при переименовании)", "new_name": "Новое название альбома (при переименовании)", "old_shared": "Был ли общим до переименования (boolean)", diff --git a/frontend/src/routes/template-configs/+page.svelte b/frontend/src/routes/template-configs/+page.svelte index 4d37a3c..6866bf3 100644 --- a/frontend/src/routes/template-configs/+page.svelte +++ b/frontend/src/routes/template-configs/+page.svelte @@ -43,7 +43,7 @@ // Debounce 800ms validateTimers[slotKey] = setTimeout(async () => { try { - const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) }); + const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template, target_type: previewTargetType }) }); slotErrors = { ...slotErrors, [slotKey]: res.error || '' }; slotErrorLines = { ...slotErrorLines, [slotKey]: res.error_line || null }; slotErrorTypes = { ...slotErrorTypes, [slotKey]: res.error_type || '' }; @@ -73,9 +73,9 @@ scheduled_assets_message: '', memory_mode_message: '', date_format: '%d.%m.%Y, %H:%M UTC', - video_warning: '\n\n⚠️ Note: Videos may not be sent due to Telegram\'s 50 MB file size limit.', }); let form = $state(defaultForm()); + let previewTargetType = $state('telegram'); const templateSlots = [ { group: 'eventMessages', slots: [ @@ -89,9 +89,8 @@ { key: 'scheduled_assets_message', label: 'scheduledAssets', rows: 6 }, { key: 'memory_mode_message', label: 'memoryMode', rows: 6 }, ]}, - { group: 'telegramSettings', slots: [ + { group: 'settings', slots: [ { key: 'date_format', label: 'dateFormat', rows: 1 }, - { key: 'video_warning', label: 'videoWarning', rows: 2 }, ]}, ]; @@ -124,7 +123,7 @@ const template = (form as any)[slotKey] || ''; if (!template) { slotPreview = { ...slotPreview, [slotKey]: '(empty)' }; return; } try { - const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) }); + const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template, target_type: previewTargetType }) }); slotPreview = { ...slotPreview, [slotKey]: res.error ? `Error: ${res.error}` : res.rendered }; } catch (err: any) { slotPreview = { ...slotPreview, [slotKey]: `Error: ${err.message}` }; } } @@ -135,7 +134,7 @@ const template = config[slotKey] || ''; if (!template) return; try { - const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) }); + const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template, target_type: previewTargetType }) }); slotPreview[slotKey + '_' + configId] = res.error ? `Error: ${res.error}` : res.rendered; } catch (err: any) { slotPreview[slotKey + '_' + configId] = `Error: ${err.message}`; } } @@ -180,6 +179,21 @@ class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> + +
+ +
+ + +
+
+ {#each templateSlots as group}
{t(`templateConfig.${group.group}`)}{#if group.group === 'eventMessages'}{:else if group.group === 'assetFormatting'}{:else if group.group === 'dateLocation'}{:else if group.group === 'scheduledMessages'}{/if} @@ -197,8 +211,11 @@ {/if} - {#if (slot.rows || 2) > 2} - { (form as any)[slot.key] = v; validateSlot(slot.key, v); }} rows={slot.rows || 6} errorLine={slotErrorLines[slot.key] || null} /> + {#if slot.key === 'date_format'} + + {:else} + { (form as any)[slot.key] = v; validateSlot(slot.key, v); }} rows={slot.rows || 3} errorLine={slotErrorLines[slot.key] || null} /> {#if slotErrors[slot.key]} {#if slotErrorTypes[slot.key] === 'undefined'}

⚠ {t('common.undefinedVar')}: {slotErrors[slot.key]}

@@ -211,9 +228,6 @@
{slotPreview[slot.key]}
{/if} - {:else} - {/if} {/each} diff --git a/packages/server/src/immich_watcher_server/api/template_configs.py b/packages/server/src/immich_watcher_server/api/template_configs.py index 4bb66f9..fedeecb 100644 --- a/packages/server/src/immich_watcher_server/api/template_configs.py +++ b/packages/server/src/immich_watcher_server/api/template_configs.py @@ -67,7 +67,9 @@ _SAMPLE_CONTEXT = { "removed_assets": ["asset-id-1", "asset-id-2"], "people": ["Alice", "Bob"], "shared": True, - "video_warning": "⚠️ Note: Videos may not be sent due to Telegram's 50 MB file size limit.", + "target_type": "telegram", + "has_videos": True, + "has_photos": True, # Rename fields (always present, empty for non-rename events) "old_name": "Old Album", "new_name": "New Album", @@ -82,27 +84,16 @@ _SAMPLE_CONTEXT = { class TemplateConfigCreate(BaseModel): name: str + description: str | None = None + icon: str | None = None message_assets_added: str | None = None message_assets_removed: str | None = None message_album_renamed: str | None = None message_album_deleted: str | None = None - message_asset_image: str | None = None - message_asset_video: str | None = None - message_assets_format: str | None = None - message_assets_more: str | None = None - message_people_format: str | None = None - date_format: str | None = None - common_date_template: str | None = None - date_if_unique_template: str | None = None - location_format: str | None = None - common_location_template: str | None = None - location_if_unique_template: str | None = None - favorite_indicator: str | None = None periodic_summary_message: str | None = None - periodic_album_template: str | None = None scheduled_assets_message: str | None = None memory_mode_message: str | None = None - video_warning: str | None = None + date_format: str | None = None TemplateConfigUpdate = TemplateConfigCreate # Same shape, all optional @@ -203,6 +194,7 @@ async def preview_config( class PreviewRequest(BaseModel): template: str + target_type: str = "telegram" # "telegram" or "webhook" @router.post("/preview-raw") @@ -229,9 +221,10 @@ async def preview_raw( # Pass 2: render with strict undefined to catch unknown variables try: + ctx = {**_SAMPLE_CONTEXT, "target_type": body.target_type} strict_env = SandboxedEnvironment(autoescape=False, undefined=StrictUndefined) tmpl = strict_env.from_string(body.template) - rendered = tmpl.render(**_SAMPLE_CONTEXT) + rendered = tmpl.render(**ctx) return {"rendered": rendered} except UndefinedError as e: # Still a valid template syntactically, but references unknown variable diff --git a/packages/server/src/immich_watcher_server/api/template_vars.py b/packages/server/src/immich_watcher_server/api/template_vars.py index bcce7fa..c20c36a 100644 --- a/packages/server/src/immich_watcher_server/api/template_vars.py +++ b/packages/server/src/immich_watcher_server/api/template_vars.py @@ -47,7 +47,9 @@ TEMPLATE_VARIABLES: dict[str, dict] = { "removed_assets": "Always empty list", "people": "Detected people across all added assets (list of strings)", "shared": "Whether album is shared (boolean)", - "video_warning": "Video size warning text (set from template config if videos present)", + "target_type": "Target type: 'telegram' or 'webhook'", + "has_videos": "Whether added assets contain videos (boolean)", + "has_photos": "Whether added assets contain photos (boolean)", "old_name": "Always empty (for rename events)", "new_name": "Always empty (for rename events)", }, @@ -66,7 +68,7 @@ TEMPLATE_VARIABLES: dict[str, dict] = { "removed_assets": "List of removed asset IDs (strings)", "people": "People in the album (list of strings)", "shared": "Whether album is shared (boolean)", - "video_warning": "Always empty", + "target_type": "Target type: 'telegram' or 'webhook'", "old_name": "Always empty", "new_name": "Always empty", }, diff --git a/packages/server/src/immich_watcher_server/database/models.py b/packages/server/src/immich_watcher_server/database/models.py index 38ff0d9..4cc1298 100644 --- a/packages/server/src/immich_watcher_server/database/models.py +++ b/packages/server/src/immich_watcher_server/database/models.py @@ -135,9 +135,6 @@ class TemplateConfig(SQLModel, table=True): # Settings date_format: str = Field(default="%d.%m.%Y, %H:%M UTC") - video_warning: str = Field( - default="⚠️ Note: Videos may not be sent due to Telegram's 50 MB file size limit." - ) created_at: datetime = Field(default_factory=_utcnow) @@ -204,7 +201,9 @@ _INLINE_TEMPLATES_REMOVED = { '{%- if asset.is_favorite %} ❤️{% endif %}\n' '{%- endfor %}' '{%- endif %}' - '{%- if video_warning %}\n\n{{ video_warning }}{%- endif %}' + '{%- if target_type == "telegram" and has_videos %}\n\n' + '⚠️ Videos may not be sent due to Telegram\'s 50 MB file size limit.' + '{%- endif %}' ), "message_assets_removed": '🗑️ {{ removed_count }} photo(s) removed from album "{{ album_name }}".', @@ -254,7 +253,9 @@ DEFAULT_TEMPLATE_RU = { '{%- if asset.is_favorite %} ❤️{% endif %}\n' '{%- endfor %}' '{%- endif %}' - '{%- if video_warning %}\n\n{{ video_warning }}{%- endif %}' + '{%- if target_type == "telegram" and has_videos %}\n\n' + '⚠️ Видео может не отправиться из-за ограничения Telegram в 50 МБ.' + '{%- endif %}' ), "message_assets_removed": '🗑️ {{ removed_count }} фото удалено из альбома "{{ album_name }}".', diff --git a/packages/server/src/immich_watcher_server/services/notifier.py b/packages/server/src/immich_watcher_server/services/notifier.py index d2ca49c..5d64cd9 100644 --- a/packages/server/src/immich_watcher_server/services/notifier.py +++ b/packages/server/src/immich_watcher_server/services/notifier.py @@ -37,7 +37,7 @@ def _render(template_str: str, ctx: dict[str, Any]) -> str: def build_full_context( event_data: dict[str, Any], - template_config: TemplateConfig | None = None, + target_type: str = "webhook", ) -> dict[str, Any]: """Build template context by passing raw data directly to Jinja2. @@ -50,10 +50,13 @@ def build_full_context( if isinstance(ctx.get("people"), str): ctx["people"] = [ctx["people"]] if ctx["people"] else [] - # Video warning + # Asset type flags added_assets = ctx.get("added_assets", []) - has_videos = any(a.get("type") == "VIDEO" for a in added_assets) if added_assets else False - ctx["video_warning"] = (template_config.video_warning if template_config and has_videos else "") + ctx["has_videos"] = any(a.get("type") == "VIDEO" for a in added_assets) if added_assets else False + ctx["has_photos"] = any(a.get("type") == "IMAGE" for a in added_assets) if added_assets else False + + # Target type for conditional formatting (e.g. Telegram video size warning) + ctx["target_type"] = target_type return ctx @@ -75,7 +78,7 @@ async def send_notification( # Render with template engine if message is None: - ctx = build_full_context(event_data, template_config) + ctx = build_full_context(event_data, target_type=target.type) template_body = DEFAULT_TEMPLATE if template_config: diff --git a/packages/server/src/immich_watcher_server/templates/en/assets_added.jinja2 b/packages/server/src/immich_watcher_server/templates/en/assets_added.jinja2 index a3786a6..e80e6e0 100644 --- a/packages/server/src/immich_watcher_server/templates/en/assets_added.jinja2 +++ b/packages/server/src/immich_watcher_server/templates/en/assets_added.jinja2 @@ -9,7 +9,7 @@ {%- if asset.is_favorite %} ❤️{% endif %} {%- endfor %} {%- endif %} -{%- if video_warning %} +{%- if target_type == "telegram" and has_videos %} -{{ video_warning }} +⚠️ Videos may not be sent due to Telegram's 50 MB file size limit. {%- endif %} diff --git a/packages/server/src/immich_watcher_server/templates/ru/assets_added.jinja2 b/packages/server/src/immich_watcher_server/templates/ru/assets_added.jinja2 index d10f195..045140e 100644 --- a/packages/server/src/immich_watcher_server/templates/ru/assets_added.jinja2 +++ b/packages/server/src/immich_watcher_server/templates/ru/assets_added.jinja2 @@ -9,7 +9,7 @@ {%- if asset.is_favorite %} ❤️{% endif %} {%- endfor %} {%- endif %} -{%- if video_warning %} +{%- if target_type == "telegram" and has_videos %} -{{ video_warning }} +⚠️ Видео может не отправиться из-за ограничения Telegram в 50 МБ. {%- endif %}