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
1 change: 1 addition & 0 deletions docs/source/reference/playlists.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Playlists
.. currentmodule:: ytmusicapi
.. automethod:: YTMusic.get_playlist
.. automethod:: YTMusic.create_playlist
.. automethod:: YTMusic.join_collaborative_playlist
.. automethod:: YTMusic.edit_playlist
.. automethod:: YTMusic.delete_playlist
.. automethod:: YTMusic.add_playlist_items
Expand Down
26 changes: 26 additions & 0 deletions tests/mixins/test_playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,32 @@ def test_edit_playlist(self, config, yt_brand):
)
assert response3 == "STATUS_SUCCEEDED", "Playlist edit 3 failed"

def test_edit_playlist_collaboration(self, yt_oauth, yt_brand):
playlist_id = yt_oauth.create_playlist("test collaboration", "", privacy_status="UNLISTED")
assert len(playlist_id) == 34, "Playlist creation failed"

try:
response = yt_oauth.edit_playlist(playlist_id, collaboration=True)
assert response["status"] == ResponseStatus.SUCCEEDED
time.sleep(15) # wait for collaboration to be enabled
assert (
yt_brand.join_collaborative_playlist(playlist_id, response["joinCollaborationToken"])
== ResponseStatus.SUCCEEDED
)

playlist = yt_oauth.get_playlist(playlist_id)
assert len(playlist["collaborators"]["avatars"]) == 2
assert "author" not in playlist

assert yt_oauth.edit_playlist(playlist_id, collaboration=False) == ResponseStatus.SUCCEEDED
time.sleep(3)

playlist = yt_oauth.get_playlist(playlist_id)
assert "collaborators" not in playlist
assert playlist["author"]
finally:
yt_oauth.delete_playlist(playlist_id)

def test_create_playlist_invalid_title(self, yt_brand):
with pytest.raises(YTMusicUserError, match="invalid characters"):
yt_brand.create_playlist("test >", description="test")
Expand Down
52 changes: 50 additions & 2 deletions ytmusicapi/mixins/playlists.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from urllib.parse import parse_qs, urlparse

from ytmusicapi.continuations import *
from ytmusicapi.enums import ResponseStatus
from ytmusicapi.exceptions import YTMusicUserError
from ytmusicapi.helpers import sum_total_duration
from ytmusicapi.navigation import *
Expand Down Expand Up @@ -281,12 +284,33 @@ def create_playlist(
response = self._send_request(endpoint, body)
return response["playlistId"] if "playlistId" in response else response

def join_collaborative_playlist(self, playlistId: str, joinCollaborationToken: str) -> str | JsonDict:
"""
Given an invite token, join a collaborative playlist and add it to your library.

:param playlistId: ID of the playlist to join.
If you're already a collaborator, an Unauthorized server error is raised.
:param joinCollaborationToken: See :py:func:`edit_playlist`, or ``jct`` in YTM's invite URL
:return: Status String or full response
"""
self._check_auth()
body: JsonDict = {
"playlistId": validate_playlist_id(playlistId),
"actions": [
{"action": "ACTION_JOIN_COLLABORATION", "joinCollaborationToken": joinCollaborationToken}
],
}
endpoint = "browse/edit_playlist"
response = self._send_request(endpoint, body)
return response["status"] if "status" in response else response

def edit_playlist(
self,
playlistId: str,
title: str | None = None,
description: str | None = None,
privacyStatus: str | None = None,
collaboration: bool | None = None,
moveItem: str | tuple[str, str] | None = None,
addPlaylistId: str | None = None,
addToTop: bool | None = None,
Expand All @@ -299,16 +323,27 @@ def edit_playlist(
:param title: Optional. New title for the playlist
:param description: Optional. New description for the playlist
:param privacyStatus: Optional. New privacy status for the playlist
:param collaboration: Optional. Enable or disable collaboration.
If False and collaboration is not enabled, a Forbidden server error is raised.
If True, a new ``joinCollaborationToken`` is returned.
Collaborators cannot interact with private playlists.
:param moveItem: Optional. Move one item before another. Items are specified by setVideoId, which is the
unique id of this playlist item. See :py:func:`get_playlist`
:param addPlaylistId: Optional. Id of another playlist to add to this playlist
:param addToTop: Optional. Change the state of this playlist to add items to the top of the playlist (if True)
or the bottom of the playlist (if False - this is also the default of a new playlist).
:return: Status String or full response
:return: Status String, ``collaboration`` dict described below, or full response

Dictionary returned when ``collaboration`` is True and the request is successful::

{
"status": "STATUS_SUCCEEDED",
"joinCollaborationToken": "kM9wXdRj2p8v_qL3sHBkTz"
}
"""
self._check_auth()
body: JsonDict = {"playlistId": validate_playlist_id(playlistId)}
actions = []
actions: JsonList = []
if title:
actions.append({"action": "ACTION_SET_PLAYLIST_NAME", "playlistName": title})

Expand All @@ -318,6 +353,11 @@ def edit_playlist(
if privacyStatus:
actions.append({"action": "ACTION_SET_PLAYLIST_PRIVACY", "playlistPrivacy": privacyStatus})

if collaboration:
actions.append({"action": "ACTION_CREATE_COLLABORATION_INVITE_LINK"})
elif collaboration is False:
actions.append({"action": "ACTION_SET_CLOSED_TO_CONTRIBUTIONS", "closedToContributions": True})

if moveItem:
action = {
"action": "ACTION_MOVE_VIDEO_BEFORE",
Expand All @@ -339,6 +379,14 @@ def edit_playlist(
body["actions"] = actions
endpoint = "browse/edit_playlist"
response = self._send_request(endpoint, body)

if collaboration and response.get("status") == ResponseStatus.SUCCEEDED:
invite_link = nav(response, ["collaborationInviteLink"])
return {
"status": response["status"],
"joinCollaborationToken": nav(parse_qs(urlparse(invite_link).query), ["jct", 0]),
}

return response["status"] if "status" in response else response

def delete_playlist(self, playlistId: str) -> str | JsonDict:
Expand Down
Loading