Skip to content

Commit 884ff97

Browse files
committed
feat(Playlist): Add more edit functions
1 parent acf1cc9 commit 884ff97

5 files changed

Lines changed: 99 additions & 31 deletions

File tree

src/deezergw/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json as _json
22
from os import PathLike
3-
from typing import Any, Generator, Iterable, List, Optional, Union
3+
from typing import Any, Generator, List, Optional, Union
44
from deezergw.api import DeezerAPI
55
from deezergw import decrypt_utils as _decrypt_utils
66
from deezergw.resources.album import Album
@@ -12,7 +12,7 @@
1212
from deezergw.search_resources.playlist import SearchPlaylist
1313
from deezergw.search_resources.results import SearchResults
1414
from deezergw.search_resources.track import SearchTrack
15-
from deezergw.types import LoginDumpData
15+
from deezergw.types import LoginDumpData, ArrayLike
1616

1717

1818
class Client:
@@ -192,12 +192,12 @@ def search(self, query: str):
192192

193193
# Batch functions
194194

195-
def get_tracks(self, ids: Iterable[str]):
195+
def get_tracks(self, ids: ArrayLike[str]):
196196
"""
197197
Get multiple Track objects by there ids.
198198
199199
:param ids: The track ids in some form of a list
200-
:type ids: Iterable[str]
200+
:type ids: ArrayLike[str]
201201
:return: A list of the Tracks
202202
:rtype: List[Track]
203203
"""

src/deezergw/api.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from datetime import datetime
22
from requests import Session
3-
from typing import Any, Dict, Iterable, Optional, Tuple, Union
4-
from deezergw.exceptions import NoRightOnMedia, NotFoundException, UnauthorizedException
3+
from typing import Any, Dict, Optional, Tuple, Union
4+
from deezergw.exceptions import AlreadyExistsError, NotFoundException, RequestSpecificError, UnauthorizedException, UnknownException
55
from deezergw.globals import Qualities, QualityType
6-
from deezergw.types import LoginDumpData, MediaData
6+
from deezergw.types import LoginDumpData, MediaData, PlaylistState, ArrayLike
77

88
METHOD_GET_USER_DATA = "deezer.getUserData"
99
METHOD_GET_USER_PROFILE = "deezer.pageProfile"
@@ -63,6 +63,7 @@
6363
"UpdatePlaylist",
6464
"mutation UpdatePlaylist($input: PlaylistUpdateMutationInput!) { updatePlaylist(input: $input) { playlist { id title description isPrivate isCollaborative picture { id } } } }",
6565
)
66+
GRAPHQL_GET_PLAYLIST_STATE = ("PlaylistState","query PlaylistState($playlistId: String!) { playlist(playlistId: $playlistId) { isPrivate isCollaborative } }")
6667

6768

6869
class DeezerAPI:
@@ -167,19 +168,14 @@ def _get_api(
167168
retries -= 1
168169
self._refresh_token()
169170
return self._get_api(method, json_data, retries)
171+
elif "ERROR_DATA_EXISTS" in error_data:
172+
raise AlreadyExistsError(error_data["ERROR_DATA_EXISTS"])
170173
else:
171-
# Catch any unknown error
172-
print("[!] An unknown error was received by DeezerGW. Please report it")
173-
print("[ ] JSON-Data:")
174-
print(response.json())
174+
# Any other error is up to the specific function to handle
175175

176-
if retries <= 0:
177-
raise Exception("Results are empty")
178-
179-
print(f"Retrying ({retries} tries left) ...")
180-
retries -= 1
181-
self._refresh_token()
182-
return self._get_api(method, json_data, retries=retries)
176+
error_type: str = tuple(error_data.keys())[0]
177+
error_msg: str = error_data[error_type]
178+
raise RequestSpecificError(error_type, error_msg)
183179

184180
return results
185181

@@ -206,7 +202,10 @@ def _request_graphql(
206202
print("Refreshing JWT Token...")
207203
self._refresh_jwt_token()
208204
return self._request_graphql(query_pair, variables)
205+
elif response_json["errors"][0]["type"] == "PlaylistMutationFailedException":
206+
raise UnauthorizedException(response_json["errors"][0]["message"])
209207
else:
208+
print(response_json)
210209
raise Exception("GraphQL request failed. Unknown JSON Error")
211210

212211
#check if theres data before trying to access it
@@ -264,6 +263,12 @@ def get_playlist_data(self, id: Union[str, int], start: int = 0):
264263

265264
return data
266265

266+
def get_playlist_state(self, id: Union[str, int]) -> PlaylistState:
267+
variables = {"playlistId": str(id)}
268+
response = self._request_graphql(GRAPHQL_GET_PLAYLIST_STATE, variables)
269+
270+
return response["playlist"]
271+
267272
def create_playlist(self, title:str, description:Optional[str] = None, is_private: bool=False, is_collaborative: bool=False) -> str:
268273
"""
269274
Create a new playlist. Returns the playlist ID. Playlist cannot be both private and collaborative.
@@ -347,13 +352,13 @@ def edit_playlist(self, playlist_id:str, title: Optional[str] = None, descriptio
347352
response = self._request_graphql(GRAPHQL_EDIT_PLAYLIST, variables)
348353
return response["updatePlaylist"]["playlist"]["id"]
349354

350-
def get_track_batch_data(self, ids: Iterable[str]):
355+
def get_track_batch_data(self, ids: ArrayLike[str]):
351356
json_data = {"sng_ids": tuple(ids)}
352357
data = self._get_api(METHOD_GET_BATCH_TRACK_DATA, json_data)
353358

354359
return data
355360

356-
def add_tracks_to_playlist(self, playlist_id: str, song_ids: Iterable[str],offset: int = -1) -> None:
361+
def add_tracks_to_playlist(self, playlist_id: str, song_ids: ArrayLike[str],offset: int = -1) -> None:
357362
"""
358363
Add songs to a playlist.
359364
@@ -365,6 +370,9 @@ def add_tracks_to_playlist(self, playlist_id: str, song_ids: Iterable[str],offse
365370
:type offset: int
366371
"""
367372

373+
if len(song_ids) == 0:
374+
return
375+
368376
# Convert song IDs to [id, position] format
369377
songs = [[str(song_id), i] for i, song_id in enumerate(song_ids)]
370378

@@ -373,10 +381,16 @@ def add_tracks_to_playlist(self, playlist_id: str, song_ids: Iterable[str],offse
373381
"songs": songs,
374382
"offset": offset,
375383
}
376-
377-
self._get_api(METHOD_ADD_PLAYLIST_TRACK, json_data)
378384

379-
def remove_tracks_from_playlist(self, playlist_id: str,song_ids: Iterable[str]) -> None:
385+
try:
386+
self._get_api(METHOD_ADD_PLAYLIST_TRACK, json_data)
387+
except RequestSpecificError as e:
388+
if e.error_type == "REQUEST_ERROR":
389+
raise UnauthorizedException(e.error_msg)
390+
else:
391+
raise e
392+
393+
def remove_tracks_from_playlist(self, playlist_id: str,song_ids: ArrayLike[str]) -> None:
380394
"""
381395
Remove songs from a playlist.
382396
@@ -385,6 +399,9 @@ def remove_tracks_from_playlist(self, playlist_id: str,song_ids: Iterable[str])
385399
:param song_ids: A list of song IDs to remove from the playlist
386400
:type song_ids: ArrayLike[str]
387401
"""
402+
403+
if len(song_ids) == 0:
404+
return
388405

389406
songs = [[int(song_id), i] for i, song_id in enumerate(song_ids)] #convert song IDs to [id, position] format (as integers)
390407

@@ -422,7 +439,7 @@ def get_media_data(
422439
infos = response.json()["data"][0]
423440

424441
if "errors" in infos:
425-
raise NoRightOnMedia(infos["errors"][0]["message"])
442+
raise UnauthorizedException(infos["errors"][0]["message"])
426443

427444
return infos["media"][0]
428445

@@ -451,7 +468,7 @@ def get_favorited_track_ids(self):
451468

452469
return favorited_tracks
453470

454-
def add_favorite_tracks(self, ids: Iterable[str]):
471+
def add_favorite_tracks(self, ids: ArrayLike[str]):
455472
now = datetime.now()
456473

457474
json_data = {"IDS": tuple(ids)}
@@ -460,7 +477,7 @@ def add_favorite_tracks(self, ids: Iterable[str]):
460477
for id in ids:
461478
self.favorited_ids[id] = now
462479

463-
def remove_favorite_tracks(self, ids: Iterable[str]):
480+
def remove_favorite_tracks(self, ids: ArrayLike[str]):
464481
json_data = {"IDS": tuple(ids)}
465482
self._get_api(METHOD_REMOVE_FAVORITE_TRACKS, json_data)
466483

src/deezergw/exceptions.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
class ExpiredException(Exception):
22
pass
33

4-
class NoRightOnMedia(Exception):
5-
pass
6-
74
class UnknownException(Exception):
85
pass
96

107
class NotFoundException(Exception):
118
pass
129

1310
class UnauthorizedException(Exception):
14-
pass
11+
pass
12+
13+
class AlreadyExistsError(Exception):
14+
pass
15+
16+
class RequestSpecificError(Exception):
17+
def __init__(self, error_type: str, error_msg: str) -> None:
18+
self.error_type = error_type
19+
self.error_msg = error_msg
20+
21+
self.msg = f"{error_type} - {error_msg}"
22+
super().__init__(self.msg)

src/deezergw/resources/playlist.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from typing import Any, Dict, List, Optional, Union
33
from deezergw.api import IMAGE_URL, DeezerAPI
4-
from deezergw.exceptions import UnknownException
4+
from deezergw.exceptions import UnauthorizedException, UnknownException
55
from deezergw.resources.track import Track
66
from deezergw.utils import normalize_track_ids
77

@@ -40,6 +40,11 @@ def __init__(
4040
if "DATA_MOD" in data
4141
else None
4242
)
43+
self.editable: bool = data["STATUS"] == 1
44+
45+
state = api.get_playlist_state(self.id)
46+
self.is_private = state["isPrivate"]
47+
self.is_collaborative = state["isCollaborative"]
4348

4449
self._author_pic: Optional[str] = (
4550
data["PARENT_USER_PICTURE"]
@@ -92,6 +97,9 @@ def add_tracks(self, tracks: List[Track], offset: int = -1):
9297
:param offset: The position to insert the songs at. Default is -1 (add to end of playlist)
9398
:type offset: int
9499
"""
100+
if not self.editable:
101+
raise UnauthorizedException("This playlist doesn't seem editable")
102+
95103
ids = normalize_track_ids(tracks)
96104
self._api.add_tracks_to_playlist(self.id, ids, offset)
97105

@@ -113,6 +121,9 @@ def remove_tracks(self, tracks: List[Track]):
113121
:param tracks: A list of Tracks or track ids to remove from the playlist
114122
:type tracks: List[Union[str, Track]]
115123
"""
124+
if not self.editable:
125+
raise UnauthorizedException("This playlist doesn't seem editable")
126+
116127
ids = normalize_track_ids(tracks)
117128
self._api.remove_tracks_from_playlist(self.id, ids)
118129

@@ -127,6 +138,30 @@ def remove_tracks(self, tracks: List[Track]):
127138

128139
self.last_edited = datetime.now()
129140

141+
def edit_playlist(self, name: Optional[str] = None, description: Optional[str] = None, is_private: Optional[bool] = None, is_collaborative: Optional[bool] = None):
142+
"""
143+
Edit a playlist. Returns the playlist ID. Playlist cannot be both private and collaborative.
144+
145+
:param name: The new name of the playlist
146+
:type name: Optional[str]
147+
:param description: The new description of the playlist
148+
:type description: Optional[str]
149+
:param is_private: Whether the playlist should be private
150+
:type is_private: Optional[bool]
151+
:param is_collaborative: Whether the playlist should be collaborative
152+
:type is_collaborative: Optional[bool]
153+
"""
154+
self._api.edit_playlist(self.id, name, description, is_private, is_collaborative)
155+
if name:
156+
self.name = name
157+
if description:
158+
self.description = description
159+
if is_private is not None:
160+
self.is_private = is_private
161+
if is_collaborative is not None:
162+
self.is_collaborative = is_collaborative
163+
164+
130165
def delete(self):
131166
"""Delete this playlist. Use with care!"""
132167
self._api.delete_playlist(self.id)

src/deezergw/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ class DownloadInfo(TypedDict):
3737
quality: QualityType
3838
file_format: FormatType
3939
track_id: str
40+
41+
42+
class PlaylistState(TypedDict):
43+
isPrivate: bool
44+
isCollaborative: bool
45+
46+
_array = TypeVar("_array")
47+
ArrayLike = Union[List[_array], Tuple[_array, ...], Set[_array]]

0 commit comments

Comments
 (0)