Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/source/reference/browsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ Browsing

.. currentmodule:: ytmusicapi
.. 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_user_videos
.. automethod:: YTMusic.get_song
Expand Down
1 change: 1 addition & 0 deletions docs/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ Reference for the YTMusic class.
library
playlists
podcasts
profiles
uploads
api/modules
1 change: 0 additions & 1 deletion docs/source/reference/podcasts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Podcasts
--------

.. currentmodule:: ytmusicapi
.. automethod:: YTMusic.get_channel
.. automethod:: YTMusic.get_channel_episodes
.. automethod:: YTMusic.get_podcast
.. automethod:: YTMusic.get_episode
Expand Down
8 changes: 8 additions & 0 deletions docs/source/reference/profiles.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Profiles
--------

.. currentmodule:: ytmusicapi
.. automethod:: YTMusic.get_profile
.. automethod:: YTMusic.get_artist
.. automethod:: YTMusic.get_user
.. automethod:: YTMusic.get_channel
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ classifiers = [
]
dependencies = [
"requests >= 2.22",
"typing_extensions >= 4.3.0",
]
dynamic = ["version", "readme"]

Expand Down
19 changes: 0 additions & 19 deletions tests/mixins/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,6 @@ def test_get_home(self, yt, yt_auth):
# [item is not None for section in result for item in section["contents"]]
# )

def test_get_artist(self, yt):
results = yt.get_artist("MPLAUCmMUZbaYdNH0bEd1PAlAqsA")
assert len(results) == 17
assert results["shuffleId"] is not None
assert results["radioId"] is not None

# test correctness of related artists
related = results["related"]["results"]
assert len(
[x for x in related if set(x.keys()) == {"browseId", "subscribers", "title", "thumbnails"}]
) == len(related)

results = yt.get_artist("UCLZ7tlKC06ResyDmEStSrOw") # no album year
assert len(results) >= 11

def test_get_artist_shows(self, yt_oauth):
# with audiobooks - only with authentication
results = yt_oauth.get_artist("UCyiY-0Af0O6emoI3YvCEDaA")
Expand Down Expand Up @@ -76,10 +61,6 @@ def test_get_artist_albums(self, yt):
with pytest.raises(ValueError, match="Invalid order"):
yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"], order="order")

def test_get_user(self, yt):
results = yt.get_user("UC44hbeRoCZVVMVg5z0FfIww")
assert len(results) == 3

def test_get_user_playlists(self, yt, yt_auth):
channel = "UCPVhZsC2od1xjGhgEc2NEPQ" # Vevo playlists
user = yt_auth.get_user(channel)
Expand Down
6 changes: 0 additions & 6 deletions tests/mixins/test_podcasts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
class TestPodcasts:
def test_get_channel(self, config, yt):
podcast_id = config["podcasts"]["channel_id"]
channel = yt.get_channel(podcast_id)
assert len(channel["episodes"]["results"]) == 10
assert len(channel["podcasts"]["results"]) >= 5

def test_get_channel_episodes(self, config, yt_oauth):
channel_id = config["podcasts"]["channel_id"]
channel = yt_oauth.get_channel(channel_id)
Expand Down
58 changes: 58 additions & 0 deletions tests/mixins/test_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from unittest.mock import patch

from ytmusicapi.enums import ProfileTypes


class TestProfiles:
artist_id = "MPLAUCmMUZbaYdNH0bEd1PAlAqsA"
artist_id_no_album_year = "UCLZ7tlKC06ResyDmEStSrOw"
user_id = "UC44hbeRoCZVVMVg5z0FfIww"

def test_get_artist(self, yt):
results = yt.get_artist(self.artist_id)
assert len(results) == 17
assert results["shuffleId"] is not None
assert results["radioId"] is not None

# test correctness of related artists
related = results["related"]["results"]
assert len(
[x for x in related if set(x.keys()) == {"browseId", "subscribers", "title", "thumbnails"}]
) == len(related)

def test_get_artist__no_album_year(self, yt):
results = yt.get_artist(self.artist_id_no_album_year) # no album year
assert len(results) >= 11

def test_get_user(self, yt):
results = yt.get_user(self.user_id)
assert len(results) == 3

def test_get_channel(self, config, yt):
podcast_id = config["podcasts"]["channel_id"]
channel = yt.get_channel(podcast_id)
assert len(channel["episodes"]["results"]) == 10
assert len(channel["podcasts"]["results"]) >= 5

def test_determine_type__artist(self, yt):
determined_type, _ = yt.get_profile(self.artist_id)
assert determined_type == ProfileTypes.ARTIST

def test_determine_type__user(self, yt):
determined_type, _ = yt.get_profile(self.user_id)
assert determined_type == ProfileTypes.USER

def test_determine_type__channel(self, config, yt):
determined_type, _ = yt.get_profile(config["podcasts"]["channel_id"])
assert determined_type == ProfileTypes.CHANNEL

def test_profile_type_not_ignored(self, yt):
with (
patch.object(yt, "_determine_profile_type") as determine_type_mock,
patch.object(yt, "_send_request"),
patch.object(yt, "_parse_artist") as parse_artist_mock,
):
yt.get_profile(self.user_id, ProfileTypes.ARTIST)

determine_type_mock.assert_not_called()
parse_artist_mock.assert_called_once()
6 changes: 6 additions & 0 deletions ytmusicapi/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@

class ResponseStatus(str, Enum):
SUCCEEDED = "STATUS_SUCCEEDED"


class ProfileTypes(str, Enum):
ARTIST = "ARTIST"
USER = "USER"
CHANNEL = "CHANNEL" # Podcasts channel
215 changes: 0 additions & 215 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,156 +136,6 @@ def get_home(self, limit: int = 3) -> JsonList:

return home

def get_artist(self, channelId: str) -> JsonDict:
"""
Get information about an artist and their top releases (songs,
albums, singles, videos, and related artists). The top lists
contain pointers for getting the full list of releases.

Possible content types for get_artist are:

- songs
- albums
- singles
- shows
- videos
- episodes
- podcasts
- related

Each of these content keys in the response contains
``results`` and possibly ``browseId`` and ``params``.

- For songs/videos, pass the browseId to :py:func:`get_playlist`.
- For albums/singles/shows, pass browseId and params to :py:func:`get_artist_albums`.

:param channelId: channel id of the artist
:return: Dictionary with requested information.

.. warning::

The returned channelId is not the same as the one passed to the function.
It should be used only with :py:func:`subscribe_artists`.

Example::

{
"description": "Oasis were ...",
"views": "3,693,390,359 views",
"name": "Oasis",
"channelId": "UCUDVBtnOQi4c7E8jebpjc9Q",
"shuffleId": "RDAOkjHYJjL1a3xspEyVkhHAsg",
"radioId": "RDEMkjHYJjL1a3xspEyVkhHAsg",
"subscribers": "3.86M",
"monthlyListeners": "29.1M",
"subscribed": false,
"thumbnails": [...],
"songs": {
"browseId": "VLPLMpM3Z0118S42R1npOhcjoakLIv1aqnS1",
"results": [
{
"videoId": "ZrOKjDZOtkA",
"title": "Wonderwall (Remastered)",
"thumbnails": [...],
"artist": "Oasis",
"album": "(What's The Story) Morning Glory? (Remastered)"
}
]
},
"albums": {
"results": [
{
"title": "Familiar To Millions",
"thumbnails": [...],
"year": "2018",
"browseId": "MPREb_AYetWMZunqA"
}
],
"browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
"params": "6gPTAUNwc0JDbndLYlFBQV..."
},
"singles": {
"results": [
{
"title": "Stand By Me (Mustique Demo)",
"thumbnails": [...],
"year": "2016",
"browseId": "MPREb_7MPKLhibN5G"
}
],
"browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
"params": "6gPTAUNwc0JDbndLYlFBQV..."
},
"videos": {
"results": [
{
"title": "Wonderwall",
"thumbnails": [...],
"views": "358M",
"videoId": "bx1Bh8ZvH84",
"playlistId": "PLMpM3Z0118S5xuNckw1HUcj1D021AnMEB"
}
],
"browseId": "VLPLMpM3Z0118S5xuNckw1HUcj1D021AnMEB"
},
"related": {
"results": [
{
"browseId": "UCt2KxZpY5D__kapeQ8cauQw",
"subscribers": "450K",
"title": "The Verve"
},
{
"browseId": "UCwK2Grm574W1u-sBzLikldQ",
"subscribers": "341K",
"title": "Liam Gallagher"
},
...
]
}
}
"""
if channelId.startswith("MPLA"):
channelId = channelId[4:]
body = {"browseId": channelId}
endpoint = "browse"
response = self._send_request(endpoint, body)
results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)

artist: JsonDict = {"description": None, "views": None}
header = response["header"]["musicImmersiveHeaderRenderer"]
artist["name"] = nav(header, TITLE_TEXT)
descriptionShelf = find_object_by_key(results, DESCRIPTION_SHELF[0], is_key=True)
if descriptionShelf:
artist["description"] = nav(descriptionShelf, DESCRIPTION)
artist["views"] = (
None
if "subheader" not in descriptionShelf
else descriptionShelf["subheader"]["runs"][0]["text"]
)
subscription_button = header["subscriptionButton"]["subscribeButtonRenderer"]
artist["channelId"] = subscription_button["channelId"]
artist["shuffleId"] = nav(header, ["playButton", "buttonRenderer", *NAVIGATION_PLAYLIST_ID], True)
artist["radioId"] = nav(header, ["startRadioButton", "buttonRenderer", *NAVIGATION_PLAYLIST_ID], True)
artist["subscribers"] = nav(subscription_button, ["subscriberCountText", "runs", 0, "text"], True)
artist["monthlyListeners"] = nav(header, ["monthlyListenerCount", "runs", 0, "text"], True)
artist["monthlyListeners"] = (
artist["monthlyListeners"].replace(" monthly audience", "")
if artist["monthlyListeners"]
else None
)
artist["subscribed"] = subscription_button["subscribed"]
artist["thumbnails"] = nav(header, THUMBNAILS, True)
artist["songs"] = {"browseId": None}
if "musicShelfRenderer" in results[0]: # API sometimes does not return songs
musicShelf = nav(results[0], MUSIC_SHELF)
if "navigationEndpoint" in nav(musicShelf, TITLE):
artist["songs"]["browseId"] = nav(musicShelf, TITLE + NAVIGATION_BROWSE_ID)
artist["songs"]["results"] = parse_playlist_items(musicShelf["contents"])

artist.update(self.parser.parse_channel_contents(results))
return artist

ArtistOrderType = Literal["Recency", "Popularity", "Alphabetical order"]

def get_artist_albums(
Expand Down Expand Up @@ -371,71 +221,6 @@ def get_artist_albums(

return albums

def get_user(self, channelId: str) -> JsonDict:
"""
Retrieve a user's page. A user may own videos or playlists.

Use :py:func:`get_user_playlists` to retrieve all playlists::

result = get_user(channelId)
get_user_playlists(channelId, result["playlists"]["params"])

Similarly, use :py:func:`get_user_videos` to retrieve all videos::

get_user_videos(channelId, result["videos"]["params"])

:param channelId: channelId of the user
:return: Dictionary with information about a user.

Example::

{
"name": "4Tune - No Copyright Music",
"videos": {
"browseId": "UC44hbeRoCZVVMVg5z0FfIww",
"results": [
{
"title": "Epic Music Soundtracks 2019",
"videoId": "bJonJjgS2mM",
"playlistId": "RDAMVMbJonJjgS2mM",
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/bJon...",
"width": 800,
"height": 450
}
],
"views": "19K"
}
]
},
"playlists": {
"browseId": "UC44hbeRoCZVVMVg5z0FfIww",
"results": [
{
"title": "♚ Machinimasound | Playlist",
"playlistId": "PLRm766YvPiO9ZqkBuEzSTt6Bk4eWIr3gB",
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/...",
"width": 400,
"height": 225
}
]
}
],
"params": "6gO3AUNvWU..."
}
}
"""
endpoint = "browse"
body = {"browseId": channelId}
response = self._send_request(endpoint, body)
user = {"name": nav(response, [*HEADER_MUSIC_VISUAL, *TITLE_TEXT])}
results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)
user.update(self.parser.parse_channel_contents(results))
return user

def get_user_playlists(self, channelId: str, params: str) -> JsonList:
"""
Retrieve a list of playlists for a given user.
Expand Down
Loading
Loading