diff --git a/README.md b/README.md index 68c6814..1c43f8a 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,29 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/) | Telegram Bot Token | Bot token for sending media to Telegram (optional) | - | | Telegram Cache TTL | How long to cache uploaded file IDs (hours, 1-168) | 48 | +### External Domain Support + +The integration supports connecting to a local Immich server while using an external domain for user-facing URLs. This is useful when: + +- Your Home Assistant connects to Immich via local network (e.g., `http://192.168.1.100:2283`) +- But you want share links and asset URLs to use your public domain (e.g., `https://photos.example.com`) + +**How it works:** + +1. Configure "External domain" in Immich: **Administration → Settings → Server → External Domain** +2. The integration automatically fetches this setting on startup +3. All user-facing URLs (share links, asset URLs in events) use the external domain +4. API calls and file downloads still use the local connection URL for faster performance + +**Example:** + +- Server URL (in integration config): `http://192.168.1.100:2283` +- External Domain (in Immich settings): `https://photos.example.com` +- Share links in events: `https://photos.example.com/share/...` +- Telegram downloads: via `http://192.168.1.100:2283` (fast local network) + +If no external domain is configured in Immich, all URLs will use the Server URL from the integration configuration. + ## Entities Created (per album) | Entity Type | Name | Description | diff --git a/custom_components/immich_album_watcher/coordinator.py b/custom_components/immich_album_watcher/coordinator.py index 192728e..3112010 100644 --- a/custom_components/immich_album_watcher/coordinator.py +++ b/custom_components/immich_album_watcher/coordinator.py @@ -204,19 +204,39 @@ class AssetInfo: Returns: True if asset is fully processed and not trashed/offline, False otherwise """ + asset_id = data.get("id", "unknown") + asset_type = data.get("type", "unknown") + is_offline = data.get("isOffline", False) + is_trashed = data.get("isTrashed", False) + thumbhash = data.get("thumbhash") + + _LOGGER.debug( + "Asset %s (%s): isOffline=%s, isTrashed=%s, thumbhash=%s", + asset_id, + asset_type, + is_offline, + is_trashed, + bool(thumbhash), + ) + # Exclude offline assets - if data.get("isOffline", False): + if is_offline: + _LOGGER.debug("Asset %s excluded: offline", asset_id) return False # Exclude trashed assets - if data.get("isTrashed", False): + if is_trashed: + _LOGGER.debug("Asset %s excluded: trashed", asset_id) return False # Check if thumbnails have been generated # This works for both photos and videos - Immich always generates thumbnails # Note: The API doesn't expose video transcoding status (encodedVideoPath), # but thumbhash is sufficient since Immich generates thumbnails for all assets - return bool(data.get("thumbhash")) + is_processed = bool(thumbhash) + if not is_processed: + _LOGGER.debug("Asset %s excluded: no thumbhash", asset_id) + return is_processed @dataclass @@ -331,6 +351,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]): self._telegram_cache = telegram_cache self._persisted_asset_ids: set[str] | None = None self._external_domain: str | None = None # Fetched from server config + self._pending_asset_ids: set[str] = set() # Assets detected but not yet processed @property def immich_url(self) -> str: @@ -824,11 +845,16 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]): elif removed_ids and not added_ids: change_type = "assets_removed" - added_assets = [ - album.assets[aid] - for aid in added_ids - if aid in album.assets - ] + added_assets = [] + for aid in added_ids: + if aid not in album.assets: + continue + asset = album.assets[aid] + if asset.is_processed: + added_assets.append(asset) + else: + # Track unprocessed assets for later + self._pending_asset_ids.add(aid) change = AlbumChange( album_id=album.id, @@ -885,12 +911,54 @@ 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 - ] + _LOGGER.debug( + "Change detection: added_ids=%d, removed_ids=%d, pending=%d", + len(added_ids), + len(removed_ids), + len(self._pending_asset_ids), + ) + + # Track new unprocessed assets and collect processed ones + added_assets = [] + for aid in added_ids: + if aid not in new_state.assets: + _LOGGER.debug("Asset %s: not in assets dict", aid) + continue + asset = new_state.assets[aid] + _LOGGER.debug( + "New asset %s (%s): is_processed=%s, filename=%s", + aid, + asset.type, + asset.is_processed, + asset.filename, + ) + if asset.is_processed: + added_assets.append(asset) + else: + # Track unprocessed assets for later + self._pending_asset_ids.add(aid) + _LOGGER.debug("Asset %s added to pending (not yet processed)", aid) + + # Check if any pending assets are now processed + newly_processed = [] + for aid in list(self._pending_asset_ids): + if aid not in new_state.assets: + # Asset was removed, no longer pending + self._pending_asset_ids.discard(aid) + continue + asset = new_state.assets[aid] + if asset.is_processed: + _LOGGER.debug( + "Pending asset %s (%s) is now processed: filename=%s", + aid, + asset.type, + asset.filename, + ) + newly_processed.append(asset) + self._pending_asset_ids.discard(aid) + + # Include newly processed pending assets + added_assets.extend(newly_processed) # Detect metadata changes name_changed = old_state.name != new_state.name