def search(self, query=None, uris=None, exact=False): results = [] logger.debug("YoutubeMusic searching for %s", query) if "any" in query: try: res = self.backend.api.search(" ".join(query["any"]), filter=None) results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"any\"=\"%s\"", " ".join(query["any"])) elif "track_name" in query: try: res = self.backend.api.search(" ".join(query["track_name"]), filter="songs") if exact: results = self.parseSearch(res, "track", query["track_name"]) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"title\"=\"%s\"", " ".join(query["track_name"])) elif "albumartist" in query or "artist" in query: q1 = ("albumartist" in query and query["albumartist"]) or [] q2 = ("artist" in query and query["artist"]) or [] try: res = self.backend.api.search(" ".join(q1 + q2), filter="artists") if exact: results = self.parseSearch(res, "artist", q1 + q2) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"artist\"=\"%s\"", " ".join(q1 + q2)) elif "album" in query: try: res = self.backend.api.search(" ".join(query["album"]), filter="albums") if exact: results = self.parseSearch(res, "album", query["album"]) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"album\"=\"%s\"", " ".join(query["album"])) else: logger.debug( "YoutubeMusic skipping search, unsupported field types \"%s\"", " ".join(query.keys())) return None return results
def delete(self, uri): logger.debug("YoutubeMusic deleting playlist \"%s\"", uri) bId = parse_uri(uri) try: self.backend.api.delete_playlist(bId) return True except Exception: logger.exception("YoutubeMusic failed to delete playlist") return False
def artistToTracks(self, artist): if "songs" in artist and "browseId" in artist["songs"] and artist[ "songs"]["browseId"] is not None: res = self.backend.api.get_playlist( artist["songs"]["browseId"], limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) logger.debug('YoutubeMusic found %d tracks for %s', len(tracks), artist['name']) return tracks return None
def get_items(self, uri): bId = parse_uri(uri) logger.debug("YoutubeMusic getting playlist items for \"%s\"", bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic failed getting playlist items") pls = None if pls: tracks = self.backend.library.playlistToTracks(pls) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] return None
def _get_youtube_player(self): # Refresh our js player URL so YDL can decode the signature correctly. response = requests.get('https://music.youtube.com', headers=self.api.headers, proxies=self.api.proxies) m = re.search(r'jsUrl"\s*:\s*"([^"]+)"', response.text) if m: url = m.group(1) logger.debug('YoutubeMusic updated player URL to %s', url) return (url) else: logger.error('YoutubeMusic unable to extract player URL.') return (None)
def as_list(self): logger.debug("YoutubeMusic getting user playlists") refs = [] try: playlists = self.backend.api.get_library_playlists(limit=100) except Exception: logger.exception("YoutubeMusic failed getting a list of playlists") playlists = [] for pls in playlists: refs.append(Ref.playlist( uri=f"youtubemusic:playlist:{pls['playlistId']}", name=pls["title"], )) return refs
def translate_uri(self, uri): logger.debug('YoutubeMusic PlaybackProvider.translate_uri "%s"', uri) if "youtubemusic:track:" not in uri: return None try: bId = uri.split(":")[2] self.last_id = bId return self._get_track(bId) except Exception as e: logger.error('translate_uri error "%s"', str(e)) return None
def save(self, playlist): bId = parse_uri(playlist.uri) logger.debug("YoutubeMusic saving playlist \"%s\" \"%s\"", playlist.name, bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic saving playlist failed") return None oldIds = set([t["videoId"] for t in pls["tracks"]]) newIds = set([parse_uri(p.uri)[0] for p in playlist.tracks]) common = oldIds & newIds remove = oldIds ^ common add = newIds ^ common if len(remove): logger.debug("YoutubeMusic removing items \"%s\" from playlist", remove) try: videos = [t for t in pls["tracks"] if t["videoId"] in remove] self.backend.api.remove_playlist_items(bId, videos) except Exception: logger.exception("YoutubeMusic failed removing items from playlist") if len(add): logger.debug("YoutubeMusic adding items \"%s\" to playlist", add) try: self.backend.api.add_playlist_items(bId, list(add)) except Exception: logger.exception("YoutubeMusic failed adding items to playlist") if pls["title"] != playlist.name: logger.debug("Renaming playlist to \"%s\"", playlist.name) try: self.backend.api.edit_playlist(bId, title=playlist.name) except Exception: logger.exception("YoutubeMusic failed renaming playlist") return playlist
def scrobble_track(self, bId): # Called through YoutubeMusicScrobbleListener # Let YouTube Music know we're playing this track so it will be added to our history. endpoint = "https://www.youtube.com/get_video_info" params = { "video_id": bId, "hl": self.api.language, "el": "detailpage", "c": "WEB_REMIX", "cver": "0.1" } response = requests.get(endpoint, params, headers=self.api.headers, proxies=self.api.proxies) text = parse_qs(response.text) player_response = json.loads(text['player_response'][0]) trackurl = re.sub( r'plid=', 'list=', player_response['playbackTracking'] ['videostatsPlaybackUrl']['baseUrl']) CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' params = { 'cpn': ''.join((CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))), 'referrer': "https://music.youtube.com", 'cbr': text['cbr'][0], 'cbrver': text['cbrver'][0], 'c': text['c'][0], 'cver': text['cver'][0], 'cos': text['cos'][0], 'cosver': text['cosver'][0], 'cr': text['cr'][0], 'ver': 2, } tr = requests.get(trackurl, params=params, headers=self.api.headers, proxies=self.api.proxies) logger.debug("%d code from '%s'", tr.status_code, tr.url)
def lookup(self, uri): bId = parse_uri(uri) logger.debug("YoutubeMusic looking up playlist \"%s\"", bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic playlist lookup failed") pls = None if pls: tracks = self.backend.library.playlistToTracks(pls) return Playlist( uri=f"youtubemusic:playlist:{pls['id']}", name=pls["title"], tracks=tracks, last_modified=None, )
def create(self, name): logger.debug("YoutubeMusic creating playlist \"%s\"", name) try: bId = self.backend.api.create_playlist(name, "") except Exception: logger.exception("YoutubeMusic playlist creation failed") bId = None if bId: uri = f"youtubemusic:playlist:{bId}" logger.debug("YoutubeMusic created playlist \"%s\"", uri) return Playlist( uri=uri, name=name, tracks=[], last_modified=None, ) return None
def track_playback_ended(self, tl_track, time_position): if self.scrobbling: track = tl_track.track duration = track.length and track.length // 1000 or 0 time_position = time_position // 1000 if time_position < duration // 2 and time_position < 120: logger.debug( "Track not played long enough too scrobble. (50% or 120s)" ) return bId = track.uri.split(":")[2] logger.debug("Scrobbling: %s", bId) listener.send( YoutubeMusicScrobbleListener, "scrobble_track", bId=bId, )
def _get_auto_playlists(self): try: logger.debug('YoutubeMusic loading auto playlists') response = self.api._send_request('browse', {}) tab = nav(response, SINGLE_COLUMN_TAB) browse = parse_auto_playlists(nav(tab, SECTION_LIST)) if 'continuations' in tab['sectionListRenderer']: request_func = lambda additionalParams: self.api._send_request( 'browse', {}, additionalParams) parse_func = lambda contents: parse_auto_playlists(contents) browse.extend( get_continuations(tab['sectionListRenderer'], 'sectionListContinuation', 100, request_func, parse_func)) # Delete empty sections for i in range(len(browse) - 1, 0, -1): if len(browse[i]['items']) == 0: browse.pop(i) logger.debug('YoutubeMusic loaded %d auto playlists sections', len(browse)) self.library.ytbrowse = browse except Exception: logger.exception('YoutubeMusic failed to load auto playlists') return (None)
def parseSearch(self, results, field=None, queries=[]): tracks = set() salbums = set() sartists = set() for result in results: if result["resultType"] == "song": if field == "track" and not any( q.casefold() == result["title"].casefold() for q in queries): continue if result['videoId'] in self.TRACKS: tracks.add(self.TRACKS[result['videoId']]) else: try: length = [ int(i) for i in result["duration"].split(":") ] except ValueError: length = [0, 0] if result['videoId'] is None: continue if result['videoId'] not in self.TRACKS: artists = [] for a in result['artists']: if a['id'] not in self.ARTISTS: self.ARTISTS[a['id']] = Artist( uri=f"youtubemusic:artist:{a['id']}", name=a["name"], sortname=a["name"], musicbrainz_id="", ) artists.append(self.ARTISTS[a['id']]) album = None if 'album' in result: if result['album']['id'] not in self.ALBUMS: self.ALBUMS[result['album']['id']] = Album( uri= f"youtubemusic:album:{result['album']['id']}", name=result["album"]["name"], artists=artists, num_tracks=None, num_discs=None, date="0000", musicbrainz_id="", ) album = self.ALBUMS[result['album']['id']] self.TRACKS[result['videoId']] = Track( uri=f"youtubemusic:track:{result['videoId']}", name=result["title"], artists=artists, album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="0000", length=(length[0] * 60 * 1000) + (length[1] * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(self.TRACKS[result['videoId']]) elif result["resultType"] == "album": if field == "album" and not any( q.casefold() == result["title"].casefold() for q in queries): continue try: album = self.backend.api.get_album(result["browseId"]) if result["browseId"] not in self.ALBUMS: date = result['year'] self.ALBUMS[result['browseId']] = Album( uri=f"youtubemusic:album:{result['browseId']}", name=album["title"], artists=[ Artist( uri="", name=result["artist"], sortname=result["artist"], musicbrainz_id="", ) ], num_tracks=int(album["trackCount"]) if str(album["trackCount"]).isnumeric() else None, num_discs=None, date=date, musicbrainz_id="", ) salbums.add(self.ALBUMS[result['browseId']]) except Exception: logger.exception("YoutubeMusic failed parsing album %s", result["title"]) elif result["resultType"] == "artist": if field == "artist" and not any( q.casefold() == result["artist"].casefold() for q in queries): continue try: artistq = self.backend.api.get_artist(result["browseId"]) if result['browseId'] not in self.ARTISTS: self.ARTISTS[result['browseId']] = Artist( uri=f"youtubemusic:artist:{result['browseId']}", name=artistq["name"], sortname=artistq["name"], musicbrainz_id="", ) sartists.add(self.ARTISTS[result['browseId']]) if 'albums' in artistq: if 'params' in artistq['albums']: albums = self.backend.api.get_artist_albums( artistq["channelId"], artistq["albums"]["params"]) for album in albums: if album['browseId'] not in self.ALBUMS: self.ALBUMS[album['browseId']] = Album( uri= f"youtubemusic:album:{album['browseId']}", name=album["title"], artists=[ self.ARTISTS[result['browseId']] ], date=album['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[album['browseId']]) elif 'results' in artistq['albums']: for album in artistq["albums"]["results"]: if album['browseId'] not in self.ALBUMS: self.ALBUMS[album['browseId']] = Album( uri= f"youtubemusic:album:{album['browseId']}", name=album["title"], artists=[ self.ARTISTS[result['browseId']] ], date=album['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[album['browseId']]) if 'singles' in artistq and 'results' in artistq['singles']: for single in artistq['singles']['results']: if single['browseId'] not in self.ALBUMS: self.ALBUMS[single['browseId']] = Album( uri= f"youtubemusic:album:{single['browseId']}", name=single['title'], artists=[self.ARTISTS[result['browseId']]], date=single['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[single['browseId']]) if 'songs' in artistq: if 'results' in artistq['songs']: for song in artistq['songs']['results']: if song['videoId'] in self.TRACKS: tracks.add(self.TRACKS[song['videoId']]) else: album = None if 'album' in song: if song['album'][ 'id'] not in self.ALBUMS: self.ALBUMS[ song['album']['id']] = Album( uri= f"youtubemusic:album:{song['album']['id']}", name=song['album']['name'], artists=[ self.ARTISTS[ result['browseId']] ], date='1999', musicbrainz_id="", ) album = self.ALBUMS[song['album'] ['id']] if song['videoId'] not in self.TRACKS: self.TRACKS[song['videoId']] = Track( uri= f"youtubemusic:track:{song['videoId']}", name=song['title'], artists=[ self.ARTISTS[ result['browseId']] ], album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="0000", length=None, bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(self.TRACKS[song['videoId']]) except Exception: logger.exception("YoutubeMusic failed parsing artist %s", result["artist"]) tracks = list(tracks) for track in tracks: bId, _ = parse_uri(track.uri) self.TRACKS[bId] = track logger.debug("YoutubeMusic search returned %d results", len(tracks) + len(sartists) + len(salbums)) return SearchResult( uri="youtubemusic:search", tracks=tracks, artists=list(sartists), albums=list(salbums), )
def browse(self, uri): if not uri: return [] logger.debug("YoutubeMusic browsing uri \"%s\"", uri) if uri == "youtubemusic:root": dirs = [] if self.backend.auth: dirs += [ Ref.directory(uri="youtubemusic:artist", name="Artists"), Ref.directory(uri="youtubemusic:album", name="Albums"), ] if self.backend.liked_songs: dirs.append( Ref.directory(uri="youtubemusic:liked", name="Liked Songs")) if self.backend.history: dirs.append( Ref.directory(uri="youtubemusic:history", name="Recently Played")) if self.backend.subscribed_artist_limit: dirs.append( Ref.directory(uri="youtubemusic:subscriptions", name="Subscriptions")) dirs.append( Ref.directory(uri="youtubemusic:watch", name="Similar to last played")) if self.backend.mood_genre: dirs.append( Ref.directory(uri="youtubemusic:mood", name="Mood and Genre Playlists")) if self.backend._auto_playlist_refresh_rate: dirs.append( Ref.directory(uri="youtubemusic:auto", name="Auto Playlists")) return (dirs) elif uri == "youtubemusic:subscriptions" and self.backend.subscribed_artist_limit: try: subs = self.backend.api.get_library_subscriptions( limit=self.backend.subscribed_artist_limit) logger.debug("YoutubeMusic found %d artists in subscriptions", len(subs)) return [ Ref.artist(uri=f"youtubemusic:artist:{a['browseId']}", name=a["artist"]) for a in subs ] except Exception: logger.exception( "YoutubeMusic failed getting artists from subscriptions") elif uri == "youtubemusic:artist": try: library_artists = [ Ref.artist(uri=f"youtubemusic:artist:{a['browseId']}", name=a["artist"]) for a in self.backend.api.get_library_artists(limit=100) ] logger.debug("YoutubeMusic found %d artists in library", len(library_artists)) except Exception: logger.exception( "YoutubeMusic failed getting artists from library") library_artists = [] if self.backend.auth: try: upload_artists = [ Ref.artist( uri=f"youtubemusic:artist:{a['browseId']}:upload", name=a["artist"]) for a in self.backend.api.get_library_upload_artists( limit=100) ] logger.debug("YoutubeMusic found %d uploaded artists", len(upload_artists)) except Exception: logger.exception( "YoutubeMusic failed getting uploaded artists") upload_artists = [] else: upload_artists = [] return library_artists + upload_artists elif uri == "youtubemusic:album": try: library_albums = [ Ref.album(uri=f"youtubemusic:album:{a['browseId']}", name=a["title"]) for a in self.backend.api.get_library_albums(limit=100) ] logger.debug("YoutubeMusic found %d albums in library", len(library_albums)) except Exception: logger.exception( "YoutubeMusic failed getting albums from library") library_albums = [] if self.backend.auth: try: upload_albums = [ Ref.album( uri=f"youtubemusic:album:{a['browseId']}:upload", name=a["title"]) for a in self.backend.api.get_library_upload_albums( limit=100) ] logger.debug("YoutubeMusic found %d uploaded albums", len(upload_albums)) except Exception: logger.exception( "YoutubeMusic failed getting uploaded albums") upload_albums = [] else: upload_albums = [] return library_albums + upload_albums elif uri == "youtubemusic:liked": try: res = self.backend.api.get_liked_songs( limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) logger.debug("YoutubeMusic found %d liked songs", len(res["tracks"])) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception("YoutubeMusic failed getting liked songs") elif uri == "youtubemusic:history": try: res = self.backend.api.get_history() tracks = self.playlistToTracks({'tracks': res}) logger.debug("YoutubeMusic found %d songs from recent history", len(res)) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting listening history") elif uri == "youtubemusic:watch": try: playback = self.backend.playback if playback.last_id is not None: track_id = playback.last_id elif self.backend.auth: hist = self.backend.api.get_history() track_id = hist[0]['videoId'] if track_id: res = self.backend.api.get_watch_playlist( track_id, limit=self.backend.playlist_item_limit) if 'tracks' in res: logger.debug( "YoutubeMusic found %d watch songs for \"%s\"", len(res["tracks"]), track_id) res['tracks'].pop(0) tracks = self.playlistToTracks(res) return [ Ref.track(uri=t.uri, name=t.name) for t in tracks ] except Exception: logger.exception("YoutubeMusic failed getting watch songs") elif uri == "youtubemusic:mood": try: logger.debug('YoutubeMusic loading mood/genre playlists') moods = {} response = self.backend.api._send_request( 'browse', {"browseId": "FEmusic_moods_and_genres"}) for sect in nav(response, SINGLE_COLUMN_TAB + SECTION_LIST): for cat in nav(sect, ['gridRenderer', 'items']): title = nav(cat, [ 'musicNavigationButtonRenderer', 'buttonText', 'runs', 0, 'text' ]).strip() endpnt = nav(cat, [ 'musicNavigationButtonRenderer', 'clickCommand', 'browseEndpoint', 'browseId' ]) params = nav(cat, [ 'musicNavigationButtonRenderer', 'clickCommand', 'browseEndpoint', 'params' ]) moods[title] = { 'name': title, 'uri': 'youtubemusic:mood:' + params + ':' + endpnt } return [ Ref.directory(uri=moods[a]['uri'], name=moods[a]['name']) for a in sorted(moods.keys()) ] except Exception: logger.exception( 'YoutubeMusic failed to load mood/genre playlists') elif uri.startswith("youtubemusic:mood:"): try: ret = [] _, _, params, endpnt = uri.split(':') response = self.backend.api._send_request( 'browse', { "browseId": endpnt, "params": params }) for sect in nav(response, SINGLE_COLUMN_TAB + SECTION_LIST): key = [] if 'gridRenderer' in sect: key = ['gridRenderer', 'items'] elif 'musicCarouselShelfRenderer' in sect: key = ['musicCarouselShelfRenderer', 'contents'] elif 'musicImmersiveCarouselShelfRenderer' in sect: key = [ 'musicImmersiveCarouselShelfRenderer', 'contents' ] if len(key): for item in nav(sect, key): title = nav(item, ['musicTwoRowItemRenderer'] + TITLE_TEXT).strip() # if 'subtitle' in item['musicTwoRowItemRenderer']: # title += ' (' # for st in item['musicTwoRowItemRenderer']['subtitle']['runs']: # title += st['text'] # title += ')' brId = nav(item, ['musicTwoRowItemRenderer'] + NAVIGATION_BROWSE_ID) ret.append( Ref.playlist( uri=f"youtubemusic:playlist:{brId}", name=title)) return (ret) except Exception: logger.exception( 'YoutubeMusic failed getting mood/genre playlist "%s"', uri) elif uri == "youtubemusic:auto" and self.backend._auto_playlist_refresh_rate: try: return [ Ref.directory(uri=a['uri'], name=a['name']) for a in self.ytbrowse ] except Exception: logger.exception('YoutubeMusic failed getting auto playlists') elif uri.startswith("youtubemusic:auto:" ) and self.backend._auto_playlist_refresh_rate: try: for a in self.ytbrowse: if a['uri'] == uri: ret = [] for i in a['items']: if i['type'] == 'playlist': ret.append( Ref.playlist(uri=i['uri'], name=i['name'])) logger.debug("playlist: %s - %s", i['name'], i['uri']) elif i['type'] == 'artist': ret.append( Ref.artist(uri=i['uri'], name=i['name'])) logger.debug("artist: %s - %s", i['name'], i['uri']) elif i['type'] == 'album': ret.append( Ref.album(uri=i['uri'], name=i['name'])) logger.debug("album: %s - %s", i['name'], i['uri']) return (ret) except Exception: logger.exception( 'YoutubeMusic failed getting auto playlist "%s"', uri) elif uri.startswith("youtubemusic:artist:"): bId, upload = parse_uri(uri) if upload: try: res = self.backend.api.get_library_upload_artist(bId) tracks = self.uploadArtistToTracks(res) logger.debug( "YoutubeMusic found %d songs for uploaded artist \"%s\"", len(res), res[0]["artist"]["name"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for uploaded artist \"%s\"", bId) else: try: res = self.backend.api.get_artist(bId) tracks = self.artistToTracks(res) logger.debug( "YoutubeMusic found %d songs for artist \"%s\" in library", len(res["songs"]), res["name"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for artist \"%s\"", bId) elif uri.startswith("youtubemusic:album:"): bId, upload = parse_uri(uri) if upload: try: res = self.backend.api.get_library_upload_album(bId) tracks = self.uploadAlbumToTracks(res, bId) logger.debug( "YoutubeMusic found %d songs for uploaded album \"%s\"", len(res["tracks"]), res["title"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for uploaded album \"%s\"", bId) else: try: res = self.backend.api.get_album(bId) tracks = self.albumToTracks(res, bId) logger.debug( "YoutubeMusic found %d songs for album \"%s\" in library", len(res["tracks"]), res["title"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for album \"%s\"", bId) elif uri.startswith("youtubemusic:playlist:"): bId, upload = parse_uri(uri) try: res = self.backend.api.get_playlist( bId, limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed to get tracks from playlist '%s'", bId) return []
def _refresh_auto_playlists(self): t0 = time.time() self._get_auto_playlists() t = time.time() - t0 logger.debug("YoutubeMusic Auto Playlists refreshed in %.2fs", t)
def _get_track(self, bId): streams = self.backend.api.get_streaming_data(bId) playstr = None url = None if self.backend.stream_preference: # Try to find stream by our preference order. tags = {} if 'adaptiveFormats' in streams: for stream in streams['adaptiveFormats']: tags[str(stream['itag'])] = stream elif 'dashManifestUrl' in streams: # Grab the dashmanifest XML and parse out the streams from it dash = requests.get(streams['dashManifestUrl']) formats = re.findall(r'<Representation id="(\d+)" .*? bandwidth="(\d+)".*?BaseURL>(.*?)</BaseURL', dash.text) for stream in formats: tags[stream[0]] = { 'url': stream[2], 'audioQuality': 'ITAG_' + stream[0], 'averageBitrate': int(stream[1]), } for i, p in enumerate(self.backend.stream_preference, start=1): if str(p) in tags: playstr = tags[str(p)] logger.debug("Found #%d preference stream %s", i, str(p)) break if playstr is None: # Couldn't find our preference, let's try something else: if 'adaptiveFormats' in streams: # Try to find the highest quality stream. We want "AUDIO_QUALITY_HIGH", barring # that we find the highest bitrate audio/mp4 stream, after that we sort through the # garbage. bitrate = 0 crap = {} worse = {} for stream in streams['adaptiveFormats']: if 'audioQuality' in stream and stream['audioQuality'] == 'AUDIO_QUALITY_HIGH': playstr = stream break if stream['mimeType'].startswith('audio/mp4') and stream['averageBitrate'] > bitrate: bitrate = stream['averageBitrate'] playstr = stream elif stream['mimeType'].startswith('audio'): crap[stream['averageBitrate']] = stream else: worse[stream['averageBitrate']] = stream if playstr is None: # sigh. if len(crap): playstr = crap[sorted(list(crap.keys()))[-1]] if 'audioQuality' not in playstr: playstr['audioQuality'] = 'AUDIO_QUALITY_GARBAGE' elif len(worse): playstr = worse[sorted(list(worse.keys()))[-1]] if 'audioQuality' not in playstr: playstr['audioQuality'] = 'AUDIO_QUALITY_FECES' elif 'formats' in streams: # Great, we're really left with the dregs of quality. playstr = streams['formats'][0] if 'audioQuality' not in playstr: playstr['audioQuality'] = 'AUDIO_QUALITY_404' else: logger.error('No streams found for %s. Falling back to youtube-dl.', bId) if playstr is not None: # Use Youtube-DL's Info Extractor to decode the signature. if 'signatureCipher' in playstr: sc = parse_qs(playstr['signatureCipher']) sig = self.YoutubeIE._decrypt_signature( sc['s'][0], bId, self.Youtube_Player_URL, ) url = sc['url'][0] + '&sig=' + sig + '&ratebypass=yes' elif 'url' in playstr: url = playstr['url'] else: logger.error("Unable to get URL from stream for %s", bId) return(None) logger.debug('Found %s stream with %d ABR for %s', playstr['audioQuality'], playstr['averageBitrate'], bId) if url is not None: # Return the decoded youtube url to mopidy for playback. return(url) return None
def _refresh_youtube_player(self): t0 = time.time() self.playback.Youtube_Player_URL = self._get_youtube_player() t = time.time() - t0 logger.debug("Youtube Player URL refreshed in %.2fs", t)