Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b0f3b8b12 | |||
| e5e45f0fbf | |||
| 8714685d5e | |||
| bbcd97e1ac | |||
| 04dd63825c | |||
| 71d3714f6a |
188
README.md
188
README.md
@@ -4,16 +4,21 @@
|
||||
|
||||
A Home Assistant custom integration that monitors [Immich](https://immich.app/) photo/video library albums for changes and exposes them as Home Assistant entities with event-firing capabilities.
|
||||
|
||||
> **Tip:** For the best experience, use this integration with the [Immich Album Watcher Blueprint](https://github.com/DolgolyovAlexei/haos-blueprints/blob/main/Common/Immich%20Album%20Watcher.yaml) to easily create automations for album change notifications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Album Monitoring** - Watch selected Immich albums for asset additions and removals
|
||||
- **Rich Sensor Data** - Multiple sensors per album:
|
||||
- Album ID (with share URL attribute)
|
||||
- Asset count (with detected people list)
|
||||
- Photo count
|
||||
- Video count
|
||||
- Last updated timestamp
|
||||
- Creation date
|
||||
- Album ID (with album name and share URL attributes)
|
||||
- Asset Count (total assets with detected people list)
|
||||
- Photo Count (number of photos)
|
||||
- Video Count (number of videos)
|
||||
- Last Updated (last modification timestamp)
|
||||
- Created (album creation date)
|
||||
- Public URL (public share link)
|
||||
- Protected URL (password-protected share link)
|
||||
- Protected Password (password for protected link)
|
||||
- **Camera Entity** - Album thumbnail displayed as a camera entity for dashboards
|
||||
- **Binary Sensor** - "New Assets" indicator that turns on when assets are added
|
||||
- **Face Recognition** - Detects and lists people recognized in album photos
|
||||
@@ -31,13 +36,16 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/)
|
||||
- Detected people in the asset
|
||||
- **Services** - Custom service calls:
|
||||
- `immich_album_watcher.refresh` - Force immediate data refresh
|
||||
- `immich_album_watcher.get_recent_assets` - Get recent assets from an album
|
||||
- `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
|
||||
- **Share Link Management** - Button entities to create and delete share links:
|
||||
- Create/delete public (unprotected) share links
|
||||
- Create/delete password-protected share links
|
||||
- Edit protected link passwords via Text entity
|
||||
- **Configurable Polling** - Adjustable scan interval (10-3600 seconds)
|
||||
- **Localization** - Available in multiple languages:
|
||||
- English
|
||||
- Russian (Русский)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -60,8 +68,6 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/)
|
||||
3. Restart Home Assistant
|
||||
4. Add the integration via **Settings** → **Devices & Services** → **Add Integration**
|
||||
|
||||
> **Tip:** For the best experience, use this integration with the [Immich Album Watcher Blueprint](https://github.com/DolgolyovAlexei/haos-blueprints/blob/main/Common/Immich%20Album%20Watcher.yaml) to easily create automations for album change notifications.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
@@ -103,16 +109,125 @@ Force an immediate refresh of all album data:
|
||||
service: immich_album_watcher.refresh
|
||||
```
|
||||
|
||||
### Get Recent Assets
|
||||
### Get Assets
|
||||
|
||||
Get the most recent assets from a specific album (returns response data):
|
||||
Get assets from a specific album with optional filtering and ordering (returns response data). Only returns fully processed assets (videos with completed transcoding, photos with generated thumbnails).
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_recent_assets
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
count: 10
|
||||
limit: 10 # Maximum number of assets (1-100)
|
||||
favorite_only: false # true = favorites only, false = all assets
|
||||
filter_min_rating: 4 # Min rating (1-5)
|
||||
order_by: "date" # Options: "date", "rating", "name"
|
||||
order: "descending" # Options: "ascending", "descending", "random"
|
||||
asset_type: "all" # Options: "all", "photo", "video"
|
||||
min_date: "2024-01-01" # Optional: assets created on or after this date
|
||||
max_date: "2024-12-31" # Optional: assets created on or before this date
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `limit` (optional, default: 10): Maximum number of assets to return (1-100)
|
||||
- `favorite_only` (optional, default: false): Filter to show only favorite assets
|
||||
- `filter_min_rating` (optional, default: 1): Minimum rating for assets (1-5 stars). Applied independently of `favorite_only`
|
||||
- `order_by` (optional, default: "date"): Field to sort assets by
|
||||
- `"date"`: Sort by creation date
|
||||
- `"rating"`: Sort by rating (assets without rating are placed last)
|
||||
- `"name"`: Sort by filename
|
||||
- `order` (optional, default: "descending"): Sort direction
|
||||
- `"ascending"`: Ascending order
|
||||
- `"descending"`: Descending order
|
||||
- `"random"`: Random order (ignores `order_by`)
|
||||
- `asset_type` (optional, default: "all"): Filter by asset type
|
||||
- `"all"`: No type filtering, return both photos and videos
|
||||
- `"photo"`: Return only photos
|
||||
- `"video"`: Return only videos
|
||||
- `min_date` (optional): Filter assets created on or after this date. Use ISO 8601 format (e.g., `"2024-01-01"` or `"2024-01-01T10:30:00"`)
|
||||
- `max_date` (optional): Filter assets created on or before this date. Use ISO 8601 format (e.g., `"2024-12-31"` or `"2024-12-31T23:59:59"`)
|
||||
|
||||
**Examples:**
|
||||
|
||||
Get 5 most recent favorite assets:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 5
|
||||
favorite_only: true
|
||||
order_by: "date"
|
||||
order: "descending"
|
||||
```
|
||||
|
||||
Get 10 random assets rated 3 stars or higher:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 10
|
||||
filter_min_rating: 3
|
||||
order: "random"
|
||||
```
|
||||
|
||||
Get 20 most recent photos only:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 20
|
||||
asset_type: "photo"
|
||||
order_by: "date"
|
||||
order: "descending"
|
||||
```
|
||||
|
||||
Get top 10 highest rated favorite videos:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 10
|
||||
favorite_only: true
|
||||
asset_type: "video"
|
||||
order_by: "rating"
|
||||
order: "descending"
|
||||
```
|
||||
|
||||
Get photos sorted alphabetically by name:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 20
|
||||
asset_type: "photo"
|
||||
order_by: "name"
|
||||
order: "ascending"
|
||||
```
|
||||
|
||||
Get photos from a specific date range:
|
||||
|
||||
```yaml
|
||||
service: immich_album_watcher.get_assets
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
limit: 50
|
||||
asset_type: "photo"
|
||||
min_date: "2024-06-01"
|
||||
max_date: "2024-06-30"
|
||||
order_by: "date"
|
||||
order: "descending"
|
||||
```
|
||||
|
||||
### Send Telegram Notification
|
||||
@@ -133,7 +248,7 @@ Text message:
|
||||
```yaml
|
||||
service: immich_album_watcher.send_telegram_notification
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
chat_id: "-1001234567890"
|
||||
caption: "Check out the new album!"
|
||||
@@ -145,7 +260,7 @@ Single photo:
|
||||
```yaml
|
||||
service: immich_album_watcher.send_telegram_notification
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
chat_id: "-1001234567890"
|
||||
urls:
|
||||
@@ -159,7 +274,7 @@ Media group:
|
||||
```yaml
|
||||
service: immich_album_watcher.send_telegram_notification
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
chat_id: "-1001234567890"
|
||||
urls:
|
||||
@@ -176,12 +291,12 @@ HTML formatting:
|
||||
```yaml
|
||||
service: immich_album_watcher.send_telegram_notification
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
chat_id: "-1001234567890"
|
||||
caption: |
|
||||
<b>Album Updated!</b>
|
||||
New photos by <i>{{ trigger.event.data.added_assets[0].asset_owner }}</i>
|
||||
New photos by <i>{{ trigger.event.data.added_assets[0].owner }}</i>
|
||||
<a href="https://immich.example.com/album">View Album</a>
|
||||
parse_mode: "HTML" # Default, can be omitted
|
||||
```
|
||||
@@ -191,7 +306,7 @@ Non-blocking mode (fire-and-forget):
|
||||
```yaml
|
||||
service: immich_album_watcher.send_telegram_notification
|
||||
target:
|
||||
entity_id: sensor.album_name_asset_count
|
||||
entity_id: sensor.album_name_asset_limit
|
||||
data:
|
||||
chat_id: "-1001234567890"
|
||||
urls:
|
||||
@@ -213,6 +328,8 @@ 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 |
|
||||
| `send_large_photos_as_documents` | Handle photos exceeding Telegram limits (10MB or 10000px dimension sum). If `true`, send as documents. If `false`, skip oversized photos. Default: `false` | 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.
|
||||
|
||||
@@ -243,7 +360,7 @@ automation:
|
||||
- service: notify.mobile_app
|
||||
data:
|
||||
title: "New Photos"
|
||||
message: "{{ trigger.event.data.added_count }} new photos in {{ trigger.event.data.album_name }}"
|
||||
message: "{{ trigger.event.data.added_limit }} new photos in {{ trigger.event.data.album_name }}"
|
||||
|
||||
- alias: "Album renamed"
|
||||
trigger:
|
||||
@@ -276,8 +393,8 @@ automation:
|
||||
| `album_url` | Public URL to view the album (only present if album has a shared link) | All events except `album_deleted` |
|
||||
| `change_type` | Type of change (assets_added, assets_removed, album_renamed, album_sharing_changed, changed) | All events except `album_deleted` |
|
||||
| `shared` | Current sharing status of the album | All events except `album_deleted` |
|
||||
| `added_count` | Number of assets added | `album_changed`, `assets_added` |
|
||||
| `removed_count` | Number of assets removed | `album_changed`, `assets_removed` |
|
||||
| `added_limit` | Number of assets added | `album_changed`, `assets_added` |
|
||||
| `removed_limit` | Number of assets removed | `album_changed`, `assets_removed` |
|
||||
| `added_assets` | List of added assets with details (see below) | `album_changed`, `assets_added` |
|
||||
| `removed_assets` | List of removed asset IDs | `album_changed`, `assets_removed` |
|
||||
| `people` | List of all people detected in the album | All events except `album_deleted` |
|
||||
@@ -293,15 +410,22 @@ Each item in the `added_assets` list contains the following fields:
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `id` | Unique asset ID |
|
||||
| `asset_type` | Type of asset (`IMAGE` or `VIDEO`) |
|
||||
| `asset_filename` | Original filename of the asset |
|
||||
| `asset_created` | Date/time when the asset was originally created |
|
||||
| `asset_owner` | Display name of the user who owns the asset |
|
||||
| `asset_owner_id` | Unique ID of the user who owns the asset |
|
||||
| `asset_description` | Description/caption of the asset (from EXIF data) |
|
||||
| `asset_url` | Public URL to view the asset (only present if album has a shared link) |
|
||||
| `type` | Type of asset (`IMAGE` or `VIDEO`) |
|
||||
| `filename` | Original filename of the asset |
|
||||
| `created_at` | Date/time when the asset was originally created |
|
||||
| `owner` | Display name of the user who owns the asset |
|
||||
| `owner_id` | Unique ID of the user who owns the asset |
|
||||
| `description` | Description/caption of the asset (from EXIF data) |
|
||||
| `is_favorite` | Whether the asset is marked as favorite (`true` or `false`) |
|
||||
| `rating` | User rating of the asset (1-5 stars, or `null` if not rated) |
|
||||
| `url` | Public URL to view the asset (only present if album has a shared link) |
|
||||
| `download_url` | Direct download URL for the original file (if shared link exists) |
|
||||
| `playback_url` | Video playback URL (for VIDEO assets only, if shared link exists) |
|
||||
| `photo_url` | Photo preview URL (for IMAGE assets only, if shared link exists) |
|
||||
| `people` | List of people detected in this specific asset |
|
||||
|
||||
> **Note:** Assets are only included in events and service responses when they are fully processed by Immich. For videos, this means transcoding must be complete (with `encodedVideoPath`). For photos, thumbnail generation must be complete (with `thumbhash`). This ensures that all media URLs are valid and accessible. Unprocessed assets are silently ignored until their processing completes.
|
||||
|
||||
Example accessing asset owner in an automation:
|
||||
|
||||
```yaml
|
||||
@@ -315,8 +439,8 @@ automation:
|
||||
data:
|
||||
title: "New Photos"
|
||||
message: >
|
||||
{{ trigger.event.data.added_assets[0].asset_owner }} added
|
||||
{{ trigger.event.data.added_count }} photos to {{ trigger.event.data.album_name }}
|
||||
{{ trigger.event.data.added_assets[0].owner }} added
|
||||
{{ trigger.event.data.added_limit }} photos to {{ trigger.event.data.album_name }}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -57,15 +57,17 @@ ATTR_OLD_NAME: Final = "old_name"
|
||||
ATTR_NEW_NAME: Final = "new_name"
|
||||
ATTR_OLD_SHARED: Final = "old_shared"
|
||||
ATTR_NEW_SHARED: Final = "new_shared"
|
||||
ATTR_ASSET_TYPE: Final = "asset_type"
|
||||
ATTR_ASSET_FILENAME: Final = "asset_filename"
|
||||
ATTR_ASSET_CREATED: Final = "asset_created"
|
||||
ATTR_ASSET_OWNER: Final = "asset_owner"
|
||||
ATTR_ASSET_OWNER_ID: Final = "asset_owner_id"
|
||||
ATTR_ASSET_URL: Final = "asset_url"
|
||||
ATTR_ASSET_DOWNLOAD_URL: Final = "asset_download_url"
|
||||
ATTR_ASSET_PLAYBACK_URL: Final = "asset_playback_url"
|
||||
ATTR_ASSET_DESCRIPTION: Final = "asset_description"
|
||||
ATTR_ASSET_TYPE: Final = "type"
|
||||
ATTR_ASSET_FILENAME: Final = "filename"
|
||||
ATTR_ASSET_CREATED: Final = "created_at"
|
||||
ATTR_ASSET_OWNER: Final = "owner"
|
||||
ATTR_ASSET_OWNER_ID: Final = "owner_id"
|
||||
ATTR_ASSET_URL: Final = "url"
|
||||
ATTR_ASSET_DOWNLOAD_URL: Final = "download_url"
|
||||
ATTR_ASSET_PLAYBACK_URL: Final = "playback_url"
|
||||
ATTR_ASSET_DESCRIPTION: Final = "description"
|
||||
ATTR_ASSET_IS_FAVORITE: Final = "is_favorite"
|
||||
ATTR_ASSET_RATING: Final = "rating"
|
||||
|
||||
# Asset types
|
||||
ASSET_TYPE_IMAGE: Final = "IMAGE"
|
||||
@@ -76,5 +78,5 @@ PLATFORMS: Final = ["sensor", "binary_sensor", "camera", "text", "button"]
|
||||
|
||||
# Services
|
||||
SERVICE_REFRESH: Final = "refresh"
|
||||
SERVICE_GET_RECENT_ASSETS: Final = "get_recent_assets"
|
||||
SERVICE_GET_ASSETS: Final = "get_assets"
|
||||
SERVICE_SEND_TELEGRAM_NOTIFICATION: Final = "send_telegram_notification"
|
||||
|
||||
@@ -28,11 +28,13 @@ from .const import (
|
||||
ATTR_ASSET_DESCRIPTION,
|
||||
ATTR_ASSET_DOWNLOAD_URL,
|
||||
ATTR_ASSET_FILENAME,
|
||||
ATTR_ASSET_IS_FAVORITE,
|
||||
ATTR_ASSET_OWNER,
|
||||
ATTR_ASSET_OWNER_ID,
|
||||
ATTR_ASSET_PLAYBACK_URL,
|
||||
ATTR_ASSET_RATING,
|
||||
ATTR_ASSET_TYPE,
|
||||
ATTR_ASSET_URL,
|
||||
ATTR_ASSET_PLAYBACK_URL,
|
||||
ATTR_CHANGE_TYPE,
|
||||
ATTR_HUB_NAME,
|
||||
ATTR_PEOPLE,
|
||||
@@ -43,6 +45,7 @@ from .const import (
|
||||
ATTR_OLD_SHARED,
|
||||
ATTR_NEW_SHARED,
|
||||
ATTR_SHARED,
|
||||
ATTR_THUMBNAIL_URL,
|
||||
DOMAIN,
|
||||
EVENT_ALBUM_CHANGED,
|
||||
EVENT_ASSETS_ADDED,
|
||||
@@ -115,6 +118,9 @@ class AssetInfo:
|
||||
owner_name: str = ""
|
||||
description: str = ""
|
||||
people: list[str] = field(default_factory=list)
|
||||
is_favorite: bool = False
|
||||
rating: int | None = None
|
||||
is_processed: bool = True # Whether asset is fully processed by Immich
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
@@ -130,23 +136,74 @@ class AssetInfo:
|
||||
if users_cache and owner_id:
|
||||
owner_name = users_cache.get(owner_id, "")
|
||||
|
||||
# Get description from exifInfo if available
|
||||
description = ""
|
||||
# Get description - prioritize user-added description over EXIF description
|
||||
description = data.get("description", "") or ""
|
||||
exif_info = data.get("exifInfo")
|
||||
if exif_info:
|
||||
if not description and exif_info:
|
||||
# Fall back to EXIF description if no user description
|
||||
description = exif_info.get("description", "") or ""
|
||||
|
||||
# Get favorites and rating
|
||||
is_favorite = data.get("isFavorite", False)
|
||||
rating = exif_info.get("rating") if exif_info else None
|
||||
|
||||
# Check if asset is fully processed by Immich
|
||||
asset_type = data.get("type", ASSET_TYPE_IMAGE)
|
||||
is_processed = cls._check_processing_status(data, asset_type)
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
type=data.get("type", ASSET_TYPE_IMAGE),
|
||||
type=asset_type,
|
||||
filename=data.get("originalFileName", ""),
|
||||
created_at=data.get("fileCreatedAt", ""),
|
||||
owner_id=owner_id,
|
||||
owner_name=owner_name,
|
||||
description=description,
|
||||
people=people,
|
||||
is_favorite=is_favorite,
|
||||
rating=rating,
|
||||
is_processed=is_processed,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_processing_status(data: dict[str, Any], asset_type: str) -> bool:
|
||||
"""Check if asset has been fully processed by Immich.
|
||||
|
||||
For photos: Check if thumbnails/previews have been generated
|
||||
For videos: Check if video transcoding is complete
|
||||
|
||||
Args:
|
||||
data: Asset data from API response
|
||||
asset_type: Asset type (IMAGE or VIDEO)
|
||||
|
||||
Returns:
|
||||
True if asset is fully processed, False otherwise
|
||||
"""
|
||||
if asset_type == ASSET_TYPE_VIDEO:
|
||||
# For videos, check if transcoding is complete
|
||||
# Video is processed if it has an encoded video path or if isOffline is False
|
||||
is_offline = data.get("isOffline", False)
|
||||
if is_offline:
|
||||
return False
|
||||
|
||||
# Check if video has been transcoded (has encoded video path)
|
||||
# Immich uses "encodedVideoPath" or similar field when transcoding is done
|
||||
has_encoded_video = bool(data.get("encodedVideoPath"))
|
||||
return has_encoded_video
|
||||
|
||||
else: # ASSET_TYPE_IMAGE
|
||||
# For photos, check if thumbnails have been generated
|
||||
# Photos are processed if they have thumbnail/preview paths
|
||||
is_offline = data.get("isOffline", False)
|
||||
if is_offline:
|
||||
return False
|
||||
|
||||
# Check if thumbnails exist
|
||||
has_thumbhash = bool(data.get("thumbhash"))
|
||||
has_thumbnail = has_thumbhash # If thumbhash exists, thumbnails should exist
|
||||
|
||||
return has_thumbnail
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlbumData:
|
||||
@@ -303,33 +360,91 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
self._album_name,
|
||||
)
|
||||
|
||||
async def async_get_recent_assets(self, count: int = 10) -> list[dict[str, Any]]:
|
||||
"""Get recent assets from the album."""
|
||||
async def async_get_assets(
|
||||
self,
|
||||
limit: int = 10,
|
||||
favorite_only: bool = False,
|
||||
filter_min_rating: int = 1,
|
||||
order_by: str = "date",
|
||||
order: str = "descending",
|
||||
asset_type: str = "all",
|
||||
min_date: str | None = None,
|
||||
max_date: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get assets from the album with optional filtering and ordering.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of assets to return (1-100)
|
||||
favorite_only: Filter to show only favorite assets
|
||||
filter_min_rating: Minimum rating for assets (1-5)
|
||||
order_by: Field to sort by - 'date', 'rating', or 'name'
|
||||
order: Sort direction - 'ascending', 'descending', or 'random'
|
||||
asset_type: Asset type filter - 'all', 'photo', or 'video'
|
||||
min_date: Filter assets created on or after this date (ISO 8601 format)
|
||||
max_date: Filter assets created on or before this date (ISO 8601 format)
|
||||
|
||||
Returns:
|
||||
List of asset data dictionaries
|
||||
"""
|
||||
if self.data is None:
|
||||
return []
|
||||
|
||||
# Sort assets by created_at descending
|
||||
sorted_assets = sorted(
|
||||
self.data.assets.values(),
|
||||
key=lambda a: a.created_at,
|
||||
reverse=True,
|
||||
)[:count]
|
||||
# Start with all processed assets only
|
||||
assets = [a for a in self.data.assets.values() if a.is_processed]
|
||||
|
||||
# Apply favorite filter
|
||||
if favorite_only:
|
||||
assets = [a for a in assets if a.is_favorite]
|
||||
|
||||
# Apply rating filter
|
||||
if filter_min_rating > 1:
|
||||
assets = [a for a in assets if a.rating is not None and a.rating >= filter_min_rating]
|
||||
|
||||
# Apply asset type filtering
|
||||
if asset_type == "photo":
|
||||
assets = [a for a in assets if a.type == ASSET_TYPE_IMAGE]
|
||||
elif asset_type == "video":
|
||||
assets = [a for a in assets if a.type == ASSET_TYPE_VIDEO]
|
||||
|
||||
# Apply date filtering
|
||||
if min_date:
|
||||
assets = [a for a in assets if a.created_at >= min_date]
|
||||
if max_date:
|
||||
assets = [a for a in assets if a.created_at <= max_date]
|
||||
|
||||
# Apply ordering
|
||||
if order == "random":
|
||||
import random
|
||||
random.shuffle(assets)
|
||||
else:
|
||||
# Determine sort key based on order_by
|
||||
if order_by == "rating":
|
||||
# Sort by rating, putting None values last
|
||||
assets = sorted(
|
||||
assets,
|
||||
key=lambda a: (a.rating is None, a.rating if a.rating is not None else 0),
|
||||
reverse=(order == "descending")
|
||||
)
|
||||
elif order_by == "name":
|
||||
assets = sorted(
|
||||
assets,
|
||||
key=lambda a: a.filename.lower(),
|
||||
reverse=(order == "descending")
|
||||
)
|
||||
else: # date (default)
|
||||
assets = sorted(
|
||||
assets,
|
||||
key=lambda a: a.created_at,
|
||||
reverse=(order == "descending")
|
||||
)
|
||||
|
||||
# Limit results
|
||||
assets = assets[:limit]
|
||||
|
||||
# Build result with all available asset data (matching event data)
|
||||
result = []
|
||||
for asset in sorted_assets:
|
||||
asset_data = {
|
||||
"id": asset.id,
|
||||
"type": asset.type,
|
||||
"filename": asset.filename,
|
||||
"created_at": asset.created_at,
|
||||
"description": asset.description,
|
||||
"people": asset.people,
|
||||
"thumbnail_url": f"{self._url}/api/assets/{asset.id}/thumbnail",
|
||||
}
|
||||
if asset.type == ASSET_TYPE_VIDEO:
|
||||
video_url = self._get_asset_video_url(asset.id)
|
||||
if video_url:
|
||||
asset_data["video_url"] = video_url
|
||||
for asset in assets:
|
||||
asset_data = self._build_asset_detail(asset, include_thumbnail=True)
|
||||
result.append(asset_data)
|
||||
return result
|
||||
|
||||
@@ -501,6 +616,68 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
return f"{self._url}/api/assets/{asset_id}/video/playback?key={non_expired[0].key}"
|
||||
return None
|
||||
|
||||
def _get_asset_photo_url(self, asset_id: str) -> str | None:
|
||||
"""Get the preview-sized thumbnail URL for a photo asset."""
|
||||
accessible_links = self._get_accessible_links()
|
||||
if accessible_links:
|
||||
return f"{self._url}/api/assets/{asset_id}/thumbnail?size=preview&key={accessible_links[0].key}"
|
||||
non_expired = [link for link in self._shared_links if not link.is_expired]
|
||||
if non_expired:
|
||||
return f"{self._url}/api/assets/{asset_id}/thumbnail?size=preview&key={non_expired[0].key}"
|
||||
return None
|
||||
|
||||
def _build_asset_detail(
|
||||
self, asset: AssetInfo, include_thumbnail: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""Build asset detail dictionary with all available data.
|
||||
|
||||
Args:
|
||||
asset: AssetInfo object
|
||||
include_thumbnail: If True, include thumbnail_url
|
||||
|
||||
Returns:
|
||||
Dictionary with asset details (using ATTR_* constants for consistency)
|
||||
"""
|
||||
# Base asset data
|
||||
asset_detail = {
|
||||
"id": asset.id,
|
||||
ATTR_ASSET_TYPE: asset.type,
|
||||
ATTR_ASSET_FILENAME: asset.filename,
|
||||
ATTR_ASSET_CREATED: asset.created_at,
|
||||
ATTR_ASSET_OWNER: asset.owner_name,
|
||||
ATTR_ASSET_OWNER_ID: asset.owner_id,
|
||||
ATTR_ASSET_DESCRIPTION: asset.description,
|
||||
ATTR_PEOPLE: asset.people,
|
||||
ATTR_ASSET_IS_FAVORITE: asset.is_favorite,
|
||||
ATTR_ASSET_RATING: asset.rating,
|
||||
}
|
||||
|
||||
# Add thumbnail URL if requested
|
||||
if include_thumbnail:
|
||||
asset_detail[ATTR_THUMBNAIL_URL] = f"{self._url}/api/assets/{asset.id}/thumbnail"
|
||||
|
||||
# Add public viewer URL (web page)
|
||||
asset_url = self._get_asset_public_url(asset.id)
|
||||
if asset_url:
|
||||
asset_detail[ATTR_ASSET_URL] = asset_url
|
||||
|
||||
# Add download URL (direct media file)
|
||||
asset_download_url = self._get_asset_download_url(asset.id)
|
||||
if asset_download_url:
|
||||
asset_detail[ATTR_ASSET_DOWNLOAD_URL] = asset_download_url
|
||||
|
||||
# Add type-specific URLs
|
||||
if asset.type == ASSET_TYPE_VIDEO:
|
||||
asset_video_url = self._get_asset_video_url(asset.id)
|
||||
if asset_video_url:
|
||||
asset_detail[ATTR_ASSET_PLAYBACK_URL] = asset_video_url
|
||||
elif asset.type == ASSET_TYPE_IMAGE:
|
||||
asset_photo_url = self._get_asset_photo_url(asset.id)
|
||||
if asset_photo_url:
|
||||
asset_detail["photo_url"] = asset_photo_url # TODO: Add ATTR_ASSET_PHOTO_URL constant
|
||||
|
||||
return asset_detail
|
||||
|
||||
async def _async_update_data(self) -> AlbumData | None:
|
||||
"""Fetch data from Immich API."""
|
||||
if self._session is None:
|
||||
@@ -620,34 +797,37 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
added_ids = new_state.asset_ids - old_state.asset_ids
|
||||
removed_ids = old_state.asset_ids - new_state.asset_ids
|
||||
|
||||
# Only include fully processed assets in added_assets
|
||||
added_assets = [
|
||||
new_state.assets[aid]
|
||||
for aid in added_ids
|
||||
if aid in new_state.assets and new_state.assets[aid].is_processed
|
||||
]
|
||||
|
||||
# Detect metadata changes
|
||||
name_changed = old_state.name != new_state.name
|
||||
sharing_changed = old_state.shared != new_state.shared
|
||||
|
||||
# Return None only if nothing changed at all
|
||||
if not added_ids and not removed_ids and not name_changed and not sharing_changed:
|
||||
if not added_assets and not removed_ids and not name_changed and not sharing_changed:
|
||||
return None
|
||||
|
||||
# Determine primary change type
|
||||
# Determine primary change type (use added_assets not added_ids)
|
||||
change_type = "changed"
|
||||
if name_changed and not added_ids and not removed_ids and not sharing_changed:
|
||||
if name_changed and not added_assets and not removed_ids and not sharing_changed:
|
||||
change_type = "album_renamed"
|
||||
elif sharing_changed and not added_ids and not removed_ids and not name_changed:
|
||||
elif sharing_changed and not added_assets and not removed_ids and not name_changed:
|
||||
change_type = "album_sharing_changed"
|
||||
elif added_ids and not removed_ids and not name_changed and not sharing_changed:
|
||||
elif added_assets and not removed_ids and not name_changed and not sharing_changed:
|
||||
change_type = "assets_added"
|
||||
elif removed_ids and not added_ids and not name_changed and not sharing_changed:
|
||||
elif removed_ids and not added_assets and not name_changed and not sharing_changed:
|
||||
change_type = "assets_removed"
|
||||
|
||||
added_assets = [
|
||||
new_state.assets[aid] for aid in added_ids if aid in new_state.assets
|
||||
]
|
||||
|
||||
return AlbumChange(
|
||||
album_id=new_state.id,
|
||||
album_name=new_state.name,
|
||||
change_type=change_type,
|
||||
added_count=len(added_ids),
|
||||
added_count=len(added_assets), # Count only processed assets
|
||||
removed_count=len(removed_ids),
|
||||
added_assets=added_assets,
|
||||
removed_asset_ids=list(removed_ids),
|
||||
@@ -661,26 +841,10 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
"""Fire Home Assistant events for album changes."""
|
||||
added_assets_detail = []
|
||||
for asset in change.added_assets:
|
||||
asset_detail = {
|
||||
"id": asset.id,
|
||||
ATTR_ASSET_TYPE: asset.type,
|
||||
ATTR_ASSET_FILENAME: asset.filename,
|
||||
ATTR_ASSET_CREATED: asset.created_at,
|
||||
ATTR_ASSET_OWNER: asset.owner_name,
|
||||
ATTR_ASSET_OWNER_ID: asset.owner_id,
|
||||
ATTR_ASSET_DESCRIPTION: asset.description,
|
||||
ATTR_PEOPLE: asset.people,
|
||||
}
|
||||
asset_url = self._get_asset_public_url(asset.id)
|
||||
if asset_url:
|
||||
asset_detail[ATTR_ASSET_URL] = asset_url
|
||||
asset_download_url = self._get_asset_download_url(asset.id)
|
||||
if asset_download_url:
|
||||
asset_detail[ATTR_ASSET_DOWNLOAD_URL] = asset_download_url
|
||||
if asset.type == ASSET_TYPE_VIDEO:
|
||||
asset_video_url = self._get_asset_video_url(asset.id)
|
||||
if asset_video_url:
|
||||
asset_detail[ATTR_ASSET_PLAYBACK_URL] = asset_video_url
|
||||
# Only include fully processed assets
|
||||
if not asset.is_processed:
|
||||
continue
|
||||
asset_detail = self._build_asset_detail(asset, include_thumbnail=False)
|
||||
added_assets_detail.append(asset_detail)
|
||||
|
||||
event_data = {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/DolgolyovAlexei/haos-hacs-immich-album-watcher/issues",
|
||||
"requirements": [],
|
||||
"version": "2.0.0"
|
||||
"version": "2.2.0"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ from .const import (
|
||||
CONF_HUB_NAME,
|
||||
CONF_TELEGRAM_BOT_TOKEN,
|
||||
DOMAIN,
|
||||
SERVICE_GET_RECENT_ASSETS,
|
||||
SERVICE_GET_ASSETS,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_SEND_TELEGRAM_NOTIFICATION,
|
||||
)
|
||||
@@ -48,6 +48,10 @@ from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Telegram photo limits
|
||||
TELEGRAM_MAX_PHOTO_SIZE = 10 * 1024 * 1024 # 10 MB - Telegram's max photo size
|
||||
TELEGRAM_MAX_DIMENSION_SUM = 10000 # Maximum sum of width + height in pixels
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -88,13 +92,18 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GET_RECENT_ASSETS,
|
||||
SERVICE_GET_ASSETS,
|
||||
{
|
||||
vol.Optional("count", default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional("filter", default="none"): vol.In(["none", "favorite", "rating"]),
|
||||
vol.Optional("filter_min_rating", default=1): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=5)
|
||||
),
|
||||
vol.Optional("order", default="descending"): vol.In(["ascending", "descending", "random"]),
|
||||
},
|
||||
"async_get_recent_assets",
|
||||
"async_get_assets",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
@@ -171,9 +180,28 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
"""Refresh data for this album."""
|
||||
await self.coordinator.async_refresh_now()
|
||||
|
||||
async def async_get_recent_assets(self, count: int = 10) -> ServiceResponse:
|
||||
"""Get recent assets for this album."""
|
||||
assets = await self.coordinator.async_get_recent_assets(count)
|
||||
async def async_get_assets(
|
||||
self,
|
||||
limit: int = 10,
|
||||
favorite_only: bool = False,
|
||||
filter_min_rating: int = 1,
|
||||
order_by: str = "date",
|
||||
order: str = "descending",
|
||||
asset_type: str = "all",
|
||||
min_date: str | None = None,
|
||||
max_date: str | None = None,
|
||||
) -> ServiceResponse:
|
||||
"""Get assets for this album with optional filtering and ordering."""
|
||||
assets = await self.coordinator.async_get_assets(
|
||||
limit=limit,
|
||||
favorite_only=favorite_only,
|
||||
filter_min_rating=filter_min_rating,
|
||||
order_by=order_by,
|
||||
order=order,
|
||||
asset_type=asset_type,
|
||||
min_date=min_date,
|
||||
max_date=max_date,
|
||||
)
|
||||
return {"assets": assets}
|
||||
|
||||
async def async_send_telegram_notification(
|
||||
@@ -188,6 +216,8 @@ 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,
|
||||
send_large_photos_as_documents: bool = False,
|
||||
) -> ServiceResponse:
|
||||
"""Send notification to Telegram.
|
||||
|
||||
@@ -216,6 +246,8 @@ 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,
|
||||
send_large_photos_as_documents=send_large_photos_as_documents,
|
||||
)
|
||||
)
|
||||
return {"success": True, "status": "queued", "message": "Notification queued for background processing"}
|
||||
@@ -231,6 +263,8 @@ 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,
|
||||
send_large_photos_as_documents=send_large_photos_as_documents,
|
||||
)
|
||||
|
||||
async def _execute_telegram_notification(
|
||||
@@ -244,6 +278,8 @@ 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,
|
||||
send_large_photos_as_documents: bool = False,
|
||||
) -> ServiceResponse:
|
||||
"""Execute the Telegram notification (internal method)."""
|
||||
import json
|
||||
@@ -270,18 +306,20 @@ 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, send_large_photos_as_documents
|
||||
)
|
||||
|
||||
# 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, send_large_photos_as_documents
|
||||
)
|
||||
|
||||
async def _send_telegram_message(
|
||||
@@ -332,6 +370,104 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
_LOGGER.error("Telegram message send failed: %s", err)
|
||||
return {"success": False, "error": str(err)}
|
||||
|
||||
def _log_telegram_error(
|
||||
self,
|
||||
error_code: int | None,
|
||||
description: str,
|
||||
data: bytes | None = None,
|
||||
media_type: str = "photo",
|
||||
) -> None:
|
||||
"""Log detailed Telegram API error with diagnostics.
|
||||
|
||||
Args:
|
||||
error_code: Telegram error code
|
||||
description: Error description from Telegram
|
||||
data: Media data bytes (optional, for size diagnostics)
|
||||
media_type: Type of media (photo/video)
|
||||
"""
|
||||
error_msg = f"Telegram API error ({error_code}): {description}"
|
||||
|
||||
# Add diagnostic information based on error type
|
||||
if data:
|
||||
error_msg += f" | Media size: {len(data)} bytes ({len(data) / (1024 * 1024):.2f} MB)"
|
||||
|
||||
# Check dimensions for photos
|
||||
if media_type == "photo":
|
||||
try:
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
img = Image.open(io.BytesIO(data))
|
||||
width, height = img.size
|
||||
dimension_sum = width + height
|
||||
error_msg += f" | Dimensions: {width}x{height} (sum={dimension_sum})"
|
||||
|
||||
# Highlight limit violations
|
||||
if len(data) > TELEGRAM_MAX_PHOTO_SIZE:
|
||||
error_msg += f" | EXCEEDS size limit ({TELEGRAM_MAX_PHOTO_SIZE / (1024 * 1024):.0f} MB)"
|
||||
if dimension_sum > TELEGRAM_MAX_DIMENSION_SUM:
|
||||
error_msg += f" | EXCEEDS dimension limit ({TELEGRAM_MAX_DIMENSION_SUM})"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Provide suggestions based on error description
|
||||
suggestions = []
|
||||
if "dimension" in description.lower() or "PHOTO_INVALID_DIMENSIONS" in description:
|
||||
suggestions.append("Photo dimensions too large - consider setting send_large_photos_as_documents=true")
|
||||
elif "too large" in description.lower() or error_code == 413:
|
||||
suggestions.append("File size too large - consider setting send_large_photos_as_documents=true or max_asset_data_size to skip large files")
|
||||
elif "entity too large" in description.lower():
|
||||
suggestions.append("Request entity too large - reduce max_group_size or set max_asset_data_size")
|
||||
|
||||
if suggestions:
|
||||
error_msg += f" | Suggestions: {'; '.join(suggestions)}"
|
||||
|
||||
_LOGGER.error(error_msg)
|
||||
|
||||
def _check_telegram_photo_limits(
|
||||
self,
|
||||
data: bytes,
|
||||
) -> tuple[bool, str | None, int | None, int | None]:
|
||||
"""Check if photo data exceeds Telegram photo limits.
|
||||
|
||||
Telegram limits for photos:
|
||||
- Max file size: 10 MB
|
||||
- Max dimension sum: ~10,000 pixels (width + height)
|
||||
|
||||
Returns:
|
||||
Tuple of (exceeds_limits, reason, width, height)
|
||||
- exceeds_limits: True if photo exceeds limits
|
||||
- reason: Human-readable reason (None if within limits)
|
||||
- width: Image width in pixels (None if PIL not available)
|
||||
- height: Image height in pixels (None if PIL not available)
|
||||
"""
|
||||
# Check file size
|
||||
if len(data) > TELEGRAM_MAX_PHOTO_SIZE:
|
||||
return True, f"size {len(data)} bytes exceeds {TELEGRAM_MAX_PHOTO_SIZE} bytes limit", None, None
|
||||
|
||||
# Try to check dimensions using PIL
|
||||
try:
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
img = Image.open(io.BytesIO(data))
|
||||
width, height = img.size
|
||||
dimension_sum = width + height
|
||||
|
||||
if dimension_sum > TELEGRAM_MAX_DIMENSION_SUM:
|
||||
return True, f"dimensions {width}x{height} (sum={dimension_sum}) exceed {TELEGRAM_MAX_DIMENSION_SUM} limit", width, height
|
||||
|
||||
return False, None, width, height
|
||||
except ImportError:
|
||||
# PIL not available, can't check dimensions
|
||||
_LOGGER.debug("PIL not available, skipping dimension check")
|
||||
return False, None, None, None
|
||||
except Exception as e:
|
||||
# Failed to check dimensions
|
||||
_LOGGER.debug("Failed to check photo dimensions: %s", e)
|
||||
return False, None, None, None
|
||||
|
||||
|
||||
async def _send_telegram_photo(
|
||||
self,
|
||||
session: Any,
|
||||
@@ -341,6 +477,8 @@ 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,
|
||||
send_large_photos_as_documents: bool = False,
|
||||
) -> ServiceResponse:
|
||||
"""Send a single photo to Telegram."""
|
||||
import aiohttp
|
||||
@@ -361,6 +499,37 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
data = await resp.read()
|
||||
_LOGGER.debug("Downloaded photo: %d bytes", len(data))
|
||||
|
||||
# Check if photo exceeds max size limit (user-defined 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,
|
||||
}
|
||||
|
||||
# Check if photo exceeds Telegram's photo limits
|
||||
exceeds_limits, reason, width, height = self._check_telegram_photo_limits(data)
|
||||
if exceeds_limits:
|
||||
if send_large_photos_as_documents:
|
||||
# Send as document instead
|
||||
_LOGGER.info("Photo %s, sending as document", reason)
|
||||
return await self._send_telegram_document(
|
||||
session, token, chat_id, data, "photo.jpg",
|
||||
caption, reply_to_message_id, parse_mode
|
||||
)
|
||||
else:
|
||||
# Skip oversized photo
|
||||
_LOGGER.warning("Photo %s, skipping (set send_large_photos_as_documents=true to send as document)", reason)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Photo {reason}",
|
||||
"skipped": True,
|
||||
}
|
||||
|
||||
# Build multipart form
|
||||
form = FormData()
|
||||
form.add_field("chat_id", chat_id)
|
||||
@@ -386,7 +555,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
"message_id": result.get("result", {}).get("message_id"),
|
||||
}
|
||||
else:
|
||||
_LOGGER.error("Telegram API error: %s", result)
|
||||
# Log detailed error with diagnostics
|
||||
self._log_telegram_error(
|
||||
error_code=result.get("error_code"),
|
||||
description=result.get("description", "Unknown Telegram error"),
|
||||
data=data,
|
||||
media_type="photo",
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.get("description", "Unknown Telegram error"),
|
||||
@@ -405,6 +580,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 +601,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)
|
||||
@@ -450,7 +638,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
"message_id": result.get("result", {}).get("message_id"),
|
||||
}
|
||||
else:
|
||||
_LOGGER.error("Telegram API error: %s", result)
|
||||
# Log detailed error with diagnostics
|
||||
self._log_telegram_error(
|
||||
error_code=result.get("error_code"),
|
||||
description=result.get("description", "Unknown Telegram error"),
|
||||
data=data,
|
||||
media_type="video",
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.get("description", "Unknown Telegram error"),
|
||||
@@ -460,6 +654,63 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
_LOGGER.error("Telegram video upload failed: %s", err)
|
||||
return {"success": False, "error": str(err)}
|
||||
|
||||
async def _send_telegram_document(
|
||||
self,
|
||||
session: Any,
|
||||
token: str,
|
||||
chat_id: str,
|
||||
data: bytes,
|
||||
filename: str = "photo.jpg",
|
||||
caption: str | None = None,
|
||||
reply_to_message_id: int | None = None,
|
||||
parse_mode: str = "HTML",
|
||||
) -> ServiceResponse:
|
||||
"""Send a photo as a document to Telegram (for oversized photos)."""
|
||||
import aiohttp
|
||||
from aiohttp import FormData
|
||||
|
||||
try:
|
||||
# Build multipart form
|
||||
form = FormData()
|
||||
form.add_field("chat_id", chat_id)
|
||||
form.add_field("document", data, filename=filename, content_type="image/jpeg")
|
||||
form.add_field("parse_mode", parse_mode)
|
||||
|
||||
if caption:
|
||||
form.add_field("caption", caption)
|
||||
|
||||
if reply_to_message_id:
|
||||
form.add_field("reply_to_message_id", str(reply_to_message_id))
|
||||
|
||||
# Send to Telegram
|
||||
telegram_url = f"https://api.telegram.org/bot{token}/sendDocument"
|
||||
|
||||
_LOGGER.debug("Uploading oversized photo as document to Telegram (%d bytes)", len(data))
|
||||
async with session.post(telegram_url, data=form) as response:
|
||||
result = await response.json()
|
||||
_LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok"))
|
||||
if response.status == 200 and result.get("ok"):
|
||||
return {
|
||||
"success": True,
|
||||
"message_id": result.get("result", {}).get("message_id"),
|
||||
}
|
||||
else:
|
||||
# Log detailed error with diagnostics
|
||||
self._log_telegram_error(
|
||||
error_code=result.get("error_code"),
|
||||
description=result.get("description", "Unknown Telegram error"),
|
||||
data=data,
|
||||
media_type="document",
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.get("description", "Unknown Telegram error"),
|
||||
"error_code": result.get("error_code"),
|
||||
}
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Telegram document upload failed: %s", err)
|
||||
return {"success": False, "error": str(err)}
|
||||
|
||||
async def _send_telegram_media_group(
|
||||
self,
|
||||
session: Any,
|
||||
@@ -471,6 +722,8 @@ 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,
|
||||
send_large_photos_as_documents: bool = False,
|
||||
) -> ServiceResponse:
|
||||
"""Send media URLs to Telegram as media group(s).
|
||||
|
||||
@@ -511,12 +764,13 @@ 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, send_large_photos_as_documents
|
||||
)
|
||||
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"):
|
||||
@@ -529,7 +783,10 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
|
||||
_LOGGER.debug("Sending chunk %d/%d as media group (%d items)", chunk_idx + 1, len(chunks), len(chunk))
|
||||
|
||||
# Download all media files for this chunk
|
||||
media_files: list[tuple[str, bytes, str]] = []
|
||||
media_files: list[tuple[str, bytes, str]] = [] # (type, data, filename)
|
||||
oversized_photos: list[tuple[bytes, str | None]] = [] # For send_large_photos_as_documents=true
|
||||
skipped_count = 0
|
||||
|
||||
for i, item in enumerate(chunk):
|
||||
url = item.get("url")
|
||||
media_type = item.get("type", "photo")
|
||||
@@ -555,72 +812,139 @@ 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_asset_data_size limit (user-defined limit for skipping)
|
||||
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
|
||||
|
||||
# For photos, check Telegram limits
|
||||
if media_type == "photo":
|
||||
exceeds_limits, reason, width, height = self._check_telegram_photo_limits(data)
|
||||
if exceeds_limits:
|
||||
if send_large_photos_as_documents:
|
||||
# Separate this photo to send as document later
|
||||
# Caption only on first item of first chunk
|
||||
photo_caption = caption if chunk_idx == 0 and i == 0 and len(media_files) == 0 else None
|
||||
oversized_photos.append((data, photo_caption))
|
||||
_LOGGER.info("Photo %d %s, will send as document", i, reason)
|
||||
continue
|
||||
else:
|
||||
# Skip oversized photo
|
||||
_LOGGER.warning("Photo %d %s, skipping (set send_large_photos_as_documents=true to send as document)", i, reason)
|
||||
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}",
|
||||
}
|
||||
|
||||
# Build multipart form
|
||||
form = FormData()
|
||||
form.add_field("chat_id", chat_id)
|
||||
# Skip this chunk if all files were filtered out
|
||||
if not media_files and not oversized_photos:
|
||||
_LOGGER.info("Chunk %d/%d: all %d media items skipped",
|
||||
chunk_idx + 1, len(chunks), len(chunk))
|
||||
continue
|
||||
|
||||
# Only use reply_to_message_id for the first chunk
|
||||
if chunk_idx == 0 and reply_to_message_id:
|
||||
form.add_field("reply_to_message_id", str(reply_to_message_id))
|
||||
# Send media group if we have normal-sized files
|
||||
if media_files:
|
||||
# Build multipart form
|
||||
form = FormData()
|
||||
form.add_field("chat_id", chat_id)
|
||||
|
||||
# Build media JSON with attach:// references
|
||||
media_json = []
|
||||
for i, (media_type, data, filename) in enumerate(media_files):
|
||||
attach_name = f"file{i}"
|
||||
media_item: dict[str, Any] = {
|
||||
"type": media_type,
|
||||
"media": f"attach://{attach_name}",
|
||||
}
|
||||
# Only add caption to the first item of the first chunk
|
||||
if chunk_idx == 0 and i == 0 and caption:
|
||||
media_item["caption"] = caption
|
||||
media_item["parse_mode"] = parse_mode
|
||||
media_json.append(media_item)
|
||||
# Only use reply_to_message_id for the first chunk
|
||||
if chunk_idx == 0 and reply_to_message_id:
|
||||
form.add_field("reply_to_message_id", str(reply_to_message_id))
|
||||
|
||||
content_type = "image/jpeg" if media_type == "photo" else "video/mp4"
|
||||
form.add_field(attach_name, data, filename=filename, content_type=content_type)
|
||||
# Build media JSON with attach:// references
|
||||
media_json = []
|
||||
for i, (media_type, data, filename) in enumerate(media_files):
|
||||
attach_name = f"file{i}"
|
||||
media_item: dict[str, Any] = {
|
||||
"type": media_type,
|
||||
"media": f"attach://{attach_name}",
|
||||
}
|
||||
# Only add caption to the first item of the first chunk (if no oversized photos with caption)
|
||||
if chunk_idx == 0 and i == 0 and caption and not oversized_photos:
|
||||
media_item["caption"] = caption
|
||||
media_item["parse_mode"] = parse_mode
|
||||
media_json.append(media_item)
|
||||
|
||||
form.add_field("media", json.dumps(media_json))
|
||||
content_type = "image/jpeg" if media_type == "photo" else "video/mp4"
|
||||
form.add_field(attach_name, data, filename=filename, content_type=content_type)
|
||||
|
||||
# Send to Telegram
|
||||
telegram_url = f"https://api.telegram.org/bot{token}/sendMediaGroup"
|
||||
form.add_field("media", json.dumps(media_json))
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Uploading media group chunk %d/%d (%d files) to Telegram",
|
||||
chunk_idx + 1, len(chunks), len(media_files))
|
||||
async with session.post(telegram_url, data=form) as response:
|
||||
result = await response.json()
|
||||
_LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok"))
|
||||
if response.status == 200 and result.get("ok"):
|
||||
chunk_message_ids = [
|
||||
msg.get("message_id") for msg in result.get("result", [])
|
||||
]
|
||||
all_message_ids.extend(chunk_message_ids)
|
||||
else:
|
||||
_LOGGER.error("Telegram API error for chunk %d: %s", chunk_idx + 1, result)
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.get("description", "Unknown Telegram error"),
|
||||
"error_code": result.get("error_code"),
|
||||
"failed_at_chunk": chunk_idx + 1,
|
||||
}
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Telegram upload failed for chunk %d: %s", chunk_idx + 1, err)
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(err),
|
||||
"failed_at_chunk": chunk_idx + 1,
|
||||
}
|
||||
# Send to Telegram
|
||||
telegram_url = f"https://api.telegram.org/bot{token}/sendMediaGroup"
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Uploading media group chunk %d/%d (%d files) to Telegram",
|
||||
chunk_idx + 1, len(chunks), len(media_files))
|
||||
async with session.post(telegram_url, data=form) as response:
|
||||
result = await response.json()
|
||||
_LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok"))
|
||||
if response.status == 200 and result.get("ok"):
|
||||
chunk_message_ids = [
|
||||
msg.get("message_id") for msg in result.get("result", [])
|
||||
]
|
||||
all_message_ids.extend(chunk_message_ids)
|
||||
else:
|
||||
# Log detailed error for media group with total size info
|
||||
total_size = sum(len(d) for _, d, _ in media_files)
|
||||
_LOGGER.error(
|
||||
"Telegram API error for chunk %d/%d: %s | Media count: %d | Total size: %d bytes (%.2f MB)",
|
||||
chunk_idx + 1, len(chunks),
|
||||
result.get("description", "Unknown Telegram error"),
|
||||
len(media_files),
|
||||
total_size,
|
||||
total_size / (1024 * 1024)
|
||||
)
|
||||
# Log detailed diagnostics for the first photo in the group
|
||||
for media_type, data, _ in media_files:
|
||||
if media_type == "photo":
|
||||
self._log_telegram_error(
|
||||
error_code=result.get("error_code"),
|
||||
description=result.get("description", "Unknown Telegram error"),
|
||||
data=data,
|
||||
media_type="photo",
|
||||
)
|
||||
break # Only log details for first photo
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.get("description", "Unknown Telegram error"),
|
||||
"error_code": result.get("error_code"),
|
||||
"failed_at_chunk": chunk_idx + 1,
|
||||
}
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Telegram upload failed for chunk %d: %s", chunk_idx + 1, err)
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(err),
|
||||
"failed_at_chunk": chunk_idx + 1,
|
||||
}
|
||||
|
||||
# Send oversized photos as documents
|
||||
for i, (data, photo_caption) in enumerate(oversized_photos):
|
||||
_LOGGER.debug("Sending oversized photo %d/%d as document", i + 1, len(oversized_photos))
|
||||
result = await self._send_telegram_document(
|
||||
session, token, chat_id, data, f"photo_{i}.jpg",
|
||||
photo_caption, None, parse_mode
|
||||
)
|
||||
if result.get("success"):
|
||||
all_message_ids.append(result.get("message_id"))
|
||||
else:
|
||||
_LOGGER.error("Failed to send oversized photo as document: %s", result.get("error"))
|
||||
# Continue with other photos even if one fails
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
@@ -6,17 +6,17 @@ refresh:
|
||||
integration: immich_album_watcher
|
||||
domain: sensor
|
||||
|
||||
get_recent_assets:
|
||||
name: Get Recent Assets
|
||||
description: Get the most recent assets from the targeted album.
|
||||
get_assets:
|
||||
name: Get Assets
|
||||
description: Get assets from the targeted album with optional filtering and ordering.
|
||||
target:
|
||||
entity:
|
||||
integration: immich_album_watcher
|
||||
domain: sensor
|
||||
fields:
|
||||
count:
|
||||
name: Count
|
||||
description: Number of recent assets to return (1-100).
|
||||
limit:
|
||||
name: Limit
|
||||
description: Maximum number of assets to return (1-100).
|
||||
required: false
|
||||
default: 10
|
||||
selector:
|
||||
@@ -24,6 +24,77 @@ get_recent_assets:
|
||||
min: 1
|
||||
max: 100
|
||||
mode: slider
|
||||
favorite_only:
|
||||
name: Favorite Only
|
||||
description: Filter to show only favorite assets.
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
filter_min_rating:
|
||||
name: Minimum Rating
|
||||
description: Minimum rating for assets (1-5). Set to filter by rating.
|
||||
required: false
|
||||
default: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 5
|
||||
mode: slider
|
||||
order_by:
|
||||
name: Order By
|
||||
description: Field to sort assets by.
|
||||
required: false
|
||||
default: "date"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "Date"
|
||||
value: "date"
|
||||
- label: "Rating"
|
||||
value: "rating"
|
||||
- label: "Name"
|
||||
value: "name"
|
||||
order:
|
||||
name: Order
|
||||
description: Sort direction.
|
||||
required: false
|
||||
default: "descending"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "Ascending"
|
||||
value: "ascending"
|
||||
- label: "Descending"
|
||||
value: "descending"
|
||||
- label: "Random"
|
||||
value: "random"
|
||||
asset_type:
|
||||
name: Asset Type
|
||||
description: Filter assets by type (all, photo, or video).
|
||||
required: false
|
||||
default: "all"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "All (no type filtering)"
|
||||
value: "all"
|
||||
- label: "Photos only"
|
||||
value: "photo"
|
||||
- label: "Videos only"
|
||||
value: "video"
|
||||
min_date:
|
||||
name: Minimum Date
|
||||
description: Filter assets created on or after this date (ISO 8601 format, e.g., 2024-01-01 or 2024-01-01T10:30:00).
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
max_date:
|
||||
name: Maximum Date
|
||||
description: Filter assets created on or before this date (ISO 8601 format, e.g., 2024-12-31 or 2024-12-31T23:59:59).
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_telegram_notification:
|
||||
name: Send Telegram Notification
|
||||
@@ -116,3 +187,21 @@ 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
|
||||
send_large_photos_as_documents:
|
||||
name: Send Large Photos As Documents
|
||||
description: How to handle photos exceeding Telegram's limits (10MB or 10000px dimension sum). If true, send as documents. If false, skip oversized photos.
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
@@ -133,13 +133,41 @@
|
||||
"name": "Refresh",
|
||||
"description": "Force an immediate refresh of album data from Immich."
|
||||
},
|
||||
"get_recent_assets": {
|
||||
"name": "Get Recent Assets",
|
||||
"description": "Get the most recent assets from the targeted album.",
|
||||
"get_assets": {
|
||||
"name": "Get Assets",
|
||||
"description": "Get assets from the targeted album with optional filtering and ordering.",
|
||||
"fields": {
|
||||
"count": {
|
||||
"name": "Count",
|
||||
"description": "Number of recent assets to return (1-100)."
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Maximum number of assets to return (1-100)."
|
||||
},
|
||||
"favorite_only": {
|
||||
"name": "Favorite Only",
|
||||
"description": "Filter to show only favorite assets."
|
||||
},
|
||||
"filter_min_rating": {
|
||||
"name": "Minimum Rating",
|
||||
"description": "Minimum rating for assets (1-5)."
|
||||
},
|
||||
"order_by": {
|
||||
"name": "Order By",
|
||||
"description": "Field to sort assets by (date, rating, or name)."
|
||||
},
|
||||
"order": {
|
||||
"name": "Order",
|
||||
"description": "Sort direction (ascending, descending, or random)."
|
||||
},
|
||||
"asset_type": {
|
||||
"name": "Asset Type",
|
||||
"description": "Filter assets by type (all, photo, or video)."
|
||||
},
|
||||
"min_date": {
|
||||
"name": "Minimum Date",
|
||||
"description": "Filter assets created on or after this date (ISO 8601 format)."
|
||||
},
|
||||
"max_date": {
|
||||
"name": "Maximum Date",
|
||||
"description": "Filter assets created on or before this date (ISO 8601 format)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -186,6 +214,14 @@
|
||||
"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."
|
||||
},
|
||||
"send_large_photos_as_documents": {
|
||||
"name": "Send Large Photos As Documents",
|
||||
"description": "How to handle photos exceeding Telegram's limits (10MB or 10000px dimension sum). If true, send as documents. If false, downsize to fit limits."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +133,41 @@
|
||||
"name": "Обновить",
|
||||
"description": "Принудительно обновить данные альбома из Immich."
|
||||
},
|
||||
"get_recent_assets": {
|
||||
"name": "Получить последние файлы",
|
||||
"description": "Получить последние файлы из выбранного альбома.",
|
||||
"get_assets": {
|
||||
"name": "Получить файлы",
|
||||
"description": "Получить файлы из выбранного альбома с возможностью фильтрации и сортировки.",
|
||||
"fields": {
|
||||
"count": {
|
||||
"name": "Количество",
|
||||
"description": "Количество возвращаемых файлов (1-100)."
|
||||
"limit": {
|
||||
"name": "Лимит",
|
||||
"description": "Максимальное количество возвращаемых файлов (1-100)."
|
||||
},
|
||||
"favorite_only": {
|
||||
"name": "Только избранные",
|
||||
"description": "Фильтр для отображения только избранных файлов."
|
||||
},
|
||||
"filter_min_rating": {
|
||||
"name": "Минимальный рейтинг",
|
||||
"description": "Минимальный рейтинг для файлов (1-5)."
|
||||
},
|
||||
"order_by": {
|
||||
"name": "Сортировать по",
|
||||
"description": "Поле для сортировки файлов (date - дата, rating - рейтинг, name - имя)."
|
||||
},
|
||||
"order": {
|
||||
"name": "Порядок",
|
||||
"description": "Направление сортировки (ascending - по возрастанию, descending - по убыванию, random - случайный)."
|
||||
},
|
||||
"asset_type": {
|
||||
"name": "Тип файла",
|
||||
"description": "Фильтровать файлы по типу (all - все, photo - только фото, video - только видео)."
|
||||
},
|
||||
"min_date": {
|
||||
"name": "Минимальная дата",
|
||||
"description": "Фильтровать файлы, созданные в эту дату или после (формат ISO 8601)."
|
||||
},
|
||||
"max_date": {
|
||||
"name": "Максимальная дата",
|
||||
"description": "Фильтровать файлы, созданные в эту дату или до (формат ISO 8601)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -186,6 +214,14 @@
|
||||
"wait_for_response": {
|
||||
"name": "Ждать ответа",
|
||||
"description": "Ждать завершения отправки в Telegram перед возвратом. Установите false для фоновой отправки (автоматизация продолжается немедленно)."
|
||||
},
|
||||
"max_asset_data_size": {
|
||||
"name": "Макс. размер ресурса",
|
||||
"description": "Максимальный размер ресурса в байтах. Ресурсы, превышающие этот лимит, будут пропущены. Оставьте пустым для отсутствия ограничения."
|
||||
},
|
||||
"send_large_photos_as_documents": {
|
||||
"name": "Большие фото как документы",
|
||||
"description": "Как обрабатывать фото, превышающие лимиты Telegram (10МБ или сумма размеров 10000пкс). Если true, отправлять как документы. Если false, уменьшать для соответствия лимитам."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user