def _get_auto_playlists(self): try: logger.debug("YTMusic 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.info("YTMusic loaded %d auto playlists sections", len(browse)) self.library.ytbrowse = browse except Exception: logger.exception("YTMusic failed to load auto playlists") return None
def _refresh_youtube_player(self): t0 = time.time() url = self._get_youtube_player() if url is not None: if self.playback.Youtube_Player_URL != url: self.playback.update_cipher(playerurl=url) t = time.time() - t0 logger.debug("YTMusic Player URL refreshed in %.2fs", t)
def delete(self, uri): logger.debug('YTMusic deleting playlist "%s"', uri) bId = parse_uri(uri) try: self.backend.api.delete_playlist(bId) return True except Exception: logger.exception("YTMusic failed to delete playlist") return False
def scrobble_track(self, bId): # Called through YTMusicScrobbleListener # Let YTMusic 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 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("YTMusic found %d tracks for %s", len(tracks), artist["name"]) return tracks return None
def get_items(self, uri): bId = parse_uri(uri) logger.debug('YTMusic getting playlist items for "%s"', bId) try: pls = self.backend.api.get_playlist( bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YTMusic 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 translate_uri(self, uri): logger.debug('YTMusic PlaybackProvider.translate_uri "%s"', uri) if "ytmusic: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('YTMusic 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("YTMusic 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('YTMusic 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("YTMusic failed removing items from playlist") if len(add): logger.debug('YTMusic adding items "%s" to playlist', add) try: self.backend.api.add_playlist_items(bId, list(add)) except Exception: logger.exception("YTMusic 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("YTMusic failed renaming playlist") return playlist
def get_video(id_): uri = f"https://music.youtube.com/watch?v={id_}" vid = YDL.extract_info( url=uri, download=False, ie_key=None, extra_info={}, process=True, force_generic_extractor=False, ) for fmt in vid["formats"]: if fmt["ext"] == "m4a": logger.debug("YTMusic Stream URI %s", fmt["url"]) return fmt["url"] return None
def as_list(self): logger.debug("YTMusic getting user playlists") refs = [] try: playlists = self.backend.api.get_library_playlists(limit=100) except Exception: logger.exception("YTMusic failed getting a list of playlists") playlists = [] for pls in playlists: refs.append( Ref.playlist( uri=f"ytmusic:playlist:{pls['playlistId']}", name=pls["title"], )) return refs
def create(self, name): logger.debug('YTMusic creating playlist "%s"', name) try: bId = self.backend.api.create_playlist(name, "") except Exception: logger.exception("YTMusic playlist creation failed") bId = None if bId: uri = f"ytmusic:playlist:{bId}" logger.debug('YTMusic created playlist "%s"', uri) return Playlist( uri=uri, name=name, tracks=[], last_modified=None, ) return None
def lookup(self, uri): bId = parse_uri(uri) logger.debug('YTMusic looking up playlist "%s"', bId) try: pls = self.backend.api.get_playlist( bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YTMusic playlist lookup failed") pls = None if pls: tracks = self.backend.library.playlistToTracks(pls) return Playlist( uri=f"ytmusic:playlist:{pls['id']}", name=pls["title"], tracks=tracks, last_modified=None, )
def _get_youtube_player(self): # Refresh our js player URL so YDL can decode the signature correctly. try: 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("YTMusic updated player URL to %s", url) return url else: logger.error("YTMusic unable to extract player URL.") return None except Exception: logger.exception("YTMusic failed to refresh player URL.") return None
def track_playback_ended(self, tl_track, time_position): if self.scrobbling: track = tl_track.track if track.uri.startswith("ytmusic:"): 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( YTMusicScrobbleListener, "scrobble_track", bId=bId, )
def save(self, playlist): id_, upload = parse_uri(playlist.uri) logger.info("YTMusic saving playlist \"%s\" \"%s\"", playlist.name, id_) try: pls = API.get_playlist(id_, limit=100) except Exception: logger.exception("YTMusic 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("YTMusic removing items \"%s\" from playlist", remove) try: videos = [t for t in pls["tracks"] if t["videoId"] in remove] API.remove_playlist_items(id_, videos) except Exception: logger.exception("YTMusic failed removing items from playlist") if len(add): logger.debug("YTMusic adding items \"%s\" to playlist", add) try: API.add_playlist_items(id_, list(add)) except Exception: logger.exception("YTMusic failed adding items to playlist") if pls["title"] != playlist.name: logger.debug("Renaming playlist to \"%s\"", playlist.name) try: API.edit_playlist(id_, title=playlist.name) except Exception: logger.exception("YTMusic failed renaming playlist") return playlist
def scrobble_track(self, bId): # Called through YTMusicScrobbleListener # Let YTMusic know we're playing this track so it will be added to our history. player_response = self.api._send_request( "player", { "playbackContext": { "contentPlaybackContext": {"signatureTimestamp": 18766} }, "video_id": bId, }, ) 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", "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 _refresh_youtube_player(self): t0 = time.time() self.playback.Youtube_Player_URL = self._get_youtube_player() t = time.time() - t0 logger.debug("YTMusic Player URL refreshed in %.2fs", t)
def browse(self, uri): if not uri: return [] logger.debug('YTMusic browsing uri "%s"', uri) if uri == "ytmusic:root": dirs = [] if self.backend.auth: dirs += [ Ref.directory(uri="ytmusic:artist", name="Artists"), Ref.directory(uri="ytmusic:album", name="Albums"), ] if self.backend.liked_songs: dirs.append( Ref.directory(uri="ytmusic:liked", name="Liked Songs")) if self.backend.history: dirs.append( Ref.directory(uri="ytmusic:history", name="Recently Played")) if self.backend.subscribed_artist_limit: dirs.append( Ref.directory(uri="ytmusic:subscriptions", name="Subscriptions")) dirs.append( Ref.directory(uri="ytmusic:watch", name="Similar to last played")) if self.backend.mood_genre: dirs.append( Ref.directory(uri="ytmusic:mood", name="Mood and Genre Playlists")) if self.backend._auto_playlist_refresh_rate: dirs.append( Ref.directory(uri="ytmusic:auto", name="Auto Playlists")) return dirs elif (uri == "ytmusic:subscriptions" and self.backend.subscribed_artist_limit): try: subs = self.backend.api.get_library_subscriptions( limit=self.backend.subscribed_artist_limit) logger.debug("YTMusic found %d artists in subscriptions", len(subs)) return [ Ref.artist(uri=f"ytmusic:artist:{a['browseId']}", name=a["artist"]) for a in subs ] except Exception: logger.exception( "YTMusic failed getting artists from subscriptions") elif uri == "ytmusic:artist": try: library_artists = [ Ref.artist(uri=f"ytmusic:artist:{a['browseId']}", name=a["artist"]) for a in self.backend.api.get_library_artists(limit=100) ] logger.debug("YTMusic found %d artists in library", len(library_artists)) except Exception: logger.exception("YTMusic failed getting artists from library") library_artists = [] if self.backend.auth: try: upload_artists = [ Ref.artist( uri=f"ytmusic:artist:{a['browseId']}:upload", name=a["artist"], ) for a in self.backend.api.get_library_upload_artists( limit=100) ] logger.debug("YTMusic found %d uploaded artists", len(upload_artists)) except Exception: logger.exception("YTMusic failed getting uploaded artists") upload_artists = [] else: upload_artists = [] return library_artists + upload_artists elif uri == "ytmusic:album": try: library_albums = [ Ref.album(uri=f"ytmusic:album:{a['browseId']}", name=a["title"]) for a in self.backend.api.get_library_albums(limit=100) ] logger.debug("YTMusic found %d albums in library", len(library_albums)) except Exception: logger.exception("YTMusic failed getting albums from library") library_albums = [] if self.backend.auth: try: upload_albums = [ Ref.album( uri=f"ytmusic:album:{a['browseId']}:upload", name=a["title"], ) for a in self.backend.api.get_library_upload_albums( limit=100) ] logger.debug("YTMusic found %d uploaded albums", len(upload_albums)) except Exception: logger.exception("YTMusic failed getting uploaded albums") upload_albums = [] else: upload_albums = [] return library_albums + upload_albums elif uri == "ytmusic:liked": try: res = self.backend.api.get_liked_songs( limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) logger.debug("YTMusic found %d liked songs", len(res["tracks"])) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception("YTMusic failed getting liked songs") elif uri == "ytmusic:history": try: res = self.backend.api.get_history() tracks = self.playlistToTracks({"tracks": res}) logger.debug("YTMusic 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("YTMusic failed getting listening history") elif uri == "ytmusic: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( 'YTMusic 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("YTMusic failed getting watch songs") elif uri == "ytmusic:mood": try: logger.debug("YTMusic 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": "ytmusic:mood:" + params + ":" + endpnt, } return [ Ref.directory(uri=moods[a]["uri"], name=moods[a]["name"]) for a in sorted(moods.keys()) ] except Exception: logger.exception("YTMusic failed to load mood/genre playlists") elif uri.startswith("ytmusic: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"ytmusic:playlist:{brId}", name=title)) return ret except Exception: logger.exception( 'YTMusic failed getting mood/genre playlist "%s"', uri) elif uri == "ytmusic: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("YTMusic failed getting auto playlists") elif (uri.startswith("ytmusic: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('YTMusic failed getting auto playlist "%s"', uri) elif uri.startswith("ytmusic:artist:"): bId, upload = parse_uri(uri) if upload: try: res = self.backend.api.get_library_upload_artist(bId) tracks = self.uploadArtistToTracks(res) logger.debug( 'YTMusic found %d songs for uploaded artist "%s"', len(res), res[0]["artist"][0]["name"], ) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( 'YTMusic failed getting tracks for uploaded artist "%s"', bId, ) else: try: res = self.backend.api.get_artist(bId) tracks = self.artistToTracks(res) logger.debug( 'YTMusic 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( 'YTMusic failed getting tracks for artist "%s"', bId) elif uri.startswith("ytmusic: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( 'YTMusic 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( 'YTMusic failed getting tracks for uploaded album "%s"', bId, ) else: try: res = self.backend.api.get_album(bId) tracks = self.albumToTracks(res, bId) logger.debug( 'YTMusic 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( 'YTMusic failed getting tracks for album "%s"', bId) elif uri.startswith("ytmusic: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( "YTMusic failed to get tracks from playlist '%s'", bId) return []
def get_images(self, uris): ret = {} for uri in uris: images = [] comp = uri.split(":") if len(comp) == 3: logger.debug("YTMusic getting images for %s", uri) func, bId = comp[1:3] if bId not in self.IMAGES: try: if func == "artist": data = self.backend.api.get_artist(bId) images = self.addThumbnails(bId, data, False) elif func == "album": data = self.backend.api.get_album(bId) images = self.addThumbnails(bId, data) elif func == "playlist": data = self.backend.api.get_playlist(bId) images = self.addThumbnails(bId, data, False) elif func == "track": if (bId in self.TRACKS and self.TRACKS[bId].album is not None and self.TRACKS[bId].album.uri is not None): album, upload = parse_uri( self.TRACKS[bId].album.uri) if upload: data = self.backend.api.get_library_upload_album( album) else: data = self.backend.api.get_album(album) images = self.addThumbnails(bId, data) except Exception: logger.error("YTMusic unable to get image url for %s", uri) else: images = self.IMAGES[bId] elif len(comp) == 4 and comp[3] == "upload": logger.debug("YTMusic getting images for %s", uri) func, bId = comp[1:3] if bId not in self.IMAGES: try: if func == "artist": data = self.backend.api.get_library_upload_artist( bId) images = self.addThumbnails(bId, data, False) elif func == "album": data = self.backend.api.get_library_upload_album( bId) images = self.addThumbnails(bId, data) elif func == "track": if (bId in self.TRACKS and self.TRACKS[bId].album is not None and self.TRACKS[bId].album.uri is not None): album, upload = parse_uri( self.TRACKS[bId].album.uri) if upload: data = self.backend.api.get_library_upload_album( album) else: data = self.backend.api.get_album(album) images = self.addThumbnails(bId, data) except Exception: logger.error( "YTMusic unable to get image url for uploaded %s", uri, ) else: images = self.IMAGES[bId] ret[uri] = images logger.debug("YTMusic found %d image urls for %s", len(images), uri) return ret
def search(self, query=None, uris=None, exact=False): results = [] logger.debug("YTMusic 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( 'YTMusic 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( 'YTMusic 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( 'YTMusic 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( 'YTMusic search failed for query "album"="%s"', " ".join(query["album"]), ) else: logger.debug( 'YTMusic skipping search, unsupported field types "%s"', " ".join(query.keys()), ) return None return results
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], "bitrate": 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["bitrate"] > bitrate): bitrate = stream["bitrate"] playstr = stream elif stream["mimeType"].startswith("audio"): crap[stream["bitrate"]] = stream else: worse[stream["bitrate"]] = 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.info( "YTMusic Found %s stream with %d bitrate for %s", playstr["audioQuality"], playstr["bitrate"], bId, ) if url is not None: if (self.backend.verify_track_url and requests.head(url).status_code == 403): # It's forbidden. Likely because the player url changed and we # decoded the signature incorrectly. # Refresh the player, log an error, and send back none. logger.error( "YTMusic found forbidden URL. Updating player URL now.") self.backend._youtube_player_refresh_timer.now() else: # Return the decoded youtube url to mopidy for playback. logger.debug("YTMusic found %s", url) return url 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"ytmusic: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"ytmusic: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"ytmusic: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: if result["browseId"] not in self.ALBUMS: date = result["year"] self.ALBUMS[result["browseId"]] = Album( uri=f"ytmusic:album:{result['browseId']}", name=result["title"], artists=[ Artist( uri="", name=result["artist"], sortname=result["artist"], musicbrainz_id="", ) ], num_tracks=None, num_discs=None, date=date, musicbrainz_id="", ) salbums.add(self.ALBUMS[result["browseId"]]) except Exception: logger.exception("YTMusic 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"ytmusic: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"ytmusic: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"ytmusic: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"ytmusic: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"ytmusic: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"ytmusic: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("YTMusic failed parsing artist %s", result["artist"]) tracks = list(tracks) for track in tracks: bId, _ = parse_uri(track.uri) self.TRACKS[bId] = track logger.debug( "YTMusic search returned %d results", len(tracks) + len(sartists) + len(salbums), ) return SearchResult( uri="ytmusic:search", tracks=tracks, artists=list(sartists), albums=list(salbums), )