spotapi 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spotapi/playlist.py ADDED
@@ -0,0 +1,332 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import time
6
+ from typing import Any, Generator, Mapping, Optional
7
+
8
+ from spotapi.exceptions import PlaylistError
9
+ from spotapi.http.request import TLSClient
10
+ from spotapi.login import Login
11
+ from spotapi.client import BaseClient
12
+ from spotapi.user import User
13
+
14
+
15
+ class PublicPlaylist(BaseClient):
16
+ """
17
+ Allows you to get all public information on a playlist.
18
+ No login is required.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ playlist: Optional[str] = None,
24
+ /,
25
+ *,
26
+ client: Optional[TLSClient] = TLSClient("chrome_120", "", auto_retries=3),
27
+ ) -> None:
28
+ super().__init__(client=client)
29
+
30
+ if playlist:
31
+ self.playlist_id = (
32
+ playlist.split("playlist/")[-1] if "playlist" in playlist else playlist
33
+ )
34
+ self.playlist_link = f"https://open.spotify.com/playlist/{self.playlist_id}"
35
+
36
+ def get_playlist_info(
37
+ self, limit: Optional[int] = 25, *, offset: Optional[int] = 0
38
+ ) -> Mapping[str, Any]:
39
+ """Gets the public playlist information"""
40
+ if not self.playlist_id:
41
+ raise ValueError("Playlist ID not set")
42
+
43
+ url = "https://api-partner.spotify.com/pathfinder/v1/query"
44
+ params = {
45
+ "operationName": "fetchPlaylist",
46
+ "variables": json.dumps(
47
+ {
48
+ "uri": f"spotify:playlist:{self.playlist_id}",
49
+ "offset": offset,
50
+ "limit": limit,
51
+ }
52
+ ),
53
+ "extensions": json.dumps(
54
+ {
55
+ "persistedQuery": {
56
+ "version": 1,
57
+ "sha256Hash": self.part_hash("fetchPlaylist"),
58
+ }
59
+ }
60
+ ),
61
+ }
62
+
63
+ resp = self.client.post(url, params=params, authenticate=True)
64
+
65
+ if resp.fail:
66
+ raise PlaylistError("Could not get playlist info", error=resp.error.string)
67
+
68
+ if not isinstance(resp.response, Mapping):
69
+ raise PlaylistError("Invalid JSON")
70
+
71
+ return resp.response
72
+
73
+ def paginate_playlist(self) -> Generator[Mapping[str, Any], None, None]:
74
+ """
75
+ Generator that fetches playlist information in chunks
76
+
77
+ Note: If total_tracks <= 343, then there is no need to paginate
78
+ """
79
+ UPPER_LIMIT: int = 343
80
+ # We need to get the total playlists first
81
+ playlist = self.get_playlist_info(limit=UPPER_LIMIT)
82
+ total_count: int = playlist["data"]["playlistV2"]["content"]["totalCount"]
83
+
84
+ yield playlist["data"]["playlistV2"]["content"]
85
+
86
+ if total_count <= UPPER_LIMIT:
87
+ return
88
+
89
+ offset = UPPER_LIMIT
90
+ while offset < total_count:
91
+ yield self.get_playlist_info(limit=UPPER_LIMIT, offset=offset)["data"][
92
+ "playlistV2"
93
+ ]["content"]
94
+ offset += UPPER_LIMIT
95
+
96
+
97
+ class PrivatePlaylist(BaseClient, Login):
98
+ """
99
+ Methods on playlists that you can only do whilst logged in.
100
+ """
101
+
102
+ def __new__(
103
+ cls,
104
+ login: Login,
105
+ playlist: Optional[str] = None,
106
+ ) -> PrivatePlaylist:
107
+ instance = super().__new__(cls)
108
+ instance.__dict__.update(login.__dict__)
109
+ return instance
110
+
111
+ def __init__(
112
+ self,
113
+ login: Login,
114
+ playlist: Optional[str] = None,
115
+ ) -> None:
116
+ if not login.logged_in:
117
+ raise ValueError("Must be logged in")
118
+
119
+ if playlist:
120
+ self.playlist_id = (
121
+ playlist.split("playlist/")[-1] if "playlist" in playlist else playlist
122
+ )
123
+
124
+ super().__init__(client=login.client)
125
+
126
+ self.user = User(login)
127
+ # We need to check if a user can use a method
128
+ self._playlist: bool = bool(playlist)
129
+
130
+ def set_playlist(self, playlist: str) -> None:
131
+ if "playlist:" in playlist:
132
+ playlist = playlist.split("playlist:")[-1]
133
+
134
+ if hasattr(playlist, "playlist_id"):
135
+ self.playlist_id = playlist
136
+
137
+ setattr(self, "playlist_id", playlist)
138
+ self._playlist = True
139
+
140
+ def add_to_library(self) -> None:
141
+ """Adds the playlist to your library"""
142
+ if not self._playlist:
143
+ raise ValueError("Playlist not set")
144
+
145
+ url = f"https://spclient.wg.spotify.com/playlist/v2/user/{self.user.username}/rootlist/changes"
146
+ payload = {
147
+ "deltas": [
148
+ {
149
+ "ops": [
150
+ {
151
+ "kind": 2,
152
+ "add": {
153
+ "items": [
154
+ {
155
+ "uri": f"spotify:playlist:{self.playlist_id}",
156
+ "attributes": {
157
+ "timestamp": int(time.time()),
158
+ "formatAttributes": [],
159
+ "availableSignals": [],
160
+ },
161
+ }
162
+ ],
163
+ "addFirst": True,
164
+ },
165
+ }
166
+ ],
167
+ "info": {"source": {"client": 5}},
168
+ }
169
+ ],
170
+ "wantResultingRevisions": False,
171
+ "wantSyncResult": False,
172
+ "nonces": [],
173
+ }
174
+
175
+ resp = self.client.post(url, json=payload, authenticate=True)
176
+
177
+ if resp.fail:
178
+ raise PlaylistError(
179
+ "Could not add playlist to library", error=resp.error.string
180
+ )
181
+
182
+ def remove_from_library(self) -> None:
183
+ """Removes the playlist from your library"""
184
+ if not self._playlist:
185
+ raise ValueError("Playlist not set")
186
+
187
+ url = f"https://spclient.wg.spotify.com/playlist/v2/user/{self.user.username}/rootlist/changes"
188
+ payload = {
189
+ "deltas": [
190
+ {
191
+ "ops": [
192
+ {
193
+ "kind": 3,
194
+ "rem": {
195
+ "items": [
196
+ {"uri": f"spotify:playlist:{self.playlist_id}"}
197
+ ],
198
+ "itemsAsKey": True,
199
+ },
200
+ }
201
+ ],
202
+ "info": {"source": {"client": 5}},
203
+ }
204
+ ],
205
+ "wantResultingRevisions": False,
206
+ "wantSyncResult": False,
207
+ "nonces": [],
208
+ }
209
+
210
+ resp = self.client.post(url, json=payload, authenticate=True)
211
+
212
+ if resp.fail:
213
+ raise PlaylistError(
214
+ "Could not remove playlist from library", error=resp.error.string
215
+ )
216
+
217
+ def delete_playlist(self) -> None:
218
+ """Deletes the playlist from your library"""
219
+
220
+ # They are the same requests
221
+ return self.remove_from_library()
222
+
223
+ def get_library(self, limit: Optional[int] = 50) -> Mapping[str, Any]:
224
+ """Gets all the playlists in your library"""
225
+ url = "https://api-partner.spotify.com/pathfinder/v1/query"
226
+ params = {
227
+ "operationName": "libraryV3",
228
+ "variables": json.dumps(
229
+ {
230
+ "filters": [],
231
+ "order": None,
232
+ "textFilter": "",
233
+ "features": ["LIKED_SONGS", "YOUR_EPISODES", "PRERELEASES"],
234
+ "limit": limit,
235
+ "offset": 0,
236
+ "flatten": False,
237
+ "expandedFolders": [],
238
+ "folderUri": None,
239
+ "includeFoldersWhenFlattening": True,
240
+ }
241
+ ),
242
+ "extensions": json.dumps(
243
+ {
244
+ "persistedQuery": {
245
+ "version": 1,
246
+ "sha256Hash": self.part_hash("libraryV3"),
247
+ }
248
+ }
249
+ ),
250
+ }
251
+
252
+ resp = self.client.post(url, params=params, authenticate=True)
253
+
254
+ if resp.fail:
255
+ raise PlaylistError("Could not get library", error=resp.error.string)
256
+
257
+ return resp.response
258
+
259
+ def __stage_create_playlist(self, name: str) -> str:
260
+ url = "https://spclient.wg.spotify.com/playlist/v2/playlist"
261
+ payload = {
262
+ "ops": [
263
+ {
264
+ "kind": 6,
265
+ "updateListAttributes": {
266
+ "newAttributes": {
267
+ "values": {
268
+ "name": name,
269
+ "formatAttributes": [],
270
+ "pictureSize": [],
271
+ },
272
+ "noValue": [],
273
+ }
274
+ },
275
+ }
276
+ ]
277
+ }
278
+
279
+ resp = self.client.post(url, json=payload, authenticate=True)
280
+
281
+ if resp.fail:
282
+ raise PlaylistError(
283
+ "Could not stage create playlist", error=resp.error.string
284
+ )
285
+
286
+ pattern = r"spotify:playlist:[a-zA-Z0-9]+"
287
+ matched = re.search(pattern, resp.response)
288
+
289
+ if not matched:
290
+ raise PlaylistError("Could not find desired playlist ID")
291
+
292
+ return matched.group(0)
293
+
294
+ def create_playlist(self, name: str) -> str:
295
+ """Creates a new playlist"""
296
+ playlist_id = self.__stage_create_playlist(name)
297
+ url = f"https://spclient.wg.spotify.com/playlist/v2/user/{self.user.username}/rootlist/changes"
298
+ payload = {
299
+ "deltas": [
300
+ {
301
+ "ops": [
302
+ {
303
+ "kind": 2,
304
+ "add": {
305
+ "items": [
306
+ {
307
+ "uri": playlist_id,
308
+ "attributes": {
309
+ "timestamp": int(time.time()),
310
+ "formatAttributes": [],
311
+ "availableSignals": [],
312
+ },
313
+ }
314
+ ],
315
+ "addFirst": True,
316
+ },
317
+ }
318
+ ],
319
+ "info": {"source": {"client": 5}},
320
+ }
321
+ ],
322
+ "wantResultingRevisions": False,
323
+ "wantSyncResult": False,
324
+ "nonces": [],
325
+ }
326
+
327
+ resp = self.client.post(url, json=payload, authenticate=True)
328
+
329
+ if resp.fail:
330
+ raise PlaylistError("Could not create playlist", error=resp.error.string)
331
+
332
+ return playlist_id
@@ -0,0 +1,15 @@
1
+ from typing import Dict, Final, Type
2
+
3
+ from spotapi.data.interfaces import CaptchaProtocol
4
+ from spotapi.solvers.capmonster import *
5
+ from spotapi.solvers.capsolver import *
6
+
7
+ solver_clients_str: Final[Dict[str, Type[CaptchaProtocol]]] = {
8
+ "capsolver": Capsolver,
9
+ "capmonster": Capmonster,
10
+ }
11
+
12
+
13
+ class solver_clients:
14
+ Capsolver: Type[CaptchaProtocol] = Capsolver
15
+ Capmonster: Type[CaptchaProtocol] = Capmonster
@@ -0,0 +1,128 @@
1
+ import json
2
+ import time
3
+ from typing import Literal, Optional
4
+
5
+ from spotapi.exceptions import CaptchaException, SolverError
6
+ from spotapi.http.request import StdClient
7
+
8
+
9
+ class Capmonster:
10
+ BaseURL = "https://api.capmonster.cloud/"
11
+
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ client: Optional[StdClient] = StdClient(3),
16
+ *,
17
+ proxy: Optional[str] = None,
18
+ retries: Optional[int] = 120,
19
+ ) -> None:
20
+ self.api_key = api_key
21
+ self.client = client
22
+ self.proxy = proxy
23
+ if self.proxy:
24
+ raise CaptchaException("Only Proxyless mode is supported with capmonster.")
25
+ self.retries = retries
26
+
27
+ self.client.authenticate = lambda kwargs: self._auth_rule(kwargs)
28
+
29
+ def _auth_rule(self, kwargs: dict) -> dict:
30
+ if not ("json" in kwargs):
31
+ kwargs["json"] = {}
32
+
33
+ kwargs["json"]["clientKey"] = self.api_key
34
+ return kwargs
35
+
36
+ def get_balance(self) -> float | None:
37
+ endpoint = self.BaseURL + "getBalance"
38
+ request = self.client.post(endpoint, authenticate=True)
39
+
40
+ if request.fail:
41
+ raise CaptchaException(
42
+ "Could not retrieve balance.", error=request.error.string
43
+ )
44
+
45
+ resp = request.response
46
+
47
+ if int(resp["errorId"]) != 0:
48
+ raise CaptchaException(
49
+ "Could not retrieve balance.", error=resp["errorCode"]
50
+ )
51
+
52
+ return resp["balance"]
53
+
54
+ def _create_task(
55
+ self,
56
+ url: str,
57
+ site_key: str,
58
+ action: str,
59
+ task: Literal["v2", "v3"],
60
+ proxy: Optional[str] = None,
61
+ ) -> str:
62
+ endpoint = self.BaseURL + "createTask"
63
+ task_type = (
64
+ "ReCaptcha{}EnterpriseTask"
65
+ if proxy
66
+ else "ReCaptcha{}EnterpriseTaskProxyLess"
67
+ ).format(task.upper())
68
+ payload = {
69
+ "task": {
70
+ "type": task_type,
71
+ "websiteURL": url,
72
+ "websiteKey": site_key,
73
+ "pageAction": action,
74
+ },
75
+ }
76
+
77
+ if proxy:
78
+ payload["task"]["proxy"] = proxy
79
+
80
+ request = self.client.post(endpoint, authenticate=True, json=payload)
81
+
82
+ if request.fail:
83
+ raise CaptchaException("Could not create task.", error=request.error.string)
84
+
85
+ resp = request.response
86
+
87
+ if int(resp["errorId"]) != 0:
88
+ raise CaptchaException("Could not create task.", error=resp["errorCode"])
89
+
90
+ return str(resp["taskId"])
91
+
92
+ def _harvest_task(self, task_id: str, retries: int) -> str | None:
93
+ for _ in range(retries):
94
+ payload = {"taskId": task_id}
95
+ endpoint = self.BaseURL + "getTaskResult"
96
+
97
+ request = self.client.post(endpoint, authenticate=True, json=payload)
98
+
99
+ if request.fail:
100
+ raise CaptchaException(
101
+ "Could not get task result", error=request.error.string
102
+ )
103
+
104
+ resp = request.response
105
+
106
+ if int(resp["errorId"]) != 0:
107
+ raise CaptchaException(
108
+ "Could not get task result.",
109
+ error=resp["errorCode"],
110
+ )
111
+
112
+ if resp["status"] == "ready":
113
+ return str(resp["solution"]["gRecaptchaResponse"])
114
+
115
+ time.sleep(1)
116
+ continue
117
+
118
+ raise SolverError("Failed to solve captcha.", error="Max retries reached")
119
+
120
+ def solve_captcha(
121
+ self,
122
+ url: str,
123
+ site_key: str,
124
+ action: str,
125
+ task: Literal["v2", "v3"],
126
+ ) -> str:
127
+ task_id = self._create_task(url, site_key, action, task, self.proxy)
128
+ return self._harvest_task(task_id, self.retries)
@@ -0,0 +1,130 @@
1
+ import json
2
+ import time
3
+ from typing import Literal, Optional
4
+
5
+ from spotapi.exceptions import CaptchaException, SolverError
6
+ from spotapi.http.request import StdClient
7
+
8
+
9
+ class Capsolver:
10
+ BaseURL = "https://api.capsolver.com/"
11
+
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ client: Optional[StdClient] = StdClient(3),
16
+ *,
17
+ proxy: Optional[str] = None,
18
+ retries: Optional[int] = 120,
19
+ ) -> None:
20
+ self.api_key = api_key
21
+ self.client = client
22
+ self.proxy = proxy
23
+ self.retries = retries
24
+
25
+ self.client.authenticate = lambda kwargs: self._auth_rule(kwargs)
26
+
27
+ def _auth_rule(self, kwargs: dict) -> dict:
28
+ if not ("json" in kwargs):
29
+ kwargs["json"] = {}
30
+
31
+ kwargs["json"]["clientKey"] = self.api_key
32
+ return kwargs
33
+
34
+ def get_balance(self) -> float | None:
35
+ endpoint = self.BaseURL + "getBalance"
36
+ request = self.client.post(endpoint, authenticate=True)
37
+
38
+ if request.fail:
39
+ raise CaptchaException(
40
+ "Could not retrieve balance.", error=request.error.string
41
+ )
42
+
43
+ resp = request.response
44
+
45
+ if int(resp["errorId"]) != 0:
46
+ raise CaptchaException(
47
+ "Could not retrieve balance.", error=resp["errorDescription"]
48
+ )
49
+
50
+ return resp["balance"]
51
+
52
+ def _create_task(
53
+ self,
54
+ url: str,
55
+ site_key: str,
56
+ action: str,
57
+ task: Literal["v2", "v3"],
58
+ proxy: Optional[str] = None,
59
+ ) -> str:
60
+ endpoint = self.BaseURL + "createTask"
61
+ task_type = (
62
+ "ReCaptcha{}EnterpriseTask"
63
+ if proxy
64
+ else "ReCaptcha{}EnterpriseTaskProxyLess"
65
+ ).format(task.upper())
66
+ payload = {
67
+ "task": {
68
+ "type": task_type,
69
+ "websiteURL": url,
70
+ "websiteKey": site_key,
71
+ "pageAction": action,
72
+ },
73
+ }
74
+
75
+ if task == "v2":
76
+ payload["task"]["isInvisible"] = True
77
+
78
+ if proxy:
79
+ payload["task"]["proxy"] = proxy
80
+
81
+ request = self.client.post(endpoint, authenticate=True, json=payload)
82
+
83
+ if request.fail:
84
+ raise CaptchaException("Could not create task.", error=request.error.string)
85
+
86
+ resp = request.response
87
+
88
+ if int(resp["errorId"]) != 0:
89
+ raise CaptchaException(
90
+ "Could not create task.", error=resp["errorDescription"]
91
+ )
92
+
93
+ return str(resp["taskId"])
94
+
95
+ def _harvest_task(self, task_id: str, retries: int) -> str | None:
96
+ for _ in range(retries):
97
+ payload = {"taskId": task_id}
98
+ endpoint = self.BaseURL + "getTaskResult"
99
+
100
+ request = self.client.post(endpoint, authenticate=True, json=payload)
101
+
102
+ if request.fail:
103
+ raise CaptchaException(
104
+ "Could not get task result", error=request.error.string
105
+ )
106
+
107
+ resp = request.response
108
+
109
+ if int(resp["errorId"]) != 0:
110
+ raise CaptchaException(
111
+ "Could not get task result.", error=resp["errorDescription"]
112
+ )
113
+
114
+ if resp["status"] == "ready":
115
+ return str(resp["solution"]["gRecaptchaResponse"])
116
+
117
+ time.sleep(1)
118
+ continue
119
+
120
+ raise SolverError("Failed to solve captcha.", error="Max retries reached")
121
+
122
+ def solve_captcha(
123
+ self,
124
+ url: str,
125
+ site_key: str,
126
+ action: str,
127
+ task: Literal["v2", "v3"],
128
+ ) -> str:
129
+ task_id = self._create_task(url, site_key, action, task, self.proxy)
130
+ return self._harvest_task(task_id, self.retries)