Add document type and content_type support for send_telegram_notification
All checks were successful
Validate / Hassfest (push) Successful in 3s

- Add type: document as default media type (instead of photo)
- Add optional content_type field for explicit MIME type specification
- Documents are sent separately (Telegram API limitation for media groups)
- Default content types: image/jpeg (photo), video/mp4 (video), auto-detect (document)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 01:35:57 +03:00
parent fde2d0ae31
commit 6ca3cae5df
5 changed files with 157 additions and 39 deletions

View File

@@ -37,7 +37,7 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/)
- **Services** - Custom service calls: - **Services** - Custom service calls:
- `immich_album_watcher.refresh` - Force immediate data refresh - `immich_album_watcher.refresh` - Force immediate data refresh
- `immich_album_watcher.get_assets` - Get assets from an album with filtering and ordering - `immich_album_watcher.get_assets` - Get assets from an album with filtering and ordering
- `immich_album_watcher.send_telegram_notification` - Send text, photo, video, or media group to Telegram - `immich_album_watcher.send_telegram_notification` - Send text, photo, video, document, or media group to Telegram
- **Share Link Management** - Button entities to create and delete share links: - **Share Link Management** - Button entities to create and delete share links:
- Create/delete public (unprotected) share links - Create/delete public (unprotected) share links
- Create/delete password-protected share links - Create/delete password-protected share links
@@ -335,11 +335,12 @@ data:
Send notifications to Telegram. Supports multiple formats: Send notifications to Telegram. Supports multiple formats:
- **Text message** - When `urls` is empty or not provided - **Text message** - When `urls` is empty or not provided
- **Single photo** - When `urls` contains one photo - **Single document** - When `urls` contains one document (default type)
- **Single video** - When `urls` contains one video - **Single photo** - When `urls` contains one photo (`type: photo`)
- **Media group** - When `urls` contains multiple items - **Single video** - When `urls` contains one video (`type: video`)
- **Media group** - When `urls` contains multiple photos/videos (documents are sent separately)
The service downloads media from Immich and uploads it to Telegram, bypassing any CORS restrictions. Large lists of media are automatically split into multiple media groups based on the `max_group_size` parameter (default: 10 items per group). The service downloads media from Immich and uploads it to Telegram, bypassing any CORS restrictions. Large lists of photos and videos are automatically split into multiple media groups based on the `max_group_size` parameter (default: 10 items per group). Documents cannot be grouped and are sent individually.
**File ID Caching:** When media is uploaded to Telegram, the service caches the returned `file_id`. Subsequent sends of the same media will use the cached `file_id` instead of re-uploading, significantly improving performance. The cache TTL is configurable in hub options (default: 48 hours, range: 1-168 hours). The cache is persistent across Home Assistant restarts and is stored per album. **File ID Caching:** When media is uploaded to Telegram, the service caches the returned `file_id`. Subsequent sends of the same media will use the cached `file_id` instead of re-uploading, significantly improving performance. The cache TTL is configurable in hub options (default: 48 hours, range: 1-168 hours). The cache is persistent across Home Assistant restarts and is stored per album.
@@ -357,6 +358,20 @@ data:
disable_web_page_preview: true disable_web_page_preview: true
``` ```
Single document (default):
```yaml
service: immich_album_watcher.send_telegram_notification
target:
entity_id: sensor.album_name_asset_limit
data:
chat_id: "-1001234567890"
urls:
- url: "https://immich.example.com/api/assets/xxx/original?key=yyy"
content_type: "image/heic" # Optional: explicit MIME type
caption: "Original file"
```
Single photo: Single photo:
```yaml ```yaml
@@ -421,7 +436,7 @@ data:
| Field | Description | Required | | Field | Description | Required |
|-------|-------------|----------| |-------|-------------|----------|
| `chat_id` | Telegram chat ID to send to | Yes | | `chat_id` | Telegram chat ID to send to | Yes |
| `urls` | List of media items with `url` and `type` (photo/video). Empty for text message. | No | | `urls` | List of media items with `url`, optional `type` (document/photo/video, default: document), and optional `content_type` (MIME type, e.g., `image/jpeg`). Empty for text message. Photos and videos can be grouped; documents are sent separately. | No |
| `bot_token` | Telegram bot token (uses configured token if not provided) | No | | `bot_token` | Telegram bot token (uses configured token if not provided) | No |
| `caption` | For media: caption applied to first item. For text: the message text. Supports HTML formatting by default. | No | | `caption` | For media: caption applied to first item. For text: the message text. Supports HTML formatting by default. | No |
| `reply_to_message_id` | Message ID to reply to | No | | `reply_to_message_id` | Message ID to reply to | No |

View File

@@ -348,18 +348,39 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
try: try:
# Handle single photo # Handle single photo
if len(urls) == 1 and urls[0].get("type", "photo") == "photo": if len(urls) == 1 and urls[0].get("type") == "photo":
return await self._send_telegram_photo( return await self._send_telegram_photo(
session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id, parse_mode, session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id, parse_mode,
max_asset_data_size, send_large_photos_as_documents max_asset_data_size, send_large_photos_as_documents, urls[0].get("content_type")
) )
# Handle single video # Handle single video
if len(urls) == 1 and urls[0].get("type") == "video": if len(urls) == 1 and urls[0].get("type") == "video":
return await self._send_telegram_video( return await self._send_telegram_video(
session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id, parse_mode, max_asset_data_size session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id, parse_mode,
max_asset_data_size, urls[0].get("content_type")
) )
# Handle single document (default type)
if len(urls) == 1 and urls[0].get("type", "document") == "document":
url = urls[0].get("url")
item_content_type = urls[0].get("content_type")
try:
download_url = self.coordinator.get_internal_download_url(url)
async with session.get(download_url) as resp:
if resp.status != 200:
return {"success": False, "error": f"Failed to download media: HTTP {resp.status}"}
data = await resp.read()
if max_asset_data_size is not None and len(data) > max_asset_data_size:
return {"success": False, "error": f"Media size ({len(data)} bytes) exceeds max_asset_data_size limit ({max_asset_data_size} bytes)"}
# Detect filename from URL or use generic name
filename = url.split("/")[-1].split("?")[0] or "file"
return await self._send_telegram_document(
session, token, chat_id, data, filename, caption, reply_to_message_id, parse_mode, url, item_content_type
)
except aiohttp.ClientError as err:
return {"success": False, "error": f"Failed to download media: {err}"}
# Handle multiple items - send as media group(s) # Handle multiple items - send as media group(s)
return await self._send_telegram_media_group( return await self._send_telegram_media_group(
session, token, chat_id, urls, caption, reply_to_message_id, max_group_size, chunk_delay, parse_mode, session, token, chat_id, urls, caption, reply_to_message_id, max_group_size, chunk_delay, parse_mode,
@@ -599,11 +620,16 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
parse_mode: str = "HTML", parse_mode: str = "HTML",
max_asset_data_size: int | None = None, max_asset_data_size: int | None = None,
send_large_photos_as_documents: bool = False, send_large_photos_as_documents: bool = False,
content_type: str | None = None,
) -> ServiceResponse: ) -> ServiceResponse:
"""Send a single photo to Telegram.""" """Send a single photo to Telegram."""
import aiohttp import aiohttp
from aiohttp import FormData from aiohttp import FormData
# Use provided content type or default to image/jpeg
if not content_type:
content_type = "image/jpeg"
if not url: if not url:
return {"success": False, "error": "Missing 'url' for photo"} return {"success": False, "error": "Missing 'url' for photo"}
@@ -689,7 +715,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Build multipart form # Build multipart form
form = FormData() form = FormData()
form.add_field("chat_id", chat_id) form.add_field("chat_id", chat_id)
form.add_field("photo", data, filename="photo.jpg", content_type="image/jpeg") form.add_field("photo", data, filename="photo.jpg", content_type=content_type)
form.add_field("parse_mode", parse_mode) form.add_field("parse_mode", parse_mode)
if caption: if caption:
@@ -745,11 +771,16 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
reply_to_message_id: int | None = None, reply_to_message_id: int | None = None,
parse_mode: str = "HTML", parse_mode: str = "HTML",
max_asset_data_size: int | None = None, max_asset_data_size: int | None = None,
content_type: str | None = None,
) -> ServiceResponse: ) -> ServiceResponse:
"""Send a single video to Telegram.""" """Send a single video to Telegram."""
import aiohttp import aiohttp
from aiohttp import FormData from aiohttp import FormData
# Use provided content type or default to video/mp4
if not content_type:
content_type = "video/mp4"
if not url: if not url:
return {"success": False, "error": "Missing 'url' for video"} return {"success": False, "error": "Missing 'url' for video"}
@@ -816,7 +847,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Build multipart form # Build multipart form
form = FormData() form = FormData()
form.add_field("chat_id", chat_id) form.add_field("chat_id", chat_id)
form.add_field("video", data, filename="video.mp4", content_type="video/mp4") form.add_field("video", data, filename="video.mp4", content_type=content_type)
form.add_field("parse_mode", parse_mode) form.add_field("parse_mode", parse_mode)
if caption: if caption:
@@ -867,16 +898,24 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
token: str, token: str,
chat_id: str, chat_id: str,
data: bytes, data: bytes,
filename: str = "photo.jpg", filename: str = "file",
caption: str | None = None, caption: str | None = None,
reply_to_message_id: int | None = None, reply_to_message_id: int | None = None,
parse_mode: str = "HTML", parse_mode: str = "HTML",
source_url: str | None = None, source_url: str | None = None,
content_type: str | None = None,
) -> ServiceResponse: ) -> ServiceResponse:
"""Send a photo as a document to Telegram (for oversized photos).""" """Send a file as a document to Telegram."""
import aiohttp import aiohttp
import mimetypes
from aiohttp import FormData from aiohttp import FormData
# Use provided content type or detect from filename
if not content_type:
content_type, _ = mimetypes.guess_type(filename)
if not content_type:
content_type = "application/octet-stream"
# Check cache for file_id if source_url is provided # Check cache for file_id if source_url is provided
cache = self.coordinator.telegram_cache cache = self.coordinator.telegram_cache
if source_url: if source_url:
@@ -915,7 +954,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Build multipart form # Build multipart form
form = FormData() form = FormData()
form.add_field("chat_id", chat_id) form.add_field("chat_id", chat_id)
form.add_field("document", data, filename=filename, content_type="image/jpeg") form.add_field("document", data, filename=filename, content_type=content_type)
form.add_field("parse_mode", parse_mode) form.add_field("parse_mode", parse_mode)
if caption: if caption:
@@ -927,7 +966,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Send to Telegram # Send to Telegram
telegram_url = f"https://api.telegram.org/bot{token}/sendDocument" telegram_url = f"https://api.telegram.org/bot{token}/sendDocument"
_LOGGER.debug("Uploading oversized photo as document to Telegram (%d bytes)", len(data)) _LOGGER.debug("Uploading document to Telegram (%d bytes, %s)", len(data), content_type)
async with session.post(telegram_url, data=form) as response: async with session.post(telegram_url, data=form) as response:
result = await response.json() result = await response.json()
_LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok")) _LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok"))
@@ -1003,8 +1042,9 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Optimize: Use single-item APIs for chunks with 1 item # Optimize: Use single-item APIs for chunks with 1 item
if len(chunk) == 1: if len(chunk) == 1:
item = chunk[0] item = chunk[0]
media_type = item.get("type", "photo") media_type = item.get("type", "document")
url = item.get("url") url = item.get("url")
item_content_type = item.get("content_type")
# Only apply caption and reply_to to the first chunk # Only apply caption and reply_to to the first chunk
chunk_caption = caption if chunk_idx == 0 else None chunk_caption = caption if chunk_idx == 0 else None
@@ -1014,13 +1054,31 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.debug("Sending chunk %d/%d as single photo", chunk_idx + 1, len(chunks)) _LOGGER.debug("Sending chunk %d/%d as single photo", chunk_idx + 1, len(chunks))
result = await self._send_telegram_photo( result = await self._send_telegram_photo(
session, token, chat_id, url, chunk_caption, chunk_reply_to, parse_mode, session, token, chat_id, url, chunk_caption, chunk_reply_to, parse_mode,
max_asset_data_size, send_large_photos_as_documents max_asset_data_size, send_large_photos_as_documents, item_content_type
) )
else: # video elif media_type == "video":
_LOGGER.debug("Sending chunk %d/%d as single video", chunk_idx + 1, len(chunks)) _LOGGER.debug("Sending chunk %d/%d as single video", chunk_idx + 1, len(chunks))
result = await self._send_telegram_video( result = await self._send_telegram_video(
session, token, chat_id, url, chunk_caption, chunk_reply_to, parse_mode, max_asset_data_size session, token, chat_id, url, chunk_caption, chunk_reply_to, parse_mode,
max_asset_data_size, item_content_type
) )
else: # document
_LOGGER.debug("Sending chunk %d/%d as single document", chunk_idx + 1, len(chunks))
try:
download_url = self.coordinator.get_internal_download_url(url)
async with session.get(download_url) as resp:
if resp.status != 200:
return {"success": False, "error": f"Failed to download media: HTTP {resp.status}", "failed_at_chunk": chunk_idx + 1}
data = await resp.read()
if max_asset_data_size is not None and len(data) > max_asset_data_size:
_LOGGER.warning("Media size (%d bytes) exceeds max_asset_data_size limit (%d bytes), skipping", len(data), max_asset_data_size)
continue
filename = url.split("/")[-1].split("?")[0] or "file"
result = await self._send_telegram_document(
session, token, chat_id, data, filename, chunk_caption, chunk_reply_to, parse_mode, url, item_content_type
)
except aiohttp.ClientError as err:
return {"success": False, "error": f"Failed to download media: {err}", "failed_at_chunk": chunk_idx + 1}
if not result.get("success"): if not result.get("success"):
result["failed_at_chunk"] = chunk_idx + 1 result["failed_at_chunk"] = chunk_idx + 1
@@ -1035,15 +1093,17 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
cache = self.coordinator.telegram_cache cache = self.coordinator.telegram_cache
# Collect media items - either from cache (file_id) or by downloading # Collect media items - either from cache (file_id) or by downloading
# Each item: (type, media_ref, filename, url, is_cached) # Each item: (type, media_ref, filename, url, is_cached, content_type)
# media_ref is either file_id (str) or data (bytes) # media_ref is either file_id (str) or data (bytes)
media_items: list[tuple[str, str | bytes, str, str, bool]] = [] media_items: list[tuple[str, str | bytes, str, str, bool, str | None]] = []
oversized_photos: list[tuple[bytes, str | None, str]] = [] # (data, caption, url) oversized_photos: list[tuple[bytes, str | None, str]] = [] # (data, caption, url)
documents_to_send: list[tuple[bytes, str | None, str, str, str | None]] = [] # (data, caption, url, filename, content_type)
skipped_count = 0 skipped_count = 0
for i, item in enumerate(chunk): for i, item in enumerate(chunk):
url = item.get("url") url = item.get("url")
media_type = item.get("type", "photo") media_type = item.get("type", "document")
item_content_type = item.get("content_type")
if not url: if not url:
return { return {
@@ -1051,19 +1111,48 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"error": f"Missing 'url' in item {chunk_idx * max_group_size + i}", "error": f"Missing 'url' in item {chunk_idx * max_group_size + i}",
} }
if media_type not in ("photo", "video"): if media_type not in ("photo", "video", "document"):
return { return {
"success": False, "success": False,
"error": f"Invalid type '{media_type}' in item {chunk_idx * max_group_size + i}. Must be 'photo' or 'video'.", "error": f"Invalid type '{media_type}' in item {chunk_idx * max_group_size + i}. Must be 'photo', 'video', or 'document'.",
} }
# Check cache first # Documents can't be in media groups - collect them for separate sending
if media_type == "document":
try:
download_url = self.coordinator.get_internal_download_url(url)
async with session.get(download_url) as resp:
if resp.status != 200:
return {
"success": False,
"error": f"Failed to download media {chunk_idx * max_group_size + i}: HTTP {resp.status}",
}
data = await resp.read()
if max_asset_data_size is not None and len(data) > max_asset_data_size:
_LOGGER.warning(
"Media %d size (%d bytes) exceeds max_asset_data_size limit (%d bytes), skipping",
chunk_idx * max_group_size + i, len(data), max_asset_data_size
)
skipped_count += 1
continue
# Caption only on first item of first chunk if no media items yet
doc_caption = caption if chunk_idx == 0 and i == 0 and len(media_items) == 0 and len(documents_to_send) == 0 else None
filename = url.split("/")[-1].split("?")[0] or f"file_{i}"
documents_to_send.append((data, doc_caption, url, filename, item_content_type))
except aiohttp.ClientError as err:
return {
"success": False,
"error": f"Failed to download media {chunk_idx * max_group_size + i}: {err}",
}
continue
# Check cache first for photos/videos
cached = cache.get(url) if cache else None cached = cache.get(url) if cache else None
if cached and cached.get("file_id"): if cached and cached.get("file_id"):
# Use cached file_id # Use cached file_id
ext = "jpg" if media_type == "photo" else "mp4" ext = "jpg" if media_type == "photo" else "mp4"
filename = f"media_{chunk_idx * max_group_size + i}.{ext}" filename = f"media_{chunk_idx * max_group_size + i}.{ext}"
media_items.append((media_type, cached["file_id"], filename, url, True)) media_items.append((media_type, cached["file_id"], filename, url, True, item_content_type))
_LOGGER.debug("Using cached file_id for media %d", chunk_idx * max_group_size + i) _LOGGER.debug("Using cached file_id for media %d", chunk_idx * max_group_size + i)
continue continue
@@ -1108,7 +1197,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
ext = "jpg" if media_type == "photo" else "mp4" ext = "jpg" if media_type == "photo" else "mp4"
filename = f"media_{chunk_idx * max_group_size + i}.{ext}" filename = f"media_{chunk_idx * max_group_size + i}.{ext}"
media_items.append((media_type, data, filename, url, False)) media_items.append((media_type, data, filename, url, False, item_content_type))
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
return { return {
"success": False, "success": False,
@@ -1124,13 +1213,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
# Send media group if we have normal-sized files # Send media group if we have normal-sized files
if media_items: if media_items:
# Check if all items are cached (can use simple JSON payload) # Check if all items are cached (can use simple JSON payload)
all_cached = all(is_cached for _, _, _, _, is_cached in media_items) all_cached = all(is_cached for _, _, _, _, is_cached, _ in media_items)
if all_cached: if all_cached:
# All items cached - use simple JSON payload with file_ids # All items cached - use simple JSON payload with file_ids
_LOGGER.debug("All %d items cached, using file_ids", len(media_items)) _LOGGER.debug("All %d items cached, using file_ids", len(media_items))
media_json = [] media_json = []
for i, (media_type, file_id, _, _, _) in enumerate(media_items): for i, (media_type, file_id, _, _, _, _) in enumerate(media_items):
media_item_json: dict[str, Any] = { media_item_json: dict[str, Any] = {
"type": media_type, "type": media_type,
"media": file_id, "media": file_id,
@@ -1178,7 +1267,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
upload_idx = 0 upload_idx = 0
urls_to_cache: list[tuple[str, int, str]] = [] # (url, result_idx, type) urls_to_cache: list[tuple[str, int, str]] = [] # (url, result_idx, type)
for i, (media_type, media_ref, filename, url, is_cached) in enumerate(media_items): for i, (media_type, media_ref, filename, url, is_cached, item_content_type) in enumerate(media_items):
if is_cached: if is_cached:
# Use file_id directly # Use file_id directly
media_item_json: dict[str, Any] = { media_item_json: dict[str, Any] = {
@@ -1192,7 +1281,8 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"type": media_type, "type": media_type,
"media": f"attach://{attach_name}", "media": f"attach://{attach_name}",
} }
content_type = "image/jpeg" if media_type == "photo" else "video/mp4" # Use provided content_type or default based on media type
content_type = item_content_type or ("image/jpeg" if media_type == "photo" else "video/mp4")
form.add_field(attach_name, media_ref, filename=filename, content_type=content_type) form.add_field(attach_name, media_ref, filename=filename, content_type=content_type)
urls_to_cache.append((url, i, media_type)) urls_to_cache.append((url, i, media_type))
upload_idx += 1 upload_idx += 1
@@ -1236,7 +1326,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
else: else:
# Log detailed error for media group with total size info # Log detailed error for media group with total size info
uploaded_data = [m for m in media_items if not m[4]] uploaded_data = [m for m in media_items if not m[4]]
total_size = sum(len(d) for _, d, _, _, _ in uploaded_data if isinstance(d, bytes)) total_size = sum(len(d) for _, d, _, _, _, _ in uploaded_data if isinstance(d, bytes))
_LOGGER.error( _LOGGER.error(
"Telegram API error for chunk %d/%d: %s | Media count: %d | Uploaded size: %d bytes (%.2f MB)", "Telegram API error for chunk %d/%d: %s | Media count: %d | Uploaded size: %d bytes (%.2f MB)",
chunk_idx + 1, len(chunks), chunk_idx + 1, len(chunks),
@@ -1246,7 +1336,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
total_size / (1024 * 1024) if total_size else 0 total_size / (1024 * 1024) if total_size else 0
) )
# Log detailed diagnostics for the first photo in the group # Log detailed diagnostics for the first photo in the group
for media_type, media_ref, _, _, is_cached in media_items: for media_type, media_ref, _, _, is_cached, _ in media_items:
if media_type == "photo" and not is_cached and isinstance(media_ref, bytes): if media_type == "photo" and not is_cached and isinstance(media_ref, bytes):
self._log_telegram_error( self._log_telegram_error(
error_code=result.get("error_code"), error_code=result.get("error_code"),
@@ -1282,6 +1372,19 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.error("Failed to send oversized photo as document: %s", result.get("error")) _LOGGER.error("Failed to send oversized photo as document: %s", result.get("error"))
# Continue with other photos even if one fails # Continue with other photos even if one fails
# Send documents (can't be in media groups)
for i, (data, doc_caption, doc_url, filename, doc_content_type) in enumerate(documents_to_send):
_LOGGER.debug("Sending document %d/%d", i + 1, len(documents_to_send))
result = await self._send_telegram_document(
session, token, chat_id, data, filename,
doc_caption, None, parse_mode, doc_url, doc_content_type
)
if result.get("success"):
all_message_ids.append(result.get("message_id"))
else:
_LOGGER.error("Failed to send document: %s", result.get("error"))
# Continue with other documents even if one fails
return { return {
"success": True, "success": True,
"message_ids": all_message_ids, "message_ids": all_message_ids,

View File

@@ -131,7 +131,7 @@ get_assets:
send_telegram_notification: send_telegram_notification:
name: Send Telegram Notification name: Send Telegram Notification
description: Send a notification to Telegram (text, photo, video, or media group). description: Send a notification to Telegram (text, photo, video, document, or media group).
target: target:
entity: entity:
integration: immich_album_watcher integration: immich_album_watcher
@@ -151,7 +151,7 @@ send_telegram_notification:
text: text:
urls: urls:
name: URLs name: URLs
description: List of media URLs to send. Each item should have 'url' and 'type' (photo/video). If empty, sends a text message. Large lists are automatically split into multiple media groups. description: "List of media URLs to send. Each item should have 'url', optional 'type' (document/photo/video, default: document), and optional 'content_type' (MIME type, e.g., 'image/jpeg'). If empty, sends a text message. Photos and videos can be grouped; documents are sent separately."
required: false required: false
selector: selector:
object: object:

View File

@@ -195,7 +195,7 @@
}, },
"send_telegram_notification": { "send_telegram_notification": {
"name": "Send Telegram Notification", "name": "Send Telegram Notification",
"description": "Send a notification to Telegram (text, photo, video, or media group).", "description": "Send a notification to Telegram (text, photo, video, document, or media group).",
"fields": { "fields": {
"bot_token": { "bot_token": {
"name": "Bot Token", "name": "Bot Token",
@@ -207,7 +207,7 @@
}, },
"urls": { "urls": {
"name": "URLs", "name": "URLs",
"description": "List of media URLs with type (photo/video). If empty, sends a text message. Large lists are automatically split into multiple media groups." "description": "List of media URLs with optional type (document/photo/video, default: document) and optional content_type (MIME type). If empty, sends a text message. Photos and videos can be grouped; documents are sent separately."
}, },
"caption": { "caption": {
"name": "Caption", "name": "Caption",

View File

@@ -195,7 +195,7 @@
}, },
"send_telegram_notification": { "send_telegram_notification": {
"name": "Отправить уведомление в Telegram", "name": "Отправить уведомление в Telegram",
"description": "Отправить уведомление в Telegram (текст, фото, видео или медиа-группу).", "description": "Отправить уведомление в Telegram (текст, фото, видео, документ или медиа-группу).",
"fields": { "fields": {
"bot_token": { "bot_token": {
"name": "Токен бота", "name": "Токен бота",
@@ -207,7 +207,7 @@
}, },
"urls": { "urls": {
"name": "URL-адреса", "name": "URL-адреса",
"description": "Список URL медиа-файлов с типом (photo/video). Если пусто, отправляет текстовое сообщение. Большие списки автоматически разделяются на несколько медиа-групп." "description": "Список URL медиа-файлов с типом (document/photo/video, по умолчанию document) и опциональным content_type (MIME-тип). Если пусто, отправляет текстовое сообщение. Фото и видео группируются; документы отправляются отдельно."
}, },
"caption": { "caption": {
"name": "Подпись", "name": "Подпись",