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>
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.tomlwith:- Name:
immich-watcher-core - Dependencies:
aiohttp,jinja2 - Optional dev deps:
pytest,pytest-asyncio,aioresponses
- Name:
- 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 depsAssetInfo(L114-249) -- dataclass, uses onlyASSET_TYPE_IMAGEfrom constantsAlbumData(L252-308) -- dataclass, usesAssetInfo+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_domainasync get_users() -> dict[str, str](L617-638) -- user_id -> nameasync get_people() -> dict[str, str](L593-615) -- person_id -> nameasync 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-- takesCacheBackendinstead ofhass + Store- All logic unchanged: TTL mode, thumbhash mode, cleanup, get/set/set_many
- Remove
hass/Storeimports
9. Extract NotificationQueue [ ]
Source: custom_components/immich_album_watcher/storage.py L265-328
Extract to notifications/queue.py:
NotificationQueue-- takesQueueBackendinstead ofhass + 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 checkstest_immich_client.py-- Mock aiohttp responses for each API call (useaioresponses)test_change_detector.py-- Various change scenarios: add only, remove only, rename, sharing changed, pending assets becoming processed, no changetest_asset_utils.py-- Filter/sort combinations, URL buildingtest_telegram_cache.py-- TTL expiry, thumbhash validation, batch set, cleanuptest_notification_queue.py-- Enqueue, get_all, remove_indices, clear
Acceptance Criteria
- All extracted modules have zero Home Assistant imports
pyproject.tomlis 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
- Session injection:
ImmichClientandTelegramClientacceptaiohttp.ClientSession-- no global session creation - Storage protocols:
CacheBackend/QueueBackendprotocols allow HA'sStoreand standalone's SQLite/JSON to satisfy the same interface - URL resolver callback: Telegram client accepts optional
url_resolver: Callable[[str], str]for converting external URLs to internal ones (coordinator owns this mapping) - Logging: Use stdlib
loggingthroughout. Consumers configure their own handlers. - No async_get_clientsession: All HA-specific session management stays in the integration