From 9e47edc27790b5009936cf8851b81db0835cbabc Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Tue, 31 Aug 2021 14:55:15 -0400 Subject: [PATCH 01/12] first attempt, not test --- custom_components/spotcast/__init__.py | 9 +++++- custom_components/spotcast/const.py | 2 ++ custom_components/spotcast/helpers.py | 40 ++++++++++++++++++++++++ custom_components/spotcast/services.yaml | 9 +++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 7222966f..3c078c0a 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -17,6 +17,7 @@ CONF_SPOTIFY_ACCOUNT, CONF_SPOTIFY_DEVICE_ID, CONF_SPOTIFY_URI, + CONF_SPOTIFY_SEARCH, CONF_START_VOL, DOMAIN, SCHEMA_PLAYLISTS, @@ -128,6 +129,7 @@ def websocket_handle_castdevices(hass, connection, msg): def start_casting(call): """service called.""" uri = call.data.get(CONF_SPOTIFY_URI) + search = call.data.get(CONF_SPOTIFY_SEARCH) random_song = call.data.get(CONF_RANDOM, False) repeat = call.data.get(CONF_REPEAT, False) shuffle = call.data.get(CONF_SHUFFLE, False) @@ -148,7 +150,7 @@ def start_casting(call): account, spotify_device_id, device_name, entity_id ) - if uri is None or uri.strip() == "": + if (uri is None or uri.strip() == "") and (search is None or search.strip() == ""): _LOGGER.debug("Transfering playback") current_playback = client.current_playback() if current_playback is not None: @@ -159,6 +161,11 @@ def start_casting(call): device_id=spotify_device_id, force_play=force_playback ) else: + + if uri is None or uri.strip() == "": + # get uri from search request + uri = helpers.get_uri_from_search(search, account) + spotcast_controller.play( client, spotify_device_id, diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index 7c8c178b..8c0ef6b7 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -10,6 +10,7 @@ CONF_SPOTIFY_DEVICE_ID = "spotify_device_id" CONF_DEVICE_NAME = "device_name" CONF_SPOTIFY_URI = "uri" +CONF_SPOTIFY_SEARCH CONF_ACCOUNTS = "accounts" CONF_SPOTIFY_ACCOUNT = "account" CONF_FORCE_PLAYBACK = "force_playback" @@ -69,6 +70,7 @@ vol.Optional(CONF_SPOTIFY_DEVICE_ID): cv.string, vol.Optional(CONF_ENTITY_ID): cv.string, vol.Optional(CONF_SPOTIFY_URI): cv.string, + vol.Optional(CONF_SPOTIFY_SEARCH): cv.string, vol.Optional(CONF_SPOTIFY_ACCOUNT): cv.string, vol.Optional(CONF_FORCE_PLAYBACK, default=False): cv.boolean, vol.Optional(CONF_RANDOM, default=False): cv.boolean, diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index bac71409..7b482e91 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -1,10 +1,14 @@ import asyncio import logging +import requests +import urllib +import difflib from functools import partial, wraps from homeassistant.components.cast.media_player import CastDevice from homeassistant.components.spotify.media_player import SpotifyMediaPlayer from homeassistant.helpers import entity_platform +from .spotcast_controller import SpotcastController _LOGGER = logging.getLogger(__name__) @@ -60,3 +64,39 @@ async def run(*args, loop=None, executor=None, **kwargs): return await loop.run_in_executor(executor, pfunc) return run + +def get_uri_from_search(search, account): + + # Get Token + token, expires = SpotcastController.get_token_instance(account).get_spotify_token() + + results = __get_search_results(search, token) + +def __get_search_results(search, token) + BASE_URL = "https://api.spotify.com/v1/search?q=" + SEARCH_TYPES = ["artist", "album", "track", "playlist"] + + search = search.upper() + + headers = { + 'Authorization': 'Bearer {token}'.format(token=token) + } + + results = [] + + for searchType in SEARCH_TYPES + result = requests.get( + BASE_URL + + searchType + "%3A" + urllib.parse.quote(search, safe='') + + "&type=" + searchType + + "&limit=1", + headers=headers).json()[searchType + 's']['items'][0] + + results.append( + { + 'name':result['name'].lower(), + 'uri': result['uri'] + } + ) + + return sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True) \ No newline at end of file diff --git a/custom_components/spotcast/services.yaml b/custom_components/spotcast/services.yaml index 5aad205b..9a46ece2 100644 --- a/custom_components/spotcast/services.yaml +++ b/custom_components/spotcast/services.yaml @@ -18,7 +18,7 @@ start: entity_id: name: "Entity ID" description: "The entity_id of the chromecast mediaplayer. Friendly name MUST match the spotify connect device name (not used together with device_name and spotify_device_id)." - example: "media_player.vardagsrum" + example: "media_player.vardagsrum" required: false selector: entity: @@ -31,6 +31,13 @@ start: required: false selector: text: + + search: + name: "Search" + description: "A Search request to the spotify API. Will play the most relevant search result." + required: false + selector: + text: account: name: "Account" description: "Optionally starts Spotify using an alternative account specified in config." From 1c428294e4247b483c814211ab3a493457d82170 Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Tue, 31 Aug 2021 16:30:45 -0400 Subject: [PATCH 02/12] correction from testing --- custom_components/spotcast/__init__.py | 3 +- custom_components/spotcast/const.py | 2 +- custom_components/spotcast/helpers.py | 43 ++++++++++++++------------ 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 3c078c0a..837bddc9 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -164,7 +164,8 @@ def start_casting(call): if uri is None or uri.strip() == "": # get uri from search request - uri = helpers.get_uri_from_search(search, account) + token, expires = spotcast_controller.get_token_instance(account).get_spotify_token() + uri = helpers.get_uri_from_search(search, token) spotcast_controller.play( client, diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index 8c0ef6b7..a183ab79 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -10,7 +10,7 @@ CONF_SPOTIFY_DEVICE_ID = "spotify_device_id" CONF_DEVICE_NAME = "device_name" CONF_SPOTIFY_URI = "uri" -CONF_SPOTIFY_SEARCH +CONF_SPOTIFY_SEARCH = "search" CONF_ACCOUNTS = "accounts" CONF_SPOTIFY_ACCOUNT = "account" CONF_FORCE_PLAYBACK = "force_playback" diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 7b482e91..ac5afcdd 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -8,7 +8,6 @@ from homeassistant.components.cast.media_player import CastDevice from homeassistant.components.spotify.media_player import SpotifyMediaPlayer from homeassistant.helpers import entity_platform -from .spotcast_controller import SpotcastController _LOGGER = logging.getLogger(__name__) @@ -65,14 +64,13 @@ async def run(*args, loop=None, executor=None, **kwargs): return run -def get_uri_from_search(search, account): - - # Get Token - token, expires = SpotcastController.get_token_instance(account).get_spotify_token() +def get_uri_from_search(search, token): results = __get_search_results(search, token) -def __get_search_results(search, token) + return results[0]['name'] + +def __get_search_results(search, token): BASE_URL = "https://api.spotify.com/v1/search?q=" SEARCH_TYPES = ["artist", "album", "track", "playlist"] @@ -84,19 +82,24 @@ def __get_search_results(search, token) results = [] - for searchType in SEARCH_TYPES - result = requests.get( - BASE_URL + - searchType + "%3A" + urllib.parse.quote(search, safe='') + - "&type=" + searchType + - "&limit=1", - headers=headers).json()[searchType + 's']['items'][0] - - results.append( - { - 'name':result['name'].lower(), - 'uri': result['uri'] - } - ) + for searchType in SEARCH_TYPES: + + try: + result = requests.get( + BASE_URL + + searchType + "%3A" + urllib.parse.quote(search, safe='') + + "&type=" + searchType + + "&limit=1", + headers=headers).json()[searchType + 's']['items'][0] + + + results.append( + { + 'name':result['name'].upper(), + 'uri': result['uri'] + } + ) + except IndexError: + pass return sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True) \ No newline at end of file From 90943e979f6055ec93570df88e0b40daf456394f Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:12:00 -0400 Subject: [PATCH 03/12] added documentation for search function --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index c5989488..53d2092e 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ where: * `spotify_device_id` is the device ID of the Spotify Connect device * `device_name` is the friendly name of the chromecast device * `uri` is the Spotify uri, supports all uris including track (limit to one track) +* `search` is a search query to resolve into a uri. This parameter will be overlooked if a uri is provided * `random_song` optional parameter that starts the playback at a random position in the playlist * `repeat` optional parameter that repeats the playlist/track * `shuffle` optional parameter to set shuffle mode for playback @@ -163,6 +164,17 @@ To use the Spotcast service with a Spotify Connect device, you need the `spotify service: spotcast.start ``` +```yaml +- service: spotcast.start + data: + search: "Brown Bird" + # resolve to spotify:artist:5zzbSFZMVpvxSlWAkqqtHP at the time of writing + random_song: true + shuffle: true + start_volume: 50 + entity_id: media_player.cuisine +``` + ### Transfer current playback for the account Omitting `uri` will transfer the playback to the specified device. From cce8a66111053d46da423ffa0007a3f22f8fb992 Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Wed, 1 Sep 2021 10:15:56 -0400 Subject: [PATCH 04/12] switch from manual api to spotipy client --- custom_components/spotcast/__init__.py | 3 +-- custom_components/spotcast/helpers.py | 36 ++++++++++---------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 837bddc9..2b140a4a 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -164,8 +164,7 @@ def start_casting(call): if uri is None or uri.strip() == "": # get uri from search request - token, expires = spotcast_controller.get_token_instance(account).get_spotify_token() - uri = helpers.get_uri_from_search(search, token) + uri = helpers.get_uri_from_search(search, client) spotcast_controller.play( client, diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index ac5afcdd..b3285962 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -64,42 +64,32 @@ async def run(*args, loop=None, executor=None, **kwargs): return run -def get_uri_from_search(search, token): - - results = __get_search_results(search, token) - - return results[0]['name'] - -def __get_search_results(search, token): - BASE_URL = "https://api.spotify.com/v1/search?q=" +def get_search_results(search, spotify_client): + SEARCH_TYPES = ["artist", "album", "track", "playlist"] search = search.upper() - headers = { - 'Authorization': 'Bearer {token}'.format(token=token) - } - results = [] for searchType in SEARCH_TYPES: - + try: - result = requests.get( - BASE_URL + - searchType + "%3A" + urllib.parse.quote(search, safe='') + - "&type=" + searchType + - "&limit=1", - headers=headers).json()[searchType + 's']['items'][0] - - + + result = spotify_client.search( + searchType + ":" + urllib.parse.quote(search, safe=''), + limit=1, + offset=0, + type=searchType)[searchType + 's']['items'][0] + results.append( { - 'name':result['name'].upper(), + 'name': result['name'].upper(), 'uri': result['uri'] } ) + except IndexError: pass - return sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True) \ No newline at end of file + return sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True)[0]['uri'] From 3e22035a67327ff47aecd567d38bb4d4027158cd Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Wed, 1 Sep 2021 10:23:27 -0400 Subject: [PATCH 05/12] correction from testing --- custom_components/spotcast/__init__.py | 2 +- custom_components/spotcast/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 2b140a4a..5a997263 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -164,7 +164,7 @@ def start_casting(call): if uri is None or uri.strip() == "": # get uri from search request - uri = helpers.get_uri_from_search(search, client) + uri = helpers.get_search_results(search, client) spotcast_controller.play( client, diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index b3285962..a86d3cbc 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -77,7 +77,7 @@ def get_search_results(search, spotify_client): try: result = spotify_client.search( - searchType + ":" + urllib.parse.quote(search, safe=''), + searchType + ":" + search, limit=1, offset=0, type=searchType)[searchType + 's']['items'][0] From 320bbe64757112080677d1189b6a7827d5a9abd6 Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Wed, 8 Sep 2021 13:21:11 -0400 Subject: [PATCH 06/12] added debug logs --- custom_components/spotcast/helpers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index a86d3cbc..c02ee7c8 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -65,6 +65,8 @@ async def run(*args, loop=None, executor=None, **kwargs): return run def get_search_results(search, spotify_client): + + _LOGGER.debug("using search query to find uri") SEARCH_TYPES = ["artist", "album", "track", "playlist"] @@ -89,7 +91,13 @@ def get_search_results(search, spotify_client): } ) + _LOGGER.debug("search result for %s: %s", searchType, result['name']) + except IndexError: pass - return sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True)[0]['uri'] + bestMatch = sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True)[0] + + _LOGGER.debug("Best match for %s is %s", search. bestMatch['name']) + + return bestMatch['uri'] From 78f1c44c7b1eea256727af1f06f2641996704116 Mon Sep 17 00:00:00 2001 From: Darkfull-Dante Date: Wed, 8 Sep 2021 13:22:38 -0400 Subject: [PATCH 07/12] corrected a typo --- custom_components/spotcast/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index c02ee7c8..438b9fdc 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -98,6 +98,6 @@ def get_search_results(search, spotify_client): bestMatch = sorted(results, key=lambda x: difflib.SequenceMatcher(None, x['name'], search).ratio(), reverse=True)[0] - _LOGGER.debug("Best match for %s is %s", search. bestMatch['name']) + _LOGGER.debug("Best match for %s is %s", search, bestMatch['name']) return bestMatch['uri'] From 7981673016b213392d44e69c5ebe18151533f346 Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Wed, 22 Sep 2021 11:20:43 -0400 Subject: [PATCH 08/12] make bump --- Makefile | 2 +- custom_components/spotcast/manifest.json | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a9a06577..aa56174d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -__VERSION__ = "3.6.16" +__VERSION__ = "3.6.17" bump: bump2version --allow-dirty --current-version $(__VERSION__) patch Makefile custom_components/spotcast/manifest.json diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index 239bc25b..d16dbe66 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "spotify_token==1.0.0" ], - "version": "v3.6.16", + "version": "v3.6.17", "dependencies": [ "spotify" ], diff --git a/setup.cfg b/setup.cfg index 9521f0c2..30d77cf2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.6.16 +current_version = 3.6.17 [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build From f98f912bf54b4adab3bc5869459d96221df617ac Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:18:23 -0400 Subject: [PATCH 09/12] added method validates status of spotify core --- custom_components/spotcast/helpers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 438b9fdc..b66f5bdc 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -36,6 +36,19 @@ def get_spotify_devices(hass, spotify_user_id): _LOGGER.debug("get_spotify_devices: %s", resp) return resp +def get_spotify_install_status(hass): + + platform_string = "spotify" + platforms = entity_platform.async_get_platforms(hass, platform_string) + platform_count = len(platforms) + + if platform_count == 0: + _LOGGER.error("%s integration not found", platform_string) + else: + _LOGGER.debug("%s integration found", platform_string) + + return platform_count != 0 + def get_cast_devices(hass): platforms = entity_platform.async_get_platforms(hass, "cast") From 0756b049ebc8b08a21457709492f85621ca729e5 Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:19:58 -0400 Subject: [PATCH 10/12] quits setup if inconclusive spotify check --- custom_components/spotcast/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 5a997263..805fde41 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -42,6 +42,11 @@ def setup(hass, config): + + # get spotify core integration status + if not helpers.get_spotify_install_status(hass): + return False + """Setup the Spotcast service.""" conf = config[DOMAIN] From 25fad70fa35a92488b59087ee2c1dd9dae7f9788 Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:27:32 -0400 Subject: [PATCH 11/12] added error log when aborting setup --- custom_components/spotcast/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 805fde41..a55786e1 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -45,6 +45,7 @@ def setup(hass, config): # get spotify core integration status if not helpers.get_spotify_install_status(hass): + _LOGGER.error("Spotify integration was not found, aborting setup...") return False """Setup the Spotcast service.""" From 33b958a1cd062e8dca72b27f0e196e9eda0ce01d Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+Darkfull-Dante@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:13:22 -0400 Subject: [PATCH 12/12] make bump --- Makefile | 2 +- custom_components/spotcast/manifest.json | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index aa56174d..625cd5b7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -__VERSION__ = "3.6.17" +__VERSION__ = "3.6.18" bump: bump2version --allow-dirty --current-version $(__VERSION__) patch Makefile custom_components/spotcast/manifest.json diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index d16dbe66..36ba4913 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "spotify_token==1.0.0" ], - "version": "v3.6.17", + "version": "v3.6.18", "dependencies": [ "spotify" ], diff --git a/setup.cfg b/setup.cfg index 30d77cf2..56cda372 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.6.17 +current_version = 3.6.18 [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build