spotapi 1.1.8__tar.gz → 1.2.0__tar.gz
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-1.1.8/spotapi.egg-info → spotapi-1.2.0}/PKG-INFO +1 -1
- {spotapi-1.1.8 → spotapi-1.2.0}/setup.py +1 -1
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/client.py +5 -3
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/login.py +29 -15
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/status.py +1 -0
- {spotapi-1.1.8 → spotapi-1.2.0/spotapi.egg-info}/PKG-INFO +1 -1
- {spotapi-1.1.8 → spotapi-1.2.0}/LICENSE +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/README.md +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/setup.cfg +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/_tests/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/_tests/annotations_test.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/album.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/artist.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/creator.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/exceptions/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/exceptions/errors.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/family.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/data.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/request.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/password.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/player.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/playlist.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/podcast.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/public.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/capmonster.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/capsolver.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/song.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/alias.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/annotations.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/data.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/interfaces.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/user.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/__init__.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/logger.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/saver.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/strings.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/websocket.py +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/SOURCES.txt +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/dependency_links.txt +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/requires.txt +0 -0
- {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/top_level.txt +0 -0
|
@@ -16,9 +16,11 @@ __all__ = ["BaseClient", "BaseClientError"]
|
|
|
16
16
|
# fmt: off
|
|
17
17
|
# Currently the secret is static but could change at any moment.
|
|
18
18
|
# From the looks of it there is no easy way to get it dynamically per request due to the vaguity of the javascript variables.
|
|
19
|
-
_TOTP_SECRET = bytearray([
|
|
19
|
+
_TOTP_SECRET = bytearray([49, 48, 48, 49, 49, 49, 56, 49, 49, 49, 49, 55, 57, 56, 50, 49, 50, 51, 49, 50, 52, 54, 56, 56, 52, 54, 57, 51, 55, 56, 49,
|
|
20
|
+
51, 50, 54, 52, 52, 50, 56, 49, 57, 57, 52, 55, 57, 50, 51, 54, 53, 51, 53, 57, 49, 49, 51, 54, 52, 49, 48, 54, 50, 50, 49, 51, 49, 48, 55, 51, 48])
|
|
20
21
|
# fmt: on
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
def generate_totp(
|
|
23
25
|
secret: bytes = _TOTP_SECRET,
|
|
24
26
|
algorithm: Callable[[], object] = hashlib.sha1,
|
|
@@ -103,8 +105,8 @@ class BaseClient:
|
|
|
103
105
|
"reason": "init",
|
|
104
106
|
"productType": "web-player",
|
|
105
107
|
"totp": totp,
|
|
106
|
-
"totpVer":
|
|
107
|
-
"
|
|
108
|
+
"totpVer": 9,
|
|
109
|
+
"totpServer": totp,
|
|
108
110
|
}
|
|
109
111
|
resp = self.client.get(
|
|
110
112
|
"https://open.spotify.com/get_access_token", params=query
|
|
@@ -115,7 +115,7 @@ class Login:
|
|
|
115
115
|
|
|
116
116
|
cfg.client.cookies.clear()
|
|
117
117
|
for k, v in cookies.items():
|
|
118
|
-
cfg.client.cookies.set(k, v)
|
|
118
|
+
cfg.client.cookies.set(k, v, domain=".spotify.com", path="/")
|
|
119
119
|
|
|
120
120
|
instantiated = cls(cfg, password, email=cred, username=cred)
|
|
121
121
|
instantiated.logged_in = True
|
|
@@ -168,6 +168,22 @@ class Login:
|
|
|
168
168
|
if resp.fail:
|
|
169
169
|
raise LoginError("Could not get session", error=resp.error.string)
|
|
170
170
|
|
|
171
|
+
def _set_non_otc(self) -> None:
|
|
172
|
+
url = "https://accounts.spotify.com/en/login"
|
|
173
|
+
params = {
|
|
174
|
+
"login_hint": self.identifier_credentials,
|
|
175
|
+
"allow_password": 1,
|
|
176
|
+
"continue": f"https://open.spotify.com/?flow_ctx={self.flow_id}",
|
|
177
|
+
"flow_ctx": self.flow_id,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resp = self.client.get(url, params=params)
|
|
181
|
+
|
|
182
|
+
if resp.fail:
|
|
183
|
+
raise LoginError("Could not get non otc session", error=resp.error.string)
|
|
184
|
+
|
|
185
|
+
self.csrf_token = resp.raw.cookies.get("sp_sso_csrf_token")
|
|
186
|
+
|
|
171
187
|
def _get_session(self) -> None:
|
|
172
188
|
url = "https://accounts.spotify.com/en/login"
|
|
173
189
|
resp = self.client.get(url)
|
|
@@ -177,17 +193,17 @@ class Login:
|
|
|
177
193
|
|
|
178
194
|
self.csrf_token = resp.raw.cookies.get("sp_sso_csrf_token")
|
|
179
195
|
self.flow_id = parse_json_string(resp.response, "flowCtx")
|
|
180
|
-
|
|
196
|
+
|
|
181
197
|
self.client.cookies.set("remember", quote(self.identifier_credentials)) # type: ignore
|
|
182
198
|
self._get_add_cookie()
|
|
199
|
+
self._set_non_otc()
|
|
183
200
|
|
|
184
201
|
def _password_payload(self, captcha_key: str) -> str:
|
|
185
202
|
query = {
|
|
186
203
|
"username": self.identifier_credentials,
|
|
187
204
|
"password": self.password,
|
|
188
|
-
"remember": "true",
|
|
189
205
|
"recaptchaToken": captcha_key,
|
|
190
|
-
"continue": "https://
|
|
206
|
+
"continue": f"https://open.spotify.com/?flow_ctx={self.flow_id}",
|
|
191
207
|
"flowCtx": self.flow_id,
|
|
192
208
|
}
|
|
193
209
|
|
|
@@ -219,7 +235,6 @@ class Login:
|
|
|
219
235
|
self.logger.attempt("Challenge detected, attempting to solve")
|
|
220
236
|
LoginChallenge(self, json_data).defeat()
|
|
221
237
|
self.logger.info("Challenge solved")
|
|
222
|
-
# json_data will still be bad, but we know we are logged in now
|
|
223
238
|
return
|
|
224
239
|
|
|
225
240
|
if "error" not in json_data:
|
|
@@ -238,7 +253,7 @@ class Login:
|
|
|
238
253
|
raise LoginError(f"Unforseen Error", error=f"{str(self)}: {error_type}")
|
|
239
254
|
|
|
240
255
|
def login(self) -> None:
|
|
241
|
-
"""
|
|
256
|
+
"""Preform user login."""
|
|
242
257
|
if self.logged_in:
|
|
243
258
|
raise LoginError("User already logged in")
|
|
244
259
|
|
|
@@ -298,10 +313,10 @@ class LoginChallenge:
|
|
|
298
313
|
raise LoginError("Solver not set")
|
|
299
314
|
|
|
300
315
|
captcha_response = self.l.solver.solve_captcha(
|
|
301
|
-
|
|
302
|
-
"
|
|
316
|
+
"https://challenge.spotify.com",
|
|
317
|
+
"6LeO36obAAAAALSBZrY6RYM1hcAY7RLvpDDcJLy3",
|
|
303
318
|
"accounts/login",
|
|
304
|
-
"
|
|
319
|
+
"v2",
|
|
305
320
|
)
|
|
306
321
|
|
|
307
322
|
if not captcha_response:
|
|
@@ -314,10 +329,9 @@ class LoginChallenge:
|
|
|
314
329
|
payload = {
|
|
315
330
|
"session_id": self.session_id,
|
|
316
331
|
"challenge_id": challenge_id,
|
|
317
|
-
"
|
|
332
|
+
"recaptcha_challenge_v1": {"solve": {"recaptcha_token": captcha_response}},
|
|
318
333
|
}
|
|
319
334
|
headers = {
|
|
320
|
-
"X-Cloud-Trace-Context": "00000000000000006979d1624aa6b213/2238380859227873585;o=1",
|
|
321
335
|
"Content-Type": "application/json",
|
|
322
336
|
}
|
|
323
337
|
|
|
@@ -333,19 +347,19 @@ class LoginChallenge:
|
|
|
333
347
|
if not isinstance(resp.response, Mapping):
|
|
334
348
|
raise LoginError("Invalid JSON")
|
|
335
349
|
|
|
336
|
-
self.interaction_hash = resp.response["
|
|
337
|
-
self.interaction_reference = resp.response["
|
|
350
|
+
self.interaction_hash = resp.response["completed"]["hash"]
|
|
351
|
+
self.interaction_reference = resp.response["completed"]["interaction_reference"]
|
|
338
352
|
|
|
339
353
|
def _complete_challenge(self) -> None:
|
|
340
354
|
# We need to grab the cookies
|
|
341
|
-
url = f"https://accounts.spotify.com/login/challenge-completed?sessionId={self.session_id}&
|
|
342
|
-
|
|
355
|
+
url = f"https://accounts.spotify.com/login/challenge-completed?sessionId={self.session_id}&interact_ref={self.interaction_reference}&hash={self.interaction_hash}"
|
|
343
356
|
resp = self.l.client.get(url)
|
|
344
357
|
|
|
345
358
|
if resp.fail:
|
|
346
359
|
raise LoginError("Could not complete challenge", error=resp.error.string)
|
|
347
360
|
|
|
348
361
|
def defeat(self) -> None:
|
|
362
|
+
"""Defeats the RecaptchaV2 challenge."""
|
|
349
363
|
self._get_challenge()
|
|
350
364
|
self._submit_challenge()
|
|
351
365
|
self._complete_challenge()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|