def run(self, args, config): from ytmusicapi.ytmusic import YTMusic filepath = input( "Enter the path where you want to save auth.json [default=current dir]: " ) if not filepath: filepath = os.getcwd() path = Path(filepath + "/auth.json") print('Using "' + str(path) + '"') if path.exists(): print("File already exists!") return 1 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))) print("") print("Update your mopidy.conf to reflect the new auth file:") print(" [ytmusic]") print(" enabled=true") print(" auth_json=" + str(path)) return 0
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 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 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 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 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 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 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 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 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 lookup(self, uri): bId, upload = parse_uri(uri) if upload: if uri.startswith("ytmusic:album:"): try: res = self.backend.api.get_library_upload_album(bId) tracks = self.albumToTracks(res, bId) return tracks except Exception: logger.exception( 'YTMusic failed getting tracks for album "%s"', bId) elif uri.startswith("ytmusic:artist:"): try: res = self.backend.api.get_library_upload_artist(bId) tracks = self.artistToTracks(res) return tracks except Exception: logger.exception( 'YTMusic failed getting tracks for artist "%s"', bId) else: if uri.startswith("ytmusic:album:"): try: res = self.backend.api.get_album(bId) tracks = self.albumToTracks(res, bId) return tracks except Exception: logger.exception( 'YTMusic failed getting tracks for album "%s"', bId) elif uri.startswith("ytmusic:artist:"): try: res = self.backend.api.get_artist(bId) tracks = self.artistToTracks(res) return tracks except Exception: logger.exception( 'YTMusic failed getting tracks for artist "%s"', bId) elif uri.startswith("ytmusic:playlist:"): try: res = self.backend.api.get_playlist( bId, limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) return tracks except Exception: logger.exception( 'YTMusic failed getting tracks for playlist "%s"', bId) if (bId) in self.TRACKS: return [self.TRACKS[bId]] return []
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 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 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 _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 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 run(self, args, config): from ytmusicapi.ytmusic import YTMusic filepath = input("Enter the path where you want to save auth.json:") path = Path(filepath) if (path.exists()): print("File already exists!") return 1 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("auth.json saved to {}".format(str(path))) return 0
def get_distinct(self, field, query=None): ret = set() if field == "artist" or field == "albumartist": # try: # uploads = self.backend.api.get_library_upload_artists(limit=100) # except Exception: # logger.exception("YTMusic failed getting uploaded artists") # uploads = [] # pass try: library = self.backend.api.get_library_artists( limit=self.backend.playlist_item_limit) except Exception: logger.exception("YTMusic failed getting artists from library") library = [] pass # for a in uploads: # ret.add(a["artist"]) for a in library: ret.add(a["artist"]) # elif field == "album": # try: # uploads = self.backend.api.get_library_upload_albums(limit=self.backend.playlist_item_limit) # except Exception: # logger.exception("YTMusic failed getting uploaded albums") # uploads = [] # pass # try: # library = self.backend.api.get_library_albums(limit=self.backend.playlist_item_limit) # except Exception: # logger.exception("YTMusic failed getting albums from library") # library = [] # pass # for a in uploads: # ret.add(a["title"]) # for a in library: # ret.add(a["title"]) return ret
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 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_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 parseSearch(results, field=None, queries=[]): tracks = 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 try: length = [int(i) for i in result["duration"].split(":")] except ValueError: length = [0, 0] track = Track( uri=f"ytm:video?id={result['videoId']}", name=result["title"], artists=[ Artist( uri=f"ytm:artist?id={a['id']}&upload=false", name=a["name"], sortname=a["name"], musicbrainz_id="", ) for a in result["artists"] ], album=Album( uri=f"ytm:album?id={result['album']['id']}&upload=false", name=result["album"]["name"], artists=[ Artist( uri=f"ytm:artist?id={a['id']}&upload=false", name=a["name"], sortname=a["name"], musicbrainz_id="", ) for a in result["artists"] ], num_tracks=None, num_discs=None, date="1999", musicbrainz_id="", ) if "album" in result else None, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="1999", length=(length[0] * 60 * 1000) + (length[1] * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(track) elif result["resultType"] == "album": if field == "album" and not any( q.casefold() == result["title"].casefold() for q in queries): continue try: album = API.get_album(result["browseId"]) artists = [ Artist( uri="", name=result["artist"], sortname=result["artist"], musicbrainz_id="", ) ] albumObj = Album( uri=f"ytm:album?id={result['browseId']}&upload=false", name=album["title"], artists=artists, num_tracks=int(album["trackCount"]) if str(album["trackCount"]).isnumeric() else None, num_discs=None, date= f"{album['releaseDate']['year']}-{album['releaseDate']['month']}-{album['releaseDate']['day']}", musicbrainz_id="", ) if "tracks" in album: for song in album["tracks"]: track = Track( uri=f"ytm:video?id={song['videoId']}", name=song["title"], artists=artists, album=albumObj, composers=[], performers=[], genre="", track_no=int(song["index"]) if str(song["index"]).isnumeric() else None, disc_no=None, date=albumObj.date, length=int(song["lengthMs"]) if str(song["lengthMs"]).isnumeric() else None, bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(track) 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: artist = API.get_artist(result["browseId"]) artists = [ Artist( uri=f"ytm:artist?id={result['browseId']}&upload=false", name=artist["name"], sortname=artist["name"], musicbrainz_id="", ) ] albums = API.get_artist_albums(result["browseId"], artist["albums"]["params"]) for album in albums: if field == "album" and not any( q.casefold() == album["title"].casefold() for q in queries): continue songs = API.get_album(album["browseId"]) albumObj = Album( uri=f"ytm:album?id={album['browseId']}&upload=false", name=album["title"], artists=artists, num_tracks=int(songs["trackCount"]) if str(songs["trackCount"]).isnumeric() else None, num_discs=None, date= f"{songs['releaseDate']['year']}-{songs['releaseDate']['month']}-{songs['releaseDate']['day']}", musicbrainz_id="", ) if "tracks" in songs: for song in songs["tracks"]: track = Track( uri=f"ytm:video?id={song['videoId']}", name=song["title"], artists=artists, album=albumObj, composers=[], performers=[], genre="", track_no=int(song["index"]) if str(song["index"]).isnumeric() else None, disc_no=None, date=albumObj.date, length=int(song["lengthMs"]) if str(song["lengthMs"]).isnumeric() else None, bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(track) except Exception: logger.exception("YTMusic failed parsing artist %s", result["artist"]) tracks = list(tracks) for track in tracks: id_, upload = parse_uri(track.uri) TRACKS[id_] = track return tracks
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 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), )