Files
haos-hacs-immich-album-watcher/plans/phase-1-core-library.md
alexei.dolgolyov d0783d0b6a
Some checks failed
Validate / Hassfest (push) Has been cancelled
Add shared core library and architecture plans (Phase 1)
Extract HA-independent logic from the integration into packages/core/
as a standalone Python library (immich-watcher-core). This is the first
phase of restructuring the project to support a standalone web app
alongside the existing HAOS integration.

Core library modules:
- models: SharedLinkInfo, AssetInfo, AlbumData, AlbumChange dataclasses
- immich_client: Async Immich API client (aiohttp, session-injected)
- change_detector: Pure function for album change detection
- asset_utils: Filtering, sorting, URL building utilities
- telegram/client: Full Telegram Bot API (text, photo, video, media groups)
- telegram/cache: File ID cache with pluggable storage backend
- telegram/media: Media size checks, URL extraction, group splitting
- notifications/queue: Persistent notification queue
- storage: StorageBackend protocol + JSON file implementation

All modules have zero Home Assistant imports. 50 unit tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:40:08 +03:00

8.8 KiB

Phase 1: Extract Core Library

Status: Not started Parent: 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 [ ]

  • Create packages/core/pyproject.toml with:
    • Name: immich-watcher-core
    • Dependencies: aiohttp, jinja2
    • Optional dev deps: pytest, pytest-asyncio, aioresponses
  • 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:

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