예제 #1
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
예제 #2
0
def parse_library_artists(response, request_func, limit):
    results = find_object_by_key(nav(response, SINGLE_COLUMN_TAB + SECTION_LIST),
                                 'itemSectionRenderer')
    results = nav(results, ITEM_SECTION)
    if 'musicShelfRenderer' not in results:
        return []
    results = results['musicShelfRenderer']
    artists = parse_artists(results['contents'])

    if 'continuations' in results:
        parse_func = lambda contents: parse_artists(contents)
        artists.extend(
            get_continuations(results, 'musicShelfContinuation', limit - len(artists),
                              request_func, parse_func))

    return artists
예제 #3
0
def parse_library_albums(response, request_func, limit):
    results = find_object_by_key(nav(response, SINGLE_COLUMN_TAB + SECTION_LIST),
                                 'itemSectionRenderer')
    results = nav(results, ITEM_SECTION)
    if 'gridRenderer' not in results:
        return []
    results = nav(results, GRID)
    albums = parse_albums(results['items'])

    if 'continuations' in results:
        parse_func = lambda contents: parse_albums(contents)
        albums.extend(
            get_continuations(results, 'gridContinuation', limit - len(albums), request_func,
                              parse_func))

    return albums
예제 #4
0
    def get_home(self, limit=3) -> List[Dict]:
        """
        Get the home page.
        The home page is structured as titled rows, returning 3 rows of music suggestions at a time.
        Content varies and may contain artist, album, song or playlist suggestions, sometimes mixed within the same row

        :param limit: Number of rows to return
        :return: List of dictionaries keyed with 'title' text and 'contents' list

        Example list::

            [
                {
                    "title": "Your morning music",
                    "contents": [
                        { //album result
                            "title": "Sentiment",
                            "year": "Said The Sky",
                            "browseId": "MPREb_QtqXtd2xZMR",
                            "thumbnails": [...]
                        },
                        { //playlist result
                            "title": "r/EDM top submissions 01/28/2022",
                            "playlistId": "PLz7-xrYmULdSLRZGk-6GKUtaBZcgQNwel",
                            "thumbnails": [...],
                            "description": "redditEDM • 161 songs",
                            "count": "161",
                            "author": [
                                {
                                    "name": "redditEDM",
                                    "id": "UCaTrZ9tPiIGHrkCe5bxOGwA"
                                }
                            ]
                        }
                    ]
                },
                {
                    "title": "Your favorites",
                    "contents": [
                        { //artist result
                            "title": "Chill Satellite",
                            "browseId": "UCrPLFBWdOroD57bkqPbZJog",
                            "subscribers": "374",
                            "thumbnails": [...]
                        }
                        { //album result
                            "title": "Dragon",
                            "year": "Two Steps From Hell",
                            "browseId": "MPREb_M9aDqLRbSeg",
                            "thumbnails": [...]
                        }
                    ]
                },
                {
                    "title": "Quick picks",
                    "contents": [
                        { //song quick pick
                            "title": "Gravity",
                            "videoId": "EludZd6lfts",
                            "artists": [{
                                    "name": "yetep",
                                    "id": "UCSW0r7dClqCoCvQeqXiZBlg"
                                }],
                            "thumbnails": [...],
                            "album": {
                                "name": "Gravity",
                                "id": "MPREb_D6bICFcuuRY"
                            }
                        },
                        { //video quick pick
                            "title": "Gryffin & Illenium (feat. Daya) - Feel Good (L3V3LS Remix)",
                            "videoId": "bR5l0hJDnX8",
                            "artists": [
                                {
                                    "name": "L3V3LS",
                                    "id": "UCCVNihbOdkOWw_-ajIYhAbQ"
                                }
                            ],
                            "thumbnails": [...],
                            "views": "10M"
                        }
                    ]
                }
            ]

        """
        endpoint = 'browse'
        body = {"browseId": "FEmusic_home"}
        response = self._send_request(endpoint, body)
        results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)
        home = []
        home.extend(self.parser.parse_mixed_content(results))

        section_list = nav(response, SINGLE_COLUMN_TAB + ['sectionListRenderer'])
        if 'continuations' in section_list:
            request_func = lambda additionalParams: self._send_request(
                endpoint, body, additionalParams)

            parse_func = lambda contents: self.parser.parse_mixed_content(contents)

            home.extend(
                get_continuations(section_list, 'sectionListContinuation', limit - len(home),
                                  request_func, parse_func))

        return home
예제 #5
0
파일: watch.py 프로젝트: sigma67/ytmusicapi
    def get_watch_playlist(self,
                           videoId: str = None,
                           playlistId: str = None,
                           limit=25,
                           params: str = None) -> Dict[str, Union[List[Dict]]]:
        """
        Get a watch list of tracks. This watch playlist appears when you press
        play on a track in YouTube Music.

        Please note that the `INDIFFERENT` likeStatus of tracks returned by this
        endpoint may be either `INDIFFERENT` or `DISLIKE`, due to ambiguous data
        returned by YouTube Music.

        :param videoId: videoId of the played video
        :param playlistId: playlistId of the played playlist or album
        :param limit: minimum number of watch playlist items to return
        :param params: only used internally by :py:func:`get_watch_playlist_shuffle`
        :return: List of watch playlist items. The counterpart key is optional and only
            appears if a song has a corresponding video counterpart (UI song/video
            switcher).

        Example::

            {
                "tracks": [
                    {
                      "videoId": "9mWr4c_ig54",
                      "title": "Foolish Of Me (feat. Jonathan Mendelsohn)",
                      "length": "3:07",
                      "thumbnail": [
                        {
                          "url": "https://lh3.googleusercontent.com/ulK2YaLtOW0PzcN7ufltG6e4ae3WZ9Bvg8CCwhe6LOccu1lCKxJy2r5AsYrsHeMBSLrGJCNpJqXgwczk=w60-h60-l90-rj",
                          "width": 60,
                          "height": 60
                        }...
                      ],
                      "feedbackTokens": {
                        "add": "AB9zfpIGg9XN4u2iJ...",
                        "remove": "AB9zfpJdzWLcdZtC..."
                      },
                      "likeStatus": "INDIFFERENT",
                      "videoType": "MUSIC_VIDEO_TYPE_ATV",
                      "artists": [
                        {
                          "name": "Seven Lions",
                          "id": "UCYd2yzYRx7b9FYnBSlbnknA"
                        },
                        {
                          "name": "Jason Ross",
                          "id": "UCVCD9Iwnqn2ipN9JIF6B-nA"
                        },
                        {
                          "name": "Crystal Skies",
                          "id": "UCTJZESxeZ0J_M7JXyFUVmvA"
                        }
                      ],
                      "album": {
                        "name": "Foolish Of Me",
                        "id": "MPREb_C8aRK1qmsDJ"
                      },
                      "year": "2020",
                      "counterpart": {
                        "videoId": "E0S4W34zFMA",
                        "title": "Foolish Of Me [ABGT404] (feat. Jonathan Mendelsohn)",
                        "length": "3:07",
                        "thumbnail": [...],
                        "feedbackTokens": null,
                        "likeStatus": "LIKE",
                        "artists": [
                          {
                            "name": "Jason Ross",
                            "id": null
                          },
                          {
                            "name": "Seven Lions",
                            "id": null
                          },
                          {
                            "name": "Crystal Skies",
                            "id": null
                          }
                        ],
                        "views": "6.6K"
                      }
                    },...
                ],
                "playlistId": "RDAMVM4y33h81phKU",
                "lyrics": "MPLYt_HNNclO0Ddoc-17"
            }

        """
        body = {'enablePersistentPlaylistPanel': True, 'isAudioOnly': True}
        if not videoId and not playlistId:
            raise Exception(
                "You must provide either a video id, a playlist id, or both")
        if videoId:
            body['videoId'] = videoId
            if not playlistId:
                playlistId = "RDAMVM" + videoId
            if not params:
                body['watchEndpointMusicSupportedConfigs'] = {
                    'watchEndpointMusicConfig': {
                        'hasPersistentPlaylistPanel': True,
                        'musicVideoType': "MUSIC_VIDEO_TYPE_ATV",
                    }
                }
        body['playlistId'] = validate_playlist_id(playlistId)
        is_playlist = body['playlistId'].startswith('PL') or \
                      body['playlistId'].startswith('OLA')
        if params:
            body['params'] = params
        endpoint = 'next'
        response = self._send_request(endpoint, body)
        watchNextRenderer = nav(response, [
            'contents', 'singleColumnMusicWatchNextResultsRenderer',
            'tabbedRenderer', 'watchNextTabbedResultsRenderer'
        ])

        lyrics_browse_id = get_tab_browse_id(watchNextRenderer, 1)
        related_browse_id = get_tab_browse_id(watchNextRenderer, 2)

        results = nav(
            watchNextRenderer, TAB_CONTENT +
            ['musicQueueRenderer', 'content', 'playlistPanelRenderer'])
        playlist = next(
            filter(
                bool,
                map(
                    lambda x: nav(x, ['playlistPanelVideoRenderer'] +
                                  NAVIGATION_PLAYLIST_ID, True),
                    results['contents'])), None)
        tracks = parse_watch_playlist(results['contents'])

        if 'continuations' in results:
            request_func = lambda additionalParams: self._send_request(
                endpoint, body, additionalParams)
            parse_func = lambda contents: parse_watch_playlist(contents)
            tracks.extend(
                get_continuations(results, 'playlistPanelContinuation',
                                  limit - len(tracks), request_func,
                                  parse_func, '' if is_playlist else 'Radio'))

        return dict(tracks=tracks,
                    playlistId=playlist,
                    lyrics=lyrics_browse_id,
                    related=related_browse_id)
예제 #6
0
    def search(self,
               query: str,
               filter: str = None,
               scope: str = None,
               limit: int = 20,
               ignore_spelling: bool = False) -> List[Dict]:
        """
        Search YouTube music
        Returns results within the provided category.

        :param query: Query string, i.e. 'Oasis Wonderwall'
        :param filter: Filter for item types. Allowed values: ``songs``, ``videos``, ``albums``, ``artists``, ``playlists``, ``community_playlists``, ``featured_playlists``, ``uploads``.
          Default: Default search, including all types of items.
        :param scope: Search scope. Allowed values: ``library``, ``uploads``.
            Default: Search the public YouTube Music catalogue.
        :param limit: Number of search results to return
          Default: 20
        :param ignore_spelling: Whether to ignore YTM spelling suggestions.
          If True, the exact search term will be searched for, and will not be corrected.
          This does not have any effect when the filter is set to ``uploads``.
          Default: False, will use YTM's default behavior of autocorrecting the search.
        :return: List of results depending on filter.
          resultType specifies the type of item (important for default search).
          albums, artists and playlists additionally contain a browseId, corresponding to
          albumId, channelId and playlistId (browseId=``VL``+playlistId)

          Example list for default search with one result per resultType for brevity. Normally
          there are 3 results per resultType and an additional ``thumbnails`` key::

            [
              {
                "category": "Top result",
                "resultType": "video",
                "videoId": "vU05Eksc_iM",
                "title": "Wonderwall",
                "artists": [
                  {
                    "name": "Oasis",
                    "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
                  }
                ],
                "views": "1.4M",
                "videoType": "MUSIC_VIDEO_TYPE_OMV",
                "duration": "4:38",
                "duration_seconds": 278
              },
              {
                "category": "Songs",
                "resultType": "song",
                "videoId": "ZrOKjDZOtkA",
                "title": "Wonderwall",
                "artists": [
                  {
                    "name": "Oasis",
                    "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
                  }
                ],
                "album": {
                  "name": "(What's The Story) Morning Glory? (Remastered)",
                  "id": "MPREb_9nqEki4ZDpp"
                },
                "duration": "4:19",
                "duration_seconds": 259
                "isExplicit": false,
                "feedbackTokens": {
                  "add": null,
                  "remove": null
                }
              },
              {
                "category": "Albums",
                "resultType": "album",
                "browseId": "MPREb_9nqEki4ZDpp",
                "title": "(What's The Story) Morning Glory? (Remastered)",
                "type": "Album",
                "artist": "Oasis",
                "year": "1995",
                "isExplicit": false
              },
              {
                "category": "Community playlists",
                "resultType": "playlist",
                "browseId": "VLPLK1PkWQlWtnNfovRdGWpKffO1Wdi2kvDx",
                "title": "Wonderwall - Oasis",
                "author": "Tate Henderson",
                "itemCount": "174"
              },
              {
                "category": "Videos",
                "resultType": "video",
                "videoId": "bx1Bh8ZvH84",
                "title": "Wonderwall",
                "artists": [
                  {
                    "name": "Oasis",
                    "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
                  }
                ],
                "views": "386M",
                "duration": "4:38",
                "duration_seconds": 278
              },
              {
                "category": "Artists",
                "resultType": "artist",
                "browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
                "artist": "Oasis",
                "shuffleId": "RDAOkjHYJjL1a3xspEyVkhHAsg",
                "radioId": "RDEMkjHYJjL1a3xspEyVkhHAsg"
              }
            ]


        """
        body = {'query': query}
        endpoint = 'search'
        search_results = []
        filters = [
            'albums', 'artists', 'playlists', 'community_playlists',
            'featured_playlists', 'songs', 'videos'
        ]
        if filter and filter not in filters:
            raise Exception(
                "Invalid filter provided. Please use one of the following filters or leave out the parameter: "
                + ', '.join(filters))

        scopes = ['library', 'uploads']
        if scope and scope not in scopes:
            raise Exception(
                "Invalid scope provided. Please use one of the following scopes or leave out the parameter: "
                + ', '.join(scopes))

        params = get_search_params(filter, scope, ignore_spelling)
        if params:
            body['params'] = params

        response = self._send_request(endpoint, body)

        # no results
        if 'contents' not in response:
            return search_results

        if 'tabbedSearchResultsRenderer' in response['contents']:
            tab_index = 0 if not scope or filter else scopes.index(scope) + 1
            results = response['contents']['tabbedSearchResultsRenderer'][
                'tabs'][tab_index]['tabRenderer']['content']
        else:
            results = response['contents']

        results = nav(results, SECTION_LIST)

        # no results
        if len(results) == 1 and 'itemSectionRenderer' in results:
            return search_results

        # set filter for parser
        if filter and 'playlists' in filter:
            filter = 'playlists'
        elif scope == scopes[1]:
            filter = scopes[1]

        for res in results:
            if 'musicShelfRenderer' in res:
                results = res['musicShelfRenderer']['contents']
                original_filter = filter
                category = nav(res, MUSIC_SHELF + TITLE_TEXT, True)
                if not filter and scope == scopes[0]:
                    filter = category

                type = filter[:-1].lower() if filter else None
                search_results.extend(
                    self.parser.parse_search_results(results, type, category))
                filter = original_filter

                if 'continuations' in res['musicShelfRenderer']:
                    request_func = lambda additionalParams: self._send_request(
                        endpoint, body, additionalParams)

                    parse_func = lambda contents: self.parser.parse_search_results(
                        contents, type, category)

                    search_results.extend(
                        get_continuations(res['musicShelfRenderer'],
                                          'musicShelfContinuation',
                                          limit - len(search_results),
                                          request_func, parse_func))

        return search_results
예제 #7
0
    def get_playlist(self, playlistId: str, limit: int = 100) -> Dict:
        """
        Returns a list of playlist items

        :param playlistId: Playlist id
        :param limit: How many songs to return. Default: 100
        :return: Dictionary with information about the playlist.
            The key ``tracks`` contains a List of playlistItem dictionaries

        Each item is in the following format::

            {
              "id": "PLQwVIlKxHM6qv-o99iX9R85og7IzF9YS_",
              "privacy": "PUBLIC",
              "title": "New EDM This Week 03/13/2020",
              "thumbnails": [...]
              "description": "Weekly r/EDM new release roundup. Created with github.com/sigma67/spotifyplaylist_to_gmusic",
              "author": "sigmatics",
              "year": "2020",
              "duration": "6+ hours",
              "duration_seconds": 52651,
              "trackCount": 237,
              "tracks": [
                {
                  "videoId": "bjGppZKiuFE",
                  "title": "Lost",
                  "artists": [
                    {
                      "name": "Guest Who",
                      "id": "UCkgCRdnnqWnUeIH7EIc3dBg"
                    },
                    {
                      "name": "Kate Wild",
                      "id": "UCwR2l3JfJbvB6aq0RnnJfWg"
                    }
                  ],
                  "album": {
                    "name": "Lost",
                    "id": "MPREb_PxmzvDuqOnC"
                  },
                  "duration": "2:58",
                  "likeStatus": "INDIFFERENT",
                  "thumbnails": [...],
                  "isAvailable": True,
                  "isExplicit": False,
                  "feedbackTokens": {
                    "add": "AB9zfpJxtvrU...",
                    "remove": "AB9zfpKTyZ..."
                }
              ]
            }

        The setVideoId is the unique id of this playlist item and
        needed for moving/removing playlist items
        """
        browseId = "VL" + playlistId if not playlistId.startswith(
            "VL") else playlistId
        body = {'browseId': browseId}
        endpoint = 'browse'
        response = self._send_request(endpoint, body)
        results = nav(
            response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM +
            ['musicPlaylistShelfRenderer'])
        playlist = {'id': results['playlistId']}
        own_playlist = 'musicEditablePlaylistDetailHeaderRenderer' in response[
            'header']
        if not own_playlist:
            header = response['header']['musicDetailHeaderRenderer']
            playlist['privacy'] = 'PUBLIC'
        else:
            header = response['header'][
                'musicEditablePlaylistDetailHeaderRenderer']
            playlist['privacy'] = header['editHeader'][
                'musicPlaylistEditHeaderRenderer']['privacy']
            header = header['header']['musicDetailHeaderRenderer']

        playlist['title'] = nav(header, TITLE_TEXT)
        playlist['thumbnails'] = nav(header, THUMBNAIL_CROPPED)
        playlist["description"] = nav(header, DESCRIPTION, True)
        run_count = len(header['subtitle']['runs'])
        if run_count > 1:
            playlist['author'] = {
                'name':
                nav(header, SUBTITLE2),
                'id':
                nav(header, ['subtitle', 'runs', 2] + NAVIGATION_BROWSE_ID,
                    True)
            }
            if run_count == 5:
                playlist['year'] = nav(header, SUBTITLE3)

        song_count = to_int(
            unicodedata.normalize("NFKD",
                                  header['secondSubtitle']['runs'][0]['text']))
        if len(header['secondSubtitle']['runs']) > 1:
            playlist['duration'] = header['secondSubtitle']['runs'][2]['text']

        playlist['trackCount'] = song_count
        playlist['suggestions_token'] = nav(
            response,
            SINGLE_COLUMN_TAB + ['sectionListRenderer', 'contents', 1] +
            MUSIC_SHELF + RELOAD_CONTINUATION, True)

        playlist['tracks'] = []
        if song_count > 0:
            playlist['tracks'].extend(parse_playlist_items(
                results['contents']))
            songs_to_get = min(limit, song_count)

            if 'continuations' in results:
                request_func = lambda additionalParams: self._send_request(
                    endpoint, body, additionalParams)
                parse_func = lambda contents: parse_playlist_items(contents)
                playlist['tracks'].extend(
                    get_continuations(results,
                                      'musicPlaylistShelfContinuation',
                                      songs_to_get - len(playlist['tracks']),
                                      request_func, parse_func))

        playlist['duration_seconds'] = sum_total_duration(playlist)
        return playlist