fix(commands): enrich search assets, surface variables for all command slots

- UI: command-template-configs now resolves slot variables against the
  active provider first (varsRef[provider_type][slot]) before falling back
  to shared entries, so provider-specific slots like /search, /status,
  /repos, /issues, /boards show the Variables button and autocomplete.
- Backend: /search, /find, /person, /place now normalize raw Immich API
  responses through build_asset_dict, extracting city/country from
  exifInfo and mapping isFavorite -> is_favorite so templates render
  location and favorite indicators.
- Telegram: extract build_telegram_asset_entry into a shared helper so
  the notification dispatcher and command media groups agree on video
  typing and /video/playback URLs; videos no longer render as still
  thumbnails in /latest /random /favorites media mode.
- Commands: send_media_group now reuses the same Telegram file_id caches
  as the notification dispatcher, avoiding re-upload churn for repeated
  commands.
This commit is contained in:
2026-04-22 16:28:26 +03:00
parent 85311684d9
commit fab6169cf9
6 changed files with 146 additions and 54 deletions
@@ -6,6 +6,7 @@ import asyncio
import logging
from typing import Any
from notify_bridge_core.notifications.telegram.media import build_telegram_asset_entry
from notify_bridge_core.providers.immich.asset_utils import get_public_url
from ..handler import _render_cmd_template
@@ -74,13 +75,16 @@ def build_asset_dict(
) -> dict[str, Any]:
"""Build a rich asset dict for command templates from an ImmichAssetInfo or raw dict."""
if isinstance(asset, dict):
# Immich raw search responses nest geo under exifInfo — pull it out so
# templates can use flat asset.city / asset.country.
exif = asset.get("exifInfo") or {}
d = {
"id": asset.get("id", ""),
"originalFileName": asset.get("originalFileName", asset.get("filename", "")),
"type": asset.get("type", "IMAGE"),
"createdAt": asset.get("createdAt", asset.get("created_at", asset.get("fileCreatedAt", ""))),
"city": asset.get("city", ""),
"country": asset.get("country", ""),
"city": asset.get("city") or exif.get("city") or "",
"country": asset.get("country") or exif.get("country") or "",
"is_favorite": asset.get("is_favorite", asset.get("isFavorite", False)),
"public_url": asset.get("public_url", public_url),
}
@@ -123,16 +127,27 @@ def _format_assets(
})
if response_mode == "media":
# Reuse the same entry-building helper as the notification dispatcher
# so videos keep their "video" type and point at /video/playback —
# typing them as "photo" made Telegram render the still poster
# thumbnail in media groups instead of the real clip.
media_items: list[dict[str, Any]] = []
for asset in assets:
asset_id = asset.get("id", "")
media_items.append({
"type": "photo",
"asset_id": asset_id,
"caption": "",
"thumbnail_url": f"{client.url}/api/assets/{asset_id}/thumbnail?size=preview",
"api_key": client.api_key,
})
is_video = (asset.get("type") or "").upper() == "VIDEO"
if is_video:
url = f"{client.url}/api/assets/{asset_id}/video/playback"
else:
url = f"{client.url}/api/assets/{asset_id}/thumbnail?size=preview"
entry = build_telegram_asset_entry(
url=url,
media_type="video" if is_video else "image",
api_key=client.api_key,
internal_url=client.url,
cache_key=asset_id,
)
if entry is not None:
media_items.append(entry)
# Return text message + media items — text is sent first, media as reply
return {"text": text, "media": media_items}
@@ -5,17 +5,14 @@ from __future__ import annotations
from typing import Any
from ..handler import _render_cmd_template
from .common import _format_assets
from .common import _format_assets, build_asset_dict
def _enrich_assets(assets: list[dict[str, Any]], asset_public_urls: dict[str, str]) -> list[dict[str, Any]]:
"""Add public_url to assets from the pre-built map. Returns new list without mutating inputs."""
if not asset_public_urls:
return assets
"""Normalize raw Immich assets and attach public_url from the pre-built map."""
pub = asset_public_urls or {}
return [
{**asset, "public_url": asset_public_urls.get(asset.get("id", ""), "")}
if asset.get("id", "") in asset_public_urls and not asset.get("public_url")
else asset
build_asset_dict(asset, public_url=pub.get(asset.get("id", ""), ""))
for asset in assets
]