spotapi 1.0.0__tar.gz → 1.0.2__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 (38) hide show
  1. {spotapi-1.0.0/spotapi.egg-info → spotapi-1.0.2}/PKG-INFO +22 -8
  2. spotapi-1.0.2/README.md +56 -0
  3. {spotapi-1.0.0 → spotapi-1.0.2}/setup.py +1 -1
  4. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/__init__.py +1 -1
  5. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/artist.py +8 -12
  6. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/client.py +27 -12
  7. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/creator.py +31 -25
  8. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/family.py +10 -10
  9. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/data.py +2 -1
  10. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/request.py +23 -19
  11. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/login.py +23 -11
  12. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/password.py +3 -1
  13. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/playlist.py +15 -24
  14. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/__init__.py +1 -1
  15. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/song.py +13 -10
  16. spotapi-1.0.2/spotapi/types/__init__.py +22 -0
  17. {spotapi-1.0.0/spotapi/data → spotapi-1.0.2/spotapi/types}/data.py +3 -4
  18. {spotapi-1.0.0/spotapi/data → spotapi-1.0.2/spotapi/types}/interfaces.py +12 -7
  19. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/user.py +17 -17
  20. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/logger.py +1 -1
  21. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/saver.py +8 -8
  22. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/websocket.py +3 -2
  23. {spotapi-1.0.0 → spotapi-1.0.2/spotapi.egg-info}/PKG-INFO +22 -8
  24. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/SOURCES.txt +3 -3
  25. spotapi-1.0.0/README.md +0 -42
  26. spotapi-1.0.0/spotapi/data/__init__.py +0 -2
  27. {spotapi-1.0.0 → spotapi-1.0.2}/LICENSE +0 -0
  28. {spotapi-1.0.0 → spotapi-1.0.2}/setup.cfg +0 -0
  29. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/exceptions/__init__.py +0 -0
  30. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/exceptions/errors.py +0 -0
  31. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/__init__.py +0 -0
  32. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/capmonster.py +0 -0
  33. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/capsolver.py +0 -0
  34. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/__init__.py +0 -0
  35. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/strings.py +0 -0
  36. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/dependency_links.txt +0 -0
  37. {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/requires.txt +0 -0
  38. {spotapi-1.0.0 → spotapi-1.0.2}/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.0.0
3
+ Version: 1.0.2
4
4
  Summary: A sleek API wrapper for Spotify's private API
5
5
  Home-page: UNKNOWN
6
6
  Author: Aran
@@ -17,9 +17,19 @@ Welcome to SpotAPI! This Python library is designed to interact with the private
17
17
  **Note**: This project is intended solely for educational purposes and should be used responsibly. Accessing private endpoints and scraping data without proper authorization may violate Spotify's terms of service
18
18
 
19
19
  ## Features
20
- - **Public API Access**: Fetch and manipulate public Spotify data such as playlists, albums, and tracks.
21
- - **Private API Access**: Experiment with less commonly used Spotify endpoints.
22
- - **Browser-like Requests**: Mimics the HTTP requests Spotify makes in the browser perfectly, providing an accurate representation of what you’d see on the web interface.
20
+ - **Public API Access**: Retrieve and manipulate public Spotify data such as playlists, albums, and tracks with ease.
21
+ - **Private API Access**: Explore private Spotify endpoints to tailor your application to your needs.
22
+ - **Ready to Use**: **SpotAPI** is designed for immediate integration, allowing you to accomplish tasks with just a few lines of code.
23
+ - **No API Key Required**: Seamlessly use **SpotAPI** without needing a Spotify API key. It’s straightforward and hassle free!
24
+ - **Browser-like Requests**: Accurately replicate the HTTP requests Spotify makes in the browser, providing a true to web experience while remaining undetected.
25
+
26
+ Everything you can do with Spotify, **SpotAPI** can do with just a user’s login credentials.
27
+
28
+
29
+ ## Installation
30
+ ```
31
+ pip install spotapi
32
+ ```
23
33
 
24
34
  ## Quick Example
25
35
  ```py
@@ -33,7 +43,7 @@ from spotapi import (
33
43
  )
34
44
 
35
45
  cfg = Config(
36
- solver=solver_clients.Capsolver("YOUR_API_KEY", proxy="YOUR_PROXY"),
46
+ solver=solver_clients.Capsolver("YOUR_API_KEY", proxy="YOUR_PROXY"), # Proxy is optional
37
47
  logger=NoopLogger(),
38
48
  # You can add a proxy by passing a custom TLSClient
39
49
  )
@@ -43,14 +53,18 @@ instance = Login(cfg, "YOUR_PASSWORD", email="YOUR_EMAIL")
43
53
  instance.login()
44
54
 
45
55
  # Do whatever you want now
46
- playlist = PrivatePlaylist(instance, "SOME_PLAYLIST")
47
- playlist.create_playlist("Spotapi Showcase!")
56
+ playlist = PrivatePlaylist(instance)
57
+ playlist.create_playlist("SpotAPI Showcase!")
48
58
 
49
59
  # Save the session
50
60
  instance.save(MongoSaver())
51
61
  ```
52
62
 
53
- [GPL 3.0](https://choosealicense.com/licenses/gpl-3.0/)
63
+ ## Contributing
64
+ Contributions are welcome! If you find any issues or have suggestions, please open an issue or submit a pull request.
65
+
66
+ ## License
67
+ This project is licensed under the **GPL 3.0** License. See [LICENSE](https://choosealicense.com/licenses/gpl-3.0/) for details.
54
68
 
55
69
 
56
70
 
@@ -0,0 +1,56 @@
1
+ # SpotAPI
2
+
3
+ Welcome to SpotAPI! This Python library is designed to interact with the private and public Spotify APIs, emulating the requests typically made through a web browser. This wrapper provides a convenient way to access Spotify’s rich set of features programmatically.
4
+
5
+ **Note**: This project is intended solely for educational purposes and should be used responsibly. Accessing private endpoints and scraping data without proper authorization may violate Spotify's terms of service
6
+
7
+ ## Features
8
+ - **Public API Access**: Retrieve and manipulate public Spotify data such as playlists, albums, and tracks with ease.
9
+ - **Private API Access**: Explore private Spotify endpoints to tailor your application to your needs.
10
+ - **Ready to Use**: **SpotAPI** is designed for immediate integration, allowing you to accomplish tasks with just a few lines of code.
11
+ - **No API Key Required**: Seamlessly use **SpotAPI** without needing a Spotify API key. It’s straightforward and hassle free!
12
+ - **Browser-like Requests**: Accurately replicate the HTTP requests Spotify makes in the browser, providing a true to web experience while remaining undetected.
13
+
14
+ Everything you can do with Spotify, **SpotAPI** can do with just a user’s login credentials.
15
+
16
+
17
+ ## Installation
18
+ ```
19
+ pip install spotapi
20
+ ```
21
+
22
+ ## Quick Example
23
+ ```py
24
+ from spotapi import (
25
+ Login,
26
+ Config,
27
+ NoopLogger,
28
+ solver_clients,
29
+ PrivatePlaylist,
30
+ MongoSaver
31
+ )
32
+
33
+ cfg = Config(
34
+ solver=solver_clients.Capsolver("YOUR_API_KEY", proxy="YOUR_PROXY"), # Proxy is optional
35
+ logger=NoopLogger(),
36
+ # You can add a proxy by passing a custom TLSClient
37
+ )
38
+
39
+ instance = Login(cfg, "YOUR_PASSWORD", email="YOUR_EMAIL")
40
+ # Now we have a valid Login instance to pass around
41
+ instance.login()
42
+
43
+ # Do whatever you want now
44
+ playlist = PrivatePlaylist(instance)
45
+ playlist.create_playlist("SpotAPI Showcase!")
46
+
47
+ # Save the session
48
+ instance.save(MongoSaver())
49
+ ```
50
+
51
+ ## Contributing
52
+ Contributions are welcome! If you find any issues or have suggestions, please open an issue or submit a pull request.
53
+
54
+ ## License
55
+ This project is licensed under the **GPL 3.0** License. See [LICENSE](https://choosealicense.com/licenses/gpl-3.0/) for details.
56
+
@@ -52,5 +52,5 @@ setup(
52
52
  ],
53
53
  long_description=long_description,
54
54
  long_description_content_type="text/markdown",
55
- version="1.0.0",
55
+ version="1.0.2",
56
56
  )
@@ -7,7 +7,7 @@ from spotapi.user import User
7
7
  from spotapi.creator import Creator
8
8
  from spotapi.password import Password
9
9
  from spotapi.solvers import solver_clients, solver_clients_str
10
- from spotapi.data.data import Config
10
+ from spotapi.types.data import Config
11
11
  from spotapi.utils.logger import Logger, NoopLogger
12
12
  from spotapi.utils.saver import *
13
13
  from spotapi.websocket import WebsocketStreamer
@@ -10,17 +10,11 @@ from spotapi.login import Login
10
10
  from spotapi.client import BaseClient
11
11
 
12
12
 
13
- class Artist(BaseClient, Login):
13
+ class Artist:
14
14
  """
15
15
  A class that represents an artist in the Spotify catalog.
16
16
  """
17
17
 
18
- def __new__(cls, login: Optional[Login] = None) -> Artist:
19
- instance = super(Artist, cls).__new__(cls)
20
- if login:
21
- instance.__dict__.update(login.__dict__)
22
- return instance
23
-
24
18
  def __init__(
25
19
  self,
26
20
  login: Optional[Login] = None,
@@ -31,7 +25,7 @@ class Artist(BaseClient, Login):
31
25
  raise ValueError("Must be logged in")
32
26
 
33
27
  self._login: bool = bool(login)
34
- super().__init__(client=login.client if self._login else client)
28
+ self.base = BaseClient(client=login.client if (login is not None) else client) # type: ignore
35
29
 
36
30
  def query_artists(
37
31
  self, query: str, /, limit: Optional[int] = 10, *, offset: Optional[int] = 0
@@ -54,14 +48,15 @@ class Artist(BaseClient, Login):
54
48
  {
55
49
  "persistedQuery": {
56
50
  "version": 1,
57
- "sha256Hash": self.part_hash("searchArtists"),
51
+ "sha256Hash": self.base.part_hash("searchArtists"),
58
52
  }
59
53
  }
60
54
  ),
61
55
  }
62
56
 
63
- resp = self.client.post(url, params=params, authenticate=True)
57
+ resp = self.base.client.post(url, params=params, authenticate=True)
64
58
 
59
+
65
60
  if resp.fail:
66
61
  raise ArtistError("Could not get artists", error=resp.error.string)
67
62
 
@@ -120,12 +115,13 @@ class Artist(BaseClient, Login):
120
115
  "extensions": {
121
116
  "persistedQuery": {
122
117
  "version": 1,
123
- "sha256Hash": self.part_hash(action),
118
+ "sha256Hash": self.base.part_hash(str(action)),
124
119
  },
125
120
  },
126
121
  }
127
122
 
128
- resp = self.client.post(url, json=payload, authenticate=True)
123
+ resp = self.base.client.post(url, json=payload, authenticate=True)
124
+
129
125
 
130
126
  if resp.fail:
131
127
  raise ArtistError("Could not follow artist", error=resp.error.string)
@@ -11,19 +11,21 @@ class BaseClient:
11
11
  """
12
12
  A base class that all the Spotify classes extend.
13
13
  This base class contains all the common methods used by the Spotify classes.
14
+
15
+ NOTE: Should not be used directly. Use the Spotify classes instead.
14
16
  """
15
17
 
16
18
  def __init__(self, client: TLSClient) -> None:
17
19
  self.client = client
18
20
  self.client.authenticate = lambda kwargs: self._auth_rule(kwargs)
19
21
 
20
- self.js_pack: str = None
21
- self.client_version: str = None
22
- self.access_token: str = None
23
- self.client_token: str = None
24
- self.client_id: str = None
25
- self.device_id: str = None
26
- self.raw_hashes: str = None
22
+ self.js_pack: str | None = None
23
+ self.client_version: str | None = None
24
+ self.access_token: str | None = None
25
+ self.client_token: str | None = None
26
+ self.client_id: str | None = None
27
+ self.device_id: str | None = None
28
+ self.raw_hashes: str | None = None
27
29
 
28
30
  self.browser_version = self.client.client_identifier.split("_")[1]
29
31
  self.client.headers.update(
@@ -37,15 +39,16 @@ class BaseClient:
37
39
  atexit.register(self.client.close)
38
40
 
39
41
  def _auth_rule(self, kwargs: dict) -> dict:
40
- if not self.client_token:
42
+ if self.client_token is None:
41
43
  self.get_client_token()
42
44
 
43
- if not self.access_token:
45
+ if self.access_token is None:
44
46
  self.get_session()
45
47
 
46
48
  if not ("headers" in kwargs):
47
49
  kwargs["headers"] = {}
48
-
50
+
51
+ assert self.access_token is not None, "Access token is None"
49
52
  kwargs["headers"].update(
50
53
  {
51
54
  "Authorization": "Bearer " + self.access_token,
@@ -64,6 +67,7 @@ class BaseClient:
64
67
  },
65
68
  )
66
69
 
70
+
67
71
  if resp.fail:
68
72
  raise BaseClientError("Could not get session", error=resp.error.string)
69
73
 
@@ -103,6 +107,7 @@ class BaseClient:
103
107
 
104
108
  resp = self.client.post(url, json=payload, headers=headers)
105
109
 
110
+
106
111
  if resp.fail:
107
112
  raise BaseClientError("Could not get client token", error=resp.error.string)
108
113
 
@@ -117,8 +122,11 @@ class BaseClient:
117
122
  self.client_token = resp.response["granted_token"]["token"]
118
123
 
119
124
  def part_hash(self, name: str) -> str:
120
- if not self.raw_hashes:
125
+ if self.raw_hashes is None:
121
126
  self.get_sha256_hash()
127
+
128
+ if self.raw_hashes is None:
129
+ raise ValueError("Could not get playlist hashes")
122
130
 
123
131
  try:
124
132
  return self.raw_hashes.split(f'"{name}","query","')[1].split('"')[0]
@@ -126,16 +134,22 @@ class BaseClient:
126
134
  return self.raw_hashes.split(f'"{name}","mutation","')[1].split('"')[0]
127
135
 
128
136
  def get_sha256_hash(self) -> None:
129
- if not self.js_pack:
137
+ if self.js_pack is None:
130
138
  self.get_session()
139
+
140
+ if self.js_pack is None:
141
+ raise ValueError("Could not get playlist hashes")
131
142
 
132
143
  resp = self.client.get(self.js_pack)
133
144
 
145
+
134
146
  if resp.fail:
135
147
  raise BaseClientError(
136
148
  "Could not get playlist hashes", error=resp.error.string
137
149
  )
138
150
 
151
+ assert isinstance(resp.response, str), "Invalid HTML response"
152
+
139
153
  self.raw_hashes = resp.response
140
154
  self.client_version = resp.response.split('clientVersion:"')[1].split('"')[0]
141
155
  # Maybe it's static? Let's not take chances.
@@ -149,6 +163,7 @@ class BaseClient:
149
163
  f"https://open.spotifycdn.com/cdn/build/web-player/xpui-routes-search.{self.xpui_route}.js"
150
164
  )
151
165
 
166
+
152
167
  if resp.fail:
153
168
  raise BaseClientError("Could not get xpui hashes", error=resp.error.string)
154
169
 
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  import uuid
3
3
  from typing import Optional
4
- from spotapi.data import Config
4
+ from spotapi.types import Config
5
5
  from spotapi.exceptions import GeneratorError
6
6
  from spotapi.http.request import TLSClient
7
7
  from spotapi.utils.strings import (
@@ -32,15 +32,16 @@ class Creator:
32
32
 
33
33
  def __get_session(self) -> None:
34
34
  url = "https://www.spotify.com/ca-en/signup"
35
- request = self.client.get(url)
35
+ resp = self.client.get(url)
36
36
 
37
- if request.fail:
38
- raise GeneratorError("Could not get session", error=request.error.string)
39
37
 
40
- self.api_key = parse_json_string(request.response, "signupServiceAppKey")
41
- self.installation_id = parse_json_string(request.response, "spT")
42
- self.csrf_token = parse_json_string(request.response, "csrfToken")
43
- self.flow_id = parse_json_string(request.response, "flowId")
38
+ if resp.fail:
39
+ raise GeneratorError("Could not get session", error=resp.error.string)
40
+
41
+ self.api_key = parse_json_string(resp.response, "signupServiceAppKey")
42
+ self.installation_id = parse_json_string(resp.response, "spT")
43
+ self.csrf_token = parse_json_string(resp.response, "csrfToken")
44
+ self.flow_id = parse_json_string(resp.response, "flowId")
44
45
 
45
46
  def __process_register(self, captcha_token: str) -> None:
46
47
  payload = {
@@ -77,19 +78,21 @@ class Creator:
77
78
  }
78
79
  url = "https://spclient.wg.spotify.com/signup/public/v2/account/create"
79
80
 
80
- request = self.client.post(url, json=payload)
81
+ resp = self.client.post(url, json=payload)
82
+
81
83
 
82
- if request.fail:
84
+ if resp.fail:
83
85
  raise GeneratorError(
84
- "Could not process registration", error=request.error.string
86
+ "Could not process registration", error=resp.error.string
85
87
  )
86
88
 
87
- if "challenge" in request.response:
89
+ if "challenge" in resp.response:
88
90
  self.cfg.logger.attempt("Encountered Embedded Challenge. Defeating...")
89
- AccountChallenge(self.client, request.response, self.cfg).defeat_challenge()
91
+ AccountChallenge(self.client, resp.response, self.cfg).defeat_challenge()
90
92
 
91
93
  def register(self) -> None:
92
94
  self.__get_session()
95
+ # Pylance keeps complaining about self keyword. I don't know why.
93
96
  captcha_token = self.cfg.solver.solve_captcha(
94
97
  "https://www.spotify.com/ca-en/signup",
95
98
  "6LfCVLAUAAAAALFwwRnnCJ12DalriUGbj8FW_J39",
@@ -109,14 +112,15 @@ class AccountChallenge:
109
112
  def __get_session(self) -> None:
110
113
  url = "https://challenge.spotify.com/api/v1/get-session"
111
114
  payload = {"session_id": self.session_id}
112
- request = self.client.post(url, json=payload)
115
+ resp = self.client.post(url, json=payload)
116
+
113
117
 
114
- if request.fail:
118
+ if resp.fail:
115
119
  raise GeneratorError(
116
- "Could not get challenge session", error=request.error.string
120
+ "Could not get challenge session", error=resp.error.string
117
121
  )
118
122
 
119
- self.challenge_url = parse_json_string(request.response, "url")
123
+ self.challenge_url = parse_json_string(resp.response, "url")
120
124
 
121
125
  def __submit_challenge(self, token: str) -> None:
122
126
  session_id = self.challenge_url.split("c/")[1].split("/")[0]
@@ -127,7 +131,7 @@ class AccountChallenge:
127
131
  "challenge_id": challenge_id,
128
132
  "recaptcha_challenge_v1": {"solve": {"recaptcha_token": token}},
129
133
  }
130
- request = self.client.post(
134
+ resp = self.client.post(
131
135
  url,
132
136
  json=payload,
133
137
  headers={
@@ -135,9 +139,10 @@ class AccountChallenge:
135
139
  },
136
140
  )
137
141
 
138
- if request.fail:
142
+
143
+ if resp.fail:
139
144
  raise GeneratorError(
140
- "Could not submit challenge", error=request.error.string
145
+ "Could not submit challenge", error=resp.error.string
141
146
  )
142
147
 
143
148
  def __complete_challenge(self) -> None:
@@ -145,15 +150,16 @@ class AccountChallenge:
145
150
  "https://spclient.wg.spotify.com/signup/public/v2/account/complete-creation"
146
151
  )
147
152
  payload = {"session_id": self.session_id}
148
- request = self.client.post(url, json=payload)
153
+ resp = self.client.post(url, json=payload)
154
+
149
155
 
150
- if request.fail:
156
+ if resp.fail:
151
157
  raise GeneratorError(
152
- "Could not complete challenge", error=request.error.string
158
+ "Could not complete challenge", error=resp.error.string
153
159
  )
154
160
 
155
- if not ("success" in request.response):
156
- raise GeneratorError("Could not complete challenge", error=request.response)
161
+ if not ("success" in resp.response):
162
+ raise GeneratorError("Could not complete challenge", error=resp.response)
157
163
 
158
164
  def defeat_challenge(self) -> None:
159
165
  self.__get_session()
@@ -6,17 +6,13 @@ from spotapi.exceptions.errors import FamilyError
6
6
  from spotapi.utils.strings import parse_json_string
7
7
 
8
8
 
9
- class JoinFamily(User):
9
+ class JoinFamily:
10
10
  """
11
11
  Wrapper class for joining a family with a user and a host provided.
12
12
  """
13
13
 
14
- def __new__(cls, user_login: Login, host: "Family", country: str) -> "JoinFamily":
15
- instance = super(User, cls).__new__(cls)
16
- return instance
17
-
18
14
  def __init__(self, user_login: Login, host: "Family", country: str) -> None:
19
- super().__init__(user_login)
15
+ self.user = User(user_login)
20
16
  self.host = host
21
17
  self.country = country
22
18
  self.client = user_login.client
@@ -31,6 +27,7 @@ class JoinFamily(User):
31
27
  url = f"https://www.spotify.com/ca-en/family/join/address/{self.invite_token}/"
32
28
  resp = self.client.get(url)
33
29
 
30
+
34
31
  if resp.fail:
35
32
  raise FamilyError("Could not get session", error=resp.error.string)
36
33
 
@@ -45,6 +42,7 @@ class JoinFamily(User):
45
42
  }
46
43
  resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
47
44
 
45
+
48
46
  if resp.fail:
49
47
  raise FamilyError("Could not get address", error=resp.error.string)
50
48
 
@@ -59,6 +57,7 @@ class JoinFamily(User):
59
57
  }
60
58
  resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
61
59
 
60
+
62
61
  self.csrf = resp.raw.headers.get("X-Csrf-Token")
63
62
  if resp.fail:
64
63
  return False
@@ -84,6 +83,7 @@ class JoinFamily(User):
84
83
  }
85
84
  resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
86
85
 
86
+
87
87
  if resp.fail:
88
88
  raise FamilyError("Could not add to family", error=resp.error.string)
89
89
 
@@ -103,11 +103,12 @@ class Family(User):
103
103
  if not self.has_premium:
104
104
  raise ValueError("Must have premium to use this class")
105
105
 
106
- self._user_family: Mapping[str, Any] = None
106
+ self._user_family: Mapping[str, Any] | None = None
107
107
 
108
108
  def get_family_home(self) -> Mapping[str, Any]:
109
109
  url = "https://www.spotify.com/api/family/v1/family/home/"
110
- resp = self.client.get(url)
110
+ resp = self.login.client.get(url)
111
+
111
112
 
112
113
  if resp.fail:
113
114
  raise FamilyError("Could not get user plan info", error=resp.error.string)
@@ -115,13 +116,12 @@ class Family(User):
115
116
  if not isinstance(resp.response, Mapping):
116
117
  raise FamilyError("Invalid JSON")
117
118
 
118
- self._user_family = resp.response
119
119
  return resp.response
120
120
 
121
121
  @property
122
122
  def members(self) -> List[Mapping[str, Any]]:
123
123
  if self._user_family is None:
124
- self.get_family_home()
124
+ self._user_family = self.get_family_home()
125
125
 
126
126
  return self._user_family["members"]
127
127
 
@@ -3,13 +3,14 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Any, Union
5
5
 
6
+ from requests import Response as StdResponse
6
7
  from tls_client.response import Response as TLSResponse
7
8
 
8
9
 
9
10
  # Dataclass needs to be here to avoid circular imports
10
11
  @dataclass
11
12
  class Response:
12
- raw: TLSResponse
13
+ raw: TLSResponse | StdResponse
13
14
  status_code: int
14
15
  response: Any
15
16
 
@@ -4,6 +4,7 @@ from typing import Any, Callable, Type, Union
4
4
 
5
5
  import requests
6
6
  from tls_client import Session
7
+ from tls_client.settings import ClientIdentifiers
7
8
  from tls_client.exceptions import TLSClientExeption
8
9
  from tls_client.response import Response as TLSResponse
9
10
 
@@ -13,7 +14,7 @@ from spotapi.http.data import Response
13
14
 
14
15
  class StdClient(requests.Session):
15
16
  def __init__(
16
- self, auto_retries: int = 0, auth_rule: Callable[[dict[Any, Any]], dict] = None
17
+ self, auto_retries: int = 0, auth_rule: Callable[[dict[Any, Any]], dict] | None = None
17
18
  ) -> None:
18
19
  super().__init__()
19
20
  self.auto_retries = auto_retries + 1
@@ -23,7 +24,7 @@ class StdClient(requests.Session):
23
24
  def __call__(
24
25
  self, method: str, url: str, **kwargs
25
26
  ) -> Union[requests.Response, None]:
26
- return self.build_request(method, url, kwargs)
27
+ return self.build_request(method, url, **kwargs)
27
28
 
28
29
  def build_request(
29
30
  self, method: str, url: str, **kwargs
@@ -32,8 +33,8 @@ class StdClient(requests.Session):
32
33
  for _ in range(self.auto_retries):
33
34
  try:
34
35
  response = super().request(method.upper(), url, **kwargs)
35
- except Exception as err:
36
- err = err
36
+ except Exception as e:
37
+ err = str(e)
37
38
  continue
38
39
  else:
39
40
  return response
@@ -81,11 +82,11 @@ class StdClient(requests.Session):
81
82
  class TLSClient(Session):
82
83
  def __init__(
83
84
  self,
84
- profile: str,
85
+ profile: ClientIdentifiers,
85
86
  proxy: str,
86
87
  *,
87
88
  auto_retries: int = 0,
88
- auth_rule: Callable[[dict[Any, Any]], dict] = None,
89
+ auth_rule: Callable[[dict[Any, Any]], dict] | None = None,
89
90
  ) -> None:
90
91
  super().__init__(client_identifier=profile, random_tls_extension_order=True)
91
92
 
@@ -94,11 +95,11 @@ class TLSClient(Session):
94
95
 
95
96
  self.auto_retries = auto_retries + 1
96
97
  self.authenticate = auth_rule
97
- self.fail_exception: Type[ParentException] = None
98
+ self.fail_exception: Type[ParentException] | None = None
98
99
  atexit.register(self.close)
99
100
 
100
101
  def __call__(self, method: str, url: str, **kwargs) -> Union[TLSResponse, None]:
101
- return self.build_request(method, url, kwargs)
102
+ return self.build_request(method, url, **kwargs)
102
103
 
103
104
  def build_request(
104
105
  self, method: str, url: str, **kwargs
@@ -107,8 +108,8 @@ class TLSClient(Session):
107
108
  for _ in range(self.auto_retries):
108
109
  try:
109
110
  response = self.execute_request(method.upper(), url, **kwargs)
110
- except TLSClientExeption as err:
111
- err = err
111
+ except TLSClientExeption as e:
112
+ err = str(e)
112
113
  continue
113
114
  else:
114
115
  return response
@@ -126,7 +127,7 @@ class TLSClient(Session):
126
127
  is_dict = True
127
128
 
128
129
  try:
129
- json.loads(body)
130
+ json.loads(body) # type: ignore
130
131
  except json.JSONDecodeError:
131
132
  is_dict = False
132
133
 
@@ -137,7 +138,10 @@ class TLSClient(Session):
137
138
  if not body:
138
139
  body = None
139
140
 
140
- resp = Response(status_code=response.status_code, response=body, raw=response)
141
+ # Why is status_code a None type...
142
+ assert response.status_code is not None, "Status Code is None"
143
+
144
+ resp = Response(status_code=int(response.status_code), response=body, raw=response)
141
145
 
142
146
  if danger and self.fail_exception and resp.fail:
143
147
  raise self.fail_exception(
@@ -149,23 +153,23 @@ class TLSClient(Session):
149
153
 
150
154
  def get(
151
155
  self, url: str, *, authenticate: bool = False, **kwargs
152
- ) -> Union[Response, None]:
156
+ ) -> Response:
153
157
  """Routes a GET Request"""
154
- if authenticate:
158
+ if authenticate and self.authenticate is not None:
155
159
  kwargs = self.authenticate(kwargs)
156
160
 
157
161
  response = self.build_request("GET", url, allow_redirects=True, **kwargs)
158
162
 
159
163
  if response is None:
160
- return
164
+ raise TLSClientExeption("Request kept failing after retries.")
161
165
 
162
166
  return self.parse_response(response, "GET", True)
163
167
 
164
168
  def post(
165
169
  self, url: str, *, authenticate: bool = False, danger: bool = False, **kwargs
166
- ) -> Union[Response, None]:
170
+ ) -> Response:
167
171
  """Routes a POST Request"""
168
- if authenticate:
172
+ if authenticate and self.authenticate is not None:
169
173
  kwargs = self.authenticate(kwargs)
170
174
 
171
175
  response = self.build_request("POST", url, allow_redirects=True, **kwargs)
@@ -177,9 +181,9 @@ class TLSClient(Session):
177
181
 
178
182
  def put(
179
183
  self, url: str, *, authenticate: bool = False, danger: bool = False, **kwargs
180
- ) -> Union[Response, None]:
184
+ ) -> Response:
181
185
  """Routes a PUT Request"""
182
- if authenticate:
186
+ if authenticate and self.authenticate is not None:
183
187
  kwargs = self.authenticate(kwargs)
184
188
 
185
189
  response = self.build_request("PUT", url, allow_redirects=True, **kwargs)