outadoc / immich-home-assistant

Home Assistant component to display random pictures from your Immich library.
20 stars 7 forks source link

Immich New API change broke the intergration #10

Closed DocGun812 closed 3 weeks ago

DocGun812 commented 1 month ago

Since the new Immich v1.106.4 the changes to underlying API changes, the integration can no longer access Immich server with API key.

Kixel commented 1 month ago

I like to live dangerously too and update Immich asap. Can confirm that the Immich integration now complains about a (500) internal server error.

LZStealth commented 4 weeks ago

See here. In short hub.py needs updating (and HA restarting). https://github.com/outadoc/immich-home-assistant/issues/8

Pending pull request if you wish to update the files yourself. https://github.com/outadoc/immich-home-assistant/pull/9

DocGun812 commented 4 weeks ago

Edit: After another go around, and restarted HA and reloaded Integration a few times, I've managed to get specific album to start showing up, in my case an album called 2023. However, my random "Favorite" is still not showing up.

@LZStealth Thank you for the tip and I have followed your instructions and was able to get pass the internal 500 error and able to reconfigure Immich however, it's not showing any photos. Here's my edited version of the hub.py and I think it's identical to yours

"""Hub for Immich integration."""
from __future__ import annotations

import logging
from urllib.parse import urljoin

import aiohttp

from homeassistant.exceptions import HomeAssistantError

_HEADER_API_KEY = "x-api-key"
_LOGGER = logging.getLogger(__name__)

_ALLOWED_MIME_TYPES = ["image/png", "image/jpeg"]

class ImmichHub:
    """Immich API hub."""

    def __init__(self, host: str, api_key: str) -> None:
        """Initialize."""
        self.host = host
        self.api_key = api_key

    async def authenticate(self) -> bool:
        """Test if we can authenticate with the host."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, "/api/auth/validateToken")
                headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}

                async with session.post(url=url, headers=headers) as response:
                    if response.status != 200:
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        return False

                    auth_result = await response.json()

                    if not auth_result.get("authStatus"):
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        return False

                    return True
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

    async def get_my_user_info(self) -> dict:
        """Get user info."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, "/api/users/me")
                headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}

                async with session.get(url=url, headers=headers) as response:
                    if response.status != 200:
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        raise ApiError()

                    user_info: dict = await response.json()

                    return user_info
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

    async def download_asset(self, asset_id: str) -> bytes | None:
        """Download the asset."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, f"/api/assets/{asset_id}/original")
                headers = {_HEADER_API_KEY: self.api_key}

                async with session.get(url=url, headers=headers) as response:
                    if response.status != 200:
                        _LOGGER.error("Error from API: status=%d", response.status)
                        return None

                    if response.content_type not in _ALLOWED_MIME_TYPES:
                        _LOGGER.error(
                            "MIME type is not supported: %s", response.content_type
                        )
                        return None

                    return await response.read()
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

    async def list_favorite_images(self) -> list[dict]:
        """List all favorite images."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, "/api/search/metadata")
                headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}
                data = {"isFavorite": "true"}
                async with session.post(url=url, headers=headers, data=data) as response:
                    if response.status != 200:
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        raise ApiError()

                    assets: list[dict] = await response.json()

                    filtered_assets: list[dict] = [
                        asset for asset in assets if asset["type"] == "IMAGE"
                    ]

                    return filtered_assets
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

    async def list_all_albums(self) -> list[dict]:
        """List all albums."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, "/api/albums")
                headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}

                async with session.get(url=url, headers=headers) as response:
                    if response.status != 200:
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        raise ApiError()

                    album_list: list[dict] = await response.json()

                    return album_list
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

    async def list_album_images(self, album_id: str) -> list[dict]:
        """List all images in an album."""
        try:
            async with aiohttp.ClientSession() as session:
                url = urljoin(self.host, f"/api/albums/{album_id}")
                headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}

                async with session.get(url=url, headers=headers) as response:
                    if response.status != 200:
                        raw_result = await response.text()
                        _LOGGER.error("Error from API: body=%s", raw_result)
                        raise ApiError()

                    album_info: dict = await response.json()
                    assets: list[dict] = album_info["assets"]

                    filtered_assets: list[dict] = [
                        asset for asset in assets if asset["type"] == "IMAGE"
                    ]

                    return filtered_assets
        except aiohttp.ClientError as exception:
            _LOGGER.error("Error connecting to the API: %s", exception)
            raise CannotConnect from exception

class CannotConnect(HomeAssistantError):
    """Error to indicate we cannot connect."""

class InvalidAuth(HomeAssistantError):
    """Error to indicate there is invalid auth."""

class ApiError(HomeAssistantError):
    """Error to indicate that the API returned an error."""
LZStealth commented 3 weeks ago

Images also needed this section changing (should be around like 107)

assets: list[dict] = await response.json()

to this

favorites = await response.json()
assets: list[dict] = favorites["assets"]["items"]

File is here with the changes if that helps. https://github.com/LZStealth/immich-home-assistant/blob/API-Changes-v1.160.1/custom_components/immich/hub.py

as the format of the response has changed. Mainly due to the old query no longer responding. This is because favourites don't appear to be callable in the same way that albums are.

Otherwise on a quick glance that looks fine to me.

DocGun812 commented 3 weeks ago

@LZStealth you're awesome, that solved it.

outadoc commented 3 weeks ago

Should be fixed by #9 and the new update (check HACS). Thanks again @LZStealth! And thank you all for your reports, sorry I noticed the issue a bit late.