Skip to content

Commit

Permalink
feature: get_home (closes #251)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigma67 committed Feb 27, 2022
1 parent d046011 commit c1170b9
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 9 deletions.
4 changes: 3 additions & 1 deletion docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ Search

Browsing
--------
.. automethod:: YTMusic.get_home
.. automethod:: YTMusic.get_artist
.. automethod:: YTMusic.get_artist_albums
.. automethod:: YTMusic.get_album
.. automethod:: YTMusic.get_album_browse_id
.. automethod:: YTMusic.get_user
.. automethod:: YTMusic.get_user_playlists
.. automethod:: YTMusic.get_album
.. automethod:: YTMusic.get_song
.. automethod:: YTMusic.get_lyrics

Expand Down
11 changes: 9 additions & 2 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def test_setup(self):
# BROWSING
###############

def test_get_home(self):
result = self.yt_auth.get_home(limit=15)
self.assertGreaterEqual(len(result), 15)

def test_search(self):
query = "edm playlist"
self.assertRaises(Exception, self.yt_auth.search, query, filter="song")
Expand All @@ -55,7 +59,9 @@ def test_search(self):
self.assertGreater(len(results), 5)
results = self.yt_auth.search("clasical music", filter='playlists', ignore_spelling=True)
self.assertGreater(len(results), 5)
results = self.yt_auth.search("clasic rock", filter='community_playlists', ignore_spelling=True)
results = self.yt_auth.search("clasic rock",
filter='community_playlists',
ignore_spelling=True)
self.assertGreater(len(results), 5)
results = self.yt_auth.search("hip hop", filter='featured_playlists')
self.assertGreater(len(results), 5)
Expand Down Expand Up @@ -175,7 +181,8 @@ def test_get_charts(self):
###############

def test_get_watch_playlist(self):
playlist = self.yt_auth.get_watch_playlist(playlistId="OLAK5uy_ln_o1YXFqK4nfiNuTfhJK2XcRNCxml0fY", limit=90)
playlist = self.yt_auth.get_watch_playlist(
playlistId="OLAK5uy_ln_o1YXFqK4nfiNuTfhJK2XcRNCxml0fY", limit=90)
self.assertGreaterEqual(len(playlist['tracks']), 90)
playlist = self.yt_auth.get_watch_playlist("9mWr4c_ig54", limit=50)
self.assertGreater(len(playlist['tracks']), 45)
Expand Down
107 changes: 107 additions & 0 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,112 @@


class BrowsingMixin:
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": {
"title": "Gravity",
"browseId": "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 views"
}
]
}
]
"""
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_home(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_home(contents)

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

return home

def search(self,
query: str,
filter: str = None,
Expand Down Expand Up @@ -416,6 +522,7 @@ def get_user_playlists(self, channelId: str, params: str) -> List[Dict]:
def get_album_browse_id(self, audioPlaylistId: str):
"""
Get an album's browseId based on its audioPlaylistId
:param audioPlaylistId: id of the audio playlist (starting with `OLAK5uy_`)
:return: browseId (starting with `MPREb_`)
"""
Expand Down
13 changes: 9 additions & 4 deletions ytmusicapi/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
PLAY_BUTTON = [
'overlay', 'musicItemThumbnailOverlayRenderer', 'content', 'musicPlayButtonRenderer'
]
NAVIGATION_BROWSE_ID = ['navigationEndpoint', 'browseEndpoint', 'browseId']
NAVIGATION_BROWSE = ['navigationEndpoint', 'browseEndpoint']
NAVIGATION_BROWSE_ID = NAVIGATION_BROWSE + ['browseId']
PAGE_TYPE = [
'browseEndpointContextSupportedConfigs', 'browseEndpointContextMusicConfig', 'pageType'
]
NAVIGATION_VIDEO_ID = ['navigationEndpoint', 'watchEndpoint', 'videoId']
NAVIGATION_PLAYLIST_ID = ['navigationEndpoint', 'watchEndpoint', 'playlistId']
NAVIGATION_WATCH_PLAYLIST_ID = ['navigationEndpoint', 'watchPlaylistEndpoint', 'playlistId']
Expand All @@ -28,8 +32,9 @@
FRAMEWORK_MUTATIONS = ['frameworkUpdates', 'entityBatchUpdate', 'mutations']
TITLE = ['title', 'runs', 0]
TITLE_TEXT = ['title'] + RUN_TEXT
TEXT_RUN = ['text', 'runs', 0]
TEXT_RUN_TEXT = ['text', 'runs', 0, 'text']
TEXT_RUNS = ['text', 'runs']
TEXT_RUN = TEXT_RUNS + [0]
TEXT_RUN_TEXT = TEXT_RUN + ['text']
SUBTITLE = ['subtitle'] + RUN_TEXT
SUBTITLE2 = ['subtitle', 'runs', 2, 'text']
SUBTITLE3 = ['subtitle', 'runs', 4, 'text']
Expand All @@ -45,4 +50,4 @@
CATEGORY_TITLE = ['musicNavigationButtonRenderer', 'buttonText'] + RUN_TEXT
CATEGORY_PARAMS = ['musicNavigationButtonRenderer', 'clickCommand', 'browseEndpoint', 'params']
MRLIR = 'musicResponsiveListItemRenderer'
MTRIR = 'musicTwoRowItemRenderer'
MTRIR = 'musicTwoRowItemRenderer'
62 changes: 60 additions & 2 deletions ytmusicapi/parsers/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,49 @@ class Parser:
def __init__(self, language):
self.lang = language

def parse_home(self, rows):
items = []
for row in rows:
contents = []
results = nav(row, CAROUSEL)
for result in results['contents']:
data = nav(result, [MTRIR], True)
content = None
if data:
page_type = nav(data, TITLE + NAVIGATION_BROWSE + PAGE_TYPE, True)
if page_type is None: # song
content = parse_song(data)
elif page_type == "MUSIC_PAGE_TYPE_ALBUM":
content = parse_album(data)
elif page_type == "MUSIC_PAGE_TYPE_ARTIST":
content = parse_related_artist(data)
elif page_type == "MUSIC_PAGE_TYPE_PLAYLIST":
content = parse_playlist(data)
else:
data = nav(result, [MRLIR])
columns = [
get_flex_column_item(data, i) for i in range(0, len(data['flexColumns']))
]
content = {
'title': nav(columns[0], TEXT_RUN_TEXT),
'videoId': nav(columns[0], TEXT_RUN + NAVIGATION_VIDEO_ID),
'artists': parse_song_artists_runs(nav(columns[1], TEXT_RUNS)),
'thumbnails': nav(data, THUMBNAILS)
}
if len(columns) > 2 and columns[2] is not None:
content['album'] = {
'title': nav(columns[2], TEXT_RUN_TEXT),
'browseId': nav(columns[2], TEXT_RUN + NAVIGATION_BROWSE_ID)
}
else:
content['artists'].pop()
content['views'] = nav(columns[1], TEXT_RUNS + [2, 'text'])

contents.append(content)

items.append({'title': nav(results, CAROUSEL_TITLE + ['text']), 'contents': contents})
return items

@i18n
def parse_search_results(self, results, resultType=None, category=None):
search_results = []
Expand Down Expand Up @@ -178,6 +221,16 @@ def parse_single(result):
}


def parse_song(result):
return {
'title': nav(result, TITLE_TEXT),
'artists': parse_song_artists_runs(result['subtitle']['runs'][2:]),
'videoId': nav(result, NAVIGATION_VIDEO_ID),
'playlistId': nav(result, NAVIGATION_PLAYLIST_ID, True),
'thumbnails': nav(result, THUMBNAIL_RENDERER)
}


def parse_video(result):
runs = result['subtitle']['runs']
artists_len = get_dot_separator_index(runs)
Expand All @@ -198,8 +251,13 @@ def parse_playlist(data):
'playlistId': nav(data, TITLE + NAVIGATION_BROWSE_ID)[2:],
'thumbnails': nav(data, THUMBNAIL_RENDERER)
}
if len(data['subtitle']['runs']) == 3 and re.search(r'\d+ ', nav(data, SUBTITLE2)):
playlist['count'] = nav(data, SUBTITLE2).split(' ')[0]
subtitle = data['subtitle']
if 'runs' in subtitle:
playlist['description'] = "".join([run['text'] for run in subtitle['runs']])
if len(subtitle['runs']) == 3 and re.search(r'\d+ ', nav(data, SUBTITLE2)):
playlist['count'] = nav(data, SUBTITLE2).split(' ')[0]
playlist['author'] = parse_song_artists_runs(subtitle['runs'][:1])

return playlist


Expand Down

0 comments on commit c1170b9

Please sign in to comment.