From 5b5db363a128f12fd62ebdad79d0c50f14923fc6 Mon Sep 17 00:00:00 2001 From: Todd Lucas Date: Sat, 21 Dec 2024 16:39:37 -0600 Subject: [PATCH] [ 1.0.75 ] * Added service `get_cover_image_file` to get the contents of an image url and transfer the contents to the local file system. This service should only be used to download images for playlists that contain public domain images. It should not be used to download copyright protected images, as that would violate the Spotify Web API Terms of Service. * Updated underlying `spotifywebapiPython` package requirement to version 1.0.129. --- CHANGELOG.md | 5 ++ LICENSE | 2 +- custom_components/spotifyplus/__init__.py | 26 ++++++++++ custom_components/spotifyplus/manifest.json | 4 +- custom_components/spotifyplus/media_player.py | 50 +++++++++++++++++++ custom_components/spotifyplus/services.yaml | 28 +++++++++++ custom_components/spotifyplus/strings.json | 18 +++++++ .../spotifyplus/translations/en.json | 18 +++++++ requirements.txt | 2 +- 9 files changed, 149 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e40fae..2ca7cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Change are listed in reverse chronological order (newest to oldest). +###### [ 1.0.75 ] - 2024/12/21 + + * Added service `get_cover_image_file` to get the contents of an image url and transfer the contents to the local file system. This service should only be used to download images for playlists that contain public domain images. It should not be used to download copyright protected images, as that would violate the Spotify Web API Terms of Service. + * Updated underlying `spotifywebapiPython` package requirement to version 1.0.129. + ###### [ 1.0.74 ] - 2024/12/20 * Updated underlying `spotifywebapiPython` package requirement to version 1.0.128. diff --git a/LICENSE b/LICENSE index 4f39543..6eb993b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 - 2023 Joakim Sørensen @ludeeus +Copyright (c) 2019 - 2025 Todd Lucas @thlucas1 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/custom_components/spotifyplus/__init__.py b/custom_components/spotifyplus/__init__.py index 713a403..13c20f6 100644 --- a/custom_components/spotifyplus/__init__.py +++ b/custom_components/spotifyplus/__init__.py @@ -114,6 +114,7 @@ SERVICE_SPOTIFY_GET_BROWSE_CATEGORYS_LIST:str = 'get_browse_categorys_list' SERVICE_SPOTIFY_GET_CATEGORY_PLAYLISTS:str = 'get_category_playlists' SERVICE_SPOTIFY_GET_CHAPTER:str = 'get_chapter' +SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE:str = 'get_cover_image_file' SERVICE_SPOTIFY_GET_EPISODE:str = 'get_episode' SERVICE_SPOTIFY_GET_EPISODE_FAVORITES:str = 'get_episode_favorites' SERVICE_SPOTIFY_GET_FEATURED_PLAYLISTS:str = 'get_featured_playlists' @@ -428,6 +429,14 @@ } ) +SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE_SCHEMA = vol.Schema( + { + vol.Required("entity_id"): cv.entity_id, + vol.Required("image_url"): cv.string, + vol.Required("output_path"): cv.string, + } +) + SERVICE_SPOTIFY_GET_EPISODE_SCHEMA = vol.Schema( { vol.Required("entity_id"): cv.entity_id, @@ -1253,6 +1262,14 @@ async def service_handle_spotify_command(service: ServiceCall) -> None: _logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name)) await hass.async_add_executor_job(entity.service_spotify_follow_users, ids) + elif service.service == SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE: + + # get cover image file. + image_url = service.data.get("image_url") + output_path = service.data.get("output_path") + _logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name)) + await hass.async_add_executor_job(entity.service_spotify_get_cover_image_file, image_url, output_path) + elif service.service == SERVICE_SPOTIFY_PLAYER_MEDIA_PAUSE: # pause media play. @@ -2554,6 +2571,15 @@ def _GetEntityFromServiceData(hass:HomeAssistant, service:ServiceCall, field_id: supports_response=SupportsResponse.ONLY, ) + _logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE, SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE_SCHEMA) + hass.services.async_register( + DOMAIN, + SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE, + service_handle_spotify_command, + schema=SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + _logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_SPOTIFY_GET_EPISODE, SERVICE_SPOTIFY_GET_EPISODE_SCHEMA) hass.services.async_register( DOMAIN, diff --git a/custom_components/spotifyplus/manifest.json b/custom_components/spotifyplus/manifest.json index 147dc8e..a92c1c7 100644 --- a/custom_components/spotifyplus/manifest.json +++ b/custom_components/spotifyplus/manifest.json @@ -18,10 +18,10 @@ "requests_oauthlib>=1.3.1", "soco>=0.30.4", "smartinspectPython>=3.0.33", - "spotifywebapiPython>=1.0.128", + "spotifywebapiPython>=1.0.129", "urllib3>=1.21.1,<1.27", "zeroconf>=0.132.2" ], - "version": "1.0.74", + "version": "1.0.75", "zeroconf": [ "_spotify-connect._tcp.local." ] } diff --git a/custom_components/spotifyplus/media_player.py b/custom_components/spotifyplus/media_player.py index a82e1e8..f8e7d5b 100644 --- a/custom_components/spotifyplus/media_player.py +++ b/custom_components/spotifyplus/media_player.py @@ -3674,6 +3674,56 @@ def service_spotify_get_chapter( _logsi.LeaveMethod(SILevel.Debug, apiMethodName) + def service_spotify_get_cover_image_file( + self, + imageUrl:str, + outputPath:str, + ) -> None: + """ + Gets the contents of an image url and transfers the contents to the local file system. + + Args: + imageUrl (str | list[ImageObject]): + The cover image url whose contents are to be retrieved. + outputPath (str): + Fully-qualified path to store the downloaded image to. + + The output path supports the replacement of the following keyword parameters: + - `{dotfileextn}` - a "." followed by the file extension based on response content + type (for known types: JPG,PNG,APNG,BMP,GIF - defaults to JPG). + + This method should only be used to download images for playlists that contain + public domain images. It should not be used to download copyright protected images, + as that would violate the Spotify Web API Terms of Service. + """ + apiMethodName:str = 'service_spotify_get_cover_image_file' + apiMethodParms:SIMethodParmListContext = None + + try: + + # trace. + apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName) + apiMethodParms.AppendKeyValue("imageUrl", imageUrl) + apiMethodParms.AppendKeyValue("outputPath", outputPath) + _logsi.LogMethodParmList(SILevel.Verbose, "Spotify Get Cover Image File Service", apiMethodParms) + + # get cover image file. + _logsi.LogVerbose("Retrieving cover image file") + self.data.spotifyClient.GetCoverImageFile(imageUrl, outputPath) + + # the following exceptions have already been logged, so we just need to + # pass them back to HA for display in the log (or service UI). + except SpotifyApiError as ex: + raise ServiceValidationError(ex.Message) + except SpotifyWebApiError as ex: + raise ServiceValidationError(ex.Message) + + finally: + + # trace. + _logsi.LeaveMethod(SILevel.Debug, apiMethodName) + + def service_spotify_get_episode( self, episodeId:str=None, diff --git a/custom_components/spotifyplus/services.yaml b/custom_components/spotifyplus/services.yaml index e52352f..1a002c9 100644 --- a/custom_components/spotifyplus/services.yaml +++ b/custom_components/spotifyplus/services.yaml @@ -963,6 +963,34 @@ get_chapter: selector: text: +get_cover_image_file: + name: Get Cover Image File + description: Gets the contents of an image url and transfers the contents to the local file system. + fields: + entity_id: + name: Entity ID + description: Entity ID of the SpotifyPlus device that will make the request to the Spotify Web API. + example: "media_player.spotifyplus_username" + required: true + selector: + entity: + integration: spotifyplus + domain: media_player + image_url: + name: Image URL + description: The cover image url whose contents are to be retrieved. + example: "https://i.scdn.co/image/ab67616d0000b27316c019c87a927829804caf0b" + required: true + selector: + text: + output_path: + name: Output Path + description: Fully-qualified path to store the downloaded image to. + example: "/config/www/images/cover_file_image.jpg" + required: true + selector: + text: + get_episode: name: Get Episode description: Get Spotify catalog information for a single episode identified by its unique Spotify ID. diff --git a/custom_components/spotifyplus/strings.json b/custom_components/spotifyplus/strings.json index f969dfe..ec137cb 100644 --- a/custom_components/spotifyplus/strings.json +++ b/custom_components/spotifyplus/strings.json @@ -640,6 +640,24 @@ } } }, + "get_cover_image_file": { + "name": "Get Cover Image File", + "description": "Gets the contents of an image url and transfers the contents to the local file system.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Entity ID of the SpotifyPlus device that will make the request to the Spotify Web API." + }, + "image_url": { + "name": "Image URL", + "description": "The cover image url whose contents are to be retrieved." + }, + "output_path": { + "name": "Output Path", + "description": "Fully-qualified path to store the downloaded image to." + } + } + }, "get_episode": { "name": "Get Episode", "description": "Get Spotify catalog information for a single episode.", diff --git a/custom_components/spotifyplus/translations/en.json b/custom_components/spotifyplus/translations/en.json index f969dfe..ec137cb 100644 --- a/custom_components/spotifyplus/translations/en.json +++ b/custom_components/spotifyplus/translations/en.json @@ -640,6 +640,24 @@ } } }, + "get_cover_image_file": { + "name": "Get Cover Image File", + "description": "Gets the contents of an image url and transfers the contents to the local file system.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Entity ID of the SpotifyPlus device that will make the request to the Spotify Web API." + }, + "image_url": { + "name": "Image URL", + "description": "The cover image url whose contents are to be retrieved." + }, + "output_path": { + "name": "Output Path", + "description": "Fully-qualified path to store the downloaded image to." + } + } + }, "get_episode": { "name": "Get Episode", "description": "Get Spotify catalog information for a single episode.", diff --git a/requirements.txt b/requirements.txt index e915b32..2cc9c3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ homeassistant==2024.5.0 ruff==0.1.3 soco>=0.30.4 smartinspectPython>=3.0.33 -spotifywebapiPython>=1.0.128 +spotifywebapiPython>=1.0.129