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 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 lookup(self, uri): id_, upload = parse_uri(uri) logger.info("YTMusic looking up playlist \"%s\"", id_) try: pls = API.get_playlist(id_, limit=100) except Exception: logger.exception("YTMusic playlist lookup failed") pls = None if pls: tracks = [] if "tracks" in pls: for track in pls["tracks"]: duration = track["duration"].split(":") artists = [Artist( uri=f"ytm:artist?id={a['id']}&upload=false", name=a["name"], sortname=a["name"], musicbrainz_id="", ) for a in track["artists"]] if track["album"]: album = Album( uri=f"ytm:album?id={track['album']['id']}&upload=false", name=track["album"]["name"], artists=artists, num_tracks=None, num_discs=None, date="1999", musicbrainz_id="", ) else: album = None tracks.append(Track( uri=f"ytm:video?id={track['videoId']}", name=track["title"], artists=artists, album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="1999", length=(int(duration[0]) * 60 * 1000) + (int(duration[1]) * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, )) for track in tracks: tid, tupload = parse_uri(track.uri) TRACKS[tid] = track return Playlist( uri=f"ytm:playlist?id={pls['id']}", name=pls["title"], tracks=tracks, last_modified=None, )
def delete(self, uri): logger.info("YTMusic deleting playlist \"%s\"", uri) id_, upload = parse_uri(uri) try: API.delete_playlist(id_) return True except Exception: logger.exception("YTMusic failed to delete playlist") return False
def translate_uri(self, uri): logger.info('YTMusic PlaybackProvider.translate_uri "%s"', uri) if "ytm:video?" not in uri: return None try: id_ = parse_qs(urlparse(uri).query)["id"][0] return get_video(id_) except Exception as e: logger.error('translate_uri error "%s"', e) return None
def as_list(self): logger.info("YTMusic getting user playlists") refs = [] try: playlists = 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"ytm:playlist?id={pls['playlistId']}", name=pls["title"], )) return refs
def get_items(self, uri): id_, upload = parse_uri(uri) logger.info("YTMusic getting playlist items for \"%s\"", id_) try: pls = API.get_playlist(id_, limit=100) except Exception: logger.exception("YTMusic failed getting playlist items") pls = None if pls: refs = [] if "tracks" in pls: for track in pls["tracks"]: refs.append(Ref.track(uri=f"ytm:video?id={track['videoId']}", name=track["title"])) duration = track["duration"].split(":") artists = [Artist( uri=f"ytm:artist?id={a['id']}&upload=false", name=a["name"], sortname=a["name"], musicbrainz_id="", ) for a in track["artists"]] if track["album"]: album = Album( uri=f"ytm:album?id={track['album']['id']}&upload=false", name=track["album"]["name"], artists=artists, num_tracks=None, num_discs=None, date="1999", musicbrainz_id="", ) else: album = None TRACKS[track["videoId"]] = Track( uri=f"ytm:video?id={track['videoId']}", name=track["title"], artists=artists, album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="1999", length=(int(duration[0]) * 60 * 1000) + (int(duration[1]) * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) return refs 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.info("YTMusic updated player URL to %s", url) return url else: logger.error("YTMusic unable to extract player URL.") return None
def create(self, name): logger.info("YTMusic creating playlist \"%s\"", name) try: id_ = API.create_playlist(name, "") except Exception: logger.exception("YTMusic playlist creation failed") id_ = None if id_: uri = f"ytm:playlist?id={id_}" logger.info("YTMusic created playlist \"%s\"", uri) return Playlist( uri=uri, name=name, tracks=[], last_modified=None, ) return None
def search(self, query=None, uris=None, exact=False): logger.info("YTMusic searching for %s", query) tracks = [] if "any" in query: try: res = API.search(" ".join(query["any"]), filter=None) tracks.extend(parseSearch(res)) if (exact): for track in tracks: for q in query["any"]: q = q.casefold() if q != track.name.casefold(): tracks.remove(track) if q == track.album.name.casefold(): tracks.remove(track) for artist in track.artists: if q == artist.name.casefold(): tracks.remove(track) except Exception: logger.exception("YTMusic search failed for query \"%s\"", " ".join(query["any"])) elif "track_name" in query: try: res = API.search(" ".join(query["track_name"]), filter="songs") if exact: tracks.extend(parseSearch(res, "track", query["track_name"])) else: tracks.extend(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 = API.search(" ".join(q1 + q2), filter="artists") if exact: tracks.extend(parseSearch(res, "artist", q1 + q2)) else: tracks.extend(parseSearch(res)) except Exception: logger.exception("YTMusic search failed for query \"artist\"=\"%s\"", " ".join(q1 + q2)) elif "album" in query: try: res = API.search(" ".join(query["album"]), filter="albums") if exact: tracks.extend(parseSearch(res, "album", query["album"])) else: tracks.extend(parseSearch(res)) except Exception: logger.exception("YTMusic search failed for query \"album\"=\"%s\"", " ".join(query["album"])) else: logger.info("YTMusic skipping search, unsupported field types \"%s\"", " ".join(query.keys())) return None logger.info("YTMusic search returned %d results", len(tracks)) return SearchResult( uri="", tracks=tracks, artists=None, albums=None, )
def as_list(self): logger.info("YTMusic getting user playlists") refs = [] # playlist with songs similar to the last played refs.append( Ref.playlist( uri=f"ytm:playlist?id=watch", name="Similar to last played", )) # system playlists try: playlists = 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"ytm:playlist?id={pls['playlistId']}", name=pls["title"], )) return refs
def _refresh_auto_playlists(self): t0 = time.time() self._get_auto_playlists() t = time.time() - t0 logger.info("YTMusic Auto Playlists refreshed in %.2fs", t)
def get_items(self, uri): id_, upload = parse_uri(uri) tracks = None try: if id_ == 'watch': playback = self.backend.playback if playback.last_id is not None: track_id = playback.last_id logger.info( "YTMusic getting watch playlist items for \"%s\"", track_id) tracks = API.get_watch_playlist(track_id, limit=100) else: logger.info("YTMusic getting playlist items for \"%s\"", id_) pls = API.get_playlist(id_, limit=100) if "tracks" in pls: tracks = pls["tracks"] except Exception: logger.exception("YTMusic failed getting playlist items") if tracks: refs = [] for track in tracks: refs.append( Ref.track(uri=f"ytm:video?id={track['videoId']}", name=track["title"])) duration = (track['duration'] if 'duration' in track else track['length']).split(":") if 'artists' in track: artists = [ Artist( uri=f"ytm:artist?id={a['id']}&upload=false", name=a["name"], sortname=a["name"], musicbrainz_id="", ) for a in track["artists"] ] elif 'byline' in track: artists = [ Artist( name=track["byline"], sortname=track["byline"], musicbrainz_id="", ) ] else: artists = None if 'album' in track: album = Album( uri=f"ytm:album?id={track['album']['id']}&upload=false", name=track["album"]["name"], artists=artists, num_tracks=None, num_discs=None, date="1999", musicbrainz_id="", ) else: album = None TRACKS[track["videoId"]] = Track( uri=f"ytm:video?id={track['videoId']}", name=track["title"], artists=artists, album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="1999", length=(int(duration[0]) * 60 * 1000) + (int(duration[1]) * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) return refs return None
def browse(self, uri): logger.info("YTMusic browsing uri \"%s\"", uri) if uri == "ytm:root": return [ Ref.directory(uri="ytm:artist", name="Artists"), Ref.directory(uri="ytm:album", name="Albums"), Ref.directory(uri="ytm:liked", name="Liked Songs"), ] elif uri == "ytm:artist": try: library_artists = [ Ref.artist( uri=f"ytm:artist?id={a['browseId']}&upload=false", name=a["artist"]) for a in API.get_library_artists(limit=100) ] logger.info("YTMusic found %d artists in library", len(library_artists)) except Exception: logger.exception("YTMusic failed getting artists from library") library_artists = [] # try: # upload_artists = [ # Ref.artist(uri=f"ytm:artist?id={a['browseId']}&upload=true", name=a["artist"]) # for a in API.get_library_upload_artists(limit=100) # ] # logger.info("YTMusic found %d uploaded artists", len(upload_artists)) # except Exception: # logger.exception("YTMusic failed getting uploaded artists") # upload_artists = [] return library_artists # + upload_artists elif uri == "ytm:album": try: library_albums = [ Ref.album(uri=f"ytm:album?id={a['browseId']}&upload=false", name=a["title"]) for a in API.get_library_albums(limit=100) ] logger.info("YTMusic found %d albums in library", len(library_albums)) except Exception: logger.exception("YTMusic failed getting albums from library") library_albums = [] # try: # upload_albums = [ # Ref.album(uri=f"ytm:album?id={a['browseId']}&upload=true", name=a["title"]) # for a in API.get_library_upload_albums(limit=100) # ] # logger.info("YTMusic found %d uploaded albums", len(upload_albums)) # except Exception: # logger.exception("YTMusic failed getting uploaded albums") # upload_albums = [] return library_albums # + upload_albums elif uri == "ytm:liked": try: res = API.get_liked_songs(limit=100) logger.info("YTMusic found %d liked songs", len(res["tracks"])) playlistToTracks(res) return [ Ref.track(uri=f"ytm:video?id={t['videoId']}", name=t["title"]) for t in ("tracks" in res and res["tracks"]) or [] ] except Exception: logger.exception("YTMusic failed getting liked songs") elif uri.startswith("ytm:artist?"): id_, upload = parse_uri(uri) # if upload: # try: # res = API.get_library_upload_artist(id_) # uploadArtistToTracks(res) # return [ # Ref.track(uri=f"ytm:album?id={t['videoId']}", name=t["title"]) # for t in res # ] # logger.info("YTMusic found %d songs for uploaded artist \"%s\"", len(res), res[0]["artist"]["name"]) # except Exception: # logger.exception("YTMusic failed getting tracks for uploaded artist \"%s\"", id_) # else: try: res = API.get_artist(id_) logger.info( "YTMusic found %d songs for artist \"%s\" in library", len(res["songs"]), res["name"]) artistToTracks(res) return [ Ref.track(uri=f"ytm:video?id={t['videoId']}", name=t["title"]) for t in ("songs" in res and "results" in res["songs"] and res["songs"]["results"]) or [] ] except Exception: logger.exception( "YTMusic failed getting tracks for artist \"%s\"", id_) elif uri.startswith("ytm:album?"): id_, upload = parse_uri(uri) # if upload: # try: # res = API.get_library_upload_album(id_) # uploadAlbumToTracks(res, id_) # return [ # Ref.track(uri=f"ytm:video?id={t['videoId']}", name=t["title"]) # for t in ("tracks" in res and res["tracks"]) or [] # ] # logger.info("YTMusic found %d songs for uploaded album \"%s\"", len(res["tracks"]), res["title"]) # except Exception: # logger.exception("YTMusic failed getting tracks for uploaded album \"%s\"", id_) # else: try: res = API.get_album(id_) logger.info( "YTMusic found %d songs for album \"%s\" in library", len(res["tracks"]), res["title"]) albumToTracks(res, id_) return [ Ref.track(uri=f"ytm:video?id={t['videoId']}", name=t["title"]) for t in ("tracks" in res and res["tracks"]) or [] ] except Exception: logger.exception( "YTMusic failed getting tracks for album \"%s\"", id_) return []
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