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.
Files changed (45) hide show
  1. {spotapi-1.1.8/spotapi.egg-info → spotapi-1.2.0}/PKG-INFO +1 -1
  2. {spotapi-1.1.8 → spotapi-1.2.0}/setup.py +1 -1
  3. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/client.py +5 -3
  4. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/login.py +29 -15
  5. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/status.py +1 -0
  6. {spotapi-1.1.8 → spotapi-1.2.0/spotapi.egg-info}/PKG-INFO +1 -1
  7. {spotapi-1.1.8 → spotapi-1.2.0}/LICENSE +0 -0
  8. {spotapi-1.1.8 → spotapi-1.2.0}/README.md +0 -0
  9. {spotapi-1.1.8 → spotapi-1.2.0}/setup.cfg +0 -0
  10. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/__init__.py +0 -0
  11. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/_tests/__init__.py +0 -0
  12. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/_tests/annotations_test.py +0 -0
  13. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/album.py +0 -0
  14. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/artist.py +0 -0
  15. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/creator.py +0 -0
  16. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/exceptions/__init__.py +0 -0
  17. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/exceptions/errors.py +0 -0
  18. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/family.py +0 -0
  19. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/__init__.py +0 -0
  20. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/data.py +0 -0
  21. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/http/request.py +0 -0
  22. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/password.py +0 -0
  23. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/player.py +0 -0
  24. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/playlist.py +0 -0
  25. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/podcast.py +0 -0
  26. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/public.py +0 -0
  27. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/__init__.py +0 -0
  28. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/capmonster.py +0 -0
  29. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/solvers/capsolver.py +0 -0
  30. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/song.py +0 -0
  31. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/__init__.py +0 -0
  32. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/alias.py +0 -0
  33. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/annotations.py +0 -0
  34. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/data.py +0 -0
  35. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/types/interfaces.py +0 -0
  36. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/user.py +0 -0
  37. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/__init__.py +0 -0
  38. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/logger.py +0 -0
  39. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/saver.py +0 -0
  40. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/utils/strings.py +0 -0
  41. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi/websocket.py +0 -0
  42. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/SOURCES.txt +0 -0
  43. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/dependency_links.txt +0 -0
  44. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/requires.txt +0 -0
  45. {spotapi-1.1.8 → spotapi-1.2.0}/spotapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spotapi
3
- Version: 1.1.8
3
+ Version: 1.2.0
4
4
  Summary: A sleek API wrapper for Spotify's private API
5
5
  Home-page: UNKNOWN
6
6
  Author: Aran
@@ -55,5 +55,5 @@ setup(
55
55
  ],
56
56
  long_description=long_description,
57
57
  long_description_content_type="text/markdown",
58
- version="1.1.8",
58
+ version="1.2.0",
59
59
  )
@@ -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([53,53,48,55,49,52,53,56,53,51,52,56,55,52,57,57,53,57,50,50,52,56,54,51,48,51,50,57,51,52,55])
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": 5,
107
- "ts": timestamp,
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
- # Some additional cookies
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://accounts.spotify.com/en/status",
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
- """Logins the user"""
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
- self.challenge_url,
302
- "6LfCVLAUAAAAALFwwRnnCJ12DalriUGbj8FW_J39",
316
+ "https://challenge.spotify.com",
317
+ "6LeO36obAAAAALSBZrY6RYM1hcAY7RLvpDDcJLy3",
303
318
  "accounts/login",
304
- "v3",
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
- "recaptcha_challenge_id": {"solve": {"recaptcha_token": captcha_response}},
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["Completed"]["Hash"]
337
- self.interaction_reference = resp.response["Completed"]["InteractionReference"]
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}&interactRef={self.interaction_reference}&hash={self.interaction_hash}"
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()
@@ -18,6 +18,7 @@ __all__ = [
18
18
  R = TypeVar("R")
19
19
  P = ParamSpec("P")
20
20
 
21
+
21
22
  @enforce
22
23
  class PlayerStatus(WebsocketStreamer):
23
24
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spotapi
3
- Version: 1.1.8
3
+ Version: 1.2.0
4
4
  Summary: A sleek API wrapper for Spotify's private API
5
5
  Home-page: UNKNOWN
6
6
  Author: Aran
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