From d0b7066f593be8fe515dae20028aa5702eed086b Mon Sep 17 00:00:00 2001 From: dreamtheater39 Date: Sun, 20 Nov 2022 00:55:44 +0530 Subject: [PATCH 01/31] Improved search functionality --- custom_components/spotcast/helpers.py | 101 +++++++++++++++++++------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index f8988c2f..b60edabc 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -6,6 +6,7 @@ import urllib import difflib import random +import time from functools import partial, wraps from homeassistant.components.cast.media_player import CastDevice @@ -92,44 +93,90 @@ async def run(*args, loop=None, executor=None, **kwargs): return run -def get_search_results(search:str, spotify_client:spotipy.Spotify, country:str=None) -> str: - - _LOGGER.debug("using search query to find uri") +def get_top_tracks(artistName:str, spotify_client:spotipy.Spotify, limit:int=20, country:str = None): + + _LOGGER.debug("Searching for top tracks for the artist: %s", artistName) + searchType = "artist" + search = searchType + ":" + artistName - SEARCH_TYPES = ["artist", "album", "track", "playlist"] + artistUri = "" + + # get artist uri + try: - search = search.upper() + artist = spotify_client.search( + artistName, + limit=1, + offset=0, + type="artist", + market=country)["artists"]['items'][0] - results = [] + _LOGGER.debug("found artist %s: %s", artist['name'], artist['uri']) + artistUri = artist['uri'] - for searchType in SEARCH_TYPES: + except IndexError: + pass - try: + results = spotify_client.artist_top_tracks(artistUri) + for track in results['tracks'][:10]: + _LOGGER.debug('track : ' + track['name']) - result = spotify_client.search( - searchType + ":" + search, - limit=1, - offset=0, - type=searchType, - market=country)[searchType + 's']['items'][0] + return results['tracks'] - results.append( - { - 'name': result['name'].upper(), - 'uri': result['uri'] - } - ) +def get_search_string(search:str, artistName:str) -> str: + finalString = search.upper() + if not is_empty_str(artistName): + finalString += " artist:" + artistName + return finalString - _LOGGER.debug("search result for %s: %s", searchType, result['name']) +def get_search_results(search:str, spotify_client:spotipy.Spotify, artistName:str=None, limit:int=10, country:str=None): + _LOGGER.debug("using search query to find uri") + searchResults = [] + if is_empty_str(search) and artistName != None: + searchResults = get_top_tracks(artistName, spotify_client) + _LOGGER.debug("Playing top tracks for artist: %s", searchResults[0]['name']) + else: + # Get search type + search = get_search_string(search, artistName) + searchType = "track" + try: + searchResults = spotify_client.search( + search, + limit, + offset=0, + type=searchType, + market=country)["tracks"]['items'] except IndexError: pass - - 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'] + _LOGGER.debug("Found %d results for %s. First Track name: %s", len(searchResults), search, searchResults[0]['name']) + + return searchResults + +def search_tracks(search:str, spotify_client:spotipy.Spotify, + appendToQueue:bool=False, shuffle:bool=False, startRandom:bool=False, + limit:int=20, artistName:str=None, country:str=None): + results = get_search_results(search, spotify_client, artistName, limit, country) + if len(results) > 0: + firstResult = [results[0]] + if not startRandom: + results = results[1:limit] + if shuffle: + random.shuffle(results) + if not startRandom: + results = firstResult + results + + return results + +def add_tracks_to_queue(spotify_client:spotipy.Spotify, tracks:list=[], limit:int=20): + if len(tracks) == 0: + _LOGGER.debug("Cannot add ZERO tracks to the queue!") + return + + for track in tracks[:limit]: + _LOGGER.debug("Adding " + track['name'] + " to the playback queue | " + track['uri']) + spotify_client.add_to_queue(track['uri']) + time.sleep(0.5) def get_random_playlist_from_category(spotify_client:spotipy.Spotify, category:str, country:str=None, limit:int=20) -> str: From f60b22252a15da13b1b963bcf4e6ba7c51a50821 Mon Sep 17 00:00:00 2001 From: dreamtheater39 Date: Sun, 20 Nov 2022 00:57:39 +0530 Subject: [PATCH 02/31] Better search --- custom_components/spotcast/__init__.py | 16 ++++++++++++---- custom_components/spotcast/const.py | 2 ++ custom_components/spotcast/services.yaml | 7 +++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index af19f723..b7dcdaa3 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -23,6 +23,7 @@ CONF_SPOTIFY_DEVICE_ID, CONF_SPOTIFY_URI, CONF_SPOTIFY_SEARCH, + CONF_SPOTIFY_ARTISTNAME, CONF_SPOTIFY_CATEGORY, CONF_SPOTIFY_COUNTRY, CONF_SPOTIFY_LIMIT, @@ -148,6 +149,7 @@ def start_casting(call: ha_core.ServiceCall): country = call.data.get(CONF_SPOTIFY_COUNTRY) limit = call.data.get(CONF_SPOTIFY_LIMIT) search = call.data.get(CONF_SPOTIFY_SEARCH) + artistName = call.data.get(CONF_SPOTIFY_ARTISTNAME) random_song = call.data.get(CONF_RANDOM, False) repeat = call.data.get(CONF_REPEAT, False) shuffle = call.data.get(CONF_SHUFFLE, False) @@ -191,7 +193,7 @@ def start_casting(call: ha_core.ServiceCall): account, spotify_device_id, device_name, entity_id ) - if helpers.is_empty_str(uri) and helpers.is_empty_str(search) and helpers.is_empty_str(category): + if helpers.is_empty_str(uri) and helpers.is_empty_str(search) and helpers.is_empty_str(artistName) and helpers.is_empty_str(category): _LOGGER.debug("Transfering playback") current_playback = client.current_playback() if current_playback is not None: @@ -217,11 +219,14 @@ def start_casting(call: ha_core.ServiceCall): ignore_fully_played, ) else: - + searchResults = [] if helpers.is_empty_str(uri): # get uri from search request - uri = helpers.get_search_results(search, client, country) - + searchResults = helpers.search_tracks(search, client, False, shuffle, random_song, limit, artistName, country) + # play the first track + if len(searchResults) > 0: + uri = searchResults[0]['uri'] + spotcast_controller.play( client, spotify_device_id, @@ -231,6 +236,9 @@ def start_casting(call: ha_core.ServiceCall): ignore_fully_played, ) + if len(searchResults) > 1: + helpers.add_tracks_to_queue(client, searchResults[1:len(searchResults)]) + if start_volume <= 100: _LOGGER.debug("Setting volume to %d", start_volume) time.sleep(2) diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index b75244e1..3f0d6fd6 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -13,6 +13,7 @@ CONF_DEVICE_NAME = "device_name" CONF_SPOTIFY_URI = "uri" CONF_SPOTIFY_SEARCH = "search" +CONF_SPOTIFY_ARTISTNAME = "artistName" CONF_SPOTIFY_CATEGORY = "category" CONF_SPOTIFY_COUNTRY = "country" CONF_SPOTIFY_LIMIT = "limit" @@ -76,6 +77,7 @@ 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_ARTISTNAME): cv.string, vol.Optional(CONF_SPOTIFY_CATEGORY): cv.string, vol.Optional(CONF_SPOTIFY_COUNTRY): cv.string, vol.Optional(CONF_SPOTIFY_LIMIT, default=20): cv.positive_int, diff --git a/custom_components/spotcast/services.yaml b/custom_components/spotcast/services.yaml index 22547901..51e5c53f 100644 --- a/custom_components/spotcast/services.yaml +++ b/custom_components/spotcast/services.yaml @@ -61,6 +61,13 @@ start: required: false selector: text: + artistName: + name: "Artist Name" + example: "pink floyd" + description: "This will filter search results to match the provided artist name" + required: false + selector: + text: account: name: "Account" description: "Optionally starts Spotify using an alternative account specified in config." From 748d4476b2389ffdb34044c4aff75773f0a96d77 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 22:14:23 +0000 Subject: [PATCH 03/31] add more search options --- custom_components/spotcast/cast.py | 23 +- custom_components/spotcast/const.py | 19 +- custom_components/spotcast/helpers.py | 264 +++++++++++++----- custom_components/spotcast/sensor.py | 18 +- custom_components/spotcast/services.yaml | 51 +++- .../spotcast/spotcast_controller.py | 117 ++++---- .../spotcast/spotify_controller.py | 57 ++-- 7 files changed, 353 insertions(+), 196 deletions(-) diff --git a/custom_components/spotcast/cast.py b/custom_components/spotcast/cast.py index b4cf3f01..96562d36 100644 --- a/custom_components/spotcast/cast.py +++ b/custom_components/spotcast/cast.py @@ -2,18 +2,16 @@ import logging +import homeassistant.core as ha_core from homeassistant.components import spotify as ha_spotify from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import MEDIA_CLASS_APP -import homeassistant.core as ha_core from pychromecast import Chromecast _LOGGER = logging.getLogger(__name__) async def async_get_media_browser_root_object( - hass: ha_core.HomeAssistant, - cast_type: str + hass: ha_core.HomeAssistant, cast_type: str ) -> list[BrowseMedia]: """Create a root object for media browsing.""" try: @@ -35,11 +33,7 @@ async def async_browse_media( cast_type: str, ) -> BrowseMedia | None: """Browse media.""" - _LOGGER.debug( - "async_browse_media %s, %s", - media_content_type, - media_content_id - ) + _LOGGER.debug("async_browse_media %s, %s", media_content_type, media_content_id) result = None # Check if this media is handled by Spotify, if it isn't just return None. if ha_spotify.is_spotify_media_type(media_content_type): @@ -47,10 +41,7 @@ async def async_browse_media( result = await ha_spotify.async_browse_media( hass, media_content_type, media_content_id, can_play_artist=False ) - _LOGGER.debug( - "async_browse_media return: %s", - result - ) + _LOGGER.debug("async_browse_media return: %s", result) return result @@ -62,11 +53,7 @@ async def async_play_media( media_id: str, ) -> bool: """Play media.""" - _LOGGER.debug( - "async_browse_media %s, %s", - media_type, - media_id - ) + _LOGGER.debug("async_browse_media %s, %s", media_type, media_id) # If this is a spotify URI, forward to the the spotcast.start service, if not return # False if media_id and media_id.startswith("spotify:"): diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index 3f0d6fd6..13ee5404 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -12,8 +12,14 @@ CONF_SPOTIFY_DEVICE_ID = "spotify_device_id" CONF_DEVICE_NAME = "device_name" CONF_SPOTIFY_URI = "uri" -CONF_SPOTIFY_SEARCH = "search" -CONF_SPOTIFY_ARTISTNAME = "artistName" +CONF_SPOTIFY_TRACK_NAME = "trackName" +CONF_SPOTIFY_ARTIST_NAME = "artistName" +CONF_SPOTIFY_ALBUM_NAME = "albumName" +CONF_SPOTIFY_PLAYLIST_NAME = "playlistName" +CONF_SPOTIFY_SHOW_NAME = "showName" +CONF_SPOTIFY_EPISODE_NAME = "episodeName" +CONF_SPOTIFY_AUDIOBOOK_NAME = "audiobookName" +CONF_SPOTIFY_GENRE_NAME = "genreName" CONF_SPOTIFY_CATEGORY = "category" CONF_SPOTIFY_COUNTRY = "country" CONF_SPOTIFY_LIMIT = "limit" @@ -76,8 +82,13 @@ 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_ARTISTNAME): cv.string, + vol.Optional(CONF_SPOTIFY_TRACK_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_ALBUM_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_ARTIST_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_PLAYLIST_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_SHOW_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_EPISODE_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_AUDIOBOOK_NAME): cv.string, vol.Optional(CONF_SPOTIFY_CATEGORY): cv.string, vol.Optional(CONF_SPOTIFY_COUNTRY): cv.string, vol.Optional(CONF_SPOTIFY_LIMIT, default=20): cv.positive_int, diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 10c33a3c..c27df2fe 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -2,26 +2,25 @@ import asyncio import logging -import requests -import urllib -import difflib import random import time from functools import partial, wraps import homeassistant.core as ha_core -from homeassistant.components.cast.media_player import CastDevice -from homeassistant.components.spotify.media_player import SpotifyMediaPlayer -from homeassistant.helpers import entity_platform -from homeassistant.exceptions import HomeAssistantError # import for type inference import spotipy +from homeassistant.components.cast.media_player import CastDevice +from homeassistant.components.spotify.media_player import SpotifyMediaPlayer +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform _LOGGER = logging.getLogger(__name__) -def get_spotify_media_player(hass: ha_core.HomeAssistant, spotify_user_id: str) -> SpotifyMediaPlayer: +def get_spotify_media_player( + hass: ha_core.HomeAssistant, spotify_user_id: str +) -> SpotifyMediaPlayer: """Get the spotify media player entity from hass.""" platforms = entity_platform.async_get_platforms(hass, "spotify") spotify_media_player = None @@ -37,10 +36,13 @@ def get_spotify_media_player(hass: ha_core.HomeAssistant, spotify_user_id: str) try: entity_devices = entity._devices - except(AttributeError): + except (AttributeError): entity_devices = entity.data.devices.data - _LOGGER.debug(f"get_spotify_devices: {entity.entity_id}: {entity.name}: %s", entity_devices) + _LOGGER.debug( + f"get_spotify_devices: {entity.entity_id}: {entity.name}: %s", + entity_devices, + ) spotify_media_player = entity break @@ -55,14 +57,15 @@ def get_spotify_devices(spotify_media_player: SpotifyMediaPlayer): # Need to come from media_player spotify's sp client due to token issues try: spotify_devices = spotify_media_player._spotify.devices() - except(AttributeError): + except (AttributeError): spotify_devices = spotify_media_player.data.client.devices() _LOGGER.debug("get_spotify_devices: %s", spotify_devices) - + return spotify_devices return [] + def get_spotify_install_status(hass): platform_string = "spotify" @@ -104,69 +107,167 @@ async def run(*args, loop=None, executor=None, **kwargs): return run -def get_top_tracks(artistName:str, spotify_client:spotipy.Spotify, limit:int=20, country:str = None): - + +def get_top_tracks( + artistName: str, + spotify_client: spotipy.Spotify, + limit: int = 20, + country: str = None, +): + _LOGGER.debug("Searching for top tracks for the artist: %s", artistName) searchType = "artist" search = searchType + ":" + artistName artistUri = "" - + # get artist uri try: artist = spotify_client.search( - artistName, + q=search, + artistName=artistName, limit=1, offset=0, type="artist", - market=country)["artists"]['items'][0] + market=country, + )["artists"]["items"][0] - _LOGGER.debug("found artist %s: %s", artist['name'], artist['uri']) - artistUri = artist['uri'] + _LOGGER.debug("found artist %s: %s", artist["name"], artist["uri"]) + artistUri = artist["uri"] except IndexError: pass results = spotify_client.artist_top_tracks(artistUri) - for track in results['tracks'][:10]: - _LOGGER.debug('track : ' + track['name']) + for track in results["tracks"][:10]: + _LOGGER.debug("track : " + track["name"]) - return results['tracks'] + return results["tracks"] -def get_search_string(search:str, artistName:str) -> str: - finalString = search.upper() - if not is_empty_str(artistName): - finalString += " artist:" + artistName - return finalString -def get_search_results(search:str, spotify_client:spotipy.Spotify, artistName:str=None, limit:int=10, country:str=None): +def get_search_string( + artistName: str, albumName: str, trackName: str, genreName: str +) -> str: + search = [] + if not is_empty_str(artistName): + search.append(f"artistName:{artistName}") + if not is_empty_str(albumName): + search.append(f"albumName:{albumName}") + if not is_empty_str(trackName): + search.append(f"trackName:{trackName}") + if not is_empty_str(genreName): + search.append(f"genreName:{genreName}") + return " ".join(search) + + +# "album", "artist", "playlist", "track", "show", "episode", "audiobook" +def get_types_string( + artistName: str, + albumName: str, + trackName: str, + playlistName: str, + showName: str, + episodeName: str, + audiobookName: str, +) -> str: + types = [] + if not is_empty_str(artistName): + types.append("artist") + if not is_empty_str(albumName): + types.append("album") + if not is_empty_str(trackName): + types.append("track") + if not is_empty_str(playlistName): + types.append("playlist") + if not is_empty_str(showName): + types.append("show") + if not is_empty_str(episodeName): + types.append("episode") + if not is_empty_str(audiobookName): + types.append("audiobook") + return ",".join(types) + + +def get_search_results( + spotify_client: spotipy.Spotify, + limit: int = 10, + country: str = None, + artistName: str = None, + albumName: str = None, + playlistName: str = None, + trackName: str = None, + showName: str = None, + episodeName: str = None, + audiobookName: str = None, + genreName: str = None, +): _LOGGER.debug("using search query to find uri") searchResults = [] - if is_empty_str(search) and artistName != None: + if ( + not is_empty_str(artistName) + and len( + filter( + lambda x: not is_empty_str(x), + [ + albumName, + playlistName, + trackName, + showName, + episodeName, + audiobookName, + genreName, + ], + ) + ) + == 0 + ): searchResults = get_top_tracks(artistName, spotify_client) - _LOGGER.debug("Playing top tracks for artist: %s", searchResults[0]['name']) + _LOGGER.debug("Playing top tracks for artist: %s", searchResults[0]["name"]) else: - # Get search type - search = get_search_string(search, artistName) - searchType = "track" + searchString = get_search_string( + artistName=artistName, + albumName=albumName, + trackName=trackName, + genreName=genreName, + ) + + searchTypes = get_types_string( + artistName=artistName, + albumName=albumName, + trackName=trackName, + playlistName=playlistName, + showName=showName, + episodeName=episodeName, + audiobookName=audiobookName, + ) try: searchResults = spotify_client.search( - search, - limit, - offset=0, - type=searchType, - market=country)["tracks"]['items'] + q=searchString, limit=limit, offset=0, type=searchTypes, market=country + )["tracks"]["items"] except IndexError: pass - _LOGGER.debug("Found %d results for %s. First Track name: %s", len(searchResults), search, searchResults[0]['name']) + _LOGGER.debug( + "Found %d results for %s. First Track name: %s", + len(searchResults), + searchString, + searchResults[0]["name"], + ) return searchResults -def search_tracks(search:str, spotify_client:spotipy.Spotify, - appendToQueue:bool=False, shuffle:bool=False, startRandom:bool=False, - limit:int=20, artistName:str=None, country:str=None): + +def search_tracks( + search: str, + spotify_client: spotipy.Spotify, + appendToQueue: bool = False, + shuffle: bool = False, + startRandom: bool = False, + limit: int = 20, + artistName: str = None, + country: str = None, +): results = get_search_results(search, spotify_client, artistName, limit, country) if len(results) > 0: firstResult = [results[0]] @@ -179,34 +280,48 @@ def search_tracks(search:str, spotify_client:spotipy.Spotify, return results -def add_tracks_to_queue(spotify_client:spotipy.Spotify, tracks:list=[], limit:int=20): + +def add_tracks_to_queue( + spotify_client: spotipy.Spotify, tracks: list = [], limit: int = 20 +): if len(tracks) == 0: _LOGGER.debug("Cannot add ZERO tracks to the queue!") return for track in tracks[:limit]: - _LOGGER.debug("Adding " + track['name'] + " to the playback queue | " + track['uri']) - spotify_client.add_to_queue(track['uri']) + _LOGGER.debug( + "Adding " + track["name"] + " to the playback queue | " + track["uri"] + ) + spotify_client.add_to_queue(track["uri"]) time.sleep(0.5) -def get_random_playlist_from_category(spotify_client:spotipy.Spotify, category:str, country:str=None, limit:int=20) -> str: - + +def get_random_playlist_from_category( + spotify_client: spotipy.Spotify, category: str, country: str = None, limit: int = 20 +) -> str: + if country is None: - - _LOGGER.debug(f"Get random playlist among {limit} playlists from category {category}, no country specified.") + + _LOGGER.debug( + f"Get random playlist among {limit} playlists from category {category}, no country specified." + ) else: - _LOGGER.debug(f"Get random playlist among {limit} playlists from category {category} in country {country}") + _LOGGER.debug( + f"Get random playlist among {limit} playlists from category {category} in country {country}" + ) # validate category and country are valid entries if country.upper() not in spotify_client.country_codes: _LOGGER.error(f"{country} is not a valid country code") return None - + # get list of playlist from category and localisation provided try: - playlists = spotify_client.category_playlists(category_id=category, country=country, limit=limit)["playlists"]["items"] + playlists = spotify_client.category_playlists( + category_id=category, country=country, limit=limit + )["playlists"]["items"] except spotipy.exceptions.SpotifyException as e: _LOGGER.error(e.msg) return None @@ -214,21 +329,17 @@ def get_random_playlist_from_category(spotify_client:spotipy.Spotify, category:s # choose one at random chosen = random.choice(playlists) - _LOGGER.debug(f"Chose playlist {chosen['name']} ({chosen['uri']}) from category {category}.") + _LOGGER.debug( + f"Chose playlist {chosen['name']} ({chosen['uri']}) from category {category}." + ) + + return chosen["uri"] - return chosen['uri'] def is_valid_uri(uri: str) -> bool: - + # list of possible types - types = [ - "artist", - "album", - "track", - "playlist", - "show", - "episode" - ] + types = ["artist", "album", "track", "playlist", "show", "episode"] # split the string elems = uri.split(":") @@ -236,31 +347,42 @@ def is_valid_uri(uri: str) -> bool: # validate number of sub elements if elems[1].lower() == "user": elems = elems[0:1] + elems[3:] - types = [ "playlist" ] - _LOGGER.debug(f"Excluding user information from the Spotify URI validation. Only supported for playlists") + types = ["playlist"] + _LOGGER.debug( + "Excluding user information from the Spotify URI validation. Only supported for playlists" + ) # support playing a user's liked songs list (spotify:user:username:collection) if len(elems) == 2 and elems[1].lower() == "collection": return True if len(elems) != 3: - _LOGGER.error(f"[{uri}] is not a valid URI. The format should be [spotify::]") + _LOGGER.error( + f"[{uri}] is not a valid URI. The format should be [spotify::]" + ) return False # check correct format of the sub elements if elems[0].lower() != "spotify": - _LOGGER.error(f"This is not a valid Spotify URI. This should start with [spotify], but instead starts with [{elems[0]}]") + _LOGGER.error( + f"This is not a valid Spotify URI. This should start with [spotify], but instead starts with [{elems[0]}]" + ) return False if elems[1].lower() not in types: - _LOGGER.error(f"{elems[1]} is not a valid type for Spotify request. Please make sure to use the following list {str(types)}") + _LOGGER.error( + f"{elems[1]} is not a valid type for Spotify request. Please make sure to use the following list {str(types)}" + ) return False if "?" in elems[2]: - _LOGGER.warning(f"{elems[2]} contains query character. This should work, but you should probably remove it and anything after.") - + _LOGGER.warning( + f"{elems[2]} contains query character. This should work, but you should probably remove it and anything after." + ) + # return True if all test passes return True -def is_empty_str(string:str) -> bool: + +def is_empty_str(string: str) -> bool: return string is None or string.strip() == "" diff --git a/custom_components/spotcast/sensor.py b/custom_components/spotcast/sensor.py index 4b0eb094..cb09c870 100644 --- a/custom_components/spotcast/sensor.py +++ b/custom_components/spotcast/sensor.py @@ -5,17 +5,14 @@ import json import logging from datetime import timedelta -import homeassistant.core as ha_core +import homeassistant.core as ha_core from homeassistant.components.sensor import SensorEntity from homeassistant.const import STATE_OK, STATE_UNKNOWN from homeassistant.util import dt +from .const import CONF_SPOTIFY_COUNTRY, DOMAIN from .helpers import get_cast_devices -from .const import ( - DOMAIN, - CONF_SPOTIFY_COUNTRY -) _LOGGER = logging.getLogger(__name__) @@ -23,7 +20,12 @@ SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) -def setup_platform(hass:ha_core.HomeAssistant, config:collections.OrderedDict, add_devices, discovery_info=None): +def setup_platform( + hass: ha_core.HomeAssistant, + config: collections.OrderedDict, + add_devices, + discovery_info=None, +): try: country = config[CONF_SPOTIFY_COUNTRY] @@ -116,7 +118,9 @@ def update(self): resp = self.hass.data[DOMAIN]["controller"].get_playlists( account, playlist_type, country_code, locale, limit ) - self._attributes["playlists"] = [{ "uri": x['uri'], "name": x['name']} for x in resp['items'] ] + self._attributes["playlists"] = [ + {"uri": x["uri"], "name": x["name"]} for x in resp["items"] + ] self._attributes["last_update"] = dt.now().isoformat("T") self._state = STATE_OK diff --git a/custom_components/spotcast/services.yaml b/custom_components/spotcast/services.yaml index b809bae9..0da10358 100644 --- a/custom_components/spotcast/services.yaml +++ b/custom_components/spotcast/services.yaml @@ -56,16 +56,59 @@ start: step: 1 min: 0 max: 50 - search: - name: "Search" - description: "A Search request to the spotify API. Will play the most relevant search result." + albumName: + name: "Album Name" + example: "The Dark Side of the Moon" + description: "Filters search results for the provided album name. Don't include this if you don't want an album." required: false selector: text: + trackName: + name: "Track Name" + example: "Money" + description: "Filters search results for the provided track name. Don't include this if you don't want a particular track." + required: false + selector: + text: + playlistName: + name: "Playlist Name" + example: "Ultimate pink floyd playlist" + description: "Filters search results for the provided playlist name. Don't include this if you don't want a playlist." + required: false + selector: + text: + showName: + name: "Show Name" + example: "Hollywood Handbook" + description: "Filters search results for the provided podcast show name. Don't include this if you don't want a podcast." + required: false + selector: + text: + episodeName: + name: "Episode Name" + example: "Sarah Sherman, Our Close Friend" + description: "Filters search results for the provided podcast episode name. Don't include this if you don't want a podcast." + required: false + selector: + text: + genreName: + name: "Genre Name" + example: "post punk" + description: "Filters search results by genre of music" + required: false + selector: + text: + audiobookName: + name: "Audiobook Name" + example: "Ulysses" + description: "Filters search results for the provided audiobook name. Don't include this if you don't want an audiobook." + required: false + selector: + text: artistName: name: "Artist Name" example: "pink floyd" - description: "This will filter search results to match the provided artist name" + description: "This will filter search results to match the provided artist name. Don't include this if searching for a playlist or genre. Do include the author's name if searching for audiobooks." required: false selector: text: diff --git a/custom_components/spotcast/spotcast_controller.py b/custom_components/spotcast/spotcast_controller.py index 44683c62..3b61d91c 100644 --- a/custom_components/spotcast/spotcast_controller.py +++ b/custom_components/spotcast/spotcast_controller.py @@ -1,29 +1,25 @@ from __future__ import annotations import collections +import json import logging import random import time from asyncio import run_coroutine_threadsafe -from requests import TooManyRedirects from collections import OrderedDict from datetime import datetime -import homeassistant.core as ha_core -import pychromecast import aiohttp -import json +import homeassistant.core as ha_core +import pychromecast import spotipy from homeassistant.components.cast.helpers import ChromeCastZeroconf from homeassistant.exceptions import HomeAssistantError +from requests import TooManyRedirects -from .spotify_controller import SpotifyController from .const import CONF_SP_DC, CONF_SP_KEY -from .helpers import ( - get_cast_devices, - get_spotify_devices, - get_spotify_media_player -) +from .helpers import get_cast_devices, get_spotify_devices, get_spotify_media_player +from .spotify_controller import SpotifyController _LOGGER = logging.getLogger(__name__) @@ -40,10 +36,7 @@ class SpotifyCastDevice: spotifyController = None def __init__( - self, - hass: ha_core.HomeAssistant, - call_device_name: str, - call_entity_id: str + self, hass: ha_core.HomeAssistant, call_device_name: str, call_entity_id: str ) -> None: """Initialize a spotify cast device.""" self.hass = hass @@ -119,8 +112,9 @@ def get_spotify_device_id(self, user_id) -> None: max_retries = 5 counter = 0 devices_available = None - _LOGGER.debug("Searching for Spotify device: {}".format( - self.spotifyController.device)) + _LOGGER.debug( + "Searching for Spotify device: {}".format(self.spotifyController.device) + ) while counter < max_retries: devices_available = get_spotify_devices(spotify_media_player) # Look for device to make sure we can start playback @@ -128,7 +122,8 @@ def get_spotify_device_id(self, user_id) -> None: for device in devices: if device["id"] == self.spotifyController.device: _LOGGER.debug( - "Found matching Spotify device: {}".format(device)) + "Found matching Spotify device: {}".format(device) + ) return device["id"] sleep = random.uniform(1.5, 1.8) ** counter @@ -188,12 +183,12 @@ def get_spotify_token(self) -> tuple[str, int]: async def start_session(self): """ Starts session to get access token. """ - cookies = {'sp_dc': self.sp_dc, 'sp_key': self.sp_key} + cookies = {"sp_dc": self.sp_dc, "sp_key": self.sp_key} async with aiohttp.ClientSession(cookies=cookies) as session: headers = { - 'user-agent': ( + "user-agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 " "Safari/537.36" @@ -202,32 +197,35 @@ async def start_session(self): async with session.get( ( - 'https://open.spotify.com/get_access_token?reason=' - 'transport&productType=web_player' + "https://open.spotify.com/get_access_token?reason=" + "transport&productType=web_player" ), allow_redirects=False, - headers=headers + headers=headers, ) as response: - if (response.status == 302 and response.headers['Location'] == '/get_access_token?reason=transport&productType=web_player&_authfailed=1'): + if ( + response.status == 302 + and response.headers["Location"] + == "/get_access_token?reason=transport&productType=web_player&_authfailed=1" + ): _LOGGER.error( "Unsuccessful token request, received code 302 and " "Location header %s. sp_dc and sp_key could be " "expired. Please update in config.", - response.headers['Location'] + response.headers["Location"], ) raise HomeAssistantError("Expired sp_dc, sp_key") - if (response.status != 200): + if response.status != 200: _LOGGER.info( - "Unsuccessful token request, received code %i", - response.status + "Unsuccessful token request, received code %i", response.status ) raise TokenError() data = await response.text() config = json.loads(data) - access_token = config['accessToken'] - expires_timestamp = config['accessTokenExpirationTimestampMs'] + access_token = config["accessToken"] + expires_timestamp = config["accessTokenExpirationTimestampMs"] expiration_date = int(expires_timestamp) // 1000 return access_token, expiration_date @@ -244,12 +242,11 @@ def __init__( hass: ha_core.HomeAssistant, sp_dc: str, sp_key: str, - accs: collections.OrderedDict + accs: collections.OrderedDict, ) -> None: if accs: self.accounts = accs - self.accounts["default"] = OrderedDict( - [("sp_dc", sp_dc), ("sp_key", sp_key)]) + self.accounts["default"] = OrderedDict([("sp_dc", sp_dc), ("sp_key", sp_key)]) self.hass = hass def get_token_instance(self, account: str = None) -> any: @@ -261,42 +258,30 @@ def get_token_instance(self, account: str = None) -> any: _LOGGER.debug("setting up with account %s", account) if account not in self.spotifyTokenInstances: - self.spotifyTokenInstances[account] = SpotifyToken( - self.hass, dc, key) + self.spotifyTokenInstances[account] = SpotifyToken(self.hass, dc, key) return self.spotifyTokenInstances[account] def get_spotify_client(self, account: str) -> spotipy.Spotify: - return spotipy.Spotify( - auth=self.get_token_instance(account).access_token - ) + return spotipy.Spotify(auth=self.get_token_instance(account).access_token) def _getSpotifyConnectDeviceId(self, client, device_name): - media_player = get_spotify_media_player( - self.hass, client._get("me")["id"]) + media_player = get_spotify_media_player(self.hass, client._get("me")["id"]) devices_available = get_spotify_devices(media_player) for device in devices_available["devices"]: if device["name"] == device_name: return device["id"] return None - def get_spotify_device_id( - self, - account, - spotify_device_id, - device_name, - entity_id - ): + def get_spotify_device_id(self, account, spotify_device_id, device_name, entity_id): # login as real browser to get powerful token - access_token, expires = self.get_token_instance( - account).get_spotify_token() + access_token, expires = self.get_token_instance(account).get_spotify_token() # get the spotify web api client client = spotipy.Spotify(auth=access_token) # first, rely on spotify id given in config if not spotify_device_id: # if not present, check if there's a spotify connect device # with that name - spotify_device_id = self._getSpotifyConnectDeviceId( - client, device_name) + spotify_device_id = self._getSpotifyConnectDeviceId(client, device_name) if not spotify_device_id: # if still no id available, check cast devices and launch # the app on chromecast @@ -308,8 +293,7 @@ def get_spotify_device_id( me_resp = client._get("me") spotify_cast_device.start_spotify_controller(access_token, expires) # Make sure it is started - spotify_device_id = spotify_cast_device.get_spotify_device_id( - me_resp["id"]) + spotify_device_id = spotify_cast_device.get_spotify_device_id(me_resp["id"]) return spotify_device_id def play( @@ -320,7 +304,7 @@ def play( random_song: bool, position: str, ignore_fully_played: str, - country_code: str = None + country_code: str = None, ) -> None: _LOGGER.debug( "Playing URI: %s on device-id: %s", @@ -337,8 +321,9 @@ def play( episode_uri = episode["external_urls"]["spotify"] break else: - episode_uri = show_episodes_info["items"][0][ - "external_urls"]["spotify"] + episode_uri = show_episodes_info["items"][0]["external_urls"][ + "spotify" + ] _LOGGER.debug( ( "Playing episode using uris (latest podcast playlist)=" @@ -346,8 +331,7 @@ def play( ), episode_uri, ) - client.start_playback( - device_id=spotify_device_id, uris=[episode_uri]) + client.start_playback(device_id=spotify_device_id, uris=[episode_uri]) elif uri.find("episode") > 0: _LOGGER.debug("Playing episode using uris= for uri: %s", uri) client.start_playback(device_id=spotify_device_id, uris=[uri]) @@ -358,13 +342,11 @@ def play( else: if uri == "random": _LOGGER.debug( - "Cool, you found the easter egg with playing a random" - " playlist" + "Cool, you found the easter egg with playing a random" " playlist" ) playlists = client.user_playlists("me", 50) no_playlists = len(playlists["items"]) - uri = playlists["items"][random.randint( - 0, no_playlists - 1)]["uri"] + uri = playlists["items"][random.randint(0, no_playlists - 1)]["uri"] kwargs = {"device_id": spotify_device_id, "context_uri": uri} if random_song: @@ -377,14 +359,13 @@ def play( elif uri.find("collection") > 0: results = client.current_user_saved_tracks() position = random.randint(0, results["total"] - 1) - _LOGGER.debug( - "Start playback at random position: %s", position) + _LOGGER.debug("Start playback at random position: %s", position) if uri.find("artist") < 1: kwargs["offset"] = {"position": position} _LOGGER.debug( ( 'Playing context uri using context_uri for uri: "%s" ' - '(random_song: %s)' + "(random_song: %s)" ), uri, random_song, @@ -397,7 +378,7 @@ def get_playlists( playlist_type: str, country_code: str, locale: str, - limit: int + limit: int, ) -> dict: client = self.get_spotify_client(account) resp = {} @@ -405,11 +386,7 @@ def get_playlists( if playlist_type == "discover-weekly": playlist_type = "made-for-x" - if ( - playlist_type == "user" - or playlist_type == "default" - or playlist_type == "" - ): + if playlist_type == "user" or playlist_type == "default" or playlist_type == "": resp = client.current_user_playlists(limit=limit) elif playlist_type == "featured": diff --git a/custom_components/spotcast/spotify_controller.py b/custom_components/spotcast/spotify_controller.py index 1d6e6697..b2ed88fc 100644 --- a/custom_components/spotcast/spotify_controller.py +++ b/custom_components/spotcast/spotify_controller.py @@ -3,17 +3,17 @@ """ from __future__ import annotations +import hashlib +import json import logging import threading -import requests -import json -import hashlib - -from .const import APP_SPOTIFY +import requests from pychromecast.controllers import BaseController from pychromecast.error import LaunchError +from .const import APP_SPOTIFY + APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1" TYPE_GET_INFO = "getInfo" TYPE_GET_INFO_RESPONSE = "getInfoResponse" @@ -49,22 +49,30 @@ def receive_message(self, _message, data: dict): self.device = self.getSpotifyDeviceID() self.client = data["payload"]["clientID"] headers = { - 'authority': 'spclient.wg.spotify.com', - 'authorization': 'Bearer {}'.format(self.access_token), - 'content-type': 'text/plain;charset=UTF-8' + "authority": "spclient.wg.spotify.com", + "authorization": "Bearer {}".format(self.access_token), + "content-type": "text/plain;charset=UTF-8", } - request_body = json.dumps({'clientId': self.client, 'deviceId': self.device}) + request_body = json.dumps( + {"clientId": self.client, "deviceId": self.device} + ) - response = requests.post('https://spclient.wg.spotify.com/device-auth/v1/refresh', headers=headers, data=request_body) + response = requests.post( + "https://spclient.wg.spotify.com/device-auth/v1/refresh", + headers=headers, + data=request_body, + ) json_resp = response.json() - self.send_message({ - "type": TYPE_ADD_USER, - "payload": { - "blob": json_resp["accessToken"], - "tokenType": "accesstoken" + self.send_message( + { + "type": TYPE_ADD_USER, + "payload": { + "blob": json_resp["accessToken"], + "tokenType": "accesstoken", + }, } - }) + ) if data["type"] == TYPE_ADD_USER_RESPONSE: self.is_launched = True self.waiting.set() @@ -88,11 +96,16 @@ def launch_app(self, timeout=10): def callback(): """Callback function""" - self.send_message({"type": TYPE_GET_INFO, "payload": { - "remoteName": self.castDevice.cast_info.friendly_name, - "deviceID": self.getSpotifyDeviceID(), - "deviceAPI_isGroup": False, - },}) + self.send_message( + { + "type": TYPE_GET_INFO, + "payload": { + "remoteName": self.castDevice.cast_info.friendly_name, + "deviceID": self.getSpotifyDeviceID(), + "deviceAPI_isGroup": False, + }, + } + ) self.device = None self.credential_error = False @@ -126,4 +139,4 @@ def getSpotifyDeviceID(self) -> str: """ Retrieve the Spotify deviceID from provided chromecast info """ - return hashlib.md5(self.castDevice.cast_info.friendly_name.encode()).hexdigest() \ No newline at end of file + return hashlib.md5(self.castDevice.cast_info.friendly_name.encode()).hexdigest() From fdee2d1d5c66d1fe9734c933c027c312a4c27529 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 22:48:44 +0000 Subject: [PATCH 04/31] use underscores, not camel case --- custom_components/spotcast/const.py | 16 ++++++++-------- custom_components/spotcast/services.yaml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index 13ee5404..e9adc4d0 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -12,14 +12,14 @@ CONF_SPOTIFY_DEVICE_ID = "spotify_device_id" CONF_DEVICE_NAME = "device_name" CONF_SPOTIFY_URI = "uri" -CONF_SPOTIFY_TRACK_NAME = "trackName" -CONF_SPOTIFY_ARTIST_NAME = "artistName" -CONF_SPOTIFY_ALBUM_NAME = "albumName" -CONF_SPOTIFY_PLAYLIST_NAME = "playlistName" -CONF_SPOTIFY_SHOW_NAME = "showName" -CONF_SPOTIFY_EPISODE_NAME = "episodeName" -CONF_SPOTIFY_AUDIOBOOK_NAME = "audiobookName" -CONF_SPOTIFY_GENRE_NAME = "genreName" +CONF_SPOTIFY_TRACK_NAME = "track_name" +CONF_SPOTIFY_ARTIST_NAME = "artist_name" +CONF_SPOTIFY_ALBUM_NAME = "album_name" +CONF_SPOTIFY_PLAYLIST_NAME = "playlist_name" +CONF_SPOTIFY_SHOW_NAME = "show_name" +CONF_SPOTIFY_EPISODE_NAME = "episode_name" +CONF_SPOTIFY_AUDIOBOOK_NAME = "audiobook_name" +CONF_SPOTIFY_GENRE_NAME = "genre_name" CONF_SPOTIFY_CATEGORY = "category" CONF_SPOTIFY_COUNTRY = "country" CONF_SPOTIFY_LIMIT = "limit" diff --git a/custom_components/spotcast/services.yaml b/custom_components/spotcast/services.yaml index 0da10358..477a5b5b 100644 --- a/custom_components/spotcast/services.yaml +++ b/custom_components/spotcast/services.yaml @@ -56,56 +56,56 @@ start: step: 1 min: 0 max: 50 - albumName: + album_name: name: "Album Name" example: "The Dark Side of the Moon" description: "Filters search results for the provided album name. Don't include this if you don't want an album." required: false selector: text: - trackName: + track_name: name: "Track Name" example: "Money" description: "Filters search results for the provided track name. Don't include this if you don't want a particular track." required: false selector: text: - playlistName: + playlist_name: name: "Playlist Name" example: "Ultimate pink floyd playlist" description: "Filters search results for the provided playlist name. Don't include this if you don't want a playlist." required: false selector: text: - showName: + show_name: name: "Show Name" example: "Hollywood Handbook" description: "Filters search results for the provided podcast show name. Don't include this if you don't want a podcast." required: false selector: text: - episodeName: + episode_name: name: "Episode Name" example: "Sarah Sherman, Our Close Friend" description: "Filters search results for the provided podcast episode name. Don't include this if you don't want a podcast." required: false selector: text: - genreName: + genre_name: name: "Genre Name" example: "post punk" description: "Filters search results by genre of music" required: false selector: text: - audiobookName: + audiobook_name: name: "Audiobook Name" example: "Ulysses" description: "Filters search results for the provided audiobook name. Don't include this if you don't want an audiobook." required: false selector: text: - artistName: + artist_name: name: "Artist Name" example: "pink floyd" description: "This will filter search results to match the provided artist name. Don't include this if searching for a playlist or genre. Do include the author's name if searching for audiobooks." From 6815b62324d6c7b54ff9ecab536bb1b561fe0c69 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 23:16:38 +0000 Subject: [PATCH 05/31] oops- forgot genre --- custom_components/spotcast/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index e9adc4d0..7962efb4 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -89,6 +89,7 @@ vol.Optional(CONF_SPOTIFY_SHOW_NAME): cv.string, vol.Optional(CONF_SPOTIFY_EPISODE_NAME): cv.string, vol.Optional(CONF_SPOTIFY_AUDIOBOOK_NAME): cv.string, + vol.Optional(CONF_SPOTIFY_GENRE_NAME): cv.string, vol.Optional(CONF_SPOTIFY_CATEGORY): cv.string, vol.Optional(CONF_SPOTIFY_COUNTRY): cv.string, vol.Optional(CONF_SPOTIFY_LIMIT, default=20): cv.positive_int, From 6eee74ae704a1d85d553b9c053c7bd8e0d848c92 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 23:23:46 +0000 Subject: [PATCH 06/31] Convert iter to list --- custom_components/spotcast/__init__.py | 24 +++++++++++++----------- custom_components/spotcast/helpers.py | 24 +++++++++++++----------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index cef6c4bf..a87ea033 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -239,17 +239,19 @@ def start_casting(call: ha_core.ServiceCall): if ( is_empty_str(uri) and len( - filter( - lambda x: not is_empty_str(x), - [ - artistName, - playlistName, - trackName, - showName, - episodeName, - audiobookName, - genreName, - ], + list( + filter( + lambda x: not is_empty_str(x), + [ + artistName, + playlistName, + trackName, + showName, + episodeName, + audiobookName, + genreName, + ], + ) ) ) == 0 diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index c27df2fe..4ec113a4 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -208,17 +208,19 @@ def get_search_results( if ( not is_empty_str(artistName) and len( - filter( - lambda x: not is_empty_str(x), - [ - albumName, - playlistName, - trackName, - showName, - episodeName, - audiobookName, - genreName, - ], + list( + filter( + lambda x: not is_empty_str(x), + [ + albumName, + playlistName, + trackName, + showName, + episodeName, + audiobookName, + genreName, + ], + ) ) ) == 0 From a59e2cd8af4afb02a9b389cf32d2b1698bf02a6d Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 23:32:30 +0000 Subject: [PATCH 07/31] pass genre all the way through --- custom_components/spotcast/__init__.py | 1 + custom_components/spotcast/helpers.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index a87ea033..5560549b 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -295,6 +295,7 @@ def start_casting(call: ha_core.ServiceCall): showName=showName, episodeName=episodeName, audiobookName=audiobookName, + genreName=genreName ) # play the first track if len(searchResults) > 0: diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 4ec113a4..fe3a7efe 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -151,13 +151,13 @@ def get_search_string( ) -> str: search = [] if not is_empty_str(artistName): - search.append(f"artistName:{artistName}") + search.append(f"artist:{artistName}") if not is_empty_str(albumName): - search.append(f"albumName:{albumName}") + search.append(f"album:{albumName}") if not is_empty_str(trackName): - search.append(f"trackName:{trackName}") + search.append(f"track:{trackName}") if not is_empty_str(genreName): - search.append(f"genreName:{genreName}") + search.append(f"genre:{genreName}") return " ".join(search) From 735d8fe48455e45b94762da7df441688f6a78907 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 23:43:15 +0000 Subject: [PATCH 08/31] no artist name for top tracks --- custom_components/spotcast/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index fe3a7efe..033050ca 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -126,7 +126,6 @@ def get_top_tracks( artist = spotify_client.search( q=search, - artistName=artistName, limit=1, offset=0, type="artist", From e7770af818f85279b463ab82aea00c664d5f44e1 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Fri, 16 Feb 2024 23:58:24 +0000 Subject: [PATCH 09/31] handle more than just tracks in responses --- custom_components/spotcast/helpers.py | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 033050ca..d1f15e06 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -243,12 +243,30 @@ def get_search_results( episodeName=episodeName, audiobookName=audiobookName, ) - try: - searchResults = spotify_client.search( - q=searchString, limit=limit, offset=0, type=searchTypes, market=country - )["tracks"]["items"] - except IndexError: - pass + searchResults = spotify_client.search( + q=searchString, limit=limit, offset=0, type=searchTypes, market=country + ) + + compiledResults = [] + if "tracks" in searchResults: + for item in searchResults["tracks"]["items"]: + compiledResults.append(item) + if "albums" in searchResults: + for item in searchResults["albums"]["items"]: + compiledResults.append(item) + if "playlists" in searchResults: + for item in searchResults["playlists"]["items"]: + compiledResults.append(item) + if "shows" in searchResults: + for item in searchResults["shows"]["items"]: + compiledResults.append(item) + if "audiobooks" in searchResults: + for item in searchResults["audiobooks"]["items"]: + compiledResults.append(item) + if "episodes" in searchResults: + for item in searchResults["episodes"]["items"]: + compiledResults.append(item) + _LOGGER.debug( "Found %d results for %s. First Track name: %s", len(searchResults), From 9c05b01a5fbe699814244cd64398cae61a06de02 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Sat, 17 Feb 2024 00:04:52 +0000 Subject: [PATCH 10/31] oops - return the right object --- custom_components/spotcast/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index d1f15e06..d26bc28c 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -269,12 +269,12 @@ def get_search_results( _LOGGER.debug( "Found %d results for %s. First Track name: %s", - len(searchResults), + len(compiledResults), searchString, - searchResults[0]["name"], + compiledResults[0]["name"], ) - return searchResults + return compiledResults def search_tracks( From 8e50c6df24f16f29a7c447612b0ac99b8eb4bb81 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Sat, 17 Feb 2024 00:10:33 +0000 Subject: [PATCH 11/31] only enqueue tracks, not nothin else --- custom_components/spotcast/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index d26bc28c..bd7cd94a 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -303,11 +303,13 @@ def search_tracks( def add_tracks_to_queue( spotify_client: spotipy.Spotify, tracks: list = [], limit: int = 20 ): - if len(tracks) == 0: + filtered = list(filter(lambda x: x['type'] == 'track', tracks)) + + if len(filtered) == 0: _LOGGER.debug("Cannot add ZERO tracks to the queue!") return - for track in tracks[:limit]: + for track in filtered[:limit]: _LOGGER.debug( "Adding " + track["name"] + " to the playback queue | " + track["uri"] ) From b3e6ffee9f2c61bff59e03701e6354c2b1499029 Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Sat, 17 Feb 2024 00:23:03 +0000 Subject: [PATCH 12/31] better support for searching for things other than music --- custom_components/spotcast/__init__.py | 2 +- custom_components/spotcast/helpers.py | 30 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 5560549b..37e13859 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -295,7 +295,7 @@ def start_casting(call: ha_core.ServiceCall): showName=showName, episodeName=episodeName, audiobookName=audiobookName, - genreName=genreName + genreName=genreName, ) # play the first track if len(searchResults) > 0: diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index bd7cd94a..139cede7 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -146,17 +146,33 @@ def get_top_tracks( def get_search_string( - artistName: str, albumName: str, trackName: str, genreName: str + artistName: str, + albumName: str, + trackName: str, + genreName: str, + playlistName: str, + showName: str, + episodeName: str, + audiobookName: str, ) -> str: search = [] if not is_empty_str(artistName): search.append(f"artist:{artistName}") + search.append(artistName) if not is_empty_str(albumName): search.append(f"album:{albumName}") + search.append(albumName) if not is_empty_str(trackName): search.append(f"track:{trackName}") + search.append(trackName) if not is_empty_str(genreName): search.append(f"genre:{genreName}") + search.append(genreName) + # if we are searching for a playlist, podcast, audiobook, we need some search query which is probably just the text we are looking for + for item in [playlistName, showName, episodeName, audiobookName]: + if not is_empty_str(item): + search.append(item) + return " ".join(search) @@ -232,6 +248,10 @@ def get_search_results( albumName=albumName, trackName=trackName, genreName=genreName, + playlistName=playlistName, + showName=showName, + episodeName=episodeName, + audiobookName=audiobookName, ) searchTypes = get_types_string( @@ -246,7 +266,7 @@ def get_search_results( searchResults = spotify_client.search( q=searchString, limit=limit, offset=0, type=searchTypes, market=country ) - + compiledResults = [] if "tracks" in searchResults: for item in searchResults["tracks"]["items"]: @@ -266,7 +286,7 @@ def get_search_results( if "episodes" in searchResults: for item in searchResults["episodes"]["items"]: compiledResults.append(item) - + _LOGGER.debug( "Found %d results for %s. First Track name: %s", len(compiledResults), @@ -303,8 +323,8 @@ def search_tracks( def add_tracks_to_queue( spotify_client: spotipy.Spotify, tracks: list = [], limit: int = 20 ): - filtered = list(filter(lambda x: x['type'] == 'track', tracks)) - + filtered = list(filter(lambda x: x["type"] == "track", tracks)) + if len(filtered) == 0: _LOGGER.debug("Cannot add ZERO tracks to the queue!") return From a45538687f06c7f064b00fc15b250f3343590e1b Mon Sep 17 00:00:00 2001 From: Fripplebubby Date: Sat, 17 Feb 2024 00:27:54 +0000 Subject: [PATCH 13/31] top tracks works differently --- custom_components/spotcast/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 139cede7..28057933 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -242,6 +242,7 @@ def get_search_results( ): searchResults = get_top_tracks(artistName, spotify_client) _LOGGER.debug("Playing top tracks for artist: %s", searchResults[0]["name"]) + return searchResults else: searchString = get_search_string( artistName=artistName, @@ -294,7 +295,7 @@ def get_search_results( compiledResults[0]["name"], ) - return compiledResults + return compiledResults def search_tracks( From 041514aa2049c6f3061e2da6bce9964ba791dab6 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Thu, 14 Mar 2024 11:30:40 +0100 Subject: [PATCH 14/31] Update spotcast_controller.py Proposed (temp) fix for https://github.com/fondberg/spotcast/issues/432#issuecomment-1974763399 --- custom_components/spotcast/spotcast_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/spotcast/spotcast_controller.py b/custom_components/spotcast/spotcast_controller.py index 2d395455..0818b8e7 100644 --- a/custom_components/spotcast/spotcast_controller.py +++ b/custom_components/spotcast/spotcast_controller.py @@ -367,13 +367,13 @@ def play( if random_song: if uri.find("album") > 0: results = client.album_tracks(uri, market=country_code) - position = random.randint(0, results["total"] - 1) + position = random.randint(0, int(results["total"]) - 1) elif uri.find("playlist") > 0: results = client.playlist_tracks(uri) - position = random.randint(0, results["total"] - 1) + position = random.randint(0, int(results["total"]) - 1) elif uri.find("collection") > 0: results = client.current_user_saved_tracks() - position = random.randint(0, results["total"] - 1) + position = random.randint(0, int(results["total"]) - 1) _LOGGER.debug( "Start playback at random position: %s", position) if uri.find("artist") < 1: From 273229e9c2bba475d66e17003a2d31c8797dc3f2 Mon Sep 17 00:00:00 2001 From: Siemon Geeroms Date: Sun, 7 Apr 2024 12:55:51 +0200 Subject: [PATCH 15/31] allow the user to specify the start position of the track --- custom_components/spotcast/__init__.py | 7 +++++++ custom_components/spotcast/const.py | 2 ++ custom_components/spotcast/services.yaml | 12 ++++++++++++ custom_components/spotcast/spotcast_controller.py | 9 +++++---- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 9d33b7a8..28798031 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -37,6 +37,7 @@ SCHEMA_WS_CASTDEVICES, SCHEMA_WS_DEVICES, SCHEMA_WS_PLAYER, + CONF_START_POSITION, SERVICE_START_COMMAND_SCHEMA, SPOTCAST_CONFIG_SCHEMA, WS_TYPE_SPOTCAST_ACCOUNTS, @@ -208,6 +209,7 @@ def start_casting(call: ha_core.ServiceCall): start_volume = call.data.get(CONF_START_VOL) spotify_device_id = call.data.get(CONF_SPOTIFY_DEVICE_ID) position = call.data.get(CONF_OFFSET) + start_position = call.data.get(CONF_START_POSITION) force_playback = call.data.get(CONF_FORCE_PLAYBACK) account = call.data.get(CONF_SPOTIFY_ACCOUNT) ignore_fully_played = call.data.get(CONF_IGNORE_FULLY_PLAYED) @@ -245,6 +247,9 @@ def start_casting(call: ha_core.ServiceCall): account, spotify_device_id, device_name, entity_id ) + if (start_position): + start_position *= 1000 + if ( is_empty_str(uri) and is_empty_str(search) @@ -275,6 +280,7 @@ def start_casting(call: ha_core.ServiceCall): random_song, position, ignore_fully_played, + start_position, ) else: @@ -289,6 +295,7 @@ def start_casting(call: ha_core.ServiceCall): random_song, position, ignore_fully_played, + start_position, ) if start_volume <= 100: diff --git a/custom_components/spotcast/const.py b/custom_components/spotcast/const.py index b75244e1..5ed3a11e 100644 --- a/custom_components/spotcast/const.py +++ b/custom_components/spotcast/const.py @@ -21,6 +21,7 @@ CONF_FORCE_PLAYBACK = "force_playback" CONF_RANDOM = "random_song" CONF_SHUFFLE = "shuffle" +CONF_START_POSITION = "start_position" CONF_SP_DC = "sp_dc" CONF_SP_KEY = "sp_key" CONF_START_VOL = "start_volume" @@ -85,6 +86,7 @@ vol.Optional(CONF_REPEAT, default="off"): cv.string, vol.Optional(CONF_SHUFFLE, default=False): cv.boolean, vol.Optional(CONF_OFFSET, default=0): cv.string, + vol.Optional(CONF_START_POSITION): cv.positive_int, vol.Optional(CONF_START_VOL, default=101): cv.positive_int, vol.Optional(CONF_IGNORE_FULLY_PLAYED, default=False): cv.boolean, } diff --git a/custom_components/spotcast/services.yaml b/custom_components/spotcast/services.yaml index e24dc9c4..87aa667e 100644 --- a/custom_components/spotcast/services.yaml +++ b/custom_components/spotcast/services.yaml @@ -117,6 +117,18 @@ start: step: 1 min: 0 max: 999999 + start_position: + name: "Position" + description: "Start position of the track in seconds" + example: 1 + required: false + default: 0 + selector: + number: + mode: box + step: 1 + min: 0 + max: 999999 start_volume: name: "Start Volume" description: "Set the volume for playback in percentage." diff --git a/custom_components/spotcast/spotcast_controller.py b/custom_components/spotcast/spotcast_controller.py index 2d395455..bb0ff6c1 100644 --- a/custom_components/spotcast/spotcast_controller.py +++ b/custom_components/spotcast/spotcast_controller.py @@ -317,6 +317,7 @@ def play( random_song: bool, position: str, ignore_fully_played: str, + position_ms: str, country_code: str = None ) -> None: _LOGGER.debug( @@ -344,14 +345,14 @@ def play( episode_uri, ) client.start_playback( - device_id=spotify_device_id, uris=[episode_uri]) + device_id=spotify_device_id, uris=[episode_uri], position_ms = position_ms) elif uri.find("episode") > 0: _LOGGER.debug("Playing episode using uris= for uri: %s", uri) - client.start_playback(device_id=spotify_device_id, uris=[uri]) + client.start_playback(device_id=spotify_device_id, uris=[uri], position_ms = position_ms) elif uri.find("track") > 0: _LOGGER.debug("Playing track using uris= for uri: %s", uri) - client.start_playback(device_id=spotify_device_id, uris=[uri]) + client.start_playback(device_id=spotify_device_id, uris=[uri], position_ms = position_ms) else: if uri == "random": _LOGGER.debug( @@ -362,7 +363,7 @@ def play( no_playlists = len(playlists["items"]) uri = playlists["items"][random.randint( 0, no_playlists - 1)]["uri"] - kwargs = {"device_id": spotify_device_id, "context_uri": uri} + kwargs = {"device_id": spotify_device_id, "context_uri": uri, "position_ms": position_ms} if random_song: if uri.find("album") > 0: From bece64ff253a8f2ef6b578e686841f3e332e835c Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+fcusson@users.noreply.github.com> Date: Tue, 7 May 2024 19:58:37 +0000 Subject: [PATCH 16/31] added todo task --- custom_components/spotcast/spotcast_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/spotcast/spotcast_controller.py b/custom_components/spotcast/spotcast_controller.py index 2d395455..36f0303d 100644 --- a/custom_components/spotcast/spotcast_controller.py +++ b/custom_components/spotcast/spotcast_controller.py @@ -253,6 +253,9 @@ def get_token_instance(self, account: str = None) -> any: """Get token instance for account""" if account is None: account = "default" + + # TODO: add error logging when user provide invalid account + # name dc = self.accounts.get(account).get(CONF_SP_DC) key = self.accounts.get(account).get(CONF_SP_KEY) From ce7db878c1944c8a3a13c5051a21dc2ba8c2b116 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Thu, 8 Aug 2024 07:04:50 -0400 Subject: [PATCH 17/31] realigned versions --- config/bumpversion.cfg => .bumpversion.cfg | 5 +--- custom_components/spotcast/manifest.json | 34 +++++++++++----------- requirements.txt | 0 3 files changed, 18 insertions(+), 21 deletions(-) rename config/bumpversion.cfg => .bumpversion.cfg (75%) create mode 100644 requirements.txt diff --git a/config/bumpversion.cfg b/.bumpversion.cfg similarity index 75% rename from config/bumpversion.cfg rename to .bumpversion.cfg index 0cee8223..3e8751df 100644 --- a/config/bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,9 +3,6 @@ current_version = 3.7.1 commit = True [bumpversion:file:Makefile] - [bumpversion:file:setup.cfg] - -[bumpversion:manifest.json] - +[bumpversion:custom_componenets/spotcast/manifest.json] [bumpversion:file:custom_components/spotcast/__init__.py] diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index 6658f57c..97c9c45b 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -1,19 +1,19 @@ { - "domain": "spotcast", - "name": "Spotcast", - "after_dependencies": [ - "cast", - "spotify" - ], - "codeowners": [ - "@fondberg", - "@fcusson" - ], - "dependencies": [ - "spotify" - ], - "documentation": "https://github.com/fondberg/spotcast", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/fondberg/spotcast/issues", - "version": "v3.6.30" + "domain": "spotcast", + "name": "Spotcast", + "after_dependencies": [ + "cast", + "spotify" + ], + "codeowners": [ + "@fondberg", + "@fcusson" + ], + "dependencies": [ + "spotify" + ], + "documentation": "https://github.com/fondberg/spotcast", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/fondberg/spotcast/issues", + "version": "v3.7.1" } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e69de29b From fa4c8b5a11eafedd35991479ca2daa73b552df32 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Thu, 8 Aug 2024 07:08:25 -0400 Subject: [PATCH 18/31] =?UTF-8?q?Bump=20version:=203.7.1=20=E2=86=92=203.8?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 5 ++++- Makefile | 2 +- custom_components/spotcast/__init__.py | 2 +- setup.cfg | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3e8751df..67bfad80 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,8 +1,11 @@ [bumpversion] -current_version = 3.7.1 +current_version = 3.8.0 commit = True [bumpversion:file:Makefile] + [bumpversion:file:setup.cfg] + [bumpversion:custom_componenets/spotcast/manifest.json] + [bumpversion:file:custom_components/spotcast/__init__.py] diff --git a/Makefile b/Makefile index 811847cc..424e7316 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -__VERSION__ = "3.7.1" +__VERSION__ = "3.8.0" bump: bump2version --allow-dirty --current-version $(__VERSION__) patch Makefile custom_components/spotcast/manifest.json diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 8ad0ec0a..5f492546 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -__version__ = "3.7.1" +__version__ = "3.8.0" import collections import logging diff --git a/setup.cfg b/setup.cfg index 810910f0..37f31d3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.7.1 +current_version = 3.8.0 [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build From f77d928e200c91fa9bc8e4083cab9b00c67fe5a7 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Thu, 8 Aug 2024 07:09:34 -0400 Subject: [PATCH 19/31] bumped version to 3.8.0 --- .bumpversion.cfg | 2 +- custom_components/spotcast/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 67bfad80..b257537a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -6,6 +6,6 @@ commit = True [bumpversion:file:setup.cfg] -[bumpversion:custom_componenets/spotcast/manifest.json] +[bumpversion:file:custom_components/spotcast/manifest.json] [bumpversion:file:custom_components/spotcast/__init__.py] diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index 97c9c45b..5686e78e 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://github.com/fondberg/spotcast", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/fondberg/spotcast/issues", - "version": "v3.7.1" + "version": "v3.8.0" } From 2bbf702b8705e41d090b01e7a5d11e7c73cca64d Mon Sep 17 00:00:00 2001 From: Siemon Geeroms Date: Fri, 16 Aug 2024 09:52:18 +0200 Subject: [PATCH 20/31] check if start_position is not none --- custom_components/spotcast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 28798031..cb961e96 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -247,7 +247,7 @@ def start_casting(call: ha_core.ServiceCall): account, spotify_device_id, device_name, entity_id ) - if (start_position): + if start_position is not None: start_position *= 1000 if ( From 7cb1ecb8ed5a85e2d6d02191090901be250f70c5 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Wed, 21 Aug 2024 15:09:56 -0400 Subject: [PATCH 21/31] added delay before adding to queue --- custom_components/spotcast/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 5f492546..180fd0fc 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -288,7 +288,8 @@ def start_casting(call: ha_core.ServiceCall): device_id=spotify_device_id, force_play=force_playback ) elif not is_empty_str(category): - uri = get_random_playlist_from_category(client, category, country, limit) + uri = get_random_playlist_from_category( + client, category, country, limit) if uri is None: _LOGGER.error("No playlist returned. Stop service call") @@ -332,8 +333,13 @@ def start_casting(call: ha_core.ServiceCall): ignore_fully_played, ) + # TODO: change to an API native way to monitor if playback + # has started. developper site down at the moment of + # writting + time.sleep(2) + if len(searchResults) > 1: - add_tracks_to_queue(client, searchResults[1 : len(searchResults)]) + add_tracks_to_queue(client, searchResults[1:]) if start_volume <= 100: _LOGGER.debug("Setting volume to %d", start_volume) From 3f2f264974fef23252ca95109f4f6ed89bea1404 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Wed, 21 Aug 2024 15:32:40 -0400 Subject: [PATCH 22/31] added retry when add to queue fails --- custom_components/spotcast/__init__.py | 5 --- custom_components/spotcast/helpers.py | 60 ++++++++++++++++++++------ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 180fd0fc..49f9ff62 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -333,11 +333,6 @@ def start_casting(call: ha_core.ServiceCall): ignore_fully_played, ) - # TODO: change to an API native way to monitor if playback - # has started. developper site down at the moment of - # writting - time.sleep(2) - if len(searchResults) > 1: add_tracks_to_queue(client, searchResults[1:]) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index 28057933..ca67231c 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -10,6 +10,7 @@ # import for type inference import spotipy +from spotipy import SpotifyException from homeassistant.components.cast.media_player import CastDevice from homeassistant.components.spotify.media_player import SpotifyMediaPlayer from homeassistant.exceptions import HomeAssistantError @@ -40,7 +41,8 @@ def get_spotify_media_player( entity_devices = entity.data.devices.data _LOGGER.debug( - f"get_spotify_devices: {entity.entity_id}: {entity.name}: %s", + f"get_spotify_devices: { + entity.entity_id}: {entity.name}: %s", entity_devices, ) spotify_media_player = entity @@ -89,7 +91,8 @@ def get_cast_devices(hass): for entity in platform.entities.values(): if isinstance(entity, CastDevice): _LOGGER.debug( - f"get_cast_devices: {entity.entity_id}: {entity.name} cast info: %s", + f"get_cast_devices: {entity.entity_id}: { + entity.name} cast info: %s", entity._cast_info, ) cast_infos.append(entity._cast_info) @@ -169,7 +172,7 @@ def get_search_string( search.append(f"genre:{genreName}") search.append(genreName) # if we are searching for a playlist, podcast, audiobook, we need some search query which is probably just the text we are looking for - for item in [playlistName, showName, episodeName, audiobookName]: + for item in [playlistName, showName, episodeName, audiobookName]: if not is_empty_str(item): search.append(item) @@ -241,7 +244,8 @@ def get_search_results( == 0 ): searchResults = get_top_tracks(artistName, spotify_client) - _LOGGER.debug("Playing top tracks for artist: %s", searchResults[0]["name"]) + _LOGGER.debug("Playing top tracks for artist: %s", + searchResults[0]["name"]) return searchResults else: searchString = get_search_string( @@ -308,7 +312,8 @@ def search_tracks( artistName: str = None, country: str = None, ): - results = get_search_results(search, spotify_client, artistName, limit, country) + results = get_search_results( + search, spotify_client, artistName, limit, country) if len(results) > 0: firstResult = [results[0]] if not startRandom: @@ -332,9 +337,35 @@ def add_tracks_to_queue( for track in filtered[:limit]: _LOGGER.debug( - "Adding " + track["name"] + " to the playback queue | " + track["uri"] + "Adding " + track["name"] + + " to the playback queue | " + track["uri"] ) - spotify_client.add_to_queue(track["uri"]) + + max_attemps = 5 + backoff_rate = 1.2 + delay = 1 + current_attempt = 0 + + while True: + try: + spotify_client.add_to_queue(track["uri"]) + except SpotifyException as exc: + + if current_attempt >= max_attemps: + raise HomeAssistantError( + "Coulddn't addd song to queue" + ) from exc + + _LOGGER.warning("Couldn't add song to queue retrying") + + time.sleep(delay) + current_attempt += 1 + delay *= backoff_rate + + continue + + break + time.sleep(0.5) @@ -345,13 +376,15 @@ def get_random_playlist_from_category( if country is None: _LOGGER.debug( - f"Get random playlist among {limit} playlists from category {category}, no country specified." + f"Get random playlist among {limit} playlists from category { + category}, no country specified." ) else: _LOGGER.debug( - f"Get random playlist among {limit} playlists from category {category} in country {country}" + f"Get random playlist among {limit} playlists from category { + category} in country {country}" ) # validate category and country are valid entries @@ -372,7 +405,8 @@ def get_random_playlist_from_category( chosen = random.choice(playlists) _LOGGER.debug( - f"Chose playlist {chosen['name']} ({chosen['uri']}) from category {category}." + f"Chose playlist {chosen['name']} ({chosen['uri']}) from category { + category}." ) return chosen["uri"] @@ -407,13 +441,15 @@ def is_valid_uri(uri: str) -> bool: # check correct format of the sub elements if elems[0].lower() != "spotify": _LOGGER.error( - f"This is not a valid Spotify URI. This should start with [spotify], but instead starts with [{elems[0]}]" + f"This is not a valid Spotify URI. This should start with [spotify], but instead starts with [{ + elems[0]}]" ) return False if elems[1].lower() not in types: _LOGGER.error( - f"{elems[1]} is not a valid type for Spotify request. Please make sure to use the following list {str(types)}" + f"{elems[1]} is not a valid type for Spotify request. Please make sure to use the following list { + str(types)}" ) return False From ca8715559c402b5705b20d9a0164b3eab3ccdbc5 Mon Sep 17 00:00:00 2001 From: Felix Cusson <60357894+fcusson@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:45:20 -0400 Subject: [PATCH 23/31] text format correction --- custom_components/spotcast/helpers.py | 62 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index ca67231c..ba396311 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -41,8 +41,8 @@ def get_spotify_media_player( entity_devices = entity.data.devices.data _LOGGER.debug( - f"get_spotify_devices: { - entity.entity_id}: {entity.name}: %s", + f"get_spotify_devices: {entity.entity_id}: " + f"{entity.name}: %s", entity_devices, ) spotify_media_player = entity @@ -56,7 +56,8 @@ def get_spotify_media_player( def get_spotify_devices(spotify_media_player: SpotifyMediaPlayer): if spotify_media_player: - # Need to come from media_player spotify's sp client due to token issues + # Need to come from media_player spotify's sp client due to + # token issues try: spotify_devices = spotify_media_player._spotify.devices() except (AttributeError): @@ -91,8 +92,8 @@ def get_cast_devices(hass): for entity in platform.entities.values(): if isinstance(entity, CastDevice): _LOGGER.debug( - f"get_cast_devices: {entity.entity_id}: { - entity.name} cast info: %s", + f"get_cast_devices: {entity.entity_id}: " + f"{entity.name} cast info: % s", entity._cast_info, ) cast_infos.append(entity._cast_info) @@ -171,8 +172,10 @@ def get_search_string( if not is_empty_str(genreName): search.append(f"genre:{genreName}") search.append(genreName) - # if we are searching for a playlist, podcast, audiobook, we need some search query which is probably just the text we are looking for - for item in [playlistName, showName, episodeName, audiobookName]: + # if we are searching for a playlist, podcast, audiobook, we need + # some search query which is probably just the text we are looking + # for + for item in [playlistName, showName, episodeName, audiobookName]: if not is_empty_str(item): search.append(item) @@ -269,7 +272,11 @@ def get_search_results( audiobookName=audiobookName, ) searchResults = spotify_client.search( - q=searchString, limit=limit, offset=0, type=searchTypes, market=country + q=searchString, + limit=limit, + offset=0, + type=searchTypes, + market=country ) compiledResults = [] @@ -365,26 +372,29 @@ def add_tracks_to_queue( continue break - + time.sleep(0.5) def get_random_playlist_from_category( - spotify_client: spotipy.Spotify, category: str, country: str = None, limit: int = 20 + spotify_client: spotipy.Spotify, + category: str, + country: str = None, + limit: int = 20, ) -> str: if country is None: _LOGGER.debug( - f"Get random playlist among {limit} playlists from category { - category}, no country specified." + f"Get random playlist among {limit} playlists from category " + f"{category}, no country specified." ) else: _LOGGER.debug( - f"Get random playlist among {limit} playlists from category { - category} in country {country}" + f"Get random playlist among {limit} playlists from category " + f"{category} in country {country}" ) # validate category and country are valid entries @@ -405,8 +415,8 @@ def get_random_playlist_from_category( chosen = random.choice(playlists) _LOGGER.debug( - f"Chose playlist {chosen['name']} ({chosen['uri']}) from category { - category}." + f"Chose playlist {chosen['name']}({chosen['uri']}) from category " + f"{category}." ) return chosen["uri"] @@ -425,37 +435,41 @@ def is_valid_uri(uri: str) -> bool: elems = elems[0:1] + elems[3:] types = ["playlist"] _LOGGER.debug( - "Excluding user information from the Spotify URI validation. Only supported for playlists" + "Excluding user information from the Spotify URI validation. Only" + " supported for playlists" ) - # support playing a user's liked songs list (spotify:user:username:collection) + # support playing a user's liked songs list + # (spotify:user:username:collection) if len(elems) == 2 and elems[1].lower() == "collection": return True if len(elems) != 3: _LOGGER.error( - f"[{uri}] is not a valid URI. The format should be [spotify::]" + f"[{uri}] is not a valid URI. The format should be " + "[spotify::]" ) return False # check correct format of the sub elements if elems[0].lower() != "spotify": _LOGGER.error( - f"This is not a valid Spotify URI. This should start with [spotify], but instead starts with [{ - elems[0]}]" + f"This is not a valid Spotify URI. This should start with " + f"[spotify], but instead starts with [{elems[0]}]" ) return False if elems[1].lower() not in types: _LOGGER.error( - f"{elems[1]} is not a valid type for Spotify request. Please make sure to use the following list { - str(types)}" + f"{elems[1]} is not a valid type for Spotify request. Please " + f"make sure to use the following list {str(types)}" ) return False if "?" in elems[2]: _LOGGER.warning( - f"{elems[2]} contains query character. This should work, but you should probably remove it and anything after." + f"{elems[2]} contains query character. This should work, but you" + " should probably remove it and anything after." ) # return True if all test passes From c62408c8d661a3d2eb2b705b0bb181f4fa64115c Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Wed, 21 Aug 2024 15:46:29 -0400 Subject: [PATCH 24/31] =?UTF-8?q?Bump=20version:=203.8.0=20=E2=86=92=203.8?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- Makefile | 2 +- custom_components/spotcast/__init__.py | 2 +- custom_components/spotcast/manifest.json | 2 +- setup.cfg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b257537a..539c0ff8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.8.0 +current_version = 3.8.1 commit = True [bumpversion:file:Makefile] diff --git a/Makefile b/Makefile index 424e7316..b94fd016 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -__VERSION__ = "3.8.0" +__VERSION__ = "3.8.1" bump: bump2version --allow-dirty --current-version $(__VERSION__) patch Makefile custom_components/spotcast/manifest.json diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 49f9ff62..727fe026 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -__version__ = "3.8.0" +__version__ = "3.8.1" import collections import logging diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index 5686e78e..f60f41ff 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://github.com/fondberg/spotcast", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/fondberg/spotcast/issues", - "version": "v3.8.0" + "version": "v3.8.1" } diff --git a/setup.cfg b/setup.cfg index 37f31d3f..62651d4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.8.0 +current_version = 3.8.1 [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build From 4a73856c2bdba5c2a748b3af740b76383fdc329b Mon Sep 17 00:00:00 2001 From: Maxence Dunnewind Date: Wed, 28 Aug 2024 13:14:03 +0200 Subject: [PATCH 25/31] fix: do not go in transfer playback condition when asking for category --- 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 727fe026..593aa4e0 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -271,6 +271,7 @@ def start_casting(call: ha_core.ServiceCall): episodeName, audiobookName, genreName, + category, ], ) ) From f7f04b8e9f9bc5d0c7f1b81d0a86251d42457ea8 Mon Sep 17 00:00:00 2001 From: SkiBoard Date: Wed, 25 Sep 2024 23:20:42 +1000 Subject: [PATCH 26/31] Update README.md Spotify's developer platform must have changed since the last version. Updated documentation to reflect the new change. --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index af6430f3..6ccc943d 100644 --- a/README.md +++ b/README.md @@ -177,15 +177,8 @@ To use the Spotcast service with a Spotify Connect device, you need the `spotify #### With Spotify developer portal 1. Go to [Spotify developer console](https://developer.spotify.com/console/get-users-available-devices/) -2. Click `GET TOKEN`
-![get_token](./images/get_token.png) -3. Select `user-read-playback-state` as a scope
-![select_scope](./images/select_scope.png) -4. If prompt give permission to your Spotify profile -5. For chromecast devices, make sure to play media on the device prior to checking the logs as they will not show unless active -6. Press the option `Try it` -7. Read the result in the console in the right.
-![device_id](./images/device_id.png) +2. Click `TRY IT`
+3. Extract the ID (there could be more than one if you have more than one speaker) from the 'RESPONSE SAMPLE' on the right #### Through Spotcast log From 47d732ee76aa0aca4e97d8c4e200bfce29cf7939 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Sat, 28 Sep 2024 14:57:22 -0400 Subject: [PATCH 27/31] added config folder to ignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3577c634..929cdb70 100644 --- a/.gitignore +++ b/.gitignore @@ -114,4 +114,6 @@ venv.bak/ # mypy .mypy_cache/ -.vscode/ \ No newline at end of file +.vscode/ + +config/* From ccf408785546eb1e03ad7196504c6818edcd9666 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Sat, 28 Sep 2024 14:58:53 -0400 Subject: [PATCH 28/31] added a docker container file for working with the integration in test environment --- container/docker-compose.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 container/docker-compose.yaml diff --git a/container/docker-compose.yaml b/container/docker-compose.yaml new file mode 100644 index 00000000..8a856f72 --- /dev/null +++ b/container/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + homeassistant: + container_name: homeassistant + image: ghcr.io/home-assistant/home-assistant:beta + privileged: true + network_mode: host + volumes: + - ../custom_components/spotcast:/config/custom_components/spotcast + - ../config/:/config/ + From 2e61988b0c5e5f569e3a38aaf350b5f80ac8a573 Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Sat, 28 Sep 2024 16:15:36 -0400 Subject: [PATCH 29/31] made spotcast work with 2024.10.0b0 new DataUpdateCoordinator --- custom_components/spotcast/helpers.py | 18 ++++++++-- .../spotcast/spotcast_controller.py | 34 +++++++++++++------ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/custom_components/spotcast/helpers.py b/custom_components/spotcast/helpers.py index ba396311..e9e4ca0e 100644 --- a/custom_components/spotcast/helpers.py +++ b/custom_components/spotcast/helpers.py @@ -38,7 +38,10 @@ def get_spotify_media_player( try: entity_devices = entity._devices except (AttributeError): - entity_devices = entity.data.devices.data + try: + entity_devices = entity.data.devices.data + except AttributeError: + entity_devices = entity.devices.data _LOGGER.debug( f"get_spotify_devices: {entity.entity_id}: " @@ -54,14 +57,23 @@ def get_spotify_media_player( raise HomeAssistantError("Could not find spotify media player.") -def get_spotify_devices(spotify_media_player: SpotifyMediaPlayer): +def get_spotify_devices(spotify_media_player: SpotifyMediaPlayer, hass): + if spotify_media_player: # Need to come from media_player spotify's sp client due to # token issues try: spotify_devices = spotify_media_player._spotify.devices() except (AttributeError): - spotify_devices = spotify_media_player.data.client.devices() + try: + spotify_devices = spotify_media_player.data.client.devices() + except AttributeError: + asyncio.run_coroutine_threadsafe( + spotify_media_player.devices.async_refresh(), + hass.loop + ).result() + spotify_devices = {} + spotify_devices["devices"] = spotify_media_player.devices.data _LOGGER.debug("get_spotify_devices: %s", spotify_devices) diff --git a/custom_components/spotcast/spotcast_controller.py b/custom_components/spotcast/spotcast_controller.py index e5f72ee8..5baa7468 100644 --- a/custom_components/spotcast/spotcast_controller.py +++ b/custom_components/spotcast/spotcast_controller.py @@ -110,10 +110,14 @@ def get_spotify_device_id(self, user_id) -> None: counter = 0 devices_available = None _LOGGER.debug( - "Searching for Spotify device: {}".format(self.spotifyController.device) + "Searching for Spotify device: {}".format( + self.spotifyController.device) ) while counter < max_retries: - devices_available = get_spotify_devices(spotify_media_player) + devices_available = get_spotify_devices( + spotify_media_player, + self.hass + ) # Look for device to make sure we can start playback if devices := devices_available["devices"]: for device in devices: @@ -243,7 +247,8 @@ def __init__( ) -> None: if accs: self.accounts = accs - self.accounts["default"] = OrderedDict([("sp_dc", sp_dc), ("sp_key", sp_key)]) + self.accounts["default"] = OrderedDict( + [("sp_dc", sp_dc), ("sp_key", sp_key)]) self.hass = hass def get_token_instance(self, account: str = None) -> any: @@ -258,15 +263,17 @@ def get_token_instance(self, account: str = None) -> any: _LOGGER.debug("setting up with account %s", account) if account not in self.spotifyTokenInstances: - self.spotifyTokenInstances[account] = SpotifyToken(self.hass, dc, key) + self.spotifyTokenInstances[account] = SpotifyToken( + self.hass, dc, key) return self.spotifyTokenInstances[account] def get_spotify_client(self, account: str) -> spotipy.Spotify: return spotipy.Spotify(auth=self.get_token_instance(account).access_token) def _getSpotifyConnectDeviceId(self, client, device_name): - media_player = get_spotify_media_player(self.hass, client._get("me")["id"]) - devices_available = get_spotify_devices(media_player) + media_player = get_spotify_media_player( + self.hass, client._get("me")["id"]) + devices_available = get_spotify_devices(media_player, self.hass) for device in devices_available["devices"]: if device["name"] == device_name: return device["id"] @@ -274,14 +281,16 @@ def _getSpotifyConnectDeviceId(self, client, device_name): def get_spotify_device_id(self, account, spotify_device_id, device_name, entity_id): # login as real browser to get powerful token - access_token, expires = self.get_token_instance(account).get_spotify_token() + access_token, expires = self.get_token_instance( + account).get_spotify_token() # get the spotify web api client client = spotipy.Spotify(auth=access_token) # first, rely on spotify id given in config if not spotify_device_id: # if not present, check if there's a spotify connect device # with that name - spotify_device_id = self._getSpotifyConnectDeviceId(client, device_name) + spotify_device_id = self._getSpotifyConnectDeviceId( + client, device_name) if not spotify_device_id: # if still no id available, check cast devices and launch # the app on chromecast @@ -293,7 +302,8 @@ def get_spotify_device_id(self, account, spotify_device_id, device_name, entity_ me_resp = client._get("me") spotify_cast_device.start_spotify_controller(access_token, expires) # Make sure it is started - spotify_device_id = spotify_cast_device.get_spotify_device_id(me_resp["id"]) + spotify_device_id = spotify_cast_device.get_spotify_device_id( + me_resp["id"]) return spotify_device_id def play( @@ -331,7 +341,8 @@ def play( ), episode_uri, ) - client.start_playback(device_id=spotify_device_id, uris=[episode_uri]) + client.start_playback( + device_id=spotify_device_id, uris=[episode_uri]) elif uri.find("episode") > 0: _LOGGER.debug("Playing episode using uris= for uri: %s", uri) client.start_playback(device_id=spotify_device_id, uris=[uri]) @@ -346,7 +357,8 @@ def play( ) playlists = client.user_playlists("me", 50) no_playlists = len(playlists["items"]) - uri = playlists["items"][random.randint(0, no_playlists - 1)]["uri"] + uri = playlists["items"][random.randint( + 0, no_playlists - 1)]["uri"] kwargs = {"device_id": spotify_device_id, "context_uri": uri} if random_song: From cb2f905a6c62022b4a97a0f611026c409e661e3f Mon Sep 17 00:00:00 2001 From: Felix Cusson Date: Sat, 28 Sep 2024 16:15:45 -0400 Subject: [PATCH 30/31] =?UTF-8?q?Bump=20version:=203.8.1=20=E2=86=92=203.8?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- Makefile | 2 +- custom_components/spotcast/__init__.py | 2 +- custom_components/spotcast/manifest.json | 2 +- setup.cfg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 539c0ff8..0a90e553 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.8.1 +current_version = 3.8.2 commit = True [bumpversion:file:Makefile] diff --git a/Makefile b/Makefile index b94fd016..c4b5c5e2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -__VERSION__ = "3.8.1" +__VERSION__ = "3.8.2" bump: bump2version --allow-dirty --current-version $(__VERSION__) patch Makefile custom_components/spotcast/manifest.json diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 727fe026..9741ac24 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -__version__ = "3.8.1" +__version__ = "3.8.2" import collections import logging diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index f60f41ff..b6911852 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://github.com/fondberg/spotcast", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/fondberg/spotcast/issues", - "version": "v3.8.1" + "version": "v3.8.2" } diff --git a/setup.cfg b/setup.cfg index 62651d4e..f28f8937 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.8.1 +current_version = 3.8.2 [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build From 81a4ea220ec5d4be889eae7b0d8516562e97b351 Mon Sep 17 00:00:00 2001 From: Geoffrey Oxholm Date: Wed, 2 Oct 2024 10:48:08 -0700 Subject: [PATCH 31/31] fix as noted in issue 461 --- custom_components/spotcast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 9741ac24..1bb2eeae 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -143,7 +143,7 @@ def get_devices(): me_resp = client._get("me") # pylint: disable=W0212 spotify_media_player = get_spotify_media_player( hass, me_resp["id"]) - resp = get_spotify_devices(spotify_media_player) + resp = get_spotify_devices(spotify_media_player, hass) connection.send_message( websocket_api.result_message(msg["id"], resp))