# Phase 1: Extract Core Library **Status**: Not started **Parent**: [primary-plan.md](primary-plan.md) --- ## Goal Extract all HA-independent logic from the integration into `packages/core/` as a standalone Python library (`immich-watcher-core`). This library will be consumed by both the HAOS integration and the standalone server. --- ## Directory Structure ``` packages/core/ pyproject.toml src/immich_watcher_core/ __init__.py constants.py # Event types, attribute names, asset types, defaults models.py # SharedLinkInfo, AssetInfo, AlbumData, AlbumChange immich_client.py # Async Immich API client (aiohttp) change_detector.py # detect_album_changes() pure function asset_utils.py # Asset filtering, sorting, URL building telegram/ __init__.py client.py # TelegramClient - full Bot API operations cache.py # TelegramFileCache with CacheBackend protocol media.py # _split_media_by_upload_size, photo limit checks, URL helpers notifications/ __init__.py queue.py # NotificationQueue with QueueBackend protocol storage.py # CacheBackend/QueueBackend protocols + JSON file implementations tests/ __init__.py test_models.py test_immich_client.py test_change_detector.py test_asset_utils.py test_telegram_client.py test_telegram_cache.py test_notification_queue.py ``` --- ## Tasks ### 1. Package setup `[ ]` - [x] Create `packages/core/pyproject.toml` with: - Name: `immich-watcher-core` - Dependencies: `aiohttp`, `jinja2` - Optional dev deps: `pytest`, `pytest-asyncio`, `aioresponses` - [x] Create `packages/core/src/immich_watcher_core/__init__.py` ### 2. Extract constants `[ ]` **Source**: `custom_components/immich_album_watcher/const.py` Extract to `constants.py`: - Event names: `EVENT_ALBUM_CHANGED`, `EVENT_ASSETS_ADDED`, etc. (L29-34) - Attribute names: all `ATTR_*` constants (L37-77) - Asset types: `ASSET_TYPE_IMAGE`, `ASSET_TYPE_VIDEO` (L80-81) - Defaults: `DEFAULT_SCAN_INTERVAL`, `DEFAULT_TELEGRAM_CACHE_TTL`, `NEW_ASSETS_RESET_DELAY`, `DEFAULT_SHARE_PASSWORD` (L23-27) **Keep in HA const.py**: `DOMAIN`, `CONF_*`, `SUBENTRY_TYPE_ALBUM`, `PLATFORMS`, `SERVICE_*` (HA-specific) ### 3. Extract data models `[ ]` **Source**: `custom_components/immich_album_watcher/coordinator.py` L66-326 Extract to `models.py`: - `SharedLinkInfo` (L67-111) -- dataclass, zero HA deps - `AssetInfo` (L114-249) -- dataclass, uses only `ASSET_TYPE_IMAGE` from constants - `AlbumData` (L252-308) -- dataclass, uses `AssetInfo` + `ASSET_TYPE_*` - `AlbumChange` (L311-325) -- dataclass, pure data All use only stdlib + our constants. No changes needed except import paths. ### 4. Extract Immich API client `[ ]` **Source**: `custom_components/immich_album_watcher/coordinator.py` Extract to `immich_client.py` as `ImmichClient` class: - Constructor takes: `session: aiohttp.ClientSession`, `url: str`, `api_key: str` - `async get_server_config() -> str | None` (L640-668) -- returns external_domain - `async get_users() -> dict[str, str]` (L617-638) -- user_id -> name - `async get_people() -> dict[str, str]` (L593-615) -- person_id -> name - `async get_shared_links(album_id: str) -> list[SharedLinkInfo]` (L670-699) - `async get_album(album_id: str) -> AlbumData | None` (L876-898 fetch part) - `async create_shared_link(album_id, password?) -> bool` (L1199-1243) - `async delete_shared_link(link_id) -> bool` (L1245-1271) - `async set_shared_link_password(link_id, password?) -> bool` (L1144-1176) - `async ping() -> bool` -- validate connection (used by config_flow) - Properties: `url`, `external_url`, `api_key` - Helper: `get_internal_download_url(url)` (L384-400) **Key design**: Accept `aiohttp.ClientSession` via constructor. HA provides `async_get_clientsession(hass)`, standalone creates its own. ### 5. Extract asset utilities `[ ]` **Source**: `custom_components/immich_album_watcher/coordinator.py` L458-591, L761-856 Extract to `asset_utils.py`: - `filter_assets(assets, ...)` -- favorite_only, min_rating, asset_type, date range, memory_date, geolocation (L498-557) - `sort_assets(assets, order_by, order)` (L559-581) - `build_asset_detail(asset, external_url, shared_links, include_thumbnail)` (L801-856) - URL builders: `get_asset_public_url`, `get_asset_download_url`, `get_asset_video_url`, `get_asset_photo_url` (L761-799) - Album URL helpers: `get_public_url`, `get_any_url`, `get_protected_url`, etc. (L709-759) ### 6. Extract change detection `[ ]` **Source**: `custom_components/immich_album_watcher/coordinator.py` L979-1066 Extract to `change_detector.py`: - `detect_album_changes(old_state, new_state, pending_asset_ids) -> tuple[AlbumChange | None, set[str]]` - Pure function: takes two `AlbumData` + pending set, returns change + updated pending set - No HA dependencies ### 7. Extract storage protocols `[ ]` **Source**: `custom_components/immich_album_watcher/storage.py` Extract to `storage.py`: ```python class CacheBackend(Protocol): """Abstract storage backend for caches.""" async def load(self) -> dict[str, Any]: ... async def save(self, data: dict[str, Any]) -> None: ... async def remove(self) -> None: ... class QueueBackend(Protocol): """Abstract storage backend for queues.""" async def load(self) -> dict[str, Any]: ... async def save(self, data: dict[str, Any]) -> None: ... async def remove(self) -> None: ... class JsonFileBackend: """Simple JSON file storage backend (for standalone server).""" def __init__(self, path: Path): ... ``` ### 8. Extract TelegramFileCache `[ ]` **Source**: `custom_components/immich_album_watcher/storage.py` L71-262 Extract to `telegram/cache.py`: - `TelegramFileCache` -- takes `CacheBackend` instead of `hass + Store` - All logic unchanged: TTL mode, thumbhash mode, cleanup, get/set/set_many - Remove `hass`/`Store` imports ### 9. Extract NotificationQueue `[ ]` **Source**: `custom_components/immich_album_watcher/storage.py` L265-328 Extract to `notifications/queue.py`: - `NotificationQueue` -- takes `QueueBackend` instead of `hass + Store` - All logic unchanged: enqueue, get_all, has_pending, remove_indices, clear ### 10. Extract Telegram client `[ ]` **Source**: `custom_components/immich_album_watcher/sensor.py` L55-60, L61-111, L114-170, L455-550, L551-1700+ Extract to `telegram/client.py` as `TelegramClient`: - Constructor: `session: aiohttp.ClientSession`, `bot_token: str`, `cache: TelegramFileCache | None`, `asset_cache: TelegramFileCache | None`, `url_resolver: Callable[[str], str] | None` (for internal URL conversion) - `async send_notification(chat_id, assets?, caption?, ...)` -- main entry (L455-549) - `async send_message(...)` (L551-597) - `async send_chat_action(...)` (L599-665) - `_log_error(...)` (L667-803) - `async send_photo(...)` (L805-958) - `async send_video(...)` (L960-1105) - `async send_document(...)` (L1107-1215) - `async send_media_group(...)` (L1217-end) Extract to `telegram/media.py`: - Constants: `TELEGRAM_API_BASE_URL`, `TELEGRAM_MAX_PHOTO_SIZE`, `TELEGRAM_MAX_VIDEO_SIZE`, `TELEGRAM_MAX_DIMENSION_SUM` (L56-59) - `_is_asset_id(value)` (L76-85) - `_extract_asset_id_from_url(url)` (L88-111) - `_split_media_by_upload_size(media_items, max_upload_size)` (L114-170) ### 11. Write tests `[ ]` - `test_models.py` -- `SharedLinkInfo.from_api_response`, `AssetInfo.from_api_response`, `AlbumData.from_api_response`, processing status checks - `test_immich_client.py` -- Mock aiohttp responses for each API call (use `aioresponses`) - `test_change_detector.py` -- Various change scenarios: add only, remove only, rename, sharing changed, pending assets becoming processed, no change - `test_asset_utils.py` -- Filter/sort combinations, URL building - `test_telegram_cache.py` -- TTL expiry, thumbhash validation, batch set, cleanup - `test_notification_queue.py` -- Enqueue, get_all, remove_indices, clear --- ## Acceptance Criteria - [ ] All extracted modules have zero Home Assistant imports - [ ] `pyproject.toml` is valid and installable (`pip install -e packages/core`) - [ ] All tests pass - [ ] The HAOS integration is NOT modified yet (that's Phase 2) - [ ] No functionality is lost in extraction -- behavior matches original exactly --- ## Key Design Decisions 1. **Session injection**: `ImmichClient` and `TelegramClient` accept `aiohttp.ClientSession` -- no global session creation 2. **Storage protocols**: `CacheBackend`/`QueueBackend` protocols allow HA's `Store` and standalone's SQLite/JSON to satisfy the same interface 3. **URL resolver callback**: Telegram client accepts optional `url_resolver: Callable[[str], str]` for converting external URLs to internal ones (coordinator owns this mapping) 4. **Logging**: Use stdlib `logging` throughout. Consumers configure their own handlers. 5. **No async_get_clientsession**: All HA-specific session management stays in the integration