From c07a9e9d593aa57976426ed29e85bc8d00640bfc Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Tue, 3 Sep 2024 04:54:43 -0700
Subject: [PATCH] Add dependency on google-photos-library-api: Change the
 Google Photos client library to a new external package (#125040)

* Change the Google Photos client library to a new external package

* Remove mime type guessing

* Update tests to mock out the client library and iterators

* Update homeassistant/components/google_photos/media_source.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
---
 .../components/google_photos/__init__.py      |  13 +-
 homeassistant/components/google_photos/api.py | 198 ++----------------
 .../components/google_photos/config_flow.py   |  15 +-
 .../components/google_photos/exceptions.py    |   7 -
 .../components/google_photos/manifest.json    |   4 +-
 .../components/google_photos/media_source.py  | 105 +++++-----
 .../components/google_photos/services.py      |  44 +++-
 .../components/google_photos/strings.json     |   6 +
 .../components/google_photos/types.py         |   7 +
 requirements_all.txt                          |   4 +-
 requirements_test_all.txt                     |   4 +-
 tests/components/google_photos/conftest.py    | 127 +++++++----
 .../fixtures/api_not_enabled_response.json    |  17 --
 .../google_photos/fixtures/list_albums.json   |   1 +
 .../google_photos/fixtures/not_dict.json      |   1 -
 .../google_photos/test_config_flow.py         |  45 ++--
 .../google_photos/test_media_source.py        |  58 ++---
 .../components/google_photos/test_services.py |  51 ++---
 18 files changed, 288 insertions(+), 419 deletions(-)
 delete mode 100644 homeassistant/components/google_photos/exceptions.py
 create mode 100644 homeassistant/components/google_photos/types.py
 delete mode 100644 tests/components/google_photos/fixtures/api_not_enabled_response.json
 delete mode 100644 tests/components/google_photos/fixtures/not_dict.json

diff --git a/homeassistant/components/google_photos/__init__.py b/homeassistant/components/google_photos/__init__.py
index ee02c695f16..950995e72c0 100644
--- a/homeassistant/components/google_photos/__init__.py
+++ b/homeassistant/components/google_photos/__init__.py
@@ -3,17 +3,17 @@
 from __future__ import annotations
 
 from aiohttp import ClientError, ClientResponseError
+from google_photos_library_api.api import GooglePhotosLibraryApi
 
-from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
 from homeassistant.helpers import config_entry_oauth2_flow
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
 
 from . import api
 from .const import DOMAIN
 from .services import async_register_services
-
-type GooglePhotosConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]
+from .types import GooglePhotosConfigEntry
 
 __all__ = [
     "DOMAIN",
@@ -29,8 +29,9 @@ async def async_setup_entry(
             hass, entry
         )
     )
-    session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
-    auth = api.AsyncConfigEntryAuth(hass, session)
+    web_session = async_get_clientsession(hass)
+    oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
+    auth = api.AsyncConfigEntryAuth(web_session, oauth_session)
     try:
         await auth.async_get_access_token()
     except ClientResponseError as err:
@@ -41,7 +42,7 @@ async def async_setup_entry(
         raise ConfigEntryNotReady from err
     except ClientError as err:
         raise ConfigEntryNotReady from err
-    entry.runtime_data = auth
+    entry.runtime_data = GooglePhotosLibraryApi(auth)
 
     async_register_services(hass)
 
diff --git a/homeassistant/components/google_photos/api.py b/homeassistant/components/google_photos/api.py
index 0bbb2fe162b..35878efd792 100644
--- a/homeassistant/components/google_photos/api.py
+++ b/homeassistant/components/google_photos/api.py
@@ -1,216 +1,44 @@
 """API for Google Photos bound to Home Assistant OAuth."""
 
-from abc import ABC, abstractmethod
-from functools import partial
-import logging
-from typing import Any, cast
+from typing import cast
 
-from aiohttp.client_exceptions import ClientError
-from google.oauth2.credentials import Credentials
-from googleapiclient.discovery import Resource, build
-from googleapiclient.errors import HttpError
-from googleapiclient.http import HttpRequest
+import aiohttp
+from google_photos_library_api import api
 
 from homeassistant.const import CONF_ACCESS_TOKEN
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
+from homeassistant.helpers import config_entry_oauth2_flow
 
-from .exceptions import GooglePhotosApiError
 
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_PAGE_SIZE = 20
-
-# Only included necessary fields to limit response sizes
-GET_MEDIA_ITEM_FIELDS = (
-    "id,baseUrl,mimeType,filename,mediaMetadata(width,height,photo,video)"
-)
-LIST_MEDIA_ITEM_FIELDS = f"nextPageToken,mediaItems({GET_MEDIA_ITEM_FIELDS})"
-UPLOAD_API = "https://photoslibrary.googleapis.com/v1/uploads"
-LIST_ALBUMS_FIELDS = (
-    "nextPageToken,albums(id,title,coverPhotoBaseUrl,coverPhotoMediaItemId)"
-)
-
-
-class AuthBase(ABC):
-    """Base class for Google Photos authentication library.
-
-    Provides an asyncio interface around the blocking client library.
-    """
-
-    def __init__(
-        self,
-        hass: HomeAssistant,
-    ) -> None:
-        """Initialize Google Photos auth."""
-        self._hass = hass
-
-    @abstractmethod
-    async def async_get_access_token(self) -> str:
-        """Return a valid access token."""
-
-    async def get_user_info(self) -> dict[str, Any]:
-        """Get the user profile info."""
-        service = await self._get_profile_service()
-        cmd: HttpRequest = service.userinfo().get()
-        return await self._execute(cmd)
-
-    async def get_media_item(self, media_item_id: str) -> dict[str, Any]:
-        """Get all MediaItem resources."""
-        service = await self._get_photos_service()
-        cmd: HttpRequest = service.mediaItems().get(
-            mediaItemId=media_item_id, fields=GET_MEDIA_ITEM_FIELDS
-        )
-        return await self._execute(cmd)
-
-    async def list_media_items(
-        self,
-        page_size: int | None = None,
-        page_token: str | None = None,
-        album_id: str | None = None,
-        favorites: bool = False,
-    ) -> dict[str, Any]:
-        """Get all MediaItem resources."""
-        service = await self._get_photos_service()
-        args: dict[str, Any] = {
-            "pageSize": (page_size or DEFAULT_PAGE_SIZE),
-            "pageToken": page_token,
-        }
-        cmd: HttpRequest
-        if album_id is not None or favorites:
-            if album_id is not None:
-                args["albumId"] = album_id
-            if favorites:
-                args["filters"] = {"featureFilter": {"includedFeatures": "FAVORITES"}}
-            cmd = service.mediaItems().search(body=args, fields=LIST_MEDIA_ITEM_FIELDS)
-        else:
-            cmd = service.mediaItems().list(**args, fields=LIST_MEDIA_ITEM_FIELDS)
-        return await self._execute(cmd)
-
-    async def list_albums(
-        self, page_size: int | None = None, page_token: str | None = None
-    ) -> dict[str, Any]:
-        """Get all Album resources."""
-        service = await self._get_photos_service()
-        cmd: HttpRequest = service.albums().list(
-            pageSize=(page_size or DEFAULT_PAGE_SIZE),
-            pageToken=page_token,
-            fields=LIST_ALBUMS_FIELDS,
-        )
-        return await self._execute(cmd)
-
-    async def upload_content(self, content: bytes, mime_type: str) -> str:
-        """Upload media content to the API and return an upload token."""
-        token = await self.async_get_access_token()
-        session = aiohttp_client.async_get_clientsession(self._hass)
-        try:
-            result = await session.post(
-                UPLOAD_API, headers=_upload_headers(token, mime_type), data=content
-            )
-            result.raise_for_status()
-            return await result.text()
-        except ClientError as err:
-            raise GooglePhotosApiError(f"Failed to upload content: {err}") from err
-
-    async def create_media_items(self, upload_tokens: list[str]) -> list[str]:
-        """Create a batch of media items and return the ids."""
-        service = await self._get_photos_service()
-        cmd: HttpRequest = service.mediaItems().batchCreate(
-            body={
-                "newMediaItems": [
-                    {
-                        "simpleMediaItem": {
-                            "uploadToken": upload_token,
-                        }
-                        for upload_token in upload_tokens
-                    }
-                ]
-            }
-        )
-        result = await self._execute(cmd)
-        return [
-            media_item["mediaItem"]["id"]
-            for media_item in result["newMediaItemResults"]
-        ]
-
-    async def _get_photos_service(self) -> Resource:
-        """Get current photos library API resource."""
-        token = await self.async_get_access_token()
-        return await self._hass.async_add_executor_job(
-            partial(
-                build,
-                "photoslibrary",
-                "v1",
-                credentials=Credentials(token=token),  # type: ignore[no-untyped-call]
-                static_discovery=False,
-            )
-        )
-
-    async def _get_profile_service(self) -> Resource:
-        """Get current profile service API resource."""
-        token = await self.async_get_access_token()
-        return await self._hass.async_add_executor_job(
-            partial(build, "oauth2", "v2", credentials=Credentials(token=token))  # type: ignore[no-untyped-call]
-        )
-
-    async def _execute(self, request: HttpRequest) -> dict[str, Any]:
-        try:
-            result = await self._hass.async_add_executor_job(request.execute)
-        except HttpError as err:
-            raise GooglePhotosApiError(
-                f"Google Photos API responded with error ({err.status_code}): {err.reason}"
-            ) from err
-        if not isinstance(result, dict):
-            raise GooglePhotosApiError(
-                f"Google Photos API replied with unexpected response: {result}"
-            )
-        if error := result.get("error"):
-            message = error.get("message", "Unknown Error")
-            raise GooglePhotosApiError(f"Google Photos API response: {message}")
-        return cast(dict[str, Any], result)
-
-
-class AsyncConfigEntryAuth(AuthBase):
+class AsyncConfigEntryAuth(api.AbstractAuth):
     """Provide Google Photos authentication tied to an OAuth2 based config entry."""
 
     def __init__(
         self,
-        hass: HomeAssistant,
+        websession: aiohttp.ClientSession,
         oauth_session: config_entry_oauth2_flow.OAuth2Session,
     ) -> None:
         """Initialize AsyncConfigEntryAuth."""
-        super().__init__(hass)
-        self._oauth_session = oauth_session
+        super().__init__(websession)
+        self._session = oauth_session
 
     async def async_get_access_token(self) -> str:
         """Return a valid access token."""
-        if not self._oauth_session.valid_token:
-            await self._oauth_session.async_ensure_token_valid()
-        return cast(str, self._oauth_session.token[CONF_ACCESS_TOKEN])
+        await self._session.async_ensure_token_valid()
+        return cast(str, self._session.token[CONF_ACCESS_TOKEN])
 
 
-class AsyncConfigFlowAuth(AuthBase):
+class AsyncConfigFlowAuth(api.AbstractAuth):
     """An API client used during the config flow with a fixed token."""
 
     def __init__(
         self,
-        hass: HomeAssistant,
+        websession: aiohttp.ClientSession,
         token: str,
     ) -> None:
         """Initialize ConfigFlowAuth."""
-        super().__init__(hass)
+        super().__init__(websession)
         self._token = token
 
     async def async_get_access_token(self) -> str:
         """Return a valid access token."""
         return self._token
-
-
-def _upload_headers(token: str, mime_type: str) -> dict[str, Any]:
-    """Create the upload headers."""
-    return {
-        "Authorization": f"Bearer {token}",
-        "Content-Type": "application/octet-stream",
-        "X-Goog-Upload-Content-Type": mime_type,
-        "X-Goog-Upload-Protocol": "raw",
-    }
diff --git a/homeassistant/components/google_photos/config_flow.py b/homeassistant/components/google_photos/config_flow.py
index e5378f67ffd..6b025cac6be 100644
--- a/homeassistant/components/google_photos/config_flow.py
+++ b/homeassistant/components/google_photos/config_flow.py
@@ -4,13 +4,15 @@ from collections.abc import Mapping
 import logging
 from typing import Any
 
+from google_photos_library_api.api import GooglePhotosLibraryApi
+from google_photos_library_api.exceptions import GooglePhotosApiError
+
 from homeassistant.config_entries import ConfigFlowResult
 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
-from homeassistant.helpers import config_entry_oauth2_flow
+from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
 
 from . import GooglePhotosConfigEntry, api
 from .const import DOMAIN, OAUTH2_SCOPES
-from .exceptions import GooglePhotosApiError
 
 
 class OAuth2FlowHandler(
@@ -39,7 +41,10 @@ class OAuth2FlowHandler(
 
     async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
         """Create an entry for the flow."""
-        client = api.AsyncConfigFlowAuth(self.hass, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
+        session = aiohttp_client.async_get_clientsession(self.hass)
+        auth = api.AsyncConfigFlowAuth(session, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
+        client = GooglePhotosLibraryApi(auth)
+
         try:
             user_resource_info = await client.get_user_info()
             await client.list_media_items(page_size=1)
@@ -51,7 +56,7 @@ class OAuth2FlowHandler(
         except Exception:
             self.logger.exception("Unknown error occurred")
             return self.async_abort(reason="unknown")
-        user_id = user_resource_info["id"]
+        user_id = user_resource_info.id
 
         if self.reauth_entry:
             if self.reauth_entry.unique_id == user_id:
@@ -62,7 +67,7 @@ class OAuth2FlowHandler(
 
         await self.async_set_unique_id(user_id)
         self._abort_if_unique_id_configured()
-        return self.async_create_entry(title=user_resource_info["name"], data=data)
+        return self.async_create_entry(title=user_resource_info.name, data=data)
 
     async def async_step_reauth(
         self, entry_data: Mapping[str, Any]
diff --git a/homeassistant/components/google_photos/exceptions.py b/homeassistant/components/google_photos/exceptions.py
deleted file mode 100644
index b1a40688677..00000000000
--- a/homeassistant/components/google_photos/exceptions.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""Exceptions for Google Photos api calls."""
-
-from homeassistant.exceptions import HomeAssistantError
-
-
-class GooglePhotosApiError(HomeAssistantError):
-    """Error talking to the Google Photos API."""
diff --git a/homeassistant/components/google_photos/manifest.json b/homeassistant/components/google_photos/manifest.json
index 3fefb6cf610..5ff37135f9a 100644
--- a/homeassistant/components/google_photos/manifest.json
+++ b/homeassistant/components/google_photos/manifest.json
@@ -6,6 +6,6 @@
   "dependencies": ["application_credentials"],
   "documentation": "https://www.home-assistant.io/integrations/google_photos",
   "iot_class": "cloud_polling",
-  "loggers": ["googleapiclient"],
-  "requirements": ["google-api-python-client==2.71.0"]
+  "loggers": ["google_photos_library_api"],
+  "requirements": ["google-photos-library-api==0.8.0"]
 }
diff --git a/homeassistant/components/google_photos/media_source.py b/homeassistant/components/google_photos/media_source.py
index a709dd66a0a..63d66d5a82b 100644
--- a/homeassistant/components/google_photos/media_source.py
+++ b/homeassistant/components/google_photos/media_source.py
@@ -5,6 +5,9 @@ from enum import Enum, StrEnum
 import logging
 from typing import Any, Self, cast
 
+from google_photos_library_api.exceptions import GooglePhotosApiError
+from google_photos_library_api.model import Album, MediaItem
+
 from homeassistant.components.media_player import MediaClass, MediaType
 from homeassistant.components.media_source import (
     BrowseError,
@@ -17,17 +20,12 @@ from homeassistant.core import HomeAssistant
 
 from . import GooglePhotosConfigEntry
 from .const import DOMAIN, READ_SCOPES
-from .exceptions import GooglePhotosApiError
 
 _LOGGER = logging.getLogger(__name__)
 
-# Media Sources do not support paging, so we only show a subset of recent
-# photos when displaying the users library. We fetch a minimum of 50 photos
-# unless we run out, but in pages of 100 at a time given sometimes responses
-# may only contain a handful of items Fetches at least 50 photos.
-MAX_RECENT_PHOTOS = 50
-MAX_ALBUMS = 50
-PAGE_SIZE = 100
+MAX_RECENT_PHOTOS = 100
+MEDIA_ITEMS_PAGE_SIZE = 100
+ALBUM_PAGE_SIZE = 50
 
 THUMBNAIL_SIZE = 256
 LARGE_IMAGE_SIZE = 2160
@@ -158,14 +156,15 @@ class GooglePhotosMediaSource(MediaSource):
         entry = self._async_config_entry(identifier.config_entry_id)
         client = entry.runtime_data
         media_item = await client.get_media_item(media_item_id=identifier.media_id)
-        is_video = media_item["mediaMetadata"].get("video") is not None
+        if not media_item.mime_type:
+            raise BrowseError("Could not determine mime type of media item")
+        if media_item.media_metadata and (media_item.media_metadata.video is not None):
+            url = _video_url(media_item)
+        else:
+            url = _media_url(media_item, LARGE_IMAGE_SIZE)
         return PlayMedia(
-            url=(
-                _video_url(media_item)
-                if is_video
-                else _media_url(media_item, LARGE_IMAGE_SIZE)
-            ),
-            mime_type=media_item["mimeType"],
+            url=url,
+            mime_type=media_item.mime_type,
         )
 
     async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
@@ -199,7 +198,6 @@ class GooglePhotosMediaSource(MediaSource):
 
         source = _build_account(entry, identifier)
         if identifier.id_type is None:
-            result = await client.list_albums(page_size=MAX_ALBUMS)
             source.children = [
                 _build_album(
                     special_album.value.title,
@@ -208,17 +206,27 @@ class GooglePhotosMediaSource(MediaSource):
                     ),
                 )
                 for special_album in SpecialAlbum
-            ] + [
+            ]
+            albums: list[Album] = []
+            try:
+                async for album_result in await client.list_albums(
+                    page_size=ALBUM_PAGE_SIZE
+                ):
+                    albums.extend(album_result.albums)
+            except GooglePhotosApiError as err:
+                raise BrowseError(f"Error listing albums: {err}") from err
+
+            source.children.extend(
                 _build_album(
-                    album["title"],
+                    album.title,
                     PhotosIdentifier.album(
                         identifier.config_entry_id,
-                        album["id"],
+                        album.id,
                     ),
                     _cover_photo_url(album, THUMBNAIL_SIZE),
                 )
-                for album in result["albums"]
-            ]
+                for album in albums
+            )
             return source
 
         if (
@@ -233,28 +241,24 @@ class GooglePhotosMediaSource(MediaSource):
         else:
             list_args = {"album_id": identifier.media_id}
 
-        media_items: list[dict[str, Any]] = []
-        page_token: str | None = None
-        while (
-            not special_album
-            or (max_photos := special_album.value.max_photos) is None
-            or len(media_items) < max_photos
-        ):
-            try:
-                result = await client.list_media_items(
-                    **list_args, page_size=PAGE_SIZE, page_token=page_token
-                )
-            except GooglePhotosApiError as err:
-                raise BrowseError(f"Error listing media items: {err}") from err
-            media_items.extend(result["mediaItems"])
-            page_token = result.get("nextPageToken")
-            if page_token is None:
-                break
+        media_items: list[MediaItem] = []
+        try:
+            async for media_item_result in await client.list_media_items(
+                **list_args, page_size=MEDIA_ITEMS_PAGE_SIZE
+            ):
+                media_items.extend(media_item_result.media_items)
+                if (
+                    special_album
+                    and (max_photos := special_album.value.max_photos)
+                    and len(media_items) > max_photos
+                ):
+                    break
+        except GooglePhotosApiError as err:
+            raise BrowseError(f"Error listing media items: {err}") from err
 
-        # Render the grid of media item results
         source.children = [
             _build_media_item(
-                PhotosIdentifier.photo(identifier.config_entry_id, media_item["id"]),
+                PhotosIdentifier.photo(identifier.config_entry_id, media_item.id),
                 media_item,
             )
             for media_item in media_items
@@ -315,38 +319,41 @@ def _build_album(
 
 
 def _build_media_item(
-    identifier: PhotosIdentifier, media_item: dict[str, Any]
+    identifier: PhotosIdentifier,
+    media_item: MediaItem,
 ) -> BrowseMediaSource:
     """Build the node for an individual photo or video."""
-    is_video = media_item["mediaMetadata"].get("video") is not None
+    is_video = media_item.media_metadata and (
+        media_item.media_metadata.video is not None
+    )
     return BrowseMediaSource(
         domain=DOMAIN,
         identifier=identifier.as_string(),
         media_class=MediaClass.IMAGE if not is_video else MediaClass.VIDEO,
         media_content_type=MediaType.IMAGE if not is_video else MediaType.VIDEO,
-        title=media_item["filename"],
+        title=media_item.filename,
         can_play=is_video,
         can_expand=False,
         thumbnail=_media_url(media_item, THUMBNAIL_SIZE),
     )
 
 
-def _media_url(media_item: dict[str, Any], max_size: int) -> str:
+def _media_url(media_item: MediaItem, max_size: int) -> str:
     """Return a media item url with the specified max thumbnail size on the longest edge.
 
     See https://developers.google.com/photos/library/guides/access-media-items#base-urls
     """
-    return f"{media_item["baseUrl"]}=h{max_size}"
+    return f"{media_item.base_url}=h{max_size}"
 
 
-def _video_url(media_item: dict[str, Any]) -> str:
+def _video_url(media_item: MediaItem) -> str:
     """Return a video url for the item.
 
     See https://developers.google.com/photos/library/guides/access-media-items#base-urls
     """
-    return f"{media_item["baseUrl"]}=dv"
+    return f"{media_item.base_url}=dv"
 
 
-def _cover_photo_url(album: dict[str, Any], max_size: int) -> str:
+def _cover_photo_url(album: Album, max_size: int) -> str:
     """Return a media item url for the cover photo of the album."""
-    return f"{album["coverPhotoBaseUrl"]}=h{max_size}"
+    return f"{album.cover_photo_base_url}=h{max_size}"
diff --git a/homeassistant/components/google_photos/services.py b/homeassistant/components/google_photos/services.py
index 77015d5c700..66aa61e23a4 100644
--- a/homeassistant/components/google_photos/services.py
+++ b/homeassistant/components/google_photos/services.py
@@ -6,9 +6,10 @@ import asyncio
 import mimetypes
 from pathlib import Path
 
+from google_photos_library_api.exceptions import GooglePhotosApiError
+from google_photos_library_api.model import NewMediaItem, SimpleMediaItem
 import voluptuous as vol
 
-from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_FILENAME
 from homeassistant.core import (
     HomeAssistant,
@@ -19,14 +20,8 @@ from homeassistant.core import (
 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
 from homeassistant.helpers import config_validation as cv
 
-from . import api
 from .const import DOMAIN, UPLOAD_SCOPE
-
-type GooglePhotosConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]
-
-__all__ = [
-    "DOMAIN",
-]
+from .types import GooglePhotosConfigEntry
 
 CONF_CONFIG_ENTRY_ID = "config_entry_id"
 
@@ -98,11 +93,38 @@ def async_register_services(hass: HomeAssistant) -> None:
         )
         for mime_type, content in file_results:
             upload_tasks.append(client_api.upload_content(content, mime_type))
-        upload_tokens = await asyncio.gather(*upload_tasks)
-        media_ids = await client_api.create_media_items(upload_tokens)
+        try:
+            upload_results = await asyncio.gather(*upload_tasks)
+        except GooglePhotosApiError as err:
+            raise HomeAssistantError(
+                translation_domain=DOMAIN,
+                translation_key="upload_error",
+                translation_placeholders={"message": str(err)},
+            ) from err
+        try:
+            upload_result = await client_api.create_media_items(
+                [
+                    NewMediaItem(
+                        SimpleMediaItem(upload_token=upload_result.upload_token)
+                    )
+                    for upload_result in upload_results
+                ]
+            )
+        except GooglePhotosApiError as err:
+            raise HomeAssistantError(
+                translation_domain=DOMAIN,
+                translation_key="api_error",
+                translation_placeholders={"message": str(err)},
+            ) from err
         if call.return_response:
             return {
-                "media_items": [{"media_item_id": media_id for media_id in media_ids}]
+                "media_items": [
+                    {
+                        "media_item_id": item_result.media_item.id
+                        for item_result in upload_result.new_media_item_results
+                        if item_result.media_item and item_result.media_item.id
+                    }
+                ]
             }
         return None
 
diff --git a/homeassistant/components/google_photos/strings.json b/homeassistant/components/google_photos/strings.json
index 9e88429124e..bf2809f896f 100644
--- a/homeassistant/components/google_photos/strings.json
+++ b/homeassistant/components/google_photos/strings.json
@@ -45,6 +45,12 @@
     },
     "missing_upload_permission": {
       "message": "Home Assistnt was not granted permission to upload to Google Photos"
+    },
+    "upload_error": {
+      "message": "Failed to upload content: {message}"
+    },
+    "api_error": {
+      "message": "Google Photos API responded with error: {message}"
     }
   },
   "services": {
diff --git a/homeassistant/components/google_photos/types.py b/homeassistant/components/google_photos/types.py
new file mode 100644
index 00000000000..2fe57fe1d15
--- /dev/null
+++ b/homeassistant/components/google_photos/types.py
@@ -0,0 +1,7 @@
+"""Google Photos types."""
+
+from google_photos_library_api.api import GooglePhotosLibraryApi
+
+from homeassistant.config_entries import ConfigEntry
+
+type GooglePhotosConfigEntry = ConfigEntry[GooglePhotosLibraryApi]
diff --git a/requirements_all.txt b/requirements_all.txt
index da902149cfd..19d99787672 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -979,7 +979,6 @@ goalzero==0.2.2
 goodwe==0.3.6
 
 # homeassistant.components.google_mail
-# homeassistant.components.google_photos
 # homeassistant.components.google_tasks
 google-api-python-client==2.71.0
 
@@ -995,6 +994,9 @@ google-generativeai==0.7.2
 # homeassistant.components.nest
 google-nest-sdm==5.0.0
 
+# homeassistant.components.google_photos
+google-photos-library-api==0.8.0
+
 # homeassistant.components.google_travel_time
 googlemaps==2.5.1
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 67986dd3a53..6203d295d78 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -829,7 +829,6 @@ goalzero==0.2.2
 goodwe==0.3.6
 
 # homeassistant.components.google_mail
-# homeassistant.components.google_photos
 # homeassistant.components.google_tasks
 google-api-python-client==2.71.0
 
@@ -845,6 +844,9 @@ google-generativeai==0.7.2
 # homeassistant.components.nest
 google-nest-sdm==5.0.0
 
+# homeassistant.components.google_photos
+google-photos-library-api==0.8.0
+
 # homeassistant.components.google_travel_time
 googlemaps==2.5.1
 
diff --git a/tests/components/google_photos/conftest.py b/tests/components/google_photos/conftest.py
index f7289993258..9dbe85bd25b 100644
--- a/tests/components/google_photos/conftest.py
+++ b/tests/components/google_photos/conftest.py
@@ -1,10 +1,18 @@
 """Test fixtures for Google Photos."""
 
-from collections.abc import Awaitable, Callable, Generator
+from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
 import time
 from typing import Any
-from unittest.mock import Mock, patch
-
+from unittest.mock import AsyncMock, Mock, patch
+
+from google_photos_library_api.api import GooglePhotosLibraryApi
+from google_photos_library_api.model import (
+    Album,
+    ListAlbumResult,
+    ListMediaItemResult,
+    MediaItem,
+    UserInfoResult,
+)
 import pytest
 
 from homeassistant.components.application_credentials import (
@@ -28,6 +36,12 @@ CLIENT_SECRET = "5678"
 FAKE_ACCESS_TOKEN = "some-access-token"
 FAKE_REFRESH_TOKEN = "some-refresh-token"
 EXPIRES_IN = 3600
+USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"
+PHOTOS_BASE_URL = "https://photoslibrary.googleapis.com"
+MEDIA_ITEMS_URL = f"{PHOTOS_BASE_URL}/v1/mediaItems"
+ALBUMS_URL = f"{PHOTOS_BASE_URL}/v1/albums"
+UPLOADS_URL = f"{PHOTOS_BASE_URL}/v1/uploads"
+CREATE_MEDIA_ITEMS_URL = f"{PHOTOS_BASE_URL}/v1/mediaItems:batchCreate"
 
 
 @pytest.fixture(name="expires_at")
@@ -100,56 +114,83 @@ def mock_user_identifier() -> str | None:
     return USER_IDENTIFIER
 
 
-@pytest.fixture(name="setup_api")
-def mock_setup_api(
-    fixture_name: str, user_identifier: str
-) -> Generator[Mock, None, None]:
-    """Set up fake Google Photos API responses from fixtures."""
-    with patch("homeassistant.components.google_photos.api.build") as mock:
-        mock.return_value.userinfo.return_value.get.return_value.execute.return_value = {
-            "id": user_identifier,
-            "name": "Test Name",
-        }
-
-        responses = (
-            load_json_array_fixture(fixture_name, DOMAIN) if fixture_name else []
-        )
-
-        queue = list(responses)
-
-        def list_media_items(**kwargs: Any) -> Mock:
-            mock = Mock()
-            mock.execute.return_value = queue.pop(0)
-            return mock
-
-        mock.return_value.mediaItems.return_value.list = list_media_items
-        mock.return_value.mediaItems.return_value.search = list_media_items
+@pytest.fixture(name="api_error")
+def mock_api_error() -> Exception | None:
+    """Provide a json fixture file to load for list media item api responses."""
+    return None
 
-        # Mock a point lookup by reading contents of the fixture above
-        def get_media_item(mediaItemId: str, **kwargs: Any) -> Mock:
-            for response in responses:
-                for media_item in response["mediaItems"]:
-                    if media_item["id"] == mediaItemId:
-                        mock = Mock()
-                        mock.execute.return_value = media_item
-                        return mock
-            return None
 
-        mock.return_value.mediaItems.return_value.get = get_media_item
-        mock.return_value.albums.return_value.list.return_value.execute.return_value = (
-            load_json_object_fixture("list_albums.json", DOMAIN)
-        )
+@pytest.fixture(name="mock_api")
+def mock_client_api(
+    fixture_name: str,
+    user_identifier: str,
+    api_error: Exception,
+) -> Generator[Mock, None, None]:
+    """Set up fake Google Photos API responses from fixtures."""
+    mock_api = AsyncMock(GooglePhotosLibraryApi, autospec=True)
+    mock_api.get_user_info.return_value = UserInfoResult(
+        id=user_identifier,
+        name="Test Name",
+        email="test.name@gmail.com",
+    )
 
-        yield mock
+    responses = load_json_array_fixture(fixture_name, DOMAIN) if fixture_name else []
+
+    async def list_media_items(
+        *args: Any,
+    ) -> AsyncGenerator[ListMediaItemResult, None, None]:
+        for response in responses:
+            mock_list_media_items = Mock(ListMediaItemResult)
+            mock_list_media_items.media_items = [
+                MediaItem.from_dict(media_item) for media_item in response["mediaItems"]
+            ]
+            yield mock_list_media_items
+
+    mock_api.list_media_items.return_value.__aiter__ = list_media_items
+    mock_api.list_media_items.return_value.__anext__ = list_media_items
+    mock_api.list_media_items.side_effect = api_error
+
+    # Mock a point lookup by reading contents of the fixture above
+    async def get_media_item(media_item_id: str, **kwargs: Any) -> Mock:
+        for response in responses:
+            for media_item in response["mediaItems"]:
+                if media_item["id"] == media_item_id:
+                    return MediaItem.from_dict(media_item)
+        return None
+
+    mock_api.get_media_item = get_media_item
+
+    # Emulate an async iterator for returning pages of response objects. We just
+    # return a single page.
+
+    async def list_albums(
+        *args: Any, **kwargs: Any
+    ) -> AsyncGenerator[ListAlbumResult, None, None]:
+        mock_list_album_result = Mock(ListAlbumResult)
+        mock_list_album_result.albums = [
+            Album.from_dict(album)
+            for album in load_json_object_fixture("list_albums.json", DOMAIN)["albums"]
+        ]
+        yield mock_list_album_result
+
+    mock_api.list_albums.return_value.__aiter__ = list_albums
+    mock_api.list_albums.return_value.__anext__ = list_albums
+    mock_api.list_albums.side_effect = api_error
+    return mock_api
 
 
 @pytest.fixture(name="setup_integration")
 async def mock_setup_integration(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
+    mock_api: Mock,
 ) -> Callable[[], Awaitable[bool]]:
     """Fixture to set up the integration."""
     config_entry.add_to_hass(hass)
 
-    await hass.config_entries.async_setup(config_entry.entry_id)
-    await hass.async_block_till_done()
+    with patch(
+        "homeassistant.components.google_photos.GooglePhotosLibraryApi",
+        return_value=mock_api,
+    ):
+        await hass.config_entries.async_setup(config_entry.entry_id)
+        await hass.async_block_till_done()
diff --git a/tests/components/google_photos/fixtures/api_not_enabled_response.json b/tests/components/google_photos/fixtures/api_not_enabled_response.json
deleted file mode 100644
index 8933fcdc7bd..00000000000
--- a/tests/components/google_photos/fixtures/api_not_enabled_response.json
+++ /dev/null
@@ -1,17 +0,0 @@
-[
-  {
-    "error": {
-      "code": 403,
-      "message": "Google Photos API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/library/photoslibrary.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
-      "errors": [
-        {
-          "message": "Google Photos API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/library/photoslibrary.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
-          "domain": "usageLimits",
-          "reason": "accessNotConfigured",
-          "extendedHelp": "https://console.developers.google.com"
-        }
-      ],
-      "status": "PERMISSION_DENIED"
-    }
-  }
-]
diff --git a/tests/components/google_photos/fixtures/list_albums.json b/tests/components/google_photos/fixtures/list_albums.json
index 57f2873715b..7460e1d36f3 100644
--- a/tests/components/google_photos/fixtures/list_albums.json
+++ b/tests/components/google_photos/fixtures/list_albums.json
@@ -3,6 +3,7 @@
     {
       "id": "album-media-id-1",
       "title": "Album title",
+      "productUrl": "http://photos.google.com/album-media-id-1",
       "isWriteable": true,
       "mediaItemsCount": 7,
       "coverPhotoBaseUrl": "http://img.example.com/id3",
diff --git a/tests/components/google_photos/fixtures/not_dict.json b/tests/components/google_photos/fixtures/not_dict.json
deleted file mode 100644
index 05e325337d2..00000000000
--- a/tests/components/google_photos/fixtures/not_dict.json
+++ /dev/null
@@ -1 +0,0 @@
-["not a dictionary"]
diff --git a/tests/components/google_photos/test_config_flow.py b/tests/components/google_photos/test_config_flow.py
index 2564a8ed134..be97d7658c6 100644
--- a/tests/components/google_photos/test_config_flow.py
+++ b/tests/components/google_photos/test_config_flow.py
@@ -4,8 +4,7 @@ from collections.abc import Generator
 from typing import Any
 from unittest.mock import Mock, patch
 
-from googleapiclient.errors import HttpError
-from httplib2 import Response
+from google_photos_library_api.exceptions import GooglePhotosApiError
 import pytest
 
 from homeassistant import config_entries
@@ -20,7 +19,7 @@ from homeassistant.helpers import config_entry_oauth2_flow
 
 from .conftest import EXPIRES_IN, FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN, USER_IDENTIFIER
 
-from tests.common import MockConfigEntry, load_fixture
+from tests.common import MockConfigEntry
 from tests.test_util.aiohttp import AiohttpClientMocker
 from tests.typing import ClientSessionGenerator
 
@@ -37,6 +36,16 @@ def mock_setup_entry() -> Generator[Mock, None, None]:
         yield mock_setup
 
 
+@pytest.fixture(autouse=True)
+def mock_patch_api(mock_api: Mock) -> Generator[None, None, None]:
+    """Fixture to patch the config flow api."""
+    with patch(
+        "homeassistant.components.google_photos.config_flow.GooglePhotosLibraryApi",
+        return_value=mock_api,
+    ):
+        yield
+
+
 @pytest.fixture(name="updated_token_entry", autouse=True)
 def mock_updated_token_entry() -> dict[str, Any]:
     """Fixture to provide any test specific overrides to token data from the oauth token endpoint."""
@@ -60,7 +69,7 @@ def mock_token_request(
     )
 
 
-@pytest.mark.usefixtures("current_request_with_host", "setup_api")
+@pytest.mark.usefixtures("current_request_with_host", "mock_api")
 @pytest.mark.parametrize("fixture_name", ["list_mediaitems.json"])
 async def test_full_flow(
     hass: HomeAssistant,
@@ -126,11 +135,17 @@ async def test_full_flow(
 @pytest.mark.usefixtures(
     "current_request_with_host",
     "setup_credentials",
+    "mock_api",
+)
+@pytest.mark.parametrize(
+    "api_error",
+    [
+        GooglePhotosApiError("some error"),
+    ],
 )
 async def test_api_not_enabled(
     hass: HomeAssistant,
     hass_client_no_auth: ClientSessionGenerator,
-    setup_api: Mock,
 ) -> None:
     """Check flow aborts if api is not enabled."""
     result = await hass.config_entries.flow.async_init(
@@ -160,24 +175,18 @@ async def test_api_not_enabled(
     assert resp.status == 200
     assert resp.headers["content-type"] == "text/html; charset=utf-8"
 
-    setup_api.return_value.mediaItems.return_value.list = Mock()
-    setup_api.return_value.mediaItems.return_value.list.return_value.execute.side_effect = HttpError(
-        Response({"status": "403"}),
-        bytes(load_fixture("google_photos/api_not_enabled_response.json"), "utf-8"),
-    )
     result = await hass.config_entries.flow.async_configure(result["flow_id"])
 
     assert result["type"] is FlowResultType.ABORT
     assert result["reason"] == "access_not_configured"
-    assert result["description_placeholders"]["message"].endswith(
-        "Google Photos API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/library/photoslibrary.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry."
-    )
+    assert result["description_placeholders"]["message"].endswith("some error")
 
 
 @pytest.mark.usefixtures("current_request_with_host", "setup_credentials")
 async def test_general_exception(
     hass: HomeAssistant,
     hass_client_no_auth: ClientSessionGenerator,
+    mock_api: Mock,
 ) -> None:
     """Check flow aborts if exception happens."""
     result = await hass.config_entries.flow.async_init(
@@ -206,17 +215,15 @@ async def test_general_exception(
     assert resp.status == 200
     assert resp.headers["content-type"] == "text/html; charset=utf-8"
 
-    with patch(
-        "homeassistant.components.google_photos.api.build",
-        side_effect=Exception,
-    ):
-        result = await hass.config_entries.flow.async_configure(result["flow_id"])
+    mock_api.list_media_items.side_effect = Exception
+
+    result = await hass.config_entries.flow.async_configure(result["flow_id"])
 
     assert result["type"] is FlowResultType.ABORT
     assert result["reason"] == "unknown"
 
 
-@pytest.mark.usefixtures("current_request_with_host", "setup_api", "setup_integration")
+@pytest.mark.usefixtures("current_request_with_host", "mock_api", "setup_integration")
 @pytest.mark.parametrize("fixture_name", ["list_mediaitems.json"])
 @pytest.mark.parametrize(
     "updated_token_entry",
diff --git a/tests/components/google_photos/test_media_source.py b/tests/components/google_photos/test_media_source.py
index 1028a34aec1..762a4d5ebd1 100644
--- a/tests/components/google_photos/test_media_source.py
+++ b/tests/components/google_photos/test_media_source.py
@@ -1,10 +1,8 @@
 """Test the Google Photos media source."""
 
-from typing import Any
 from unittest.mock import Mock
 
-from googleapiclient.errors import HttpError
-from httplib2 import Response
+from google_photos_library_api.exceptions import GooglePhotosApiError
 import pytest
 
 from homeassistant.components.google_photos.const import DOMAIN, UPLOAD_SCOPE
@@ -46,7 +44,7 @@ async def test_no_config_entries(
     assert not browse.children
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
+@pytest.mark.usefixtures("setup_integration", "mock_api")
 @pytest.mark.parametrize(
     ("scopes"),
     [
@@ -64,7 +62,7 @@ async def test_no_read_scopes(
     assert not browse.children
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
+@pytest.mark.usefixtures("setup_integration", "mock_api")
 @pytest.mark.parametrize(
     ("album_path", "expected_album_title"),
     [
@@ -135,14 +133,14 @@ async def test_browse_albums(
     ] == expected_medias
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
+@pytest.mark.usefixtures("setup_integration", "mock_api")
 async def test_invalid_config_entry(hass: HomeAssistant) -> None:
     """Test browsing to a config entry that does not exist."""
     with pytest.raises(BrowseError, match="Could not find config entry"):
         await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/invalid-config-entry")
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
+@pytest.mark.usefixtures("setup_integration", "mock_api")
 @pytest.mark.parametrize("fixture_name", ["list_mediaitems.json"])
 async def test_browse_invalid_path(hass: HomeAssistant) -> None:
     """Test browsing to a photo is not possible."""
@@ -161,8 +159,8 @@ async def test_browse_invalid_path(hass: HomeAssistant) -> None:
 
 
 @pytest.mark.usefixtures("setup_integration")
-@pytest.mark.parametrize("fixture_name", ["list_mediaitems.json"])
-async def test_invalid_album_id(hass: HomeAssistant, setup_api: Mock) -> None:
+@pytest.mark.parametrize("api_error", [GooglePhotosApiError("some error")])
+async def test_invalid_album_id(hass: HomeAssistant, mock_api: Mock) -> None:
     """Test browsing to an album id that does not exist."""
     browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}")
     assert browse.domain == DOMAIN
@@ -172,11 +170,6 @@ async def test_invalid_album_id(hass: HomeAssistant, setup_api: Mock) -> None:
         (CONFIG_ENTRY_ID, "Account Name")
     ]
 
-    setup_api.return_value.mediaItems.return_value.search = Mock()
-    setup_api.return_value.mediaItems.return_value.search.return_value.execute.side_effect = HttpError(
-        Response({"status": "404"}), b""
-    )
-
     with pytest.raises(BrowseError, match="Error listing media items"):
         await async_browse_media(
             hass, f"{URI_SCHEME}{DOMAIN}/{CONFIG_ENTRY_ID}/a/invalid-album-id"
@@ -201,18 +194,9 @@ async def test_missing_photo_id(
         await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/{identifier}", None)
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
-@pytest.mark.parametrize(
-    "side_effect",
-    [
-        HttpError(Response({"status": "403"}), b""),
-    ],
-)
-async def test_list_media_items_failure(
-    hass: HomeAssistant,
-    setup_api: Any,
-    side_effect: HttpError | Response,
-) -> None:
+@pytest.mark.usefixtures("setup_integration", "mock_api")
+@pytest.mark.parametrize("api_error", [GooglePhotosApiError("some error")])
+async def test_list_albums_failure(hass: HomeAssistant) -> None:
     """Test browsing to an album id that does not exist."""
     browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}")
     assert browse.domain == DOMAIN
@@ -222,24 +206,13 @@ async def test_list_media_items_failure(
         (CONFIG_ENTRY_ID, "Account Name")
     ]
 
-    setup_api.return_value.mediaItems.return_value.list = Mock()
-    setup_api.return_value.mediaItems.return_value.list.return_value.execute.side_effect = side_effect
-
-    with pytest.raises(BrowseError, match="Error listing media items"):
-        await async_browse_media(
-            hass, f"{URI_SCHEME}{DOMAIN}/{CONFIG_ENTRY_ID}/a/recent"
-        )
+    with pytest.raises(BrowseError, match="Error listing albums"):
+        await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{CONFIG_ENTRY_ID}")
 
 
-@pytest.mark.usefixtures("setup_integration", "setup_api")
-@pytest.mark.parametrize(
-    "fixture_name",
-    [
-        "api_not_enabled_response.json",
-        "not_dict.json",
-    ],
-)
-async def test_media_items_error_parsing_response(hass: HomeAssistant) -> None:
+@pytest.mark.usefixtures("setup_integration", "mock_api")
+@pytest.mark.parametrize("api_error", [GooglePhotosApiError("some error")])
+async def test_list_media_items_failure(hass: HomeAssistant) -> None:
     """Test browsing to an album id that does not exist."""
     browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}")
     assert browse.domain == DOMAIN
@@ -248,6 +221,7 @@ async def test_media_items_error_parsing_response(hass: HomeAssistant) -> None:
     assert [(child.identifier, child.title) for child in browse.children] == [
         (CONFIG_ENTRY_ID, "Account Name")
     ]
+
     with pytest.raises(BrowseError, match="Error listing media items"):
         await async_browse_media(
             hass, f"{URI_SCHEME}{DOMAIN}/{CONFIG_ENTRY_ID}/a/recent"
diff --git a/tests/components/google_photos/test_services.py b/tests/components/google_photos/test_services.py
index 198de3295a9..10d57e1d178 100644
--- a/tests/components/google_photos/test_services.py
+++ b/tests/components/google_photos/test_services.py
@@ -1,45 +1,42 @@
 """Tests for Google Photos."""
 
-import http
 from unittest.mock import Mock, patch
 
-from googleapiclient.errors import HttpError
-from httplib2 import Response
+from google_photos_library_api.exceptions import GooglePhotosApiError
+from google_photos_library_api.model import (
+    CreateMediaItemsResult,
+    MediaItem,
+    NewMediaItemResult,
+    Status,
+)
 import pytest
 
-from homeassistant.components.google_photos.api import UPLOAD_API
 from homeassistant.components.google_photos.const import DOMAIN, READ_SCOPES
 from homeassistant.config_entries import ConfigEntryState
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import HomeAssistantError
 
 from tests.common import MockConfigEntry
-from tests.test_util.aiohttp import AiohttpClientMocker
 
 
 @pytest.mark.usefixtures("setup_integration")
 async def test_upload_service(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
-    aioclient_mock: AiohttpClientMocker,
-    setup_api: Mock,
+    mock_api: Mock,
 ) -> None:
     """Test service call to upload content."""
     assert hass.services.has_service(DOMAIN, "upload")
 
-    aioclient_mock.post(UPLOAD_API, text="some-upload-token")
-    setup_api.return_value.mediaItems.return_value.batchCreate.return_value.execute.return_value = {
-        "newMediaItemResults": [
-            {
-                "status": {
-                    "code": 200,
-                },
-                "mediaItem": {
-                    "id": "new-media-item-id-1",
-                },
-            }
+    mock_api.create_media_items.return_value = CreateMediaItemsResult(
+        new_media_item_results=[
+            NewMediaItemResult(
+                upload_token="some-upload-token",
+                status=Status(code=200),
+                media_item=MediaItem(id="new-media-item-id-1"),
+            )
         ]
-    }
+    )
 
     with (
         patch(
@@ -62,6 +59,7 @@ async def test_upload_service(
             blocking=True,
             return_response=True,
         )
+
     assert response == {"media_items": [{"media_item_id": "new-media-item-id-1"}]}
 
 
@@ -157,12 +155,11 @@ async def test_filename_does_not_exist(
 async def test_upload_service_upload_content_failure(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
-    aioclient_mock: AiohttpClientMocker,
-    setup_api: Mock,
+    mock_api: Mock,
 ) -> None:
     """Test service call to upload content."""
 
-    aioclient_mock.post(UPLOAD_API, status=http.HTTPStatus.SERVICE_UNAVAILABLE)
+    mock_api.upload_content.side_effect = GooglePhotosApiError()
 
     with (
         patch(
@@ -192,15 +189,11 @@ async def test_upload_service_upload_content_failure(
 async def test_upload_service_fails_create(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
-    aioclient_mock: AiohttpClientMocker,
-    setup_api: Mock,
+    mock_api: Mock,
 ) -> None:
     """Test service call to upload content."""
 
-    aioclient_mock.post(UPLOAD_API, text="some-upload-token")
-    setup_api.return_value.mediaItems.return_value.batchCreate.return_value.execute.side_effect = HttpError(
-        Response({"status": "403"}), b""
-    )
+    mock_api.create_media_items.side_effect = GooglePhotosApiError()
 
     with (
         patch(
@@ -238,8 +231,6 @@ async def test_upload_service_fails_create(
 async def test_upload_service_no_scope(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
-    aioclient_mock: AiohttpClientMocker,
-    setup_api: Mock,
 ) -> None:
     """Test service call to upload content but the config entry is read-only."""
 
-- 
GitLab