diff --git a/custom_components/ytube_music_player/config_flow.py b/custom_components/ytube_music_player/config_flow.py index a53a9fa..f2528fc 100644 --- a/custom_components/ytube_music_player/config_flow.py +++ b/custom_components/ytube_music_player/config_flow.py @@ -209,12 +209,24 @@ async def async_create_form(hass, user_input, page=1): data_schema[vol.Required(CONF_ADVANCE_CONFIG, default=user_input[CONF_ADVANCE_CONFIG])] = vol.Coerce(bool) # show page 2 elif(page == 3): - data_schema[vol.Optional(CONF_SHUFFLE, default=user_input[CONF_SHUFFLE])] = vol.Coerce(bool) # default duffle, TRUE/FALSE - data_schema[vol.Optional(CONF_LIKE_IN_NAME, default=user_input[CONF_LIKE_IN_NAME])] = vol.Coerce(bool) # default duffle, TRUE/FALSE - data_schema[vol.Optional(CONF_DEBUG_AS_ERROR, default=user_input[CONF_DEBUG_AS_ERROR])] = vol.Coerce(bool) # default duffle, TRUE/FALSE + data_schema[vol.Optional(CONF_SHUFFLE, default=user_input[CONF_SHUFFLE])] = vol.Coerce(bool) # default shuffle, TRUE/FALSE + data_schema[vol.Optional(CONF_SHUFFLE_MODE, default=user_input[CONF_SHUFFLE_MODE])] = selector({ # choose default shuffle mode + "select": { + "options": ALL_SHUFFLE_MODES, + "mode": "dropdown" + } + }) + data_schema[vol.Optional(CONF_LIKE_IN_NAME, default=user_input[CONF_LIKE_IN_NAME])] = vol.Coerce(bool) # default like_in_name, TRUE/FALSE + data_schema[vol.Optional(CONF_DEBUG_AS_ERROR, default=user_input[CONF_DEBUG_AS_ERROR])] = vol.Coerce(bool) # debug_as_error, TRUE/FALSE data_schema[vol.Optional(CONF_LEGACY_RADIO, default=user_input[CONF_LEGACY_RADIO])] = vol.Coerce(bool) # default radio generation typ data_schema[vol.Optional(CONF_SORT_BROWSER, default=user_input[CONF_SORT_BROWSER])] = vol.Coerce(bool) # sort browser results data_schema[vol.Optional(CONF_INIT_EXTRA_SENSOR, default=user_input[CONF_INIT_EXTRA_SENSOR])] = vol.Coerce(bool) # default radio generation typ + data_schema[vol.Optional(CONF_INIT_DROPDOWNS,default=user_input[CONF_INIT_DROPDOWNS])] = selector({ # choose dropdown(s) + "select": { + "options": ALL_DROPDOWNS, + "multiple": "true" + } + }) data_schema[vol.Optional(CONF_TRACK_LIMIT, default=user_input[CONF_TRACK_LIMIT])] = vol.Coerce(int) data_schema[vol.Optional(CONF_MAX_DATARATE, default=user_input[CONF_MAX_DATARATE])] = vol.Coerce(int) data_schema[vol.Optional(CONF_BRAND_ID, default=user_input[CONF_BRAND_ID])] = str # brand id diff --git a/custom_components/ytube_music_player/const.py b/custom_components/ytube_music_player/const.py index 826e55b..dff2b1c 100644 --- a/custom_components/ytube_music_player/const.py +++ b/custom_components/ytube_music_player/const.py @@ -115,6 +115,9 @@ CONF_LEGACY_RADIO = 'legacy_radio' CONF_SORT_BROWSER = 'sort_browser' CONF_INIT_EXTRA_SENSOR = 'extra_sensor' +CONF_INIT_DROPDOWNS = 'dropdowns' +ALL_DROPDOWNS = ["playlists","speakers","playmode","radiomode","repeatmode"] +DEFAULT_INIT_DROPDOWNS = ["playlists","speakers","playmode"] CONF_MAX_DATARATE = 'max_datarate' CONF_TRACK_LIMIT = 'track_limit' @@ -132,8 +135,6 @@ DEFAULT_MAX_DATARATE = 129000 DEFAULT_LEGACY_RADIO = True DEFAULT_SORT_BROWSER = True -DEFAULT_SHUFFLE_MODE = 1 -DEFAULT_SHUFFLE = True ERROR_COOKIE = 'ERROR_COOKIE' ERROR_AUTH_USER = 'ERROR_AUTH_USER' @@ -148,6 +149,10 @@ PLAYMODE_SHUFFLE_RANDOM = "Shuffle Random" PLAYMODE_DIRECT = "Direct" +ALL_SHUFFLE_MODES = [PLAYMODE_SHUFFLE, PLAYMODE_RANDOM, PLAYMODE_SHUFFLE_RANDOM, PLAYMODE_DIRECT] +DEFAULT_SHUFFLE_MODE = PLAYMODE_SHUFFLE_RANDOM +DEFAULT_SHUFFLE = True + SEARCH_ID = "search_id" SEARCH_TYPE = "search_type" LIB_PLAYLIST = 'library_playlists' @@ -308,6 +313,7 @@ def ensure_config(user_input): out[CONF_LEGACY_RADIO] = DEFAULT_LEGACY_RADIO out[CONF_SORT_BROWSER] = DEFAULT_SORT_BROWSER out[CONF_INIT_EXTRA_SENSOR] = DEFAULT_INIT_EXTRA_SENSOR + out[CONF_INIT_DROPDOWNS] = DEFAULT_INIT_DROPDOWNS out[CONF_MAX_DATARATE] = DEFAULT_MAX_DATARATE if user_input is not None: diff --git a/custom_components/ytube_music_player/media_player.py b/custom_components/ytube_music_player/media_player.py index d400717..4c93820 100644 --- a/custom_components/ytube_music_player/media_player.py +++ b/custom_components/ytube_music_player/media_player.py @@ -9,6 +9,7 @@ import requests import voluptuous as vol +from homeassistant.components.media_player import BrowseError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_call_later @@ -22,6 +23,7 @@ from pytube import extract # to generate cipher import ytmusicapi +from pytube.exceptions import RegexMatchError # use this to work with local version # and make sure that the local package is also only loading local files # from .ytmusicapi import YTMusic @@ -84,18 +86,20 @@ def __init__(self, hass, config, name_add): self._attr_name = self._org_name self._api_language = config.data.get(CONF_API_LANGUAGE, DEFAULT_API_LANGUAGE) self._init_extra_sensor = config.data.get(CONF_INIT_EXTRA_SENSOR, DEFAULT_INIT_EXTRA_SENSOR) + self._init_dropdowns = config.data.get(CONF_INIT_DROPDOWNS, DEFAULT_INIT_DROPDOWNS) self._maxDatarate = config.data.get(CONF_MAX_DATARATE,DEFAULT_MAX_DATARATE) # All entities are now automatically generated,will be registered in the async_update_selects method later. # This should be helpful for multiple accounts. - self._select_playlist = "" - self._select_playMode = "" - self._select_repeatMode = "" # Previously, it was _select_playContinuous. - self._select_speaker = "" # Previously, it was _select_mediaPlayer. - self._select_radioMode = "" # Previously, it was _select_source. + self._selects = dict() # use a dict to store the dropdown entity_id should be more convenient. + self._selects['playlists'] = None + self._selects['playmode'] = None + self._selects['repeatmode'] = None # Previously, it was _select_playContinuous. + self._selects['speakers'] = None # Previously, it was _select_mediaPlayer. + self._selects['radiomode'] = None # Previously, it was _select_source. self._like_in_name = config.data.get(CONF_LIKE_IN_NAME, DEFAULT_LIKE_IN_NAME) - self._shuffle = config.data.get(CONF_SHUFFLE, DEFAULT_SHUFFLE) + self._attr_shuffle = config.data.get(CONF_SHUFFLE, DEFAULT_SHUFFLE) self._shuffle_mode = config.data.get(CONF_SHUFFLE_MODE, DEFAULT_SHUFFLE_MODE) default_header_file = os.path.join(hass.config.path(STORAGE_DIR), DEFAULT_HEADER_FILENAME) @@ -114,7 +118,7 @@ def __init__(self, hass, config, name_add): self.log_me('debug', "YtubeMediaPlayer config: ") self.log_me('debug', "- Header path: " + self._header_file) self.log_me('debug', "- speakerlist: " + str(self._speakersList)) - self.log_me('debug', "- shuffle: " + str(self._shuffle)) + self.log_me('debug', "- shuffle: " + str(self._attr_shuffle)) self.log_me('debug', "- shuffle_mode: " + str(self._shuffle_mode)) self.log_me('debug', "- like_in_name: " + str(self._like_in_name)) self.log_me('debug', "- track_limit: " + str(self._trackLimit)) @@ -162,7 +166,7 @@ def __init__(self, hass, config, name_add): self._attributes['remote_player_id'] = None self._volume = 0.0 self._is_mute = False - self._playContinuous = True + self._attr_repeat = RepeatMode.ALL self._signatureTimestamp = 0 self._x_to_idle = None # Some Mediaplayer don't transition to 'idle' but to 'off' on track end. This re-routes off to idle self._ignore_paused_on_media_change = False # RobinR1, OwnTone compatibility @@ -470,27 +474,24 @@ def media_duration(self): @property def shuffle(self): # Boolean if shuffling is enabled. - return self._shuffle + return self._attr_shuffle @property def repeat(self): # Return current repeat mode. - if(self._playContinuous): - return RepeatMode.ALL - return RepeatMode.OFF + return self._attr_repeat async def async_set_repeat(self, repeat: str): - self.log_me('debug', "[S] set_repeat: " + repeat) - # Set repeat mode. - if repeat != RepeatMode.OFF: - self._playContinuous = True - else: - self._playContinuous = False - - if(self._select_repeatMode != ""): - data = {select.ATTR_OPTION: repeat, ATTR_ENTITY_ID: self._select_repeatMode} - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) - self.log_me('debug', "[E] set_repeat: " + repeat) + if self.repeat != repeat: + self.log_me('debug', f"[S] set_repeat: {repeat}") + # Set repeat mode. + self._attr_repeat = repeat + if(self._selects['repeatmode'] is not None): + if self.hass.states.get(self._selects['repeatmode']).state != repeat: + data = {select.ATTR_OPTION: repeat, ATTR_ENTITY_ID: self._selects['repeatmode']} + await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + self.log_me('debug', f"[E] set_repeat: {repeat}") + self.async_schedule_update_ha_state() @property def volume_level(self): @@ -504,7 +505,7 @@ async def async_turn_on(self, *args, **kwargs): self._started_by = "UI" # exit if we don't konw what to play (the select_playlist will be set to "" if the config provided a value but the entity_id is not in homeassistant) - if(self._select_playlist == ""): + if self._selects['playlists'] is None: self.log_me('debug', "no or wrong playlist select field in the config, exiting") msg = "You have no playlist entity_id in your config, or that entity_id is not in homeassistant. I don't know what to play and will exit. Either use the media_browser or add the playlist dropdown" data = {"title": "yTubeMediaPlayer error", "message": msg} @@ -514,7 +515,7 @@ async def async_turn_on(self, *args, **kwargs): return # set UI to correct playlist, or grab playlist if none was submitted - playlist = self.hass.states.get(self._select_playlist).state + playlist = self.hass.states.get(self._selects['playlists']).state # exit if we don't have any playlists from the account if(len(self._playlists) == 0): @@ -532,10 +533,10 @@ async def async_turn_on(self, *args, **kwargs): return # playlist or playlist_radio? - if(self._select_radioMode != ""): - _source = self.hass.states.get(self._select_radioMode) + if self._selects['radiomode'] is not None: + _source = self.hass.states.get(self._selects['radiomode']) if _source is None: - _LOGGER.error("- (%s) is not a valid select entity.", self._select_radioMode) + _LOGGER.error("- (%s) is not a valid select entity.", self._selects['radiomode']) self.log_me('debug', "[E] (fail) TURNON") return if(_source.state == "Playlist"): @@ -563,12 +564,12 @@ async def async_prepare_play(self): _player = self.hass.states.get(self._remote_player) # subscribe to changes - if(self._select_playMode != ""): - async_track_state_change(self.hass, self._select_playMode, self.async_update_playmode) - if(self._select_repeatMode != ""): - async_track_state_change(self.hass, self._select_repeatMode, self.async_update_playmode) - if(self._select_speaker != ""): - async_track_state_change(self.hass, self._select_speaker, self.async_select_source_helper) + if self._selects['playmode'] is not None: + async_track_state_change(self.hass, self._selects['playmode'], self.async_update_playmode) + if self._selects['repeatmode'] is not None: + async_track_state_change(self.hass, self._selects['repeatmode'], self.async_update_playmode) + if self._selects['speakers'] is not None: + async_track_state_change(self.hass, self._selects['speakers'], self.async_select_source_helper) # make sure that the player, is on and idle try: @@ -662,10 +663,10 @@ async def async_update_remote_player(self, remote_player=""): # make sure that the entity ID is complete remote_player = DOMAIN_MP + "." + remote_player # sets the current media_player from speaker select - elif(self._select_speaker != "" and await self.async_check_entity_exists(self._select_speaker, unavailable_is_ok=False)): # drop down for player does exist .. double check!! - media_player_select = self.hass.states.get(self._select_speaker) # Example: self.hass.states.get(select.gmusic_player_speakers) + elif(self._selects['speakers'] is not None and await self.async_check_entity_exists(self._selects['speakers'], unavailable_is_ok=False)): # drop down for player does exist .. double check!! + media_player_select = self.hass.states.get(self._selects['speakers']) # Example: self.hass.states.get(select.gmusic_player_speakers) if media_player_select is None: - self.log_me('error', "(" + self._select_speaker + ") is not a valid select entity to get the player.") + self.log_me('error', "(" + self._selects['speakers'] + ") is not a valid select entity to get the player.") else: # since we can't be sure if the MediaPlayer Domain is in the field value, add it and remove it :D remote_player = DOMAIN_MP + "." + media_player_select.state.replace(DOMAIN_MP + ".", "") @@ -794,7 +795,7 @@ async def async_sync_player(self, entity_id=None, old_state=None, new_state=None except: self.log_me('debug', "adding "+self._attributes['videoId']+" to history failed") - await self.async_get_track() + await self.async_get_track(auto_advance=True) # turn this player off when the remote_player was shut down elif((old_state.state == STATE_PLAYING or old_state.state == STATE_IDLE or old_state.state == STATE_PAUSED) and new_state.state == STATE_OFF): if(self._x_to_idle == STATE_OFF or self._x_to_idle == STATE_OFF_1X): # workaround for MPD (changes to OFF at the end of a track) @@ -805,7 +806,7 @@ async def async_sync_player(self, entity_id=None, old_state=None, new_state=None await self.hass.async_add_executor_job(lambda: self._api.add_history_item(response)) except: self.log_me('debug', "adding "+self._attributes['videoId']+" to history failed") - await self.async_get_track() + await self.async_get_track(auto_advance=True) if(self._x_to_idle == STATE_OFF_1X): self._x_to_idle = None else: @@ -823,7 +824,7 @@ async def async_sync_player(self, entity_id=None, old_state=None, new_state=None except: self.log_me('debug', "adding "+self._attributes['videoId']+" to history failed") self._allow_next = False - await self.async_get_track() + await self.async_get_track(auto_advance=True) # set this player in to pause state when the remote player does, or ignore when assumed it is a temporary state (as some players do while seeking/skipping track) elif(old_state.state == STATE_PLAYING and new_state.state == STATE_PAUSED): self.log_me('debug', "Remote Player changed from PLAYING to PAUSED.") @@ -846,7 +847,7 @@ async def async_sync_player(self, entity_id=None, old_state=None, new_state=None if self._allow_next: if (datetime.datetime.now() - self._last_auto_advance).total_seconds() > 10: self._allow_next = False - await self.async_get_track() + await self.async_get_track(auto_advance=True) # Set new volume if it has been changed on the _player # @@ -867,7 +868,7 @@ async def async_ytubemusic_play_media(self, event): if event.data['shuffle']: self.async_set_shuffle(event.data.get('shuffle')) - _LOGGER.info("- SHUFFLE: %s", self._shuffle) + _LOGGER.info("- SHUFFLE: %s", self._attr_shuffle) self.log_me('debug', "- Speakers: (%s) | Source: (%s) | Name: (%s)", _speak, _source, _media) await self.async_play_media(_source, _media, _speak) @@ -978,9 +979,9 @@ async def async_select_source(self, source=None): await self.async_update_remote_player(remote_player=DOMAIN_MP + "." + source) self.log_me('debug', "- Choosing " + self._remote_player + " as player") # try to set drop down - if(self._select_speaker != ""): - if(not await self.async_check_entity_exists(self._select_speaker, unavailable_is_ok=False)): - self.log_me('debug', "- Drop down for media player: " + str(self._select_speaker) + " not found") + if self._selects['speakers'] is not None: + if(not await self.async_check_entity_exists(self._selects['speakers'], unavailable_is_ok=False)): + self.log_me('debug', "- Drop down for media player: " + str(self._selects['speakers']) + " not found") elif source in self._friendly_speakersList: # untrack player field change (to avoid self call) if(self._untrack_remote_player_selector is not None): @@ -991,8 +992,9 @@ async def async_select_source(self, source=None): except: self.log_me('debug', "- untrack failed") pass - data = {select.ATTR_OPTION: self._friendly_speakersList[source], ATTR_ENTITY_ID: self._select_speaker} - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + if self.hass.states.get(self._selects['speakers']).state != self._friendly_speakersList[source]: + data = {select.ATTR_OPTION: self._friendly_speakersList[source], ATTR_ENTITY_ID: self._selects['speakers']} + await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) # resubscribe with 3 sec delay so the UI can settle, directly call it will trigger the change from above async_call_later(self.hass, 3, self.async_track_select_mediaplayer_helper) else: @@ -1005,7 +1007,6 @@ async def async_select_source(self, source=None): # if playing, switch player if(was_playing): # don't call "_play" here, as that resets the playlist position - self._next_track_no = max(self._next_track_no - 1, -1) # get track will increase the counter await self.async_get_track() # seek, if possible new_player = self.hass.states.get(self._remote_player) @@ -1023,22 +1024,26 @@ async def async_select_source(self, source=None): async def async_update_selects(self, now=None): self.log_me('debug', "[S] async_update_selects") - # -- all others -- # - if(not await self.async_check_entity_exists(self._select_playlist, unavailable_is_ok=False)): - self._select_playlist = self.hass.data[DOMAIN][self._attr_unique_id]['select_playlist'].entity_id # register select entity - self.log_me('debug', "- playlist select: " + str(self._select_playlist) + " registered") - if(not await self.async_check_entity_exists(self._select_playMode, unavailable_is_ok=False)): - self._select_playMode = self.hass.data[DOMAIN][self._attr_unique_id]['select_playmode'].entity_id # register select entity - self.log_me('debug', "- play mode select: " + str(self._select_playMode) + " registered") - if(not await self.async_check_entity_exists(self._select_repeatMode, unavailable_is_ok=False)): - self._select_repeatMode = self.hass.data[DOMAIN][self._attr_unique_id]['select_repeat'].entity_id # register select entity - self.log_me('debug', "- repeat mode Select: " + str(self._select_repeatMode) + " registered") - if(not await self.async_check_entity_exists(self._select_speaker, unavailable_is_ok=False)): - self._select_speaker = self.hass.data[DOMAIN][self._attr_unique_id]['select_speaker'].entity_id # register select entity - self.log_me('debug', "- speaker select: " + str(self._select_speaker) + " registered") - if(not await self.async_check_entity_exists(self._select_radioMode, unavailable_is_ok=False)): - self._select_radioMode = self.hass.data[DOMAIN][self._attr_unique_id]['select_radiomode'].entity_id # register select entity - self.log_me('debug', "- radio mode Select: " + str(self._select_radioMode) + " registered") + # -- Register dropdown(s). -- # + for dropdown in self._init_dropdowns: + if self._selects[dropdown] is None: + entity_id = self.hass.data[DOMAIN][self._attr_unique_id][f'select_{dropdown}'].entity_id + if await self.async_check_entity_exists(entity_id, unavailable_is_ok=False): + self._selects[dropdown] = entity_id + self.log_me('debug', f"- {dropdown} select: {str(entity_id)} registered") + + # track changes + if(self._untrack_remote_player_selector is not None): + try: + self._untrack_remote_player_selector() + except: + self.log_me('error', 'untrack failed') + if self._selects['speakers'] is not None: + self._untrack_remote_player_selector = async_track_state_change(self.hass, self._selects['speakers'], self.async_select_source_helper) + if self._selects['playmode'] is not None: + async_track_state_change(self.hass, self._selects['playmode'], self.async_update_playmode) + if self._selects['repeatmode'] is not None: + async_track_state_change(self.hass, self._selects['repeatmode'], self.async_update_playmode) # ----------- speaker -----# try: if(isinstance(self._speakersList, str)): @@ -1056,30 +1061,20 @@ async def async_update_selects(self, now=None): for e in all_entities: if(e.entity_id.startswith(DOMAIN_MP) and not(e.entity_id.startswith(DOMAIN_MP + "." + DOMAIN))): speakersList.append(e.entity_id.replace(DOMAIN_MP + ".", "")) - # create friendly speakerlist based on the current speakerLlist self._friendly_speakersList = dict() - friendly_speakersList = [] for a in speakersList: state = self.hass.states.get(DOMAIN_MP + "." + a) friendly_name = state.attributes.get(ATTR_FRIENDLY_NAME) if(friendly_name is None): friendly_name = a - friendly_speakersList.append(friendly_name) self._friendly_speakersList.update({a: friendly_name}) - await self.hass.data[DOMAIN][self._attr_unique_id]['select_speaker'].async_update(friendly_speakersList) # update speaker select - data = {select.ATTR_OPTION: friendly_speakersList[0], ATTR_ENTITY_ID: self._select_speaker} # select the first one in the list as the default player - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) - - # track changes - if(self._untrack_remote_player_selector is not None): - try: - self._untrack_remote_player_selector() - except: - self.log_me('error', 'untrack failed') - self._untrack_remote_player_selector = async_track_state_change(self.hass, self._select_speaker, self.async_select_source_helper) - - + friendly_speakersList = list(self._friendly_speakersList.values()) + if self._selects['speakers'] is not None: + await self.hass.data[DOMAIN][self._attr_unique_id]['select_speakers'].async_update(friendly_speakersList) # update speaker select + data = {select.ATTR_OPTION: friendly_speakersList[0], ATTR_ENTITY_ID: self._selects['speakers']} # select the first one in the list as the default player + await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + # finally call update playlist to fill the list .. if it exists await self.async_update_playlists() self.log_me('debug', "[E] async_update_selects") @@ -1102,7 +1097,7 @@ async def async_update_playlists(self, now=None): if(self._api is None): self.log_me('debug', "- no api, exit") return - if(self._select_playlist == ""): + if self._selects['playlists'] is None: self.log_me('debug', "- no playlist select field, exit") return @@ -1147,7 +1142,7 @@ async def async_update_playlists(self, now=None): # sort with case-ignore playlists = sorted(list(self._playlist_to_index.keys()), key=str.casefold) await self.async_update_extra_sensor('playlists', playlists_to_extra) # update extra sensor - await self.hass.data[DOMAIN][self._attr_unique_id]['select_playlist'].async_update() # update playlist select + await self.hass.data[DOMAIN][self._attr_unique_id]['select_playlists'].async_update() # update playlist select except: self.exc() msg = "Caught error while loading playlist. please log for details" @@ -1167,9 +1162,9 @@ async def _tracks_to_attribute(self): # fire event to let media card know to update event_data = { - "device_id": self._attr_unique_id, - "entity_id" : DOMAIN_MP+"."+self._attr_name, - "type": "reload_playlist", + "device_id": self._attr_unique_id, + "entity_id": self.entity_id, + "type": "reload_playlist", } self.hass.bus.async_fire(DOMAIN+"_event", event_data) self.log_me('debug', "[E] _tracks_to_attribute") @@ -1177,8 +1172,8 @@ async def _tracks_to_attribute(self): async def async_update_extra_sensor(self, attribute, value): # update extra sensor self.log_debug_later("[S] async_update_extra_sensor") + self.hass.data[DOMAIN][self._attr_unique_id][attribute] = value if(self._init_extra_sensor): - self.hass.data[DOMAIN][self._attr_unique_id][attribute] = value try: await self.hass.data[DOMAIN][self._attr_unique_id]['extra_sensor'].async_update() except: @@ -1189,69 +1184,76 @@ async def async_update_extra_sensor(self, attribute, value): async def async_update_playmode(self, entity_id=None, old_state=None, new_state=None): # called from HA when th user changes the input entry, will read selection to membervar + # Changing the shuffle_mode while music is playing will no longer cause interruptions. + # Only when shuffle_mode=true the next song will be random. + # By default, when shuffle_mode = "Shuffle", shuffle=False. However, + # the _attr_shuffle variable can be changed during playback without interrupting the music. self.log_me('debug', "[S] async_update_playmode") try: - if(self._select_repeatMode != ""): - if(self.hass.states.get(self._select_repeatMode).state == RepeatMode.ALL): - self._playContinuous = True - else: - self._playContinuous = False + if self._selects['repeatmode'] is not None: + await self.async_set_repeat(self.hass.states.get(self._selects['repeatmode']).state) except: - self.log_me('debug', "- Selection field " + self._select_repeatMode + " not found, skipping") + self.log_me('debug', "- Selection field " + self._selects['repeatmode'] + " not found, skipping") try: - if(self._select_playMode != ""): - _playmode = self.hass.states.get(self._select_playMode) + if self._selects['playmode'] is not None: + _playmode = self.hass.states.get(self._selects['playmode']).state if _playmode is not None: - if(_playmode.state == PLAYMODE_SHUFFLE): - self._shuffle = True - self._shuffle_mode = 1 - elif(_playmode.state == PLAYMODE_RANDOM): - self._shuffle = True - self._shuffle_mode = 2 - if(_playmode.state == PLAYMODE_SHUFFLE_RANDOM): - self._shuffle = True - self._shuffle_mode = 3 - if(_playmode.state == PLAYMODE_DIRECT): - self._shuffle = False - await self.async_set_shuffle(self._shuffle) # The non-existent set_shuffle method was incorrectly called previously. + if _playmode in (PLAYMODE_SHUFFLE,PLAYMODE_DIRECT): + shuffle = False + else: + shuffle = True + self._shuffle_mode = _playmode + await self.async_set_shuffle(shuffle) # The non-existent set_shuffle method was incorrectly called previously. + self.log_me('debug', f"- Playmode: {_playmode}") except: - self.log_me('debug', "- Selection field " + self._select_playMode + " not found, skipping") + self.log_me('debug', "- Selection field " + self._selects['playmode'] + " not found, skipping") - # if we've change the dropdown, reload the playlist and start playing - # else only change the mode - if((entity_id == self._select_playMode) and (old_state is not None) and (new_state is not None) and (self.state == STATE_PLAYING)): - self._allow_next = False # player will change to idle, avoid auto_advance - self.log_me('debug', "[E] async_update_playmode") - return await self.async_play_media(media_type=self._attributes['_media_type'], media_id=self._attributes['_media_id']) - self.log_me('debug', "[E] async_update_playmode 2") + self.log_me('debug', "[E] async_update_playmode") async def async_play(self): - self.log_me('debug', "_play") - self._next_track_no = -1 - await self.async_get_track() + self.log_me('debug', f"_play,shuffle:{self.shuffle},shuffle_mode:{self._shuffle_mode}") + self._next_track_no = 0 + if self.shuffle: + await self.async_get_track(keep_track_no=False) + else: + await self.async_get_track() - async def async_get_track(self, entity_id=None, old_state=None, new_state=None, retry=3): + async def async_get_track(self, entity_id=None, old_state=None, new_state=None, retry=3, auto_advance=False, keep_track_no=True): self.log_me('debug', "[S] async_get_track") # Get a track and play it from the track_queue. # grab next track from prefetched lis _track = None # get next track nr (randomly or by increasing). - if self._shuffle and self._shuffle_mode != 1 and len(self._tracks) > 1: # 1 will use the list as is (shuffled). 2 and 3 will also take songs randomized + if auto_advance: # auto_advance=true means that the call is coming from automatic playback of the next track. + if self.repeat == RepeatMode.ONE: + self.log_me('debug', "Single track loop.") + elif self.shuffle: + self._next_track_no = random.randrange(len(self._tracks)) - 1 + self.log_me('debug', "Random track.") + else: + self._next_track_no = self._next_track_no + 1 + self.log_me('debug', "- Playing track nr " + str(self._next_track_no + 1) + " / " + str(len(self._tracks))) # technically + 1 is wrong, but is still less confusing + if self._next_track_no >= len(self._tracks): + # we've reached the end of the playlist + if(self.repeat == RepeatMode.ALL): + # call PLAY_MEDIA with the same arguments + # return await self.async_play_media(media_type=self._attributes['_media_type'], media_id=self._attributes['_media_id']) + self._next_track_no = 0 # This maybe better. + else: + _LOGGER.info("- End of playlist and repeat mode is off") + await self.async_turn_off_media_player() + return + elif keep_track_no: + self.log_me('debug', "The track_no has already been specified,do not change it.") + elif self.shuffle: self._next_track_no = random.randrange(len(self._tracks)) - 1 + self.log_me('debug', f"auto_advance={auto_advance},keep_track_no={keep_track_no},repeat={self.repeat},shuffle_mode={self._shuffle_mode}") + self.log_me('debug', "Press the next/pref button and shuffle is true, play random track.") else: - self._next_track_no = self._next_track_no + 1 - self.log_me('debug', "- Playing track nr " + str(self._next_track_no + 1) + " / " + str(len(self._tracks))) # technically + 1 is wrong, but is still less confusing - if self._next_track_no >= len(self._tracks): - # we've reached the end of the playlist - if(self._playContinuous): - # call PLAY_MEDIA with the same arguments - return await self.async_play_media(media_type=self._attributes['_media_type'], media_id=self._attributes['_media_id']) - else: - _LOGGER.info("- End of playlist and playcontinuous is off") - await self.async_turn_off_media_player() - return + self.log_me('debug', f"Uncaught Situation,auto_advance={auto_advance},keep_track_no={keep_track_no},repeat={self.repeat},shuffle_mode={self._shuffle_mode}") + # get track from array of _trackS try: @@ -1273,7 +1275,7 @@ async def async_get_track(self, entity_id=None, old_state=None, new_state=None, if retry < 1: await self.async_turn_off_media_player() return - return await self.async_get_track(retry=retry - 1) + return await self.async_get_track(retry=retry - 1, auto_advance=auto_advance, keep_track_no=keep_track_no) # updates attributes self._attributes['current_track'] = self._next_track_no @@ -1305,7 +1307,7 @@ async def async_get_track(self, entity_id=None, old_state=None, new_state=None, return else: _LOGGER.error("- Retry with: (%i)", retry) - return await self.async_get_track(retry=retry - 1) + return await self.async_get_track(retry=retry - 1, auto_advance=auto_advance, keep_track_no=keep_track_no) # proxy playback, needed e.g. for sonos try: @@ -1569,8 +1571,9 @@ async def async_play_media(self, media_type, media_id, _player=None, **kwargs): # Update player if we got an input if _player is not None: await self.async_update_remote_player(remote_player=_player) - data = {"option": _player, "entity_id": self._select_speaker} - await (select.DOMAIN, select.SERVICE_SELECT_OPTION, data) + if self._selects['speakers'] is not None: + data = {"option": _player, "entity_id": self._selects['speakers']} + await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) # load Tracks depending on input try: @@ -1667,9 +1670,9 @@ async def async_play_media(self, media_type, media_id, _player=None, **kwargs): return self.log_me('debug', crash_extra) - # mode 1 and 3 shuffle the playlist after generation + # mode "Shuffle" and "Shuffle Random" shuffle the playlist after generation if(isinstance(self._tracks, list)): - if self._shuffle and self._shuffle_mode != 2 and len(self._tracks) > 1: + if self._shuffle_mode in (PLAYMODE_SHUFFLE,PLAYMODE_SHUFFLE_RANDOM) and len(self._tracks) > 1: random.shuffle(self._tracks) self.log_me('debug', "- shuffle new tracklist") if(len(self._tracks) == 0): @@ -1688,7 +1691,6 @@ async def async_play_media(self, media_type, media_id, _player=None, **kwargs): await self._tracks_to_attribute() # grab track from tracks[] and forward to remote player - self._next_track_no = -1 if(media_type != CHANNEL_VID_NO_INTERRUPT): await self.async_play() self.log_me('debug', "[E] play_media") @@ -1731,17 +1733,26 @@ async def async_media_play_pause(self, **kwargs): async def async_media_previous_track(self, **kwargs): # Send the previous track command. if self._playing: - self._next_track_no = max(self._next_track_no - 2, -1) self._allow_next = False self._ignore_next_remote_pause_state = True # RobinR1, OwnTone compatibility - await self.async_get_track() + if self.shuffle: + await self.async_get_track(keep_track_no=False) + else: + self._next_track_no = max(self._next_track_no - 1, 0) + await self.async_get_track() async def async_media_next_track(self, **kwargs): # Send next track command. if self._playing: self._allow_next = False self._ignore_next_remote_pause_state = True # RobinR1, OwnTone compatibility - await self.async_get_track() + if self.shuffle: + await self.async_get_track(keep_track_no=False) + else: + self._next_track_no = self._next_track_no + 1 + if self._next_track_no >= len(self._tracks): + self._next_track_no = 0 + await self.async_get_track() async def async_media_stop(self, **kwargs): # Send stop command. @@ -1769,33 +1780,13 @@ async def async_media_seek(self, position): await self.hass.services.async_call(DOMAIN_MP, 'media_seek', data) async def async_set_shuffle(self, shuffle): - self.log_me('debug', "set_shuffle: " + str(shuffle)) - self._shuffle = shuffle # True / False - - # mode 1 and 3 will shuffle the playlist after generation - if(isinstance(self._tracks, list)): - if(self._shuffle and self._shuffle_mode != 2 and len(self._tracks) > 1): - random.shuffle(self._tracks) - await self._tracks_to_attribute() - - if self._shuffle_mode == 1: - self._attributes['shuffle_mode'] = PLAYMODE_SHUFFLE - elif self._shuffle_mode == 2: - self._attributes['shuffle_mode'] = PLAYMODE_RANDOM - elif self._shuffle_mode == 3: - self._attributes['shuffle_mode'] = PLAYMODE_SHUFFLE_RANDOM - else: - self._attributes['shuffle_mode'] = self._shuffle_mode - - # setting the input will call the "input has changed" - callback .. but that should be alright - if(self._select_playMode != ""): - if(self._shuffle): - data = {select.ATTR_OPTION: self._attributes['shuffle_mode'], ATTR_ENTITY_ID: self._select_playMode} - else: - data = {select.ATTR_OPTION: PLAYMODE_DIRECT, ATTR_ENTITY_ID: self._select_playMode} - await self.hass.services.async_call(select.DOMAIN, select.SERVICE_SELECT_OPTION, data) - - self.async_schedule_update_ha_state() + # Only implement the function which the HA media_player entity built-in, without affecting the _shuffle_mode and dropdown options. + # if shuffle is true,the next song should be random. + # shuffle_mode="Shuffle" means that when starting a playlist, the original order is randomized, but it does not imply shuffle=true. + if self.shuffle != shuffle: + self.log_me('debug', f"set_shuffle: {str(shuffle)}") + self._attr_shuffle = shuffle # True / False + self.async_schedule_update_ha_state() async def async_set_volume_level(self, volume): @@ -1872,7 +1863,6 @@ async def async_call_method(self, command=None, parameters=None): await self.async_update_remote_player(remote_player=self._interrupt_data['player']) self._untrack_remote_player = async_track_state_change(self.hass, self._remote_player, self.async_sync_player) self._interrupt_data['player'] = None - self._next_track_no = max(self._next_track_no - 1, -1) await self.async_get_track() if('pos' in self._interrupt_data): player = self.hass.states.get(self._remote_player) @@ -1906,11 +1896,11 @@ async def async_call_method(self, command=None, parameters=None): self.log_me('debug', "Showing like status in name until restart") elif(command == SERVICE_CALL_GOTO_TRACK): self.log_me('debug', "Going to Track " + str(parameters[0]) + ".") - self._next_track_no = min(max(int(parameters[0]) - 1 - 1, -1), len(self._tracks) - 1) - prev_shuffle = self._shuffle # store current shuffle setting - self._shuffle = False # set false, otherwise async_get_track will override next_track + self._next_track_no = min(max(int(parameters[0]) - 1, 0), len(self._tracks) - 1) + prev_shuffle = self._attr_shuffle # store current shuffle setting + self._attr_shuffle = False # set false, otherwise async_get_track will override next_track await self.async_get_track() - self._shuffle = prev_shuffle # restore + self._attr_shuffle = prev_shuffle # restore elif(command == SERVICE_CALL_APPEND_TRACK): self.log_me('debug', "Adding track " + str(parameters[0]) + " at position " + str(parameters[1])) if(len(parameters)==2 and parameters[1].isnumeric()): @@ -2144,5 +2134,5 @@ async def async_track_select_mediaplayer_helper(self, args): self.log_me('debug', "- untrack failed") self._untrack_remote_player_selector = None self._untrack_remote_player_selector = async_track_state_change( - self.hass, self._select_speaker, self.async_select_source_helper) + self.hass, self._selects['speakers'], self.async_select_source_helper) self.log_me('debug', "- untrack resub") diff --git a/custom_components/ytube_music_player/select.py b/custom_components/ytube_music_player/select.py index f58390c..adb09b9 100644 --- a/custom_components/ytube_music_player/select.py +++ b/custom_components/ytube_music_player/select.py @@ -8,12 +8,19 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config, async_add_entities): + _LOGGER.debug("Init the dropdown(s)") + init_dropdowns = config.data.get(CONF_INIT_DROPDOWNS, DEFAULT_INIT_DROPDOWNS) + select_entities = { + "playlists": yTubeMusicPlaylistSelect(hass, config), + "speakers": yTubeMusicSpeakerSelect(hass, config), + "playmode": yTubeMusicPlayModeSelect(hass, config), + "radiomode": yTubeMusicSourceSelect(hass, config), + "repeatmode": yTubeMusicRepeatSelect(hass, config) + } entities = [] - entities.append(yTubeMusicPlaylistSelect(hass, config)) - entities.append(yTubeMusicSpeakerSelect(hass, config)) - entities.append(yTubeMusicPlayModeSelect(hass, config)) - entities.append(yTubeMusicSourceSelect(hass, config)) - entities.append(yTubeMusicRepeatSelect(hass, config)) + for dropdown,entity in select_entities.items(): + if dropdown in init_dropdowns: + entities.append(entity) async_add_entities(entities, update_before_add=True) class yTubeMusicSelectEntity(SelectEntity): @@ -48,7 +55,7 @@ def __init__(self, hass, config): self._attr_unique_id = self._device_id + "_playlist" self._attr_name = "Playlist" self._attr_icon = 'mdi:playlist-music' - self.hass.data[DOMAIN][self._device_id]['select_playlist'] = self + self.hass.data[DOMAIN][self._device_id]['select_playlists'] = self self._attr_options = ["loading"] self._attr_current_option = None @@ -56,10 +63,7 @@ async def async_update(self): # update select self._ready = True try: - als = [] - for playlist in self.hass.data[DOMAIN][self._device_id]['playlists']: - als.append(playlist) - self._attr_options = als + self._attr_options = list(self.hass.data[DOMAIN][self._device_id]['playlists'].keys()) except: pass try: @@ -74,7 +78,7 @@ def __init__(self, hass, config): self._attr_unique_id = self._device_id + "_speaker" self._attr_name = "Speaker" self._attr_icon = 'mdi:speaker' - self.hass.data[DOMAIN][self._device_id]['select_speaker'] = self + self.hass.data[DOMAIN][self._device_id]['select_speakers'] = self self._attr_options = ["loading"] self._attr_current_option = None @@ -119,6 +123,6 @@ def __init__(self, hass, config): self._attr_unique_id = self._device_id + "_repeat" self._attr_name = "Repeat Mode" self._attr_icon = 'mdi:repeat' - self.hass.data[DOMAIN][self._device_id]['select_repeat'] = self + self.hass.data[DOMAIN][self._device_id]['select_repeatmode'] = self self._attr_options = ["all", "one", "off"] # one for future self._attr_current_option = "all" \ No newline at end of file diff --git a/custom_components/ytube_music_player/sensor.py b/custom_components/ytube_music_player/sensor.py index af03935..d703902 100644 --- a/custom_components/ytube_music_player/sensor.py +++ b/custom_components/ytube_music_player/sensor.py @@ -11,7 +11,7 @@ async def async_setup_entry(hass, config, async_add_entities): # Run setup via Storage - _LOGGER.debug("Config via Storage/UI") + _LOGGER.debug("init ytube sensor") if(config.data.get(CONF_INIT_EXTRA_SENSOR, DEFAULT_INIT_EXTRA_SENSOR)): async_add_entities([yTubeMusicSensor(hass, config)], update_before_add=True) diff --git a/custom_components/ytube_music_player/translations/en.json b/custom_components/ytube_music_player/translations/en.json index 8b26b2d..2e9fffc 100644 --- a/custom_components/ytube_music_player/translations/en.json +++ b/custom_components/ytube_music_player/translations/en.json @@ -27,11 +27,13 @@ "like_in_name": "Show like status in the name", "debug_as_error": "Show all debug output as ERROR in the log", "shuffle": "Turn on shuffle on startup", + "shuffle_mode": "Playmode", "track_limit": "Limit of simultaneously loaded tracks", "max_datarate": "Limit the maximum bit rate", "legacy_radio": "Create radio as watchlist of random playlist track", "sort_browser": "Sort results in the media browser", - "extra_sensor": "Create sensor that provide extra information" + "extra_sensor": "Create sensor that provide extra information", + "dropdowns": "Create the dropdown(s) you want to use" } } }, @@ -66,11 +68,13 @@ "like_in_name": "Show like status in the name", "debug_as_error": "Show all debug output as ERROR in the log", "shuffle": "Turn on shuffle on startup", + "shuffle_mode": "Playmode", "track_limit": "Limit of simultaneously loaded tracks", "max_datarate": "Limit the maximum bit rate, 0 to disable", "legacy_radio": "Create radio as watchlist of random playlist track", "sort_browser": "Sort results in the media browser", - "extra_sensor": "Create sensor that provide extra information" + "extra_sensor": "Create sensor that provide extra information", + "dropdowns": "Create the dropdown(s) you want to use" } } }, diff --git a/custom_components/ytube_music_player/translations/zh-Hans.json b/custom_components/ytube_music_player/translations/zh-Hans.json index 83cdd4b..5e18027 100644 --- a/custom_components/ytube_music_player/translations/zh-Hans.json +++ b/custom_components/ytube_music_player/translations/zh-Hans.json @@ -27,11 +27,13 @@ "like_in_name": "在名称中显示喜欢状态", "debug_as_error": "在日志中将所有调试输出显示为ERROR", "shuffle": "启动时随机播放", + "shuffle_mode": "播放模式", "track_limit": "加载曲目数量限制", "max_datarate": "限制最大比特率,设置为0以禁用", "legacy_radio": "将随机播放列表曲目创建为收藏夹电台", "sort_browser": "在媒体浏览器中对结果进行排序", - "extra_sensor": "创建提供额外信息的传感器实体" + "extra_sensor": "创建提供额外信息的传感器实体", + "dropdowns": "创建你需要的下拉菜单实体" } } }, @@ -66,11 +68,13 @@ "like_in_name": "在名称中显示喜欢状态", "debug_as_error": "在日志中将所有调试输出显示为ERROR", "shuffle": "启动时随机播放", + "shuffle_mode": "播放模式", "track_limit": "加载曲目数量限制", "max_datarate": "限制最大比特率,设置为0以禁用", "legacy_radio": "将随机播放列表曲目创建为收藏夹电台", "sort_browser": "在媒体浏览器中对结果进行排序", - "extra_sensor": "创建提供额外信息的传感器实体" + "extra_sensor": "创建提供额外信息的传感器实体", + "dropdowns": "创建你需要的下拉菜单实体" } } },