def get_library(cookie, x_goog_authuser): auth = YTMusic.setup(headers_raw=f"cookie: {cookie}\nx-goog-authuser: {x_goog_authuser}") api = YTMusic(auth) playlists = [api.get_playlist(lpl['playlistId'], limit=10 ** 9) for lpl in api.get_library_playlists()] songs = api.get_library_songs(limit=10 ** 9) return strip_library(songs, playlists)
class YoutubeMusicApiSingleton: __instance__ = None def __init__(self): """ Constructor.""" self.__youtube_music_api = YTMusic(HEADERS_AUTH_FILE_PATH) if YoutubeMusicApiSingleton.__instance__ is None: YoutubeMusicApiSingleton.__instance__ = self else: raise Exception( "You cannot create another YoutubeMusicApiSingleton class, this class is a singleton" ) @staticmethod def get_instance() -> 'YoutubeMusicApiSingleton': """Static method to get the current instance Returns: YoutubeMusicApiSingleton: Instance of the YoutubeMusicApiSingleton """ if not YoutubeMusicApiSingleton.__instance__: YoutubeMusicApiSingleton() return YoutubeMusicApiSingleton.__instance__ def add_song_to_library(self, song_to_add: Song) -> bool: """Add a song to the current user's Youtube Music library Args: song_to_add (Song): Song to add to library Returns: bool: True -> Song was successfully added to library | False -> Song was NOT added to library """ response = self.__youtube_music_api.edit_song_library_status(song_to_add.feedback_tokens.add) # If responseContext is empty then song is already in library if not response['responseContext']: return True return to_bool(response['feedbackResponses'][0]['isProcessed']) def delete_library_uploaded_song(self, uploaded_song_to_delete: Song) -> None: """Delete an uploaded song from the current user's Youtube Music Library Args: uploaded_song_to_delete (Song): Uploaded song to delete """ self.__youtube_music_api.delete_upload_entity(uploaded_song_to_delete.entity_id) def get_library_uploaded_songs(self, song_limit: int, order: Order) -> List[Song]: """Get a list of uploaded songs from the current user's Youtube Music Library Args: song_limit (int): Max amount of songs to retrieve order (Order): Order to retrieve the songs in Returns: List[Song]: List of uploaded library songs """ return [Song( id=song_dict['videoId'], title=song_dict['title'], artists=[artist['name'] for artist in song_dict['artists']], album=song_dict['album']['name'], length=song_dict['duration'], like_status=LikeStatuses[song_dict['likeStatus']], entity_id=song_dict['entityId']) for song_dict in self.__youtube_music_api.get_library_upload_songs(song_limit, order.value)] def get_simple_library_playlists(self, playlist_limit: int) -> List[Playlist]: """Get a list of simple (no song info included) playlist information from the current user's Youtube Music library Args: playlist_limit (int): Max amount of playlists to retrieve Returns: List[Playlist]: List of simple library playlist information """ return [Playlist( id=playlist_dict['playlistId'], title=playlist_dict['title'], song_count=playlist_dict.get('count', None)) for playlist_dict in self.__youtube_music_api.get_library_playlists(playlist_limit)] def rate_song(self, song: Song, rating: LikeStatuses) -> None: """Rate a song (Like / Dislike / Indifferent) Args: song (Song): Song to rate """ self.__youtube_music_api.rate_song(song.id, rating) def perform_search( self, query: str, item_type: ItemType, item_search_limit: int, ignore_spelling: bool) -> List[Dict]: """Search for an item in Youtube Music Args: query (str): Search query item_type (ItemType): Item type to search for item_search_limit (int): Max amount of items in the search results ignore_spelling (bool): True -> Ignore spelling suggestions | False -> Use autocorrected text Returns: List[Dict]: List of search result items """ search_results = [] for result in self.__youtube_music_api.search(query=query, filter=item_type.value, limit=item_search_limit, ignore_spelling=ignore_spelling): if result['resultType'] == "song": search_results.append( Song( id=result['videoId'], title=result['title'], artists=[artist['name'] for artist in result['artists']], album=result['album']['name'], explicit=to_bool(result['isExplicit']), length=result['duration'], feedback_tokens=FeedbackTokens( add=result['feedbackTokens']['add'], remove=result['feedbackTokens']['remove']))) if result['resultType'] == "playlist": search_results.append( Playlist( id=result['browseId'], title=result['title'], song_count=result['itemCount'])) return search_results def get_complete_playlist(self, playlist: Playlist, playlist_song_limit: int) -> Playlist: """Get complete information about the given playlist (including information about the items in the playlist) Args: playlist (Playlist): Playlist to get information for playlist_song_limit (int): Max amount of songs to retrieve from the playlist Returns: Playlist: Complete playlist """ response = self.__youtube_music_api.get_playlist(playlist.id, playlist_song_limit) return Playlist( id=response['id'], title=response['title'], song_count=response['trackCount'], songs=[Song( id=song_dict['videoId'], title=song_dict['title'], artists=[artist['name'] for artist in song_dict['artists']], album=song_dict['album']['name'], explicit=to_bool(song_dict['isExplicit']), length=song_dict.get('duration'), like_status=LikeStatuses(song_dict['likeStatus']) if song_dict['likeStatus'] else None, set_id=song_dict['setVideoId'], feedback_tokens=FeedbackTokens( add=song_dict.get('feedbackTokens', {}).get('add'), remove=song_dict.get('feedbackTokens', {}).get('remove'))) for song_dict in response['tracks']]) def remove_songs_from_playlist(self, playlist: Playlist, songs: List[Song]) -> None: """Remove songs from a playlist in Youtube Music Args: playlist (Playlist): Playlist to remove the song from (Complete playlist info required) songs (List[Song]): List of songs to remove from the playlist """ self.__youtube_music_api.remove_playlist_items(playlist.id, [{'videoId': song.id, 'setVideoId': song.set_id} for song in songs])
class YTMusicTransfer: def __init__(self): self.api = YTMusic(settings['youtube']['headers'], settings['youtube']['user_id']) def create_playlist(self, name, info, privacy="PRIVATE", tracks=None): return self.api.create_playlist(name, info, privacy, video_ids=tracks) def get_best_fit_song_id(self, results, song): match_score = {} title_score = {} for res in results: if res['resultType'] not in ['song', 'video']: continue durationMatch = None if 'duration' in res and res['duration']: durationItems = res['duration'].split(':') duration = int(durationItems[0]) * 60 + int(durationItems[1]) durationMatch = 1 - abs( duration - song['duration']) * 2 / song['duration'] title = res['title'] # for videos, if res['resultType'] == 'video': titleSplit = title.split('-') if len(titleSplit) == 2: title = titleSplit[1] artists = ' '.join([a['name'] for a in res['artists']]) title_score[res['videoId']] = difflib.SequenceMatcher( a=title.lower(), b=song['name'].lower()).ratio() scores = [ title_score[res['videoId']], difflib.SequenceMatcher(a=artists.lower(), b=song['artist'].lower()).ratio() ] if durationMatch: scores.append(durationMatch * 5) #add album for songs only if res['resultType'] == 'song' and res['album'] is not None: scores.append( difflib.SequenceMatcher(a=res['album']['name'].lower(), b=song['album'].lower()).ratio()) match_score[res['videoId']] = sum(scores) / len(scores) * max( 1, int(res['resultType'] == 'song') * 1.5) if len(match_score) == 0: return None #don't return songs with titles <45% match max_score = max(match_score, key=match_score.get) return max_score def search_songs(self, tracks): videoIds = [] songs = list(tracks) notFound = list() for i, song in enumerate(songs): name = re.sub(r' \(feat.*\..+\)', '', song['name']) query = song['artist'] + ' ' + name query = query.replace(" &", "") result = self.api.search(query, ignore_spelling=True) if len(result) == 0: notFound.append(query) else: targetSong = self.get_best_fit_song_id(result, song) if targetSong is None: notFound.append(query) else: videoIds.append(targetSong) if i > 0 and i % 10 == 0: print(f"YouTube tracks: {i}/{len(songs)}") with open(path + 'noresults_youtube.txt', 'w', encoding="utf-8") as f: f.write("\n".join(notFound)) f.write("\n") f.close() return videoIds def add_playlist_items(self, playlistId, videoIds): videoIds = OrderedDict.fromkeys(videoIds) self.api.add_playlist_items(playlistId, videoIds) def get_playlist_id(self, name): pl = self.api.get_library_playlists(10000) try: playlist = next(x for x in pl if x['title'].find(name) != -1)['playlistId'] return playlist except: raise Exception("Playlist title not found in playlists") def remove_songs(self, playlistId): items = self.api.get_playlist(playlistId, 10000)['tracks'] if len(items) > 0: self.api.remove_playlist_items(playlistId, items) def remove_playlists(self, pattern): playlists = self.api.get_library_playlists(10000) p = re.compile("{0}".format(pattern)) matches = [pl for pl in playlists if p.match(pl['title'])] print("The following playlists will be removed:") print("\n".join([pl['title'] for pl in matches])) print("Please confirm (y/n):") choice = input().lower() if choice[:1] == 'y': [self.api.delete_playlist(pl['playlistId']) for pl in matches] print(str(len(matches)) + " playlists deleted.") else: print("Aborted. No playlists were deleted.")
class YTMusicTransfer: def __init__(self): self.api = YTMusic() def create_playlist(self, name, info, privacy="PRIVATE", tracks=None): return self.api.create_playlist(name, info, privacy, video_ids=tracks) def get_best_fit_song(self, results, song): match_score = {} title_score = {} for res in results: if res['resultType'] not in ['song', 'video']: continue durationMatch = None if res['duration']: durationItems = res['duration'].split(':') duration = int(durationItems[0]) * 60 + int(durationItems[1]) durationMatch = 1 - abs(duration - song['duration']) * 2 / song['duration'] title = res['title'] # for videos, if res['resultType'] == 'video': titleSplit = title.split('-') if len(titleSplit) == 2: title = titleSplit[1] artists = ' '.join([a['name'] for a in res['artists']]) title_score[res['videoId']] = difflib.SequenceMatcher(a=title.lower(), b=song['name'].lower()).ratio() scores = [title_score[res['videoId']], difflib.SequenceMatcher(a=artists.lower(), b=song['artist'].lower()).ratio()] if durationMatch: scores.append(durationMatch * 5) #add album for songs only if res['resultType'] == 'song' and res['album'] is not None: scores.append(difflib.SequenceMatcher(a=res['album']['name'].lower(), b=song['album'].lower()).ratio()) match_score[res['videoId']] = sum(scores) / (len(scores) + 1) * max(1, int(res['resultType'] == 'song') * 1.5) if len(match_score) == 0: return None #don't return songs with titles <45% match max_score = max(match_score, key=match_score.get) return [el for el in results if el['resultType'] in ['song', 'video'] and el['videoId'] == max_score][0] def search_songs(self, tracks): videos = [] songs = list(tracks) notFound = list() for i, song in enumerate(songs): query = song['artist'] + ' ' + song['name'] query = query.replace(" &", "") try: result = self.api.search(query) except: print(f'Fail for {song["artist"]} - {song["name"]}') if len(result) == 0: notFound.append(query) else: targetSong = self.get_best_fit_song(result, song) if targetSong is None: notFound.append(query) else: video = self.format_song(targetSong) videos.append(video) if i > 0 and i % 10 == 0: print(str(i) + ' searched') print(notFound) return videos def format_song(self, video): video['_id'] = video['videoId'] video['durationDisplay'] = video['duration'] if len(video['durationDisplay'].split(':')) == 3: video['duration'] = int(video['duration'].split(':')[0]) * 3600 + int(video['duration'].split(':')[1]) * 60 + int(video['duration'].split(':')[2]) if len(video['durationDisplay'].split(':')) == 2: video['duration'] = int(video['duration'].split(':')[0]) * 60 + int(video['duration'].split(':')[1]) video['thumbnail'] = video['thumbnails'][-1]['url'] video['artist'] = video['artists'][0]['name'] video['album'] = video['album']['name'] if 'album' in video and 'name' in video['album'] else None return video def add_playlist_items(self, playlistId, videoIds): videoIds = OrderedDict.fromkeys(videoIds) self.api.add_playlist_items(playlistId, videoIds) def get_playlist_id(self, name): pl = self.api.get_library_playlists(10000) try: playlist = next(x for x in pl if x['title'].find(name) != -1)['playlistId'] return playlist except: raise Exception("Playlist title not found in playlists") def remove_songs(self, playlistId): items = self.api.get_playlist(playlistId, 10000)['tracks'] if len(items) > 0: self.api.remove_playlist_items(playlistId, items) def remove_playlists(self, pattern): playlists = self.api.get_library_playlists(10000) p = re.compile("{0}".format(pattern)) matches = [pl for pl in playlists if p.match(pl['title'])] print("The following playlists will be removed:") print("\n".join([pl['title'] for pl in matches])) print("Please confirm (y/n):") choice = input().lower() if choice[:1] == 'y': [self.api.delete_playlist(pl['playlistId']) for pl in matches] print(str(len(matches)) + " playlists deleted.") else: print("Aborted. No playlists were deleted.")
ytmusic = YTMusic('headers_auth.json') #music_files = subprocess.check_output("find %s" % config['music']['local'], shell=True) #music_files = music_files.decode("utf-8").splitlines() #music_tags = [] #for music_file in music_files: # music_tags.append(TinyTag.get(music_file)) playlist_files = subprocess.check_output('find %s -name "*m3u*"' % config['playlists']['local'], shell=True) playlist_files = playlist_files.decode("utf-8").splitlines() remote_songs = ytmusic.get_library_upload_songs(99999) remote_playlists = ytmusic.get_library_playlists() print(len(remote_songs)) playlist_content = {} for playlist in playlist_files: f = open(playlist, "r") for remote_playlist in remote_playlists: if remote_playlist["title"] in playlist: playlist_content[remote_playlist["playlistId"]] = f.readlines() for playlist in playlist_content: upload = [] pl_tracks = ytmusic.get_playlist(playlist, 9999)["tracks"] pl_trackid = [] for track in pl_tracks: pl_trackid.append(track["videoId"])
class youtube_music_tasker: def __init__(self, auth_json: str): self.api = YTMusic(auth_json) # Return: # [ # { # "id": "playlistid1", # "title": "playlist_title1", # "thumbnail": "url_to_playlist1_1st_thumbnail" # }, # { # "id": "playlistid2", # "title": "playlist_title2", # "thumbnail": "url_to_playlist2_1st_thumbnail" # } # ] # def show_playlist(self): list_of_playlist = [] try: library_playlists = self.api.get_library_playlists( limit=50) # Hopefully, no one has 50+ playlists. for pl in library_playlists: # Only showing non-empty well-formed playlists if 'count' in pl and int( pl['count'] ) > 0 and 'playlistId' in pl and 'title' in pl and 'thumbnails' in pl: playlist = {} playlist['id'] = pl['playlistId'] playlist['title'] = pl['title'] if len(pl['thumbnails']) > 0: playlist['thumbnail'] = pl['thumbnails'][0]['url'] else: playlist['thumbnail'] = DEFAULT_IMG_URL list_of_playlist.append(playlist) except Exception as e: print("Unexpected Error in show_playlist:", e) return json.dumps(list_of_playlist) # Return: # [ # { # "title": "name", # "artist": "someone", # "album": "the album" # }, # { # "title": "name", # "artist": "any", # "album": "any" # } # ] # def show_song_in_playlist(self, playlist_id: str): list_of_song = [] try: pl_detail = self.api.get_playlist(playlistId=playlist_id) if 'tracks' in pl_detail: for track in pl_detail['tracks']: if 'title' in track: new_track = { 'title': track['title'], 'artist': 'any', 'album': 'any' } if 'artists' in track and len(track['artists']) > 0: new_track['artist'] = track['artists'][0]['name'] if 'album' in track and track[ 'album'] != None and 'name' in track['album']: new_track['album'] = track['album']['name'] list_of_song.append(new_track) except Exception as e: print("Unexpected Error in show_song_in_playlist:", e) return json.dumps(list_of_song) # access: 'PRIVATE', 'PUBLIC', 'UNLISTED' # Return: A tuple of (create_status, playlist_id, add_status) def new_playlist(self, playlist_name: str, desc: str = "A playlist created by PlaySync on " + str(datetime.today().strftime('%Y-%m-%d')), access: str = 'PRIVATE', tracks=[]): try: playlist_id = self.api.create_playlist(title=playlist_name, description=desc, privacy_status=access) if type(playlist_id) == str: # It is an id if len(tracks) > 0: status = self.api.add_playlist_items(playlist_id, tracks) return (0, playlist_id, status ) # Creation successful, add status attached else: return (0, playlist_id, "NULL" ) # Creation successful, didn't add else: # Status message, means error in creation return (-1, 0, playlist_id) except Exception as e: print("Unexpected Error in new_playlist:", e) return (-2, 0, e) # Didn't crash gracefully def search_song(self, song_title: str, song_artist: str = "", song_misc: str = ""): song_list = [] try: search_results = self.api.search(query=song_title + song_artist + song_misc, limit=10) for song_found in search_results: if (song_found['resultType'] in ['song', 'video']): new_song = { 'id': song_found['videoId'], 'title': song_found['title'], 'artist': 'None', 'album': 'None', 'duration': 'Unknown' } if len(song_found['artists']) > 0: new_song['artist'] = song_found['artists'][0]['name'] if 'album' in song_found: new_song['artist'] = song_found['album']['name'] if 'duration' in song_found: new_song['duration'] = song_found['duration'] song_list.append(new_song) except Exception as e: print("Unexpected Error in search_song:", e) return json.dumps(song_list) def add_songs(self, playlist_id: str, tracks=[]): try: status = self.api.add_playlist_items(playlist_id, tracks) return (0, playlist_id, status ) # Creation successful, add status attached except Exception as e: print("Unexpected Error in add_songs:", e) return (-2, 0, 0) # Didn't crash gracefully def del_songs(self, playlist_id: str, tracks=[]): try: if len(tracks) > 0: status = self.api.remove_playlist_items(playlist_id, videos=tracks) return status except Exception as e: return "UNCAUGHT ERROR" + str(e) return "NULL" def del_playlist(self, playlist_id: str): try: status = self.api.delete_playlist(playlist_id) return status except Exception as e: return "UNCAUGHT ERROR" + str(e)
class yTubeMusicComponent(MediaPlayerEntity): def __init__(self, hass, config): self.hass = hass self._name = DOMAIN self._playlist = "input_select." + config.get(CONF_SELECT_PLAYLIST, DEFAULT_SELECT_PLAYLIST) self._playMode = "input_select." + config.get(CONF_SELECT_PLAYMODE, DEFAULT_SELECT_PLAYMODE) self._media_player = "input_select." + config.get( CONF_SELECT_SPEAKERS, DEFAULT_SELECT_SPEAKERS) self._source = "input_select." + config.get(CONF_SELECT_SOURCE, DEFAULT_SELECT_SOURCE) self._speakersList = config.get(CONF_RECEIVERS) default_header_file = os.path.join(hass.config.path(STORAGE_DIR), DEFAULT_HEADER_FILENAME) _LOGGER.debug("YtubeMediaPlayer config: ") _LOGGER.debug("\tHeader path: " + config.get(CONF_HEADER_PATH, default_header_file)) _LOGGER.debug("\tplaylist: " + self._playlist) _LOGGER.debug("\tmediaplayer: " + self._media_player) _LOGGER.debug("\tsource: " + self._source) _LOGGER.debug("\tspeakerlist: " + str(self._speakersList)) _LOGGER.debug("\tplayModes: " + str(self._playMode)) if (os.path.exists(config.get(CONF_HEADER_PATH, default_header_file))): self._api = YTMusic( config.get(CONF_HEADER_PATH, default_header_file)) else: msg = "can't file header file at " + config.get( CONF_HEADER_PATH, default_header_file) _LOGGER.error(msg) data = {"title": "yTubeMediaPlayer error", "message": msg} self.hass.services.call("persistent_notification", "create", data) self._api = None self._js = "" self._get_cipher('BB2mjBuAtiQ') # embed_url = f"https://www.youtube.com/embed/D7oPc6PNCZ0" self._entity_ids = [] ## media_players - aka speakers self._playlists = [] self._playlist_to_index = {} self._tracks = [] self._track = [] self._attributes = {} self._next_track_no = 0 self._allow_next = False self._last_auto_advance = datetime.datetime.now() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self._update_sources) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self._get_speakers) hass.bus.listen('ytubemusic_player.sync_media', self._update_sources) hass.bus.listen('ytubemusic_player.play_media', self._ytubemusic_play_media) self._shuffle = config.get(CONF_SHUFFLE, DEFAULT_SHUFFLE) self._shuffle_mode = config.get(CONF_SHUFFLE_MODE, DEFAULT_SHUFFLE_MODE) self._playing = False self._state = STATE_OFF self._volume = 0.0 self._is_mute = False self._track_name = None self._track_artist = None self._track_album_name = None self._track_album_cover = None self._track_artist_cover = None self._attributes['_player_state'] = STATE_OFF # asyncio.run_coroutine_threadsafe(self.test(), hass.loop) # async def test(self): # self._reg = await device_registry.async_get_registry(self.hass) # reg = self._reg._data_to_save() # for dev in reg.devices: # # _LOGGER.error("called get registry") @property def name(self): """ Return the name of the player. """ return self._name @property def icon(self): return 'mdi:music-circle' @property def supported_features(self): """ Flag media player features that are supported. """ return SUPPORT_YTUBEMUSIC_PLAYER @property def should_poll(self): """ No polling needed. """ return False @property def state(self): """ Return the state of the device. """ return self._state @property def device_state_attributes(self): """ Return the device state attributes. """ return self._attributes @property def is_volume_muted(self): """ Return True if device is muted """ return self._is_mute @property def is_on(self): """ Return True if device is on. """ return self._playing @property def media_content_type(self): """ Content type of current playing media. """ return MEDIA_TYPE_MUSIC @property def media_title(self): """ Title of current playing media. """ return self._track_name @property def media_artist(self): """ Artist of current playing media """ return self._track_artist @property def media_album_name(self): """ Album name of current playing media """ return self._track_album_name @property def media_image_url(self): """ Image url of current playing media. """ return self._track_album_cover @property def media_image_remotely_accessible(self): # True returns: entity_picture: http://lh3.googleusercontent.com/Ndilu... # False returns: entity_picture: /api/media_player_proxy/media_player.gmusic_player?token=4454... return True @property def shuffle(self): """ Boolean if shuffling is enabled. """ return self._shuffle @property def volume_level(self): """ Volume level of the media player (0..1). """ return self._volume def turn_on(self, *args, **kwargs): """ Turn on the selected media_player from input_select """ if (self._api == None): _LOGGER.error("Can't start the player, no header file") return _LOGGER.debug("TURNON") self._playing = False if not self._update_entity_ids(): return _player = self.hass.states.get(self._entity_ids) data = {ATTR_ENTITY_ID: _player.entity_id} self._allow_next = False track_state_change(self.hass, _player.entity_id, self._sync_player) track_state_change(self.hass, self._playMode, self._update_playmode) self._turn_on_media_player(data) #_LOGGER.error("subscribe to changes of ") self._get_cipher('BB2mjBuAtiQ') # display imidiatly a loading state to provide feedback to the user self._track_name = "loading..." self._track_album_name = "" self._track_artist = "" self._track_artist_cover = None self._track_album_cover = None self._state = STATE_PLAYING # a bit early otherwise no info will be shown self.schedule_update_ha_state() # grabbing data from API, might take a 1-3 sec self._load_playlist() def _turn_on_media_player(self, data=None): """Fire the on action.""" if data is None: data = {ATTR_ENTITY_ID: self._entity_ids} self._state = STATE_IDLE self.schedule_update_ha_state() self.hass.services.call(DOMAIN_MP, 'turn_on', data) def turn_off(self, entity_id=None, old_state=None, new_state=None, **kwargs): """ Turn off the selected media_player """ self._playing = False self._track_name = None self._track_artist = None self._track_album_name = None self._track_album_cover = None _player = self.hass.states.get(self._entity_ids) data = {ATTR_ENTITY_ID: _player.entity_id} self._turn_off_media_player(data) def _turn_off_media_player(self, data=None): """Fire the off action.""" self._playing = False self._state = STATE_OFF self._attributes['_player_state'] = STATE_OFF self.schedule_update_ha_state() if data is None: data = {ATTR_ENTITY_ID: self._entity_ids} self.hass.services.call(DOMAIN_MP, 'turn_off', data) def _update_entity_ids(self): """ sets the current media_player from input_select """ media_player = self.hass.states.get( self._media_player ) # Example: self.hass.states.get(input_select.gmusic_player_speakers) if media_player is None: _LOGGER.error("(%s) is not a valid input_select entity.", self._media_player) return False _entity_ids = "media_player." + media_player.state if self.hass.states.get(_entity_ids) is None: _LOGGER.error("(%s) is not a valid media player.", media_player.state) return False # Example: self._entity_ids = media_player.bedroom_stereo self._entity_ids = _entity_ids return True def _get_cipher(self, videoId): embed_url = "https://www.youtube.com/embed/" + videoId embed_html = request.get(url=embed_url) js_url = extract.js_url(embed_html) self._js = request.get(js_url) self._cipher = Cipher(js=self._js) #2do some sort of check if tis worked def _sync_player(self, entity_id=None, old_state=None, new_state=None): """ Perform actions based on the state of the selected (Speakers) media_player """ if not self._playing: return """ _player = The selected speakers """ _player = self.hass.states.get(self._entity_ids) #""" Entire state of the _player, include attributes. """ # self._attributes['_player'] = _player """ entity_id of selected speakers. """ self._attributes['_player_id'] = _player.entity_id """ _player state - Example [playing -or- idle]. """ self._attributes['_player_state'] = _player.state #_LOGGER.error("State change of ") #_LOGGER.error(self._entity_ids) #_LOGGER.error(" to ") #_LOGGER.error(_player.state) #try: # _LOGGER.error(_player.attributes['media_position']) #except: # pass if 'media_position' in _player.attributes: if _player.state == 'playing' and _player.attributes[ 'media_position'] > 0: self._allow_next = True if _player.state == 'idle': if self._allow_next: if (datetime.datetime.now() - self._last_auto_advance).total_seconds() > 10: self._allow_next = False self._last_auto_advance = datetime.datetime.now() self._get_track() elif _player.state == 'off': self._state = STATE_OFF self.turn_off() """ Set new volume if it has been changed on the _player """ if 'volume_level' in _player.attributes: self._volume = round(_player.attributes['volume_level'], 2) self.schedule_update_ha_state() def _ytubemusic_play_media(self, event): _speak = event.data.get('speakers') _source = event.data.get('source') _media = event.data.get('name') if event.data['shuffle_mode']: self._shuffle_mode = event.data.get('shuffle_mode') _LOGGER.info("SHUFFLE_MODE: %s", self._shuffle_mode) if event.data['shuffle']: self.set_shuffle(event.data.get('shuffle')) _LOGGER.info("SHUFFLE: %s", self._shuffle) _LOGGER.debug("YTUBEMUSIC PLAY MEDIA") _LOGGER.debug("Speakers: (%s) | Source: (%s) | Name: (%s)", _speak, _source, _media) self.play_media(_source, _media, _speak) def _update_sources(self, now=None): _LOGGER.debug("Load source lists") self._update_playlists() #self._update_library() #self._update_songs() def _get_speakers(self, now=None): defaultPlayer = '' try: speakersList = list(self._speakersList) except: speakersList = list() if (len(speakersList) <= 1): if (len(speakersList) == 1): defaultPlayer = speakersList[0] all_entities = self.hass.states.all() for e in all_entities: if (e.entity_id.startswith(media_player.DOMAIN)): speakersList.append( e.entity_id.replace(media_player.DOMAIN + ".", "")) speakersList = list(dict.fromkeys(speakersList)) data = { input_select.ATTR_OPTIONS: list(speakersList), ATTR_ENTITY_ID: self._media_player } self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SET_OPTIONS, data) if (defaultPlayer != ''): if (defaultPlayer in speakersList): data = { input_select.ATTR_OPTION: defaultPlayer, ATTR_ENTITY_ID: self._media_player } self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION, data) def _update_playlists(self, now=None): """ Sync playlists from Google Music library """ if (self._api == None): return self._playlist_to_index = {} self._playlists = self._api.get_library_playlists(limit=99) idx = -1 for playlist in self._playlists: idx = idx + 1 name = playlist.get('title', '') if len(name) < 1: continue self._playlist_to_index[name] = idx # the "your likes" playlist won't return a count of tracks if not ('count' in playlist): extra_info = self._api.get_playlist( playlistId=playlist['playlistId']) try: self._playlists[idx]['count'] = max( 25, int(''.join([ x for x in extra_info['duration'] if x.isdigit() ]))) except: self._playlists[idx]['count'] = 25 playlists = list(self._playlist_to_index.keys()) self._attributes['playlists'] = playlists data = {"options": list(playlists), "entity_id": self._playlist} self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SET_OPTIONS, data) def _load_playlist(self, playlist=None, play=True): _LOGGER.info("Reloading Playlist!") """ Load selected playlist to the track_queue """ if not self._update_entity_ids(): return """ if source == Playlist """ _playlist_id = self.hass.states.get(self._playlist) if _playlist_id is None: _LOGGER.error("(%s) is not a valid input_select entity.", self._playlist) return if playlist is None: playlist = _playlist_id.state idx = self._playlist_to_index.get(playlist) if idx is None: _LOGGER.error("playlist to index is none!") self._turn_off_media_player() return self._tracks = None _source = self.hass.states.get(self._source) if _source is None: _LOGGER.error("(%s) is not a valid input_select entity.", self._source) return my_radio = self._api.get_playlist( playlistId=self._playlists[idx]['playlistId'], limit=int(self._playlists[idx]['count']))['tracks'] #my_radio = self._api.get_watch_playlist(playlistId = self._playlists[idx]['playlistId'])#, limit = int(self._playlists[idx]['count'])) if _source.state != 'Playlist': r_track = my_radio[random.randrange(0, len(my_radio) - 1)] self._tracks = self._api.get_watch_playlist( videoId=r_track['videoId']) else: self._tracks = my_radio _LOGGER.debug("New Track database loaded, contains " + str(len(self._tracks)) + " Tracks") self._total_tracks = len(self._tracks) #self.log("Loading [{}] Tracks From: {}".format(len(self._tracks), _playlist_id)) # get current playmode self._update_playmode() if self._shuffle and self._shuffle_mode != 2: random.shuffle(self._tracks) if play: self._play() # called from HA when th user changes the input entry, will read selection to membervar def _update_playmode(self, entity_id=None, old_state=None, new_state=None): _LOGGER.debug("running update playmode") if (entity_id == None): _playmode = self.hass.states.get(self._playMode) else: _playmode = self.hass.states.get(entity_id) if _playmode != None: if (_playmode.state == "Shuffle"): self._shuffle = True self._shuffle_mode = 1 elif (_playmode.state == "Random"): self._shuffle = True self._shuffle_mode = 2 if (_playmode.state == "Shuffle Random"): self._shuffle = True self._shuffle_mode = 3 if (_playmode.state == "Direct"): self._shuffle = False self._shuffle_mode = 0 self.set_shuffle(self._shuffle) # if we've change the dropdown, reload the playlist and start playing # else only change the mode if (old_state != None and new_state != None): self._allow_next = False # player will change to idle, avoid auto_advance self._load_playlist(play=True) def _play(self): self._playing = True self._next_track_no = -1 self._get_track() def _get_track(self, entity_id=None, old_state=None, new_state=None, retry=3): """ Get a track and play it from the track_queue. """ _LOGGER.info(" NEXT TRACK ") """ grab next track from prefetched list """ _track = None if self._shuffle and self._shuffle_mode != 1: self._next_track_no = random.randrange(self._total_tracks) - 1 else: self._next_track_no = self._next_track_no + 1 if self._next_track_no >= self._total_tracks: # we've reached the end of the playlist # reset the inner playlist counter, call _update_playlist to update lib self._next_track_no = 0 self._load_playlist(play=False) try: _track = self._tracks[self._next_track_no] except IndexError: _LOGGER.error( "Out of range! Number of tracks in track_queue == (%s)", self._total_tracks) self._turn_off_media_player() return if _track is None: _LOGGER.error("_track is None!") self._turn_off_media_player() return """ Find the unique track id. """ uid = '' if 'videoId' in _track: uid = _track['videoId'] else: _LOGGER.error("Failed to get ID for track: (%s)", _track) _LOGGER.error(_track) if retry < 1: self._turn_off_media_player() return return self._get_track(retry=retry - 1) """ If available, get track information. """ self._track_album_name = None self._track_artist_cover = None self._track_name = None self._track_artist = None self._track_album_cover = None if 'title' in _track: self._track_name = _track['title'] if 'byline' in _track: self._track_artist = _track['byline'] elif 'artists' in _track: self._track_artist = _track['artists'][0]['name'] if 'thumbnail' in _track: _album_art_ref = _track['thumbnail'] ## returns a list, # thumbnail [0] is super tiny 32x32? / thumbnail [1] is ok-ish / thumbnail [2] is quite nice quality self._track_album_cover = _album_art_ref[len(_album_art_ref) - 1]['url'] elif 'thumbnails' in _track: _album_art_ref = _track['thumbnails'] ## returns a list self._track_album_cover = _album_art_ref[len(_album_art_ref) - 1]['url'] self.schedule_update_ha_state() """@@@ Get the stream URL and play on media_player @@@""" _url = '' try: _LOGGER.debug("-- try to find streaming url --") streamingData = self._api.get_streaming_data(_track['videoId']) if ('adaptiveFormats' in streamingData): streamingData = streamingData['adaptiveFormats'] elif ( 'formats' in streamingData ): #backup, not sure if that is ever needed, or if adaptiveFormats are always present streamingData = streamingData['formats'] streamId = 0 # try to find audio only stream for i in range(0, len(streamingData)): if (streamingData[i]['mimeType'].startswith('audio/mp4')): streamId = i break elif (streamingData[i]['mimeType'].startswith('audio')): streamId = i if (streamingData[streamId].get('url') is None): sigCipher_ch = streamingData[streamId]['signatureCipher'] sigCipher_ex = sigCipher_ch.split('&') res = dict({'s': '', 'url': ''}) for sig in sigCipher_ex: for key in res: if (sig.find(key + "=") >= 0): res[key] = unquote(sig[len(key + "="):]) # I'm just not sure if the original video from the init will stay online forever # in case it's down the player might not load and thus we won't have a javascript loaded # so if that happens: we try with this url, might work better (at least the file should be online) # the only trouble i could see is that this video is private and thus also won't load the player .. if (self._js == ""): self._get_cipher(_track['videoId']) signature = self._cipher.get_signature( ciphered_signature=res['s']) _url = res['url'] + "&sig=" + signature else: _url = streamingData[streamId]['url'] except Exception as err: _LOGGER.error( "Failed to get own(!) URL for track, further details below. Will not try YouTube method" ) _LOGGER.error(traceback.format_exc()) _LOGGER.error(_track['videoId']) _LOGGER.error(self._api.get_song(_track['videoId'])) # backup: run youtube stack, only if we failed if (_url == ""): try: streams = YouTube('https://www.youtube.com/watch?v=' + _track['videoId']).streams streams_audio = streams.filter(only_audio=True) if (len(streams_audio)): _url = streams_audio.order_by('abr').last().url else: _url = streams.order_by('abr').last().url _LOGGER.error("ultimatly") _LOGGER.error(_url) except Exception as err: _LOGGER.error(traceback.format_exc()) _LOGGER.error("Failed to get URL for track: (%s)", uid) _LOGGER.error(err) if retry < 1: self._turn_off_media_player() return else: _LOGGER.error("Retry with: (%i)", retry) return self._get_track(retry=retry - 1) self._state = STATE_PLAYING self.schedule_update_ha_state() data = { ATTR_MEDIA_CONTENT_ID: _url, ATTR_MEDIA_CONTENT_TYPE: "audio/mp3", ATTR_ENTITY_ID: self._entity_ids } self.hass.services.call(DOMAIN_MP, SERVICE_PLAY_MEDIA, data) """@@@ Get the stream URL and play on media_player @@@""" #_LOGGER.error("register call later") # just to make sure that we check the status of the media player to free the "go to next" call_later(self.hass, 15, self._sync_player) def play_media(self, media_type, media_id, _player=None, **kwargs): if not self._update_entity_ids(): return # Should skip this if input_select does not exist if _player is not None: _option = {"option": _player, "entity_id": self._media_player} self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION, _option) _source = {"option": "Playlist", "entity_id": self._source} _option = {"option": media_id, "entity_id": self._playlist} self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION, _source) self.hass.services.call(input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION, _option) _player = self.hass.states.get(self._entity_ids) if self._playing == True: self.media_stop() self.media_play() elif self._playing == False and self._state == STATE_OFF: if _player.state == STATE_OFF: self.turn_on() else: data = {ATTR_ENTITY_ID: _player.entity_id} self._turn_off_media_player(data) call_later(self.hass, 1, self.turn_on) else: _LOGGER.error("self._state is: (%s).", self._state) def media_play(self, entity_id=None, old_state=None, new_state=None, **kwargs): """Send play command.""" if self._state == STATE_PAUSED: self._state = STATE_PLAYING self.schedule_update_ha_state() data = {ATTR_ENTITY_ID: self._entity_ids} self.hass.services.call(DOMAIN_MP, 'media_play', data) else: _source = self.hass.states.get(self._source) source = _source.state self._load_playlist() def media_pause(self, **kwargs): """ Send media pause command to media player """ self._state = STATE_PAUSED #_LOGGER.error(" PAUSE ") self.schedule_update_ha_state() data = {ATTR_ENTITY_ID: self._entity_ids} self.hass.services.call(DOMAIN_MP, 'media_pause', data) def media_play_pause(self, **kwargs): """Simulate play pause media player.""" if self._state == STATE_PLAYING: self._allow_next = False self.media_pause() else: self._allow_next = False self.media_play() def 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._get_track() def media_next_track(self, **kwargs): """Send next track command.""" if self._playing: self._allow_next = False self._get_track() def media_stop(self, **kwargs): """Send stop command.""" self._state = STATE_IDLE self._playing = False self._track_artist = None self._track_album_name = None self._track_name = None self._track_album_cover = None self.schedule_update_ha_state() data = {ATTR_ENTITY_ID: self._entity_ids} self.hass.services.call(DOMAIN_MP, 'media_stop', data) def set_shuffle(self, shuffle): self._shuffle = shuffle if self._shuffle_mode == 1: self._attributes['shuffle_mode'] = 'Shuffle' elif self._shuffle_mode == 2: self._attributes['shuffle_mode'] = 'Random' elif self._shuffle_mode == 3: self._attributes['shuffle_mode'] = 'Shuffle Random' else: self._attributes['shuffle_mode'] = self._shuffle_mode return self.schedule_update_ha_state() def set_volume_level(self, volume): """Set volume level.""" self._volume = round(volume, 2) data = {ATTR_ENTITY_ID: self._entity_ids, 'volume_level': self._volume} self.hass.services.call(DOMAIN_MP, 'volume_set', data) self.schedule_update_ha_state() def volume_up(self, **kwargs): """Volume up the media player.""" newvolume = min(self._volume + 0.05, 1) self.set_volume_level(newvolume) def volume_down(self, **kwargs): """Volume down media player.""" newvolume = max(self._volume - 0.05, 0.01) self.set_volume_level(newvolume) def mute_volume(self, mute): """Send mute command.""" if self._is_mute == False: self._is_mute = True else: self._is_mute = False self.schedule_update_ha_state() data = { ATTR_ENTITY_ID: self._entity_ids, "is_volume_muted": self._is_mute } self.hass.services.call(DOMAIN_MP, 'volume_mute', data)
from ytmusicapi import YTMusic ytmusic = YTMusic('headers_auth.json') playlists = ytmusic.get_library_playlists(50) for i in playlists: print(i['title'], i['playlistId'])
class YoutubeMusicLibrary(MusicLibrary): def __init__(self): self.ytmusic = YTMusic("headers_auth.json") def add_album(self, album: Album): raise NotImplementedError() def like_track(self, track: Track): raise NotImplementedError() def subscribe_to_artist(self, artist: Artist): raise NotImplementedError() def create_playlist(self, playlist: Playlist): raise NotImplementedError() def add_tracks_to_playlist(self, playlist: Playlist, tracks: List[Track]): return super().add_tracks_to_playlist(playlist, tracks) def get_subscribed_artists(self): return [ Artist(name=artist["artist"]) for artist in self.ytmusic.get_library_subscriptions(limit=10000) ] def get_playlists(self): playlists = [] for basic_playlist_data in self.ytmusic.get_library_playlists( limit=1000): playlist_id = basic_playlist_data["playlistId"] playlist_data = self.ytmusic.get_playlist(playlist_id, limit=1000) public = True if playlist_data["privacy"] == "PUBLIC" else False playlists.append( Playlist( name=basic_playlist_data["title"], id=basic_playlist_data["playlistId"], count=int(playlist_data.get("trackCount", 0)), public=public, description=playlist_data.get("description", None), tracks=[ self._track_data_to_track(track_data) for track_data in playlist_data["tracks"] ], )) return playlists def get_liked_songs(self): tracks = [] for track_data in self.ytmusic.get_liked_songs(limit=10000)["tracks"]: track = self._track_data_to_track(track_data) tracks.append(track) return tracks def get_albums(self): album_data = self.ytmusic.get_library_albums(limit=10000) albums: List[Album] = [] for album in album_data: artists = Artist(album["artists"][0]["name"]) albums.append( Album( artists=artists, year=album["year"], name=album["title"], type_=album["type"], )) return albums @staticmethod def _track_data_to_track(track_data) -> Track: artist = Artist(name=track_data["artists"][0]["name"]) album = None if track_data["album"]: album = Album( name=track_data["album"]["name"], artists=artist, year=None, type_="Album", ) return Track(album=album, artist=artist, name=track_data["title"]) def _get_playlist_tracks(self, playlist: Playlist): return [ self._track_data_to_track(track_data) for track_data in self.ytmusic.get_playlist(playlist.id)["tracks"] ]