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.
- {spotapi-1.0.0/spotapi.egg-info → spotapi-1.0.2}/PKG-INFO +22 -8
- spotapi-1.0.2/README.md +56 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/setup.py +1 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/__init__.py +1 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/artist.py +8 -12
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/client.py +27 -12
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/creator.py +31 -25
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/family.py +10 -10
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/data.py +2 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/request.py +23 -19
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/login.py +23 -11
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/password.py +3 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/playlist.py +15 -24
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/__init__.py +1 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/song.py +13 -10
- spotapi-1.0.2/spotapi/types/__init__.py +22 -0
- {spotapi-1.0.0/spotapi/data → spotapi-1.0.2/spotapi/types}/data.py +3 -4
- {spotapi-1.0.0/spotapi/data → spotapi-1.0.2/spotapi/types}/interfaces.py +12 -7
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/user.py +17 -17
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/logger.py +1 -1
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/saver.py +8 -8
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/websocket.py +3 -2
- {spotapi-1.0.0 → spotapi-1.0.2/spotapi.egg-info}/PKG-INFO +22 -8
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/SOURCES.txt +3 -3
- spotapi-1.0.0/README.md +0 -42
- spotapi-1.0.0/spotapi/data/__init__.py +0 -2
- {spotapi-1.0.0 → spotapi-1.0.2}/LICENSE +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/setup.cfg +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/exceptions/__init__.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/exceptions/errors.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/http/__init__.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/capmonster.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/solvers/capsolver.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/__init__.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi/utils/strings.py +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/dependency_links.txt +0 -0
- {spotapi-1.0.0 → spotapi-1.0.2}/spotapi.egg-info/requires.txt +0 -0
- {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.
|
|
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**:
|
|
21
|
-
- **Private API Access**:
|
|
22
|
-
- **
|
|
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
|
|
47
|
-
playlist.create_playlist("
|
|
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
|
-
|
|
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
|
|
spotapi-1.0.2/README.md
ADDED
|
@@ -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
|
+
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
42
|
+
if self.client_token is None:
|
|
41
43
|
self.get_client_token()
|
|
42
44
|
|
|
43
|
-
if
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self.
|
|
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
|
-
|
|
81
|
+
resp = self.client.post(url, json=payload)
|
|
82
|
+
|
|
81
83
|
|
|
82
|
-
if
|
|
84
|
+
if resp.fail:
|
|
83
85
|
raise GeneratorError(
|
|
84
|
-
"Could not process registration", error=
|
|
86
|
+
"Could not process registration", error=resp.error.string
|
|
85
87
|
)
|
|
86
88
|
|
|
87
|
-
if "challenge" in
|
|
89
|
+
if "challenge" in resp.response:
|
|
88
90
|
self.cfg.logger.attempt("Encountered Embedded Challenge. Defeating...")
|
|
89
|
-
AccountChallenge(self.client,
|
|
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
|
-
|
|
115
|
+
resp = self.client.post(url, json=payload)
|
|
116
|
+
|
|
113
117
|
|
|
114
|
-
if
|
|
118
|
+
if resp.fail:
|
|
115
119
|
raise GeneratorError(
|
|
116
|
-
"Could not get challenge session", error=
|
|
120
|
+
"Could not get challenge session", error=resp.error.string
|
|
117
121
|
)
|
|
118
122
|
|
|
119
|
-
self.challenge_url = parse_json_string(
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
|
|
143
|
+
if resp.fail:
|
|
139
144
|
raise GeneratorError(
|
|
140
|
-
"Could not submit challenge", error=
|
|
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
|
-
|
|
153
|
+
resp = self.client.post(url, json=payload)
|
|
154
|
+
|
|
149
155
|
|
|
150
|
-
if
|
|
156
|
+
if resp.fail:
|
|
151
157
|
raise GeneratorError(
|
|
152
|
-
"Could not complete challenge", error=
|
|
158
|
+
"Could not complete challenge", error=resp.error.string
|
|
153
159
|
)
|
|
154
160
|
|
|
155
|
-
if not ("success" in
|
|
156
|
-
raise GeneratorError("Could not complete challenge", error=
|
|
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
|
|
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
|
-
|
|
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
|
|
36
|
-
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:
|
|
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
|
|
111
|
-
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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)
|