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/__init__.py +17 -0
- spotapi/artist.py +139 -0
- spotapi/client.py +158 -0
- spotapi/creator.py +168 -0
- spotapi/data/__init__.py +2 -0
- spotapi/data/data.py +18 -0
- spotapi/data/interfaces.py +59 -0
- spotapi/exceptions/__init__.py +15 -0
- spotapi/exceptions/errors.py +68 -0
- spotapi/family.py +130 -0
- spotapi/login.py +260 -0
- spotapi/password.py +76 -0
- spotapi/playlist.py +332 -0
- spotapi/solvers/__init__.py +15 -0
- spotapi/solvers/capmonster.py +128 -0
- spotapi/solvers/capsolver.py +130 -0
- spotapi/song.py +234 -0
- spotapi/user.py +114 -0
- spotapi/websocket.py +89 -0
- spotapi-0.0.0.dist-info/LICENSE +674 -0
- spotapi-0.0.0.dist-info/METADATA +23 -0
- spotapi-0.0.0.dist-info/RECORD +24 -0
- spotapi-0.0.0.dist-info/WHEEL +5 -0
- spotapi-0.0.0.dist-info/top_level.txt +1 -0
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)
|