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 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 _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 run(self, args, config): from ytmusicapi.ytmusic import YTMusic path = config["ytmusic"]["auth_json"] if not path: logger.error("auth_json path not defined in config") return 1 print('Updating credentials in "' + str(path) + '"') print( "Open Youtube Music, open developer tools (F12), go to Network tab," ) print( 'right click on a POST request and choose "Copy request headers".') print("Then paste (CTRL+SHIFT+V) them here and press CTRL+D.") try: print(YTMusic.setup(filepath=str(path))) except Exception: logger.exception("YTMusic setup failed") return 1 print("Authentication JSON data saved to {}".format(str(path))) return 0
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 _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