1 """Media source for Google Photos."""
3 from __future__
import annotations
5 from dataclasses
import dataclass
6 from enum
import StrEnum
8 from typing
import Self, cast
10 from google_photos_library_api.exceptions
import GooglePhotosApiError
11 from google_photos_library_api.model
import Album, MediaItem
23 from .
import GooglePhotosConfigEntry
24 from .const
import DOMAIN, READ_SCOPE
26 _LOGGER = logging.getLogger(__name__)
28 MEDIA_ITEMS_PAGE_SIZE = 100
32 LARGE_IMAGE_SIZE = 2160
45 """Type for a PhotosIdentifier."""
51 def of(cls, name: str) -> PhotosIdentifierType:
52 """Parse a PhotosIdentifierType by string value."""
53 for enum
in PhotosIdentifierType:
54 if enum.value == name:
56 raise ValueError(f
"Invalid PhotosIdentifierType: {name}")
61 """Google Photos item identifier in a media source URL."""
64 """Identifies the account for the media item."""
66 id_type: PhotosIdentifierType |
None =
None
67 """Type of identifier"""
69 media_id: str |
None =
None
70 """Identifies the album or photo contents to show."""
73 """Serialize the identifier as a string."""
74 if self.id_type
is None:
75 return self.config_entry_id
76 return f
"{self.config_entry_id}/{self.id_type}/{self.media_id}"
79 def of(cls, identifier: str) -> Self:
80 """Parse a PhotosIdentifier form a string."""
81 parts = identifier.split(
"/")
85 raise BrowseError(f
"Invalid identifier: {identifier}")
86 return cls(parts[0], PhotosIdentifierType.of(parts[1]), parts[2])
89 def album(cls, config_entry_id: str, media_id: str) -> Self:
90 """Create an album PhotosIdentifier."""
91 return cls(config_entry_id, PhotosIdentifierType.ALBUM, media_id)
94 def photo(cls, config_entry_id: str, media_id: str) -> Self:
95 """Create an album PhotosIdentifier."""
96 return cls(config_entry_id, PhotosIdentifierType.PHOTO, media_id)
100 """Set up Google Photos media source."""
105 """Provide Google Photos as media sources."""
107 name =
"Google Photos"
110 """Initialize Google Photos source."""
115 """Resolve media identifier to a url.
117 This will resolve a specific media item to a url for the full photo or video contents.
120 identifier = PhotosIdentifier.of(item.identifier)
121 except ValueError
as err:
122 raise BrowseError(f
"Could not parse identifier: {item.identifier}")
from err
124 identifier.media_id
is None
125 or identifier.id_type != PhotosIdentifierType.PHOTO
128 f
"Could not resolve identiifer that is not a Photo: {identifier}"
131 client = entry.runtime_data.client
132 media_item = await client.get_media_item(media_item_id=identifier.media_id)
133 if not media_item.mime_type:
134 raise BrowseError(
"Could not determine mime type of media item")
135 if media_item.media_metadata
and (media_item.media_metadata.video
is not None):
138 url =
_media_url(media_item, LARGE_IMAGE_SIZE)
141 mime_type=media_item.mime_type,
145 """Return details about the media source.
147 This renders the multi-level album structure for an account, its albums,
148 or the contents of an album. This will return a BrowseMediaSource with a
149 single level of children at the next level of the hierarchy.
151 if not item.identifier:
153 return BrowseMediaSource(
156 media_class=MediaClass.DIRECTORY,
157 media_content_type=MediaClass.IMAGE,
158 title=
"Google Photos",
161 children_media_class=MediaClass.DIRECTORY,
169 identifier = PhotosIdentifier.of(item.identifier)
171 coordinator = entry.runtime_data
172 client = coordinator.client
175 if identifier.id_type
is None:
176 albums = await coordinator.list_albums()
180 PhotosIdentifier.album(
181 identifier.config_entry_id,
191 identifier.id_type != PhotosIdentifierType.ALBUM
192 or identifier.media_id
is None
194 raise BrowseError(f
"Unsupported identifier: {identifier}")
196 media_items: list[MediaItem] = []
198 async
for media_item_result
in await client.list_media_items(
199 album_id=identifier.media_id, page_size=MEDIA_ITEMS_PAGE_SIZE
201 media_items.extend(media_item_result.media_items)
202 except GooglePhotosApiError
as err:
203 raise BrowseError(f
"Error listing media items: {err}")
from err
207 PhotosIdentifier.photo(identifier.config_entry_id, media_item.id),
210 for media_item
in media_items
215 """Return all config entries that support photo library reads."""
217 for entry
in self.
hasshass.config_entries.async_loaded_entries(DOMAIN):
218 scopes = entry.data[
"token"][
"scope"].split(
" ")
219 if READ_SCOPE
in scopes:
220 entries.append(entry)
224 """Return a config entry with the specified id."""
225 entry = self.
hasshass.config_entries.async_entry_for_domain_unique_id(
226 DOMAIN, config_entry_id
230 f
"Could not find config entry for identifier: {config_entry_id}"
236 config_entry: GooglePhotosConfigEntry,
237 identifier: PhotosIdentifier,
238 ) -> BrowseMediaSource:
239 """Build the root node for a Google Photos account for a config entry."""
240 return BrowseMediaSource(
242 identifier=identifier.as_string(),
243 media_class=MediaClass.DIRECTORY,
244 media_content_type=MediaClass.IMAGE,
245 title=config_entry.title,
252 title: str, identifier: PhotosIdentifier, thumbnail_url: str |
None =
None
253 ) -> BrowseMediaSource:
254 """Build an album node."""
255 return BrowseMediaSource(
257 identifier=identifier.as_string(),
258 media_class=MediaClass.ALBUM,
259 media_content_type=MediaClass.ALBUM,
263 thumbnail=thumbnail_url,
268 identifier: PhotosIdentifier,
269 media_item: MediaItem,
270 ) -> BrowseMediaSource:
271 """Build the node for an individual photo or video."""
272 is_video = media_item.media_metadata
and (
273 media_item.media_metadata.video
is not None
275 return BrowseMediaSource(
277 identifier=identifier.as_string(),
278 media_class=MediaClass.IMAGE
if not is_video
else MediaClass.VIDEO,
279 media_content_type=MediaType.IMAGE
if not is_video
else MediaType.VIDEO,
280 title=media_item.filename,
283 thumbnail=
_media_url(media_item, THUMBNAIL_SIZE),
288 """Return a media item url with the specified max thumbnail size on the longest edge.
290 See https://developers.google.com/photos/library/guides/access-media-items#base-urls
292 return f
"{media_item.base_url}=h{max_size}"
296 """Return a video url for the item.
298 See https://developers.google.com/photos/library/guides/access-media-items#base-urls
300 return f
"{media_item.base_url}=dv"
304 """Return a media item url for the cover photo of the album."""
305 return f
"{album.cover_photo_base_url}=h{max_size}"