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("YoutubeMusic 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(' [youtubemusic]') print(' enabled=true') print(' auth_json=' + str(path)) return 0
def delete(self, uri): logger.debug("YoutubeMusic deleting playlist \"%s\"", uri) bId = parse_uri(uri) try: self.backend.api.delete_playlist(bId) return True except Exception: logger.exception("YoutubeMusic failed to delete playlist") return False
def get_items(self, uri): bId = parse_uri(uri) logger.debug("YoutubeMusic getting playlist items for \"%s\"", bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic failed getting playlist items") pls = None if pls: tracks = self.backend.library.playlistToTracks(pls) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] return None
def as_list(self): logger.debug("YoutubeMusic getting user playlists") refs = [] try: playlists = self.backend.api.get_library_playlists(limit=100) except Exception: logger.exception("YoutubeMusic failed getting a list of playlists") playlists = [] for pls in playlists: refs.append(Ref.playlist( uri=f"youtubemusic:playlist:{pls['playlistId']}", name=pls["title"], )) return refs
def save(self, playlist): bId = parse_uri(playlist.uri) logger.debug("YoutubeMusic saving playlist \"%s\" \"%s\"", playlist.name, bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic saving playlist failed") return None oldIds = set([t["videoId"] for t in pls["tracks"]]) newIds = set([parse_uri(p.uri)[0] for p in playlist.tracks]) common = oldIds & newIds remove = oldIds ^ common add = newIds ^ common if len(remove): logger.debug("YoutubeMusic removing items \"%s\" from playlist", remove) try: videos = [t for t in pls["tracks"] if t["videoId"] in remove] self.backend.api.remove_playlist_items(bId, videos) except Exception: logger.exception("YoutubeMusic failed removing items from playlist") if len(add): logger.debug("YoutubeMusic adding items \"%s\" to playlist", add) try: self.backend.api.add_playlist_items(bId, list(add)) except Exception: logger.exception("YoutubeMusic failed adding items to playlist") if pls["title"] != playlist.name: logger.debug("Renaming playlist to \"%s\"", playlist.name) try: self.backend.api.edit_playlist(bId, title=playlist.name) except Exception: logger.exception("YoutubeMusic failed renaming playlist") return playlist
def lookup(self, uri): bId = parse_uri(uri) logger.debug("YoutubeMusic looking up playlist \"%s\"", bId) try: pls = self.backend.api.get_playlist(bId, limit=self.backend.playlist_item_limit) except Exception: logger.exception("YoutubeMusic playlist lookup failed") pls = None if pls: tracks = self.backend.library.playlistToTracks(pls) return Playlist( uri=f"youtubemusic:playlist:{pls['id']}", name=pls["title"], tracks=tracks, last_modified=None, )
def search(self, query=None, uris=None, exact=False): results = [] logger.debug("YoutubeMusic searching for %s", query) if "any" in query: try: res = self.backend.api.search(" ".join(query["any"]), filter=None) results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"any\"=\"%s\"", " ".join(query["any"])) elif "track_name" in query: try: res = self.backend.api.search(" ".join(query["track_name"]), filter="songs") if exact: results = self.parseSearch(res, "track", query["track_name"]) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"title\"=\"%s\"", " ".join(query["track_name"])) elif "albumartist" in query or "artist" in query: q1 = ("albumartist" in query and query["albumartist"]) or [] q2 = ("artist" in query and query["artist"]) or [] try: res = self.backend.api.search(" ".join(q1 + q2), filter="artists") if exact: results = self.parseSearch(res, "artist", q1 + q2) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"artist\"=\"%s\"", " ".join(q1 + q2)) elif "album" in query: try: res = self.backend.api.search(" ".join(query["album"]), filter="albums") if exact: results = self.parseSearch(res, "album", query["album"]) else: results = self.parseSearch(res) except Exception: logger.exception( "YoutubeMusic search failed for query \"album\"=\"%s\"", " ".join(query["album"])) else: logger.debug( "YoutubeMusic skipping search, unsupported field types \"%s\"", " ".join(query.keys())) return None return results
def create(self, name): logger.debug("YoutubeMusic creating playlist \"%s\"", name) try: bId = self.backend.api.create_playlist(name, "") except Exception: logger.exception("YoutubeMusic playlist creation failed") bId = None if bId: uri = f"youtubemusic:playlist:{bId}" logger.debug("YoutubeMusic created playlist \"%s\"", uri) return Playlist( uri=uri, name=name, tracks=[], last_modified=None, ) return None
def lookup(self, uri): bId, _ = parse_uri(uri) if (uri.startswith("youtubemusic:album:")): try: res = self.backend.api.get_album(bId) tracks = self.albumToTracks(res, bId) return (tracks) except Exception: logger.exception( "YoutubeMusic failed getting tracks for album \"%s\"", bId) elif (uri.startswith("youtubemusic:artist:")): try: res = self.backend.api.get_artist(bId) tracks = self.artistToTracks(res) return (tracks) except Exception: logger.exception( "YoutubeMusic failed getting tracks for artist \"%s\"", bId) elif (uri.startswith("youtubemusic: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( "YoutubeMusic failed getting tracks for playlist \"%s\"", bId) elif (bId) in self.TRACKS: return [self.TRACKS[bId]] return []
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("YoutubeMusic failed getting uploaded artists") # uploads = [] # pass try: library = self.backend.api.get_library_artists( limit=self.backend.playlist_item_limit) except Exception: logger.exception( "YoutubeMusic 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("YoutubeMusic failed getting uploaded albums") # uploads = [] # pass # try: # library = self.backend.api.get_library_albums(limit=self.backend.playlist_item_limit) # except Exception: # logger.exception("YoutubeMusic 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 _get_auto_playlists(self): try: logger.debug('YoutubeMusic loading auto playlists') response = self.api._send_request('browse', {}) tab = nav(response, SINGLE_COLUMN_TAB) browse = parse_auto_playlists(nav(tab, SECTION_LIST)) if 'continuations' in tab['sectionListRenderer']: request_func = lambda additionalParams: self.api._send_request( 'browse', {}, additionalParams) parse_func = lambda contents: parse_auto_playlists(contents) browse.extend( get_continuations(tab['sectionListRenderer'], 'sectionListContinuation', 100, request_func, parse_func)) # Delete empty sections for i in range(len(browse) - 1, 0, -1): if len(browse[i]['items']) == 0: browse.pop(i) logger.debug('YoutubeMusic loaded %d auto playlists sections', len(browse)) self.library.ytbrowse = browse except Exception: logger.exception('YoutubeMusic failed to load auto playlists') return (None)
def parseSearch(self, results, field=None, queries=[]): tracks = set() salbums = set() sartists = set() for result in results: if result["resultType"] == "song": if field == "track" and not any( q.casefold() == result["title"].casefold() for q in queries): continue if result['videoId'] in self.TRACKS: tracks.add(self.TRACKS[result['videoId']]) else: try: length = [ int(i) for i in result["duration"].split(":") ] except ValueError: length = [0, 0] if result['videoId'] is None: continue if result['videoId'] not in self.TRACKS: artists = [] for a in result['artists']: if a['id'] not in self.ARTISTS: self.ARTISTS[a['id']] = Artist( uri=f"youtubemusic:artist:{a['id']}", name=a["name"], sortname=a["name"], musicbrainz_id="", ) artists.append(self.ARTISTS[a['id']]) album = None if 'album' in result: if result['album']['id'] not in self.ALBUMS: self.ALBUMS[result['album']['id']] = Album( uri= f"youtubemusic:album:{result['album']['id']}", name=result["album"]["name"], artists=artists, num_tracks=None, num_discs=None, date="0000", musicbrainz_id="", ) album = self.ALBUMS[result['album']['id']] self.TRACKS[result['videoId']] = Track( uri=f"youtubemusic:track:{result['videoId']}", name=result["title"], artists=artists, album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="0000", length=(length[0] * 60 * 1000) + (length[1] * 1000), bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(self.TRACKS[result['videoId']]) elif result["resultType"] == "album": if field == "album" and not any( q.casefold() == result["title"].casefold() for q in queries): continue try: album = self.backend.api.get_album(result["browseId"]) if result["browseId"] not in self.ALBUMS: date = result['year'] self.ALBUMS[result['browseId']] = Album( uri=f"youtubemusic:album:{result['browseId']}", name=album["title"], artists=[ Artist( uri="", name=result["artist"], sortname=result["artist"], musicbrainz_id="", ) ], num_tracks=int(album["trackCount"]) if str(album["trackCount"]).isnumeric() else None, num_discs=None, date=date, musicbrainz_id="", ) salbums.add(self.ALBUMS[result['browseId']]) except Exception: logger.exception("YoutubeMusic failed parsing album %s", result["title"]) elif result["resultType"] == "artist": if field == "artist" and not any( q.casefold() == result["artist"].casefold() for q in queries): continue try: artistq = self.backend.api.get_artist(result["browseId"]) if result['browseId'] not in self.ARTISTS: self.ARTISTS[result['browseId']] = Artist( uri=f"youtubemusic:artist:{result['browseId']}", name=artistq["name"], sortname=artistq["name"], musicbrainz_id="", ) sartists.add(self.ARTISTS[result['browseId']]) if 'albums' in artistq: if 'params' in artistq['albums']: albums = self.backend.api.get_artist_albums( artistq["channelId"], artistq["albums"]["params"]) for album in albums: if album['browseId'] not in self.ALBUMS: self.ALBUMS[album['browseId']] = Album( uri= f"youtubemusic:album:{album['browseId']}", name=album["title"], artists=[ self.ARTISTS[result['browseId']] ], date=album['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[album['browseId']]) elif 'results' in artistq['albums']: for album in artistq["albums"]["results"]: if album['browseId'] not in self.ALBUMS: self.ALBUMS[album['browseId']] = Album( uri= f"youtubemusic:album:{album['browseId']}", name=album["title"], artists=[ self.ARTISTS[result['browseId']] ], date=album['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[album['browseId']]) if 'singles' in artistq and 'results' in artistq['singles']: for single in artistq['singles']['results']: if single['browseId'] not in self.ALBUMS: self.ALBUMS[single['browseId']] = Album( uri= f"youtubemusic:album:{single['browseId']}", name=single['title'], artists=[self.ARTISTS[result['browseId']]], date=single['year'], musicbrainz_id="", ) salbums.add(self.ALBUMS[single['browseId']]) if 'songs' in artistq: if 'results' in artistq['songs']: for song in artistq['songs']['results']: if song['videoId'] in self.TRACKS: tracks.add(self.TRACKS[song['videoId']]) else: album = None if 'album' in song: if song['album'][ 'id'] not in self.ALBUMS: self.ALBUMS[ song['album']['id']] = Album( uri= f"youtubemusic:album:{song['album']['id']}", name=song['album']['name'], artists=[ self.ARTISTS[ result['browseId']] ], date='1999', musicbrainz_id="", ) album = self.ALBUMS[song['album'] ['id']] if song['videoId'] not in self.TRACKS: self.TRACKS[song['videoId']] = Track( uri= f"youtubemusic:track:{song['videoId']}", name=song['title'], artists=[ self.ARTISTS[ result['browseId']] ], album=album, composers=[], performers=[], genre="", track_no=None, disc_no=None, date="0000", length=None, bitrate=0, comment="", musicbrainz_id="", last_modified=None, ) tracks.add(self.TRACKS[song['videoId']]) except Exception: logger.exception("YoutubeMusic failed parsing artist %s", result["artist"]) tracks = list(tracks) for track in tracks: bId, _ = parse_uri(track.uri) self.TRACKS[bId] = track logger.debug("YoutubeMusic search returned %d results", len(tracks) + len(sartists) + len(salbums)) return SearchResult( uri="youtubemusic:search", tracks=tracks, artists=list(sartists), albums=list(salbums), )
def browse(self, uri): if not uri: return [] logger.debug("YoutubeMusic browsing uri \"%s\"", uri) if uri == "youtubemusic:root": dirs = [] if self.backend.auth: dirs += [ Ref.directory(uri="youtubemusic:artist", name="Artists"), Ref.directory(uri="youtubemusic:album", name="Albums"), ] if self.backend.liked_songs: dirs.append( Ref.directory(uri="youtubemusic:liked", name="Liked Songs")) if self.backend.history: dirs.append( Ref.directory(uri="youtubemusic:history", name="Recently Played")) if self.backend.subscribed_artist_limit: dirs.append( Ref.directory(uri="youtubemusic:subscriptions", name="Subscriptions")) dirs.append( Ref.directory(uri="youtubemusic:watch", name="Similar to last played")) if self.backend.mood_genre: dirs.append( Ref.directory(uri="youtubemusic:mood", name="Mood and Genre Playlists")) if self.backend._auto_playlist_refresh_rate: dirs.append( Ref.directory(uri="youtubemusic:auto", name="Auto Playlists")) return (dirs) elif uri == "youtubemusic:subscriptions" and self.backend.subscribed_artist_limit: try: subs = self.backend.api.get_library_subscriptions( limit=self.backend.subscribed_artist_limit) logger.debug("YoutubeMusic found %d artists in subscriptions", len(subs)) return [ Ref.artist(uri=f"youtubemusic:artist:{a['browseId']}", name=a["artist"]) for a in subs ] except Exception: logger.exception( "YoutubeMusic failed getting artists from subscriptions") elif uri == "youtubemusic:artist": try: library_artists = [ Ref.artist(uri=f"youtubemusic:artist:{a['browseId']}", name=a["artist"]) for a in self.backend.api.get_library_artists(limit=100) ] logger.debug("YoutubeMusic found %d artists in library", len(library_artists)) except Exception: logger.exception( "YoutubeMusic failed getting artists from library") library_artists = [] if self.backend.auth: try: upload_artists = [ Ref.artist( uri=f"youtubemusic:artist:{a['browseId']}:upload", name=a["artist"]) for a in self.backend.api.get_library_upload_artists( limit=100) ] logger.debug("YoutubeMusic found %d uploaded artists", len(upload_artists)) except Exception: logger.exception( "YoutubeMusic failed getting uploaded artists") upload_artists = [] else: upload_artists = [] return library_artists + upload_artists elif uri == "youtubemusic:album": try: library_albums = [ Ref.album(uri=f"youtubemusic:album:{a['browseId']}", name=a["title"]) for a in self.backend.api.get_library_albums(limit=100) ] logger.debug("YoutubeMusic found %d albums in library", len(library_albums)) except Exception: logger.exception( "YoutubeMusic failed getting albums from library") library_albums = [] if self.backend.auth: try: upload_albums = [ Ref.album( uri=f"youtubemusic:album:{a['browseId']}:upload", name=a["title"]) for a in self.backend.api.get_library_upload_albums( limit=100) ] logger.debug("YoutubeMusic found %d uploaded albums", len(upload_albums)) except Exception: logger.exception( "YoutubeMusic failed getting uploaded albums") upload_albums = [] else: upload_albums = [] return library_albums + upload_albums elif uri == "youtubemusic:liked": try: res = self.backend.api.get_liked_songs( limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) logger.debug("YoutubeMusic found %d liked songs", len(res["tracks"])) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception("YoutubeMusic failed getting liked songs") elif uri == "youtubemusic:history": try: res = self.backend.api.get_history() tracks = self.playlistToTracks({'tracks': res}) logger.debug("YoutubeMusic found %d songs from recent history", len(res)) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting listening history") elif uri == "youtubemusic:watch": try: playback = self.backend.playback if playback.last_id is not None: track_id = playback.last_id elif self.backend.auth: hist = self.backend.api.get_history() track_id = hist[0]['videoId'] if track_id: res = self.backend.api.get_watch_playlist( track_id, limit=self.backend.playlist_item_limit) if 'tracks' in res: logger.debug( "YoutubeMusic found %d watch songs for \"%s\"", len(res["tracks"]), track_id) res['tracks'].pop(0) tracks = self.playlistToTracks(res) return [ Ref.track(uri=t.uri, name=t.name) for t in tracks ] except Exception: logger.exception("YoutubeMusic failed getting watch songs") elif uri == "youtubemusic:mood": try: logger.debug('YoutubeMusic loading mood/genre playlists') moods = {} response = self.backend.api._send_request( 'browse', {"browseId": "FEmusic_moods_and_genres"}) for sect in nav(response, SINGLE_COLUMN_TAB + SECTION_LIST): for cat in nav(sect, ['gridRenderer', 'items']): title = nav(cat, [ 'musicNavigationButtonRenderer', 'buttonText', 'runs', 0, 'text' ]).strip() endpnt = nav(cat, [ 'musicNavigationButtonRenderer', 'clickCommand', 'browseEndpoint', 'browseId' ]) params = nav(cat, [ 'musicNavigationButtonRenderer', 'clickCommand', 'browseEndpoint', 'params' ]) moods[title] = { 'name': title, 'uri': 'youtubemusic:mood:' + params + ':' + endpnt } return [ Ref.directory(uri=moods[a]['uri'], name=moods[a]['name']) for a in sorted(moods.keys()) ] except Exception: logger.exception( 'YoutubeMusic failed to load mood/genre playlists') elif uri.startswith("youtubemusic:mood:"): try: ret = [] _, _, params, endpnt = uri.split(':') response = self.backend.api._send_request( 'browse', { "browseId": endpnt, "params": params }) for sect in nav(response, SINGLE_COLUMN_TAB + SECTION_LIST): key = [] if 'gridRenderer' in sect: key = ['gridRenderer', 'items'] elif 'musicCarouselShelfRenderer' in sect: key = ['musicCarouselShelfRenderer', 'contents'] elif 'musicImmersiveCarouselShelfRenderer' in sect: key = [ 'musicImmersiveCarouselShelfRenderer', 'contents' ] if len(key): for item in nav(sect, key): title = nav(item, ['musicTwoRowItemRenderer'] + TITLE_TEXT).strip() # if 'subtitle' in item['musicTwoRowItemRenderer']: # title += ' (' # for st in item['musicTwoRowItemRenderer']['subtitle']['runs']: # title += st['text'] # title += ')' brId = nav(item, ['musicTwoRowItemRenderer'] + NAVIGATION_BROWSE_ID) ret.append( Ref.playlist( uri=f"youtubemusic:playlist:{brId}", name=title)) return (ret) except Exception: logger.exception( 'YoutubeMusic failed getting mood/genre playlist "%s"', uri) elif uri == "youtubemusic:auto" and self.backend._auto_playlist_refresh_rate: try: return [ Ref.directory(uri=a['uri'], name=a['name']) for a in self.ytbrowse ] except Exception: logger.exception('YoutubeMusic failed getting auto playlists') elif uri.startswith("youtubemusic:auto:" ) and self.backend._auto_playlist_refresh_rate: try: for a in self.ytbrowse: if a['uri'] == uri: ret = [] for i in a['items']: if i['type'] == 'playlist': ret.append( Ref.playlist(uri=i['uri'], name=i['name'])) logger.debug("playlist: %s - %s", i['name'], i['uri']) elif i['type'] == 'artist': ret.append( Ref.artist(uri=i['uri'], name=i['name'])) logger.debug("artist: %s - %s", i['name'], i['uri']) elif i['type'] == 'album': ret.append( Ref.album(uri=i['uri'], name=i['name'])) logger.debug("album: %s - %s", i['name'], i['uri']) return (ret) except Exception: logger.exception( 'YoutubeMusic failed getting auto playlist "%s"', uri) elif uri.startswith("youtubemusic:artist:"): bId, upload = parse_uri(uri) if upload: try: res = self.backend.api.get_library_upload_artist(bId) tracks = self.uploadArtistToTracks(res) logger.debug( "YoutubeMusic found %d songs for uploaded artist \"%s\"", len(res), res[0]["artist"]["name"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for uploaded artist \"%s\"", bId) else: try: res = self.backend.api.get_artist(bId) tracks = self.artistToTracks(res) logger.debug( "YoutubeMusic found %d songs for artist \"%s\" in library", len(res["songs"]), res["name"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for artist \"%s\"", bId) elif uri.startswith("youtubemusic:album:"): bId, upload = parse_uri(uri) if upload: try: res = self.backend.api.get_library_upload_album(bId) tracks = self.uploadAlbumToTracks(res, bId) logger.debug( "YoutubeMusic found %d songs for uploaded album \"%s\"", len(res["tracks"]), res["title"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for uploaded album \"%s\"", bId) else: try: res = self.backend.api.get_album(bId) tracks = self.albumToTracks(res, bId) logger.debug( "YoutubeMusic found %d songs for album \"%s\" in library", len(res["tracks"]), res["title"]) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed getting tracks for album \"%s\"", bId) elif uri.startswith("youtubemusic:playlist:"): bId, upload = parse_uri(uri) try: res = self.backend.api.get_playlist( bId, limit=self.backend.playlist_item_limit) tracks = self.playlistToTracks(res) return [Ref.track(uri=t.uri, name=t.name) for t in tracks] except Exception: logger.exception( "YoutubeMusic failed to get tracks from playlist '%s'", bId) return []