From 71d3714f6ace63b566445e2e8c349d5fd55ea6e2 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 31 Jan 2026 17:31:14 +0300 Subject: [PATCH] Add max_asset_data_size parameter to Telegram service Introduces optional max_asset_data_size parameter (in bytes) to filter out oversized photos and videos from Telegram notifications. Assets exceeding the limit are skipped with a warning, preventing PHOTO_INVALID_DIMENSIONS errors for large images (e.g., 26MP photos). Changes: - Add max_asset_data_size parameter to service signature - Implement size checking for single photos/videos - Filter oversized assets in media groups - Update services.yaml, translations, and documentation Co-Authored-By: Claude Sonnet 4.5 --- README.md | 1 + .../immich_album_watcher/sensor.py | 60 +++++++++++++++++-- .../immich_album_watcher/services.yaml | 11 ++++ .../immich_album_watcher/translations/en.json | 4 ++ .../immich_album_watcher/translations/ru.json | 4 ++ 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dafaea0..0fdb0d2 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,7 @@ data: | `max_group_size` | Maximum media items per group (2-10). Large lists split into multiple groups. Default: 10 | No | | `chunk_delay` | Delay in milliseconds between sending multiple groups (0-60000). Useful for rate limiting. Default: 0 | No | | `wait_for_response` | Wait for Telegram to finish processing. Set to `false` for fire-and-forget (automation continues immediately). Default: `true` | No | +| `max_asset_data_size` | Maximum asset size in bytes. Assets exceeding this limit will be skipped. Default: no limit | No | The service returns a response with `success` status and `message_id` (single message), `message_ids` (media group), or `groups_sent` (number of groups when split). When `wait_for_response` is `false`, the service returns immediately with `{"success": true, "status": "queued"}` while processing continues in the background. diff --git a/custom_components/immich_album_watcher/sensor.py b/custom_components/immich_album_watcher/sensor.py index 93a08fb..7381f5c 100644 --- a/custom_components/immich_album_watcher/sensor.py +++ b/custom_components/immich_album_watcher/sensor.py @@ -188,6 +188,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se max_group_size: int = 10, chunk_delay: int = 0, wait_for_response: bool = True, + max_asset_data_size: int | None = None, ) -> ServiceResponse: """Send notification to Telegram. @@ -216,6 +217,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se parse_mode=parse_mode, max_group_size=max_group_size, chunk_delay=chunk_delay, + max_asset_data_size=max_asset_data_size, ) ) return {"success": True, "status": "queued", "message": "Notification queued for background processing"} @@ -231,6 +233,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se parse_mode=parse_mode, max_group_size=max_group_size, chunk_delay=chunk_delay, + max_asset_data_size=max_asset_data_size, ) async def _execute_telegram_notification( @@ -244,6 +247,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se parse_mode: str = "HTML", max_group_size: int = 10, chunk_delay: int = 0, + max_asset_data_size: int | None = None, ) -> ServiceResponse: """Execute the Telegram notification (internal method).""" import json @@ -270,18 +274,18 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se # Handle single photo if len(urls) == 1 and urls[0].get("type", "photo") == "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 ) # Handle single video if len(urls) == 1 and urls[0].get("type") == "video": return await self._send_telegram_video( - 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 ) # Handle multiple items - send as media group(s) 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, max_asset_data_size ) async def _send_telegram_message( @@ -341,6 +345,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se caption: str | None = None, reply_to_message_id: int | None = None, parse_mode: str = "HTML", + max_asset_data_size: int | None = None, ) -> ServiceResponse: """Send a single photo to Telegram.""" import aiohttp @@ -361,6 +366,18 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se data = await resp.read() _LOGGER.debug("Downloaded photo: %d bytes", len(data)) + # Check if photo exceeds max size limit + if max_asset_data_size is not None and len(data) > max_asset_data_size: + _LOGGER.warning( + "Photo size (%d bytes) exceeds max_asset_data_size limit (%d bytes), skipping", + len(data), max_asset_data_size + ) + return { + "success": False, + "error": f"Photo size ({len(data)} bytes) exceeds max_asset_data_size limit ({max_asset_data_size} bytes)", + "skipped": True, + } + # Build multipart form form = FormData() form.add_field("chat_id", chat_id) @@ -405,6 +422,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se caption: str | None = None, reply_to_message_id: int | None = None, parse_mode: str = "HTML", + max_asset_data_size: int | None = None, ) -> ServiceResponse: """Send a single video to Telegram.""" import aiohttp @@ -425,6 +443,18 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se data = await resp.read() _LOGGER.debug("Downloaded video: %d bytes", len(data)) + # Check if video exceeds max size limit + if max_asset_data_size is not None and len(data) > max_asset_data_size: + _LOGGER.warning( + "Video size (%d bytes) exceeds max_asset_data_size limit (%d bytes), skipping", + len(data), max_asset_data_size + ) + return { + "success": False, + "error": f"Video size ({len(data)} bytes) exceeds max_asset_data_size limit ({max_asset_data_size} bytes)", + "skipped": True, + } + # Build multipart form form = FormData() form.add_field("chat_id", chat_id) @@ -471,6 +501,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se max_group_size: int = 10, chunk_delay: int = 0, parse_mode: str = "HTML", + max_asset_data_size: int | None = None, ) -> ServiceResponse: """Send media URLs to Telegram as media group(s). @@ -511,12 +542,12 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se if media_type == "photo": _LOGGER.debug("Sending chunk %d/%d as single photo", chunk_idx + 1, len(chunks)) 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 ) else: # video _LOGGER.debug("Sending chunk %d/%d as single video", chunk_idx + 1, len(chunks)) result = await self._send_telegram_video( - 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 ) if not result.get("success"): @@ -530,6 +561,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se # Download all media files for this chunk media_files: list[tuple[str, bytes, str]] = [] + skipped_count = 0 for i, item in enumerate(chunk): url = item.get("url") media_type = item.get("type", "photo") @@ -555,16 +587,32 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se "error": f"Failed to download media {chunk_idx * max_group_size + i}: HTTP {resp.status}", } data = await resp.read() + _LOGGER.debug("Downloaded media %d: %d bytes", chunk_idx * max_group_size + i, len(data)) + + # Check if media exceeds max size limit + 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 + ext = "jpg" if media_type == "photo" else "mp4" filename = f"media_{chunk_idx * max_group_size + i}.{ext}" media_files.append((media_type, data, filename)) - _LOGGER.debug("Downloaded media %d: %d bytes", chunk_idx * max_group_size + i, len(data)) except aiohttp.ClientError as err: return { "success": False, "error": f"Failed to download media {chunk_idx * max_group_size + i}: {err}", } + # Skip this chunk if all files were filtered out + if not media_files: + _LOGGER.info("Chunk %d/%d: all %d media items skipped due to size limit", + chunk_idx + 1, len(chunks), len(chunk)) + continue + # Build multipart form form = FormData() form.add_field("chat_id", chat_id) diff --git a/custom_components/immich_album_watcher/services.yaml b/custom_components/immich_album_watcher/services.yaml index 618ee30..470002a 100644 --- a/custom_components/immich_album_watcher/services.yaml +++ b/custom_components/immich_album_watcher/services.yaml @@ -116,3 +116,14 @@ send_telegram_notification: default: true selector: boolean: + max_asset_data_size: + name: Max Asset Data Size + description: Maximum asset size in bytes. Assets exceeding this limit will be skipped. Leave empty for no limit. + required: false + selector: + number: + min: 1 + max: 52428800 + step: 1048576 + unit_of_measurement: "bytes" + mode: box diff --git a/custom_components/immich_album_watcher/translations/en.json b/custom_components/immich_album_watcher/translations/en.json index ef45e30..08dad1b 100644 --- a/custom_components/immich_album_watcher/translations/en.json +++ b/custom_components/immich_album_watcher/translations/en.json @@ -186,6 +186,10 @@ "wait_for_response": { "name": "Wait For Response", "description": "Wait for Telegram to finish processing before returning. Set to false for fire-and-forget (automation continues immediately)." + }, + "max_asset_data_size": { + "name": "Max Asset Data Size", + "description": "Maximum asset size in bytes. Assets exceeding this limit will be skipped. Leave empty for no limit." } } } diff --git a/custom_components/immich_album_watcher/translations/ru.json b/custom_components/immich_album_watcher/translations/ru.json index e75f475..e60dc4a 100644 --- a/custom_components/immich_album_watcher/translations/ru.json +++ b/custom_components/immich_album_watcher/translations/ru.json @@ -186,6 +186,10 @@ "wait_for_response": { "name": "Ждать ответа", "description": "Ждать завершения отправки в Telegram перед возвратом. Установите false для фоновой отправки (автоматизация продолжается немедленно)." + }, + "max_asset_data_size": { + "name": "Макс. размер ресурса", + "description": "Максимальный размер ресурса в байтах. Ресурсы, превышающие этот лимит, будут пропущены. Оставьте пустым для отсутствия ограничения." } } }