diff --git a/README.md b/README.md index c3de98a..919e8f1 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ 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 @@ -31,7 +33,7 @@ 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 @@ -60,8 +62,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 +103,59 @@ 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): ```yaml -service: immich_album_watcher.get_recent_assets +service: immich_album_watcher.get_assets +target: + entity_id: sensor.album_name_asset_count +data: + count: 10 # Maximum number of assets (1-100) + filter: "favorite" # Options: "none", "favorite", "rating" + filter_min_rating: 4 # Min rating (1-5), used when filter="rating" + order: "descending" # Options: "ascending", "descending", "random" +``` + +**Parameters:** + +- `count` (optional, default: 10): Maximum number of assets to return (1-100) +- `filter` (optional, default: "none"): Filter type + - `"none"`: No filtering, return all assets + - `"favorite"`: Return only favorite assets + - `"rating"`: Return assets with rating >= `filter_min_rating` +- `filter_min_rating` (optional, default: 1): Minimum rating (1-5 stars), used when `filter="rating"` +- `order` (optional, default: "descending"): Sort order by creation date + - `"ascending"`: Oldest first + - `"descending"`: Newest first + - `"random"`: Random order + +**Examples:** + +Get 5 most recent favorite assets: + +```yaml +service: immich_album_watcher.get_assets +target: + entity_id: sensor.album_name_asset_count +data: + count: 5 + filter: "favorite" + 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_count data: count: 10 + filter: "rating" + filter_min_rating: 3 + order: "random" ``` ### Send Telegram Notification diff --git a/custom_components/immich_album_watcher/const.py b/custom_components/immich_album_watcher/const.py index 745c739..df01808 100644 --- a/custom_components/immich_album_watcher/const.py +++ b/custom_components/immich_album_watcher/const.py @@ -57,17 +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_IS_FAVORITE: Final = "asset_is_favorite" -ATTR_ASSET_RATING: Final = "asset_rating" +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" @@ -78,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" diff --git a/custom_components/immich_album_watcher/coordinator.py b/custom_components/immich_album_watcher/coordinator.py index e7440ad..9ac6419 100644 --- a/custom_components/immich_album_watcher/coordinator.py +++ b/custom_components/immich_album_watcher/coordinator.py @@ -45,6 +45,7 @@ from .const import ( ATTR_OLD_SHARED, ATTR_NEW_SHARED, ATTR_SHARED, + ATTR_THUMBNAIL_URL, DOMAIN, EVENT_ALBUM_CHANGED, EVENT_ASSETS_ADDED, @@ -313,35 +314,52 @@ 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, + count: int = 10, + filter: str = "none", + filter_min_rating: int = 1, + order: str = "descending", + ) -> list[dict[str, Any]]: + """Get assets from the album with optional filtering and ordering. + + Args: + count: Maximum number of assets to return (1-100) + filter: Filter type - 'none', 'favorite', or 'rating' + filter_min_rating: Minimum rating for assets (1-5), used when filter='rating' + order: Sort order - 'ascending', 'descending', or 'random' + + 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 assets + assets = list(self.data.assets.values()) + # Apply filtering + if filter == "favorite": + assets = [a for a in assets if a.is_favorite] + elif filter == "rating": + assets = [a for a in assets if a.rating is not None and a.rating >= filter_min_rating] + + # Apply ordering + if order == "random": + import random + random.shuffle(assets) + elif order == "ascending": + assets = sorted(assets, key=lambda a: a.created_at, reverse=False) + else: # descending (default) + assets = sorted(assets, key=lambda a: a.created_at, reverse=True) + + # Limit count + assets = assets[:count] + + # 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, - "is_favorite": asset.is_favorite, - "rating": asset.rating, - "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 @@ -513,6 +531,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 transcoded/preview URL for a photo asset.""" + accessible_links = self._get_accessible_links() + if accessible_links: + return f"{self._url}/api/assets/{asset_id}/original?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}/original?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: @@ -673,28 +753,7 @@ 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, - ATTR_ASSET_IS_FAVORITE: asset.is_favorite, - ATTR_ASSET_RATING: asset.rating, - } - 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 + asset_detail = self._build_asset_detail(asset, include_thumbnail=False) added_assets_detail.append(asset_detail) event_data = { diff --git a/custom_components/immich_album_watcher/sensor.py b/custom_components/immich_album_watcher/sensor.py index ebbc4d3..fa84c69 100644 --- a/custom_components/immich_album_watcher/sensor.py +++ b/custom_components/immich_album_watcher/sensor.py @@ -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,20 @@ 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, + count: int = 10, + filter: str = "none", + filter_min_rating: int = 1, + order: str = "descending", + ) -> ServiceResponse: + """Get assets for this album with optional filtering and ordering.""" + assets = await self.coordinator.async_get_assets( + count=count, + filter=filter, + filter_min_rating=filter_min_rating, + order=order, + ) return {"assets": assets} async def async_send_telegram_notification( @@ -342,6 +362,60 @@ 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, @@ -359,9 +433,6 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se - width: Image width in pixels (None if PIL not available) - height: Image height in pixels (None if PIL not available) """ - TELEGRAM_MAX_PHOTO_SIZE = 10 * 1024 * 1024 # 10 MB - TELEGRAM_MAX_DIMENSION_SUM = 10000 - # 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 @@ -388,73 +459,6 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se _LOGGER.debug("Failed to check photo dimensions: %s", e) return False, None, None, None - def _downsize_photo( - self, - data: bytes, - max_dimension_sum: int = 10000, - max_file_size: int = 9 * 1024 * 1024, # 9 MB to be safe - ) -> tuple[bytes | None, str | None]: - """Downsize photo to fit within Telegram limits. - - Args: - data: Original photo bytes - max_dimension_sum: Maximum sum of width + height - max_file_size: Maximum file size in bytes - - Returns: - Tuple of (downsized_data, error) - - downsized_data: Downsized photo bytes (None if failed) - - error: Error message (None if successful) - """ - try: - from PIL import Image - import io - - img = Image.open(io.BytesIO(data)) - width, height = img.size - dimension_sum = width + height - - # Calculate scale factor based on dimensions - if dimension_sum > max_dimension_sum: - scale = max_dimension_sum / dimension_sum - new_width = int(width * scale) - new_height = int(height * scale) - - _LOGGER.info( - "Downsizing photo from %dx%d to %dx%d to fit Telegram limits", - width, height, new_width, new_height - ) - - # Resize with high-quality Lanczos resampling - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # Save with progressive quality reduction until size fits - output = io.BytesIO() - quality = 95 - - while quality >= 50: - output.seek(0) - output.truncate() - img.save(output, format='JPEG', quality=quality, optimize=True) - output_size = output.tell() - - if output_size <= max_file_size: - _LOGGER.debug( - "Photo downsized successfully: %d bytes (quality=%d)", - output_size, quality - ) - return output.getvalue(), None - - quality -= 5 - - # If we can't get it small enough, return error - return None, f"Unable to downsize photo below {max_file_size} bytes (final size: {output_size} bytes at quality {quality + 5})" - - except ImportError: - return None, "PIL not available, cannot downsize photo" - except Exception as e: - _LOGGER.error("Failed to downsize photo: %s", e) - return None, f"Failed to downsize photo: {e}" async def _send_telegram_photo( self, @@ -510,16 +514,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se caption, reply_to_message_id, parse_mode ) else: - # Try to downsize the photo - _LOGGER.info("Photo %s, attempting to downsize", reason) - downsized_data, error = self._downsize_photo(data) - if downsized_data: - data = downsized_data - else: - return { - "success": False, - "error": f"Photo exceeds Telegram limits and downsize failed: {error}", - } + # 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() @@ -546,7 +547,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"), @@ -623,7 +630,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"), @@ -674,7 +687,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="document", + ) return { "success": False, "error": result.get("description", "Unknown Telegram error"), @@ -756,7 +775,7 @@ 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 @@ -808,15 +827,10 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se _LOGGER.info("Photo %d %s, will send as document", i, reason) continue else: - # Try to downsize the photo - _LOGGER.info("Photo %d %s, attempting to downsize", i, reason) - downsized_data, error = self._downsize_photo(data) - if downsized_data: - data = downsized_data - else: - _LOGGER.error("Failed to downsize photo %d: %s, skipping", i, error) - skipped_count += 1 - continue + # 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}" @@ -877,7 +891,26 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se ] all_message_ids.extend(chunk_message_ids) else: - _LOGGER.error("Telegram API error for chunk %d: %s", chunk_idx + 1, result) + # 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"), diff --git a/custom_components/immich_album_watcher/services.yaml b/custom_components/immich_album_watcher/services.yaml index 8506321..0e96eb4 100644 --- a/custom_components/immich_album_watcher/services.yaml +++ b/custom_components/immich_album_watcher/services.yaml @@ -6,9 +6,9 @@ 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 @@ -16,7 +16,7 @@ get_recent_assets: fields: count: name: Count - description: Number of recent assets to return (1-100). + description: Maximum number of assets to return (1-100). required: false default: 10 selector: @@ -24,6 +24,44 @@ get_recent_assets: min: 1 max: 100 mode: slider + filter: + name: Filter + description: Filter assets by type (none, favorite, or rating-based). + required: false + default: "none" + selector: + select: + options: + - label: "None (no filtering)" + value: "none" + - label: "Favorites only" + value: "favorite" + - label: "By minimum rating" + value: "rating" + filter_min_rating: + name: Minimum Rating + description: Minimum rating for assets (1-5). Only used when filter is set to 'rating'. + required: false + default: 1 + selector: + number: + min: 1 + max: 5 + mode: slider + order: + name: Order + description: Sort order for assets by creation date. + required: false + default: "descending" + selector: + select: + options: + - label: "Ascending (oldest first)" + value: "ascending" + - label: "Descending (newest first)" + value: "descending" + - label: "Random" + value: "random" send_telegram_notification: name: Send Telegram Notification diff --git a/custom_components/immich_album_watcher/translations/en.json b/custom_components/immich_album_watcher/translations/en.json index 3c1c9cc..a9fae48 100644 --- a/custom_components/immich_album_watcher/translations/en.json +++ b/custom_components/immich_album_watcher/translations/en.json @@ -133,13 +133,25 @@ "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)." + "description": "Maximum number of assets to return (1-100)." + }, + "filter": { + "name": "Filter", + "description": "Filter assets by type (none, favorite, or rating-based)." + }, + "filter_min_rating": { + "name": "Minimum Rating", + "description": "Minimum rating for assets (1-5). Only used when filter is set to 'rating'." + }, + "order": { + "name": "Order", + "description": "Sort order for assets by creation date." } } }, diff --git a/custom_components/immich_album_watcher/translations/ru.json b/custom_components/immich_album_watcher/translations/ru.json index 2e2e61f..848ead7 100644 --- a/custom_components/immich_album_watcher/translations/ru.json +++ b/custom_components/immich_album_watcher/translations/ru.json @@ -133,13 +133,25 @@ "name": "Обновить", "description": "Принудительно обновить данные альбома из Immich." }, - "get_recent_assets": { - "name": "Получить последние файлы", - "description": "Получить последние файлы из выбранного альбома.", + "get_assets": { + "name": "Получить файлы", + "description": "Получить файлы из выбранного альбома с возможностью фильтрации и сортировки.", "fields": { "count": { "name": "Количество", - "description": "Количество возвращаемых файлов (1-100)." + "description": "Максимальное количество возвращаемых файлов (1-100)." + }, + "filter": { + "name": "Фильтр", + "description": "Фильтровать файлы по типу (none - без фильтра, favorite - только избранные, rating - по рейтингу)." + }, + "filter_min_rating": { + "name": "Минимальный рейтинг", + "description": "Минимальный рейтинг для файлов (1-5). Используется только при filter='rating'." + }, + "order": { + "name": "Порядок", + "description": "Порядок сортировки файлов по дате создания." } } },