spotapi 1.0.3__tar.gz → 1.0.4__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.3/spotapi.egg-info → spotapi-1.0.4}/PKG-INFO +79 -2
- spotapi-1.0.4/README.md +130 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/setup.py +7 -4
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/artist.py +4 -6
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/client.py +6 -10
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/creator.py +7 -14
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/family.py +0 -5
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/http/request.py +15 -17
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/login.py +26 -15
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/password.py +0 -2
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/playlist.py +2 -3
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/solvers/capmonster.py +4 -4
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/solvers/capsolver.py +6 -7
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/song.py +8 -6
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/types/__init__.py +7 -6
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/types/interfaces.py +8 -8
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/user.py +2 -6
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/utils/saver.py +1 -3
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/websocket.py +2 -3
- {spotapi-1.0.3 → spotapi-1.0.4/spotapi.egg-info}/PKG-INFO +79 -2
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi.egg-info/requires.txt +8 -2
- spotapi-1.0.3/README.md +0 -56
- {spotapi-1.0.3 → spotapi-1.0.4}/LICENSE +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/setup.cfg +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/__init__.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/exceptions/__init__.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/exceptions/errors.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/http/__init__.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/http/data.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/solvers/__init__.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/types/data.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/utils/__init__.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/utils/logger.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi/utils/strings.py +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi.egg-info/SOURCES.txt +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/spotapi.egg-info/dependency_links.txt +0 -0
- {spotapi-1.0.3 → spotapi-1.0.4}/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.4
|
|
4
4
|
Summary: A sleek API wrapper for Spotify's private API
|
|
5
5
|
Home-page: UNKNOWN
|
|
6
6
|
Author: Aran
|
|
@@ -8,6 +8,9 @@ License: UNKNOWN
|
|
|
8
8
|
Keywords: Spotify,API,Spotify API,Spotify Private API,Follow,Like,Creator,Music,Music API,Streaming,Music Data,Track,Playlist,Album,Artist,Music Search,Music Metadata,SpotAPI,Python Spotify Wrapper,Music Automation,Web Scraping,Python Music API,Spotify Integration,Spotify Playlist,Spotify Tracks
|
|
9
9
|
Platform: UNKNOWN
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: websocket
|
|
12
|
+
Provides-Extra: redis
|
|
13
|
+
Provides-Extra: pymongo
|
|
11
14
|
License-File: LICENSE
|
|
12
15
|
|
|
13
16
|
# SpotAPI
|
|
@@ -16,6 +19,18 @@ Welcome to SpotAPI! This Python library is designed to interact with the private
|
|
|
16
19
|
|
|
17
20
|
**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
21
|
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
1. [Introduction](#spotapi)
|
|
25
|
+
2. [Features](#features)
|
|
26
|
+
3. [Installation](#installation)
|
|
27
|
+
4. [Import Cookies](#import-cookies)
|
|
28
|
+
5. [Quick Examples](#quick-examples)
|
|
29
|
+
6. [Contributing](#contributing)
|
|
30
|
+
7. [Roadmap](#roadmap)
|
|
31
|
+
8. [License](#license)
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
## Features
|
|
20
35
|
- **Public API Access**: Retrieve and manipulate public Spotify data such as playlists, albums, and tracks with ease.
|
|
21
36
|
- **Private API Access**: Explore private Spotify endpoints to tailor your application to your needs.
|
|
@@ -31,7 +46,27 @@ Everything you can do with Spotify, **SpotAPI** can do with just a user’s
|
|
|
31
46
|
pip install spotapi
|
|
32
47
|
```
|
|
33
48
|
|
|
34
|
-
##
|
|
49
|
+
## Import Cookies
|
|
50
|
+
If you prefer not to use a third party CAPTCHA solver, you can import cookies to manage your session.
|
|
51
|
+
|
|
52
|
+
### Steps to Import Cookies:
|
|
53
|
+
|
|
54
|
+
1. **Choose a Session Saver**:
|
|
55
|
+
- Select a session saver for storing your session data.
|
|
56
|
+
- For simplicity, you should use `JSONSaver`, especially if performance or quantity of sessions is not a big concern.
|
|
57
|
+
|
|
58
|
+
2. **Prepare Session Data**:
|
|
59
|
+
- Create an object with the following keys:
|
|
60
|
+
- **`identifier`**: This should be your email address or username.
|
|
61
|
+
- **`cookies`**: These are the cookies you obtain when logged in. To get these cookies, visit [Spotify](https://open.spotify.com/), log in, and copy the cookies from your browser.
|
|
62
|
+
- It can be a dict[str, str] or a string representation
|
|
63
|
+
|
|
64
|
+
3. **Load the Session**:
|
|
65
|
+
- Use your program to load the session manually with the saved data. This will enable you to use Spotify with a fully functional session without needing additional CAPTCHA solving.
|
|
66
|
+
|
|
67
|
+
## Quick Examples
|
|
68
|
+
|
|
69
|
+
### With User Authentication
|
|
35
70
|
```py
|
|
36
71
|
from spotapi import (
|
|
37
72
|
Login,
|
|
@@ -60,9 +95,51 @@ playlist.create_playlist("SpotAPI Showcase!")
|
|
|
60
95
|
instance.save(MongoSaver())
|
|
61
96
|
```
|
|
62
97
|
|
|
98
|
+
### Without User Authentication
|
|
99
|
+
```py
|
|
100
|
+
"""Here's the example from spotipy https://github.com/spotipy-dev/spotipy?tab=readme-ov-file#quick-start"""
|
|
101
|
+
from spotapi import Song
|
|
102
|
+
|
|
103
|
+
song = Song()
|
|
104
|
+
gen = song.paginate_songs("weezer")
|
|
105
|
+
|
|
106
|
+
# Paginates 100 songs at a time till there's no more
|
|
107
|
+
for batch in gen:
|
|
108
|
+
for idx, item in enumerate(batch):
|
|
109
|
+
print(idx, item['item']['data']['name'])
|
|
110
|
+
|
|
111
|
+
# ^ ONLY 6 LINES OF CODE
|
|
112
|
+
|
|
113
|
+
# Alternatively, you can query a specfic amount
|
|
114
|
+
songs = song.query_songs("weezer", limit=20)
|
|
115
|
+
data = songs["data"]["searchV2"]["tracksV2"]["items"]
|
|
116
|
+
for idx, item in enumerate(data):
|
|
117
|
+
print(idx, item['item']['data']['name'])
|
|
118
|
+
```
|
|
119
|
+
### Results
|
|
120
|
+
```
|
|
121
|
+
0 Island In The Sun
|
|
122
|
+
1 Say It Ain't So
|
|
123
|
+
2 Buddy Holly
|
|
124
|
+
.
|
|
125
|
+
.
|
|
126
|
+
.
|
|
127
|
+
18 Holiday
|
|
128
|
+
19 We Are All On d***s
|
|
129
|
+
```
|
|
130
|
+
|
|
63
131
|
## Contributing
|
|
64
132
|
Contributions are welcome! If you find any issues or have suggestions, please open an issue or submit a pull request.
|
|
65
133
|
|
|
134
|
+
## Roadmap
|
|
135
|
+
> I'll most likely do these if the project gains some traction
|
|
136
|
+
|
|
137
|
+
- [ ] No Captcha For Login (**100 Stars**)
|
|
138
|
+
- [ ] In Depth Documentation
|
|
139
|
+
- [ ] Websocket Listener (Is not working ATM)
|
|
140
|
+
- [ ] Player
|
|
141
|
+
- [ ] More wrappers around this project
|
|
142
|
+
|
|
66
143
|
## License
|
|
67
144
|
This project is licensed under the **GPL 3.0** License. See [LICENSE](https://choosealicense.com/licenses/gpl-3.0/) for details.
|
|
68
145
|
|
spotapi-1.0.4/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Introduction](#spotapi)
|
|
10
|
+
2. [Features](#features)
|
|
11
|
+
3. [Installation](#installation)
|
|
12
|
+
4. [Import Cookies](#import-cookies)
|
|
13
|
+
5. [Quick Examples](#quick-examples)
|
|
14
|
+
6. [Contributing](#contributing)
|
|
15
|
+
7. [Roadmap](#roadmap)
|
|
16
|
+
8. [License](#license)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Features
|
|
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
|
+
```
|
|
33
|
+
|
|
34
|
+
## Import Cookies
|
|
35
|
+
If you prefer not to use a third party CAPTCHA solver, you can import cookies to manage your session.
|
|
36
|
+
|
|
37
|
+
### Steps to Import Cookies:
|
|
38
|
+
|
|
39
|
+
1. **Choose a Session Saver**:
|
|
40
|
+
- Select a session saver for storing your session data.
|
|
41
|
+
- For simplicity, you should use `JSONSaver`, especially if performance or quantity of sessions is not a big concern.
|
|
42
|
+
|
|
43
|
+
2. **Prepare Session Data**:
|
|
44
|
+
- Create an object with the following keys:
|
|
45
|
+
- **`identifier`**: This should be your email address or username.
|
|
46
|
+
- **`cookies`**: These are the cookies you obtain when logged in. To get these cookies, visit [Spotify](https://open.spotify.com/), log in, and copy the cookies from your browser.
|
|
47
|
+
- It can be a dict[str, str] or a string representation
|
|
48
|
+
|
|
49
|
+
3. **Load the Session**:
|
|
50
|
+
- Use your program to load the session manually with the saved data. This will enable you to use Spotify with a fully functional session without needing additional CAPTCHA solving.
|
|
51
|
+
|
|
52
|
+
## Quick Examples
|
|
53
|
+
|
|
54
|
+
### With User Authentication
|
|
55
|
+
```py
|
|
56
|
+
from spotapi import (
|
|
57
|
+
Login,
|
|
58
|
+
Config,
|
|
59
|
+
NoopLogger,
|
|
60
|
+
solver_clients,
|
|
61
|
+
PrivatePlaylist,
|
|
62
|
+
MongoSaver
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
cfg = Config(
|
|
66
|
+
solver=solver_clients.Capsolver("YOUR_API_KEY", proxy="YOUR_PROXY"), # Proxy is optional
|
|
67
|
+
logger=NoopLogger(),
|
|
68
|
+
# You can add a proxy by passing a custom TLSClient
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
instance = Login(cfg, "YOUR_PASSWORD", email="YOUR_EMAIL")
|
|
72
|
+
# Now we have a valid Login instance to pass around
|
|
73
|
+
instance.login()
|
|
74
|
+
|
|
75
|
+
# Do whatever you want now
|
|
76
|
+
playlist = PrivatePlaylist(instance)
|
|
77
|
+
playlist.create_playlist("SpotAPI Showcase!")
|
|
78
|
+
|
|
79
|
+
# Save the session
|
|
80
|
+
instance.save(MongoSaver())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Without User Authentication
|
|
84
|
+
```py
|
|
85
|
+
"""Here's the example from spotipy https://github.com/spotipy-dev/spotipy?tab=readme-ov-file#quick-start"""
|
|
86
|
+
from spotapi import Song
|
|
87
|
+
|
|
88
|
+
song = Song()
|
|
89
|
+
gen = song.paginate_songs("weezer")
|
|
90
|
+
|
|
91
|
+
# Paginates 100 songs at a time till there's no more
|
|
92
|
+
for batch in gen:
|
|
93
|
+
for idx, item in enumerate(batch):
|
|
94
|
+
print(idx, item['item']['data']['name'])
|
|
95
|
+
|
|
96
|
+
# ^ ONLY 6 LINES OF CODE
|
|
97
|
+
|
|
98
|
+
# Alternatively, you can query a specfic amount
|
|
99
|
+
songs = song.query_songs("weezer", limit=20)
|
|
100
|
+
data = songs["data"]["searchV2"]["tracksV2"]["items"]
|
|
101
|
+
for idx, item in enumerate(data):
|
|
102
|
+
print(idx, item['item']['data']['name'])
|
|
103
|
+
```
|
|
104
|
+
### Results
|
|
105
|
+
```
|
|
106
|
+
0 Island In The Sun
|
|
107
|
+
1 Say It Ain't So
|
|
108
|
+
2 Buddy Holly
|
|
109
|
+
.
|
|
110
|
+
.
|
|
111
|
+
.
|
|
112
|
+
18 Holiday
|
|
113
|
+
19 We Are All On d***s
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
Contributions are welcome! If you find any issues or have suggestions, please open an issue or submit a pull request.
|
|
118
|
+
|
|
119
|
+
## Roadmap
|
|
120
|
+
> I'll most likely do these if the project gains some traction
|
|
121
|
+
|
|
122
|
+
- [ ] No Captcha For Login (**100 Stars**)
|
|
123
|
+
- [ ] In Depth Documentation
|
|
124
|
+
- [ ] Websocket Listener (Is not working ATM)
|
|
125
|
+
- [ ] Player
|
|
126
|
+
- [ ] More wrappers around this project
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
This project is licensed under the **GPL 3.0** License. See [LICENSE](https://choosealicense.com/licenses/gpl-3.0/) for details.
|
|
130
|
+
|
|
@@ -5,14 +5,16 @@ __install_require__ = [
|
|
|
5
5
|
"requests",
|
|
6
6
|
"colorama",
|
|
7
7
|
"Pillow",
|
|
8
|
-
"pymongo",
|
|
9
8
|
"readerwriterlock",
|
|
10
|
-
"redis",
|
|
11
9
|
"tls_client",
|
|
12
10
|
"typing_extensions",
|
|
13
11
|
"validators",
|
|
14
|
-
"websockets",
|
|
15
12
|
]
|
|
13
|
+
__extras__ = {
|
|
14
|
+
"websocket": ["websockets"],
|
|
15
|
+
"redis": ["redis"],
|
|
16
|
+
"pymongo": ["pymongo"],
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
with open("README.md", "r") as f:
|
|
18
20
|
long_description = f.read()
|
|
@@ -23,6 +25,7 @@ setup(
|
|
|
23
25
|
description=__description__,
|
|
24
26
|
packages=find_packages(),
|
|
25
27
|
install_requires=__install_require__,
|
|
28
|
+
extras_require=__extras__,
|
|
26
29
|
keywords=[
|
|
27
30
|
"Spotify",
|
|
28
31
|
"API",
|
|
@@ -52,5 +55,5 @@ setup(
|
|
|
52
55
|
],
|
|
53
56
|
long_description=long_description,
|
|
54
57
|
long_description_content_type="text/markdown",
|
|
55
|
-
version="1.0.
|
|
58
|
+
version="1.0.4",
|
|
56
59
|
)
|
|
@@ -19,16 +19,16 @@ class Artist:
|
|
|
19
19
|
self,
|
|
20
20
|
login: Optional[Login] = None,
|
|
21
21
|
*,
|
|
22
|
-
client:
|
|
22
|
+
client: TLSClient = TLSClient("chrome_120", "", auto_retries=3),
|
|
23
23
|
) -> None:
|
|
24
24
|
if login and not login.logged_in:
|
|
25
25
|
raise ValueError("Must be logged in")
|
|
26
26
|
|
|
27
27
|
self._login: bool = bool(login)
|
|
28
|
-
self.base = BaseClient(client=login.client if (login is not None) else client)
|
|
28
|
+
self.base = BaseClient(client=login.client if (login is not None) else client) # type: ignore
|
|
29
29
|
|
|
30
30
|
def query_artists(
|
|
31
|
-
self, query: str, /, limit:
|
|
31
|
+
self, query: str, /, limit: int = 10, *, offset: int = 0
|
|
32
32
|
) -> Mapping[str, Any]:
|
|
33
33
|
"""Searches for an artist in the Spotify catalog"""
|
|
34
34
|
url = "https://api-partner.spotify.com/pathfinder/v1/query"
|
|
@@ -56,7 +56,6 @@ class Artist:
|
|
|
56
56
|
|
|
57
57
|
resp = self.base.client.post(url, params=params, authenticate=True)
|
|
58
58
|
|
|
59
|
-
|
|
60
59
|
if resp.fail:
|
|
61
60
|
raise ArtistError("Could not get artists", error=resp.error.string)
|
|
62
61
|
|
|
@@ -96,7 +95,7 @@ class Artist:
|
|
|
96
95
|
artist_id: str,
|
|
97
96
|
/,
|
|
98
97
|
*,
|
|
99
|
-
action:
|
|
98
|
+
action: Literal["addToLibrary", "removeFromLibrary"] = "addToLibrary",
|
|
100
99
|
) -> None:
|
|
101
100
|
if not self._login:
|
|
102
101
|
raise ValueError("Must be logged in")
|
|
@@ -122,7 +121,6 @@ class Artist:
|
|
|
122
121
|
|
|
123
122
|
resp = self.base.client.post(url, json=payload, authenticate=True)
|
|
124
123
|
|
|
125
|
-
|
|
126
124
|
if resp.fail:
|
|
127
125
|
raise ArtistError("Could not follow artist", error=resp.error.string)
|
|
128
126
|
|
|
@@ -11,7 +11,7 @@ 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
|
-
|
|
14
|
+
|
|
15
15
|
NOTE: Should not be used directly. Use the Spotify classes instead.
|
|
16
16
|
"""
|
|
17
17
|
|
|
@@ -45,9 +45,9 @@ class BaseClient:
|
|
|
45
45
|
if self.access_token is None:
|
|
46
46
|
self.get_session()
|
|
47
47
|
|
|
48
|
-
if
|
|
48
|
+
if "headers" not in kwargs:
|
|
49
49
|
kwargs["headers"] = {}
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
assert self.access_token is not None, "Access token is None"
|
|
52
52
|
kwargs["headers"].update(
|
|
53
53
|
{
|
|
@@ -67,7 +67,6 @@ class BaseClient:
|
|
|
67
67
|
},
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
|
|
71
70
|
if resp.fail:
|
|
72
71
|
raise BaseClientError("Could not get session", error=resp.error.string)
|
|
73
72
|
|
|
@@ -107,7 +106,6 @@ class BaseClient:
|
|
|
107
106
|
|
|
108
107
|
resp = self.client.post(url, json=payload, headers=headers)
|
|
109
108
|
|
|
110
|
-
|
|
111
109
|
if resp.fail:
|
|
112
110
|
raise BaseClientError("Could not get client token", error=resp.error.string)
|
|
113
111
|
|
|
@@ -124,7 +122,7 @@ class BaseClient:
|
|
|
124
122
|
def part_hash(self, name: str) -> str:
|
|
125
123
|
if self.raw_hashes is None:
|
|
126
124
|
self.get_sha256_hash()
|
|
127
|
-
|
|
125
|
+
|
|
128
126
|
if self.raw_hashes is None:
|
|
129
127
|
raise ValueError("Could not get playlist hashes")
|
|
130
128
|
|
|
@@ -136,20 +134,19 @@ class BaseClient:
|
|
|
136
134
|
def get_sha256_hash(self) -> None:
|
|
137
135
|
if self.js_pack is None:
|
|
138
136
|
self.get_session()
|
|
139
|
-
|
|
137
|
+
|
|
140
138
|
if self.js_pack is None:
|
|
141
139
|
raise ValueError("Could not get playlist hashes")
|
|
142
140
|
|
|
143
141
|
resp = self.client.get(self.js_pack)
|
|
144
142
|
|
|
145
|
-
|
|
146
143
|
if resp.fail:
|
|
147
144
|
raise BaseClientError(
|
|
148
145
|
"Could not get playlist hashes", error=resp.error.string
|
|
149
146
|
)
|
|
150
147
|
|
|
151
148
|
assert isinstance(resp.response, str), "Invalid HTML response"
|
|
152
|
-
|
|
149
|
+
|
|
153
150
|
self.raw_hashes = resp.response
|
|
154
151
|
self.client_version = resp.response.split('clientVersion:"')[1].split('"')[0]
|
|
155
152
|
# Maybe it's static? Let's not take chances.
|
|
@@ -163,7 +160,6 @@ class BaseClient:
|
|
|
163
160
|
f"https://open.spotifycdn.com/cdn/build/web-player/xpui-routes-search.{self.xpui_route}.js"
|
|
164
161
|
)
|
|
165
162
|
|
|
166
|
-
|
|
167
163
|
if resp.fail:
|
|
168
164
|
raise BaseClientError("Could not get xpui hashes", error=resp.error.string)
|
|
169
165
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import uuid
|
|
3
|
-
from typing import Optional
|
|
4
3
|
from spotapi.types import Config
|
|
5
4
|
from spotapi.exceptions import GeneratorError
|
|
6
5
|
from spotapi.http.request import TLSClient
|
|
@@ -20,11 +19,13 @@ class Creator:
|
|
|
20
19
|
def __init__(
|
|
21
20
|
self,
|
|
22
21
|
cfg: Config,
|
|
23
|
-
email:
|
|
24
|
-
|
|
22
|
+
email: str = random_email(),
|
|
23
|
+
display_name: str = random_string(10),
|
|
24
|
+
password: str = random_string(10, True),
|
|
25
25
|
) -> None:
|
|
26
26
|
self.email = email
|
|
27
27
|
self.password = password
|
|
28
|
+
self.display_name = display_name
|
|
28
29
|
self.cfg = cfg
|
|
29
30
|
|
|
30
31
|
self.client = self.cfg.client
|
|
@@ -34,7 +35,6 @@ class Creator:
|
|
|
34
35
|
url = "https://www.spotify.com/ca-en/signup"
|
|
35
36
|
resp = self.client.get(url)
|
|
36
37
|
|
|
37
|
-
|
|
38
38
|
if resp.fail:
|
|
39
39
|
raise GeneratorError("Could not get session", error=resp.error.string)
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@ class Creator:
|
|
|
52
52
|
"send_email": True,
|
|
53
53
|
"third_party_email": False,
|
|
54
54
|
},
|
|
55
|
-
"display_name":
|
|
55
|
+
"display_name": self.display_name,
|
|
56
56
|
"email_and_password_identifier": {
|
|
57
57
|
"email": self.email,
|
|
58
58
|
"password": self.password,
|
|
@@ -80,7 +80,6 @@ class Creator:
|
|
|
80
80
|
|
|
81
81
|
resp = self.client.post(url, json=payload)
|
|
82
82
|
|
|
83
|
-
|
|
84
83
|
if resp.fail:
|
|
85
84
|
raise GeneratorError(
|
|
86
85
|
"Could not process registration", error=resp.error.string
|
|
@@ -92,7 +91,6 @@ class Creator:
|
|
|
92
91
|
|
|
93
92
|
def register(self) -> None:
|
|
94
93
|
self.__get_session()
|
|
95
|
-
# Pylance keeps complaining about self keyword. I don't know why.
|
|
96
94
|
captcha_token = self.cfg.solver.solve_captcha(
|
|
97
95
|
"https://www.spotify.com/ca-en/signup",
|
|
98
96
|
"6LfCVLAUAAAAALFwwRnnCJ12DalriUGbj8FW_J39",
|
|
@@ -114,7 +112,6 @@ class AccountChallenge:
|
|
|
114
112
|
payload = {"session_id": self.session_id}
|
|
115
113
|
resp = self.client.post(url, json=payload)
|
|
116
114
|
|
|
117
|
-
|
|
118
115
|
if resp.fail:
|
|
119
116
|
raise GeneratorError(
|
|
120
117
|
"Could not get challenge session", error=resp.error.string
|
|
@@ -139,11 +136,8 @@ class AccountChallenge:
|
|
|
139
136
|
},
|
|
140
137
|
)
|
|
141
138
|
|
|
142
|
-
|
|
143
139
|
if resp.fail:
|
|
144
|
-
raise GeneratorError(
|
|
145
|
-
"Could not submit challenge", error=resp.error.string
|
|
146
|
-
)
|
|
140
|
+
raise GeneratorError("Could not submit challenge", error=resp.error.string)
|
|
147
141
|
|
|
148
142
|
def __complete_challenge(self) -> None:
|
|
149
143
|
url = (
|
|
@@ -152,13 +146,12 @@ class AccountChallenge:
|
|
|
152
146
|
payload = {"session_id": self.session_id}
|
|
153
147
|
resp = self.client.post(url, json=payload)
|
|
154
148
|
|
|
155
|
-
|
|
156
149
|
if resp.fail:
|
|
157
150
|
raise GeneratorError(
|
|
158
151
|
"Could not complete challenge", error=resp.error.string
|
|
159
152
|
)
|
|
160
153
|
|
|
161
|
-
if
|
|
154
|
+
if "success" not in resp.response:
|
|
162
155
|
raise GeneratorError("Could not complete challenge", error=resp.response)
|
|
163
156
|
|
|
164
157
|
def defeat_challenge(self) -> None:
|
|
@@ -27,7 +27,6 @@ class JoinFamily:
|
|
|
27
27
|
url = f"https://www.spotify.com/ca-en/family/join/address/{self.invite_token}/"
|
|
28
28
|
resp = self.client.get(url)
|
|
29
29
|
|
|
30
|
-
|
|
31
30
|
if resp.fail:
|
|
32
31
|
raise FamilyError("Could not get session", error=resp.error.string)
|
|
33
32
|
|
|
@@ -42,7 +41,6 @@ class JoinFamily:
|
|
|
42
41
|
}
|
|
43
42
|
resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
|
|
44
43
|
|
|
45
|
-
|
|
46
44
|
if resp.fail:
|
|
47
45
|
raise FamilyError("Could not get address", error=resp.error.string)
|
|
48
46
|
|
|
@@ -57,7 +55,6 @@ class JoinFamily:
|
|
|
57
55
|
}
|
|
58
56
|
resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
|
|
59
57
|
|
|
60
|
-
|
|
61
58
|
self.csrf = resp.raw.headers.get("X-Csrf-Token")
|
|
62
59
|
if resp.fail:
|
|
63
60
|
return False
|
|
@@ -83,7 +80,6 @@ class JoinFamily:
|
|
|
83
80
|
}
|
|
84
81
|
resp = self.client.post(url, headers={"X-Csrf-Token": self.csrf}, json=payload)
|
|
85
82
|
|
|
86
|
-
|
|
87
83
|
if resp.fail:
|
|
88
84
|
raise FamilyError("Could not add to family", error=resp.error.string)
|
|
89
85
|
|
|
@@ -109,7 +105,6 @@ class Family(User):
|
|
|
109
105
|
url = "https://www.spotify.com/api/family/v1/family/home/"
|
|
110
106
|
resp = self.login.client.get(url)
|
|
111
107
|
|
|
112
|
-
|
|
113
108
|
if resp.fail:
|
|
114
109
|
raise FamilyError("Could not get user plan info", error=resp.error.string)
|
|
115
110
|
|
|
@@ -14,7 +14,9 @@ from spotapi.http.data import Response
|
|
|
14
14
|
|
|
15
15
|
class StdClient(requests.Session):
|
|
16
16
|
def __init__(
|
|
17
|
-
self,
|
|
17
|
+
self,
|
|
18
|
+
auto_retries: int = 0,
|
|
19
|
+
auth_rule: Callable[[dict[Any, Any]], dict] | None = None,
|
|
18
20
|
) -> None:
|
|
19
21
|
super().__init__()
|
|
20
22
|
self.auto_retries = auto_retries + 1
|
|
@@ -54,7 +56,7 @@ class StdClient(requests.Session):
|
|
|
54
56
|
|
|
55
57
|
def request(
|
|
56
58
|
self, method: str, url: str, *, authenticate: bool = False, **kwargs
|
|
57
|
-
) ->
|
|
59
|
+
) -> Response:
|
|
58
60
|
if authenticate and self.authenticate:
|
|
59
61
|
kwargs = self.authenticate(kwargs)
|
|
60
62
|
|
|
@@ -62,20 +64,16 @@ class StdClient(requests.Session):
|
|
|
62
64
|
|
|
63
65
|
if response is not None:
|
|
64
66
|
return self.parse_response(response)
|
|
67
|
+
else:
|
|
68
|
+
raise RequestError("Request kept failing after retries.")
|
|
65
69
|
|
|
66
|
-
def post(
|
|
67
|
-
self, url: str, *, authenticate: bool = False, **kwargs
|
|
68
|
-
) -> Union[Response, None]:
|
|
70
|
+
def post(self, url: str, *, authenticate: bool = False, **kwargs) -> Response:
|
|
69
71
|
return self.request("POST", url, authenticate=authenticate, **kwargs)
|
|
70
72
|
|
|
71
|
-
def get(
|
|
72
|
-
self, url: str, *, authenticate: bool = False, **kwargs
|
|
73
|
-
) -> Union[Response, None]:
|
|
73
|
+
def get(self, url: str, *, authenticate: bool = False, **kwargs) -> Response:
|
|
74
74
|
return self.request("GET", url, authenticate=authenticate, **kwargs)
|
|
75
75
|
|
|
76
|
-
def put(
|
|
77
|
-
self, url: str, *, authenticate: bool = False, **kwargs
|
|
78
|
-
) -> Union[Response, None]:
|
|
76
|
+
def put(self, url: str, *, authenticate: bool = False, **kwargs) -> Response:
|
|
79
77
|
return self.request("PUT", url, authenticate=authenticate, **kwargs)
|
|
80
78
|
|
|
81
79
|
|
|
@@ -127,7 +125,7 @@ class TLSClient(Session):
|
|
|
127
125
|
is_dict = True
|
|
128
126
|
|
|
129
127
|
try:
|
|
130
|
-
json.loads(body)
|
|
128
|
+
json.loads(body) # type: ignore
|
|
131
129
|
except json.JSONDecodeError:
|
|
132
130
|
is_dict = False
|
|
133
131
|
|
|
@@ -140,8 +138,10 @@ class TLSClient(Session):
|
|
|
140
138
|
|
|
141
139
|
# Why is status_code a None type...
|
|
142
140
|
assert response.status_code is not None, "Status Code is None"
|
|
143
|
-
|
|
144
|
-
resp = Response(
|
|
141
|
+
|
|
142
|
+
resp = Response(
|
|
143
|
+
status_code=int(response.status_code), response=body, raw=response
|
|
144
|
+
)
|
|
145
145
|
|
|
146
146
|
if danger and self.fail_exception and resp.fail:
|
|
147
147
|
raise self.fail_exception(
|
|
@@ -151,9 +151,7 @@ class TLSClient(Session):
|
|
|
151
151
|
|
|
152
152
|
return resp
|
|
153
153
|
|
|
154
|
-
def get(
|
|
155
|
-
self, url: str, *, authenticate: bool = False, **kwargs
|
|
156
|
-
) -> Response:
|
|
154
|
+
def get(self, url: str, *, authenticate: bool = False, **kwargs) -> Response:
|
|
157
155
|
"""Routes a GET Request"""
|
|
158
156
|
if authenticate and self.authenticate is not None:
|
|
159
157
|
kwargs = self.authenticate(kwargs)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any, Mapping, Optional
|
|
4
|
+
from typing import Any, Mapping, Optional
|
|
5
5
|
from urllib.parse import urlencode
|
|
6
6
|
|
|
7
7
|
from spotapi.types import Config, SaverProtocol
|
|
@@ -55,19 +55,31 @@ class Login:
|
|
|
55
55
|
]
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
-
|
|
59
58
|
@classmethod
|
|
60
59
|
def from_cookies(cls, dump: Mapping[str, Any], cfg: Config) -> Login:
|
|
61
60
|
"""
|
|
62
61
|
Constructs a Login instance using cookie data and configuration.
|
|
63
62
|
"""
|
|
64
63
|
password = dump.get("password")
|
|
64
|
+
password = "" if password is None else password
|
|
65
|
+
|
|
65
66
|
cred = dump.get("identifier")
|
|
66
|
-
cookies
|
|
67
|
+
cookies = dump.get("cookies")
|
|
68
|
+
|
|
69
|
+
if isinstance(cookies, str):
|
|
70
|
+
_cookies = cookies.strip().split(";")
|
|
71
|
+
cookies = {}
|
|
72
|
+
|
|
73
|
+
for cookie in _cookies:
|
|
74
|
+
k, v = cookie.split("=")
|
|
75
|
+
cookies[k] = v
|
|
76
|
+
|
|
77
|
+
if isinstance(cookies, Mapping):
|
|
78
|
+
cookies = cookies # autotype
|
|
67
79
|
|
|
68
|
-
if not (
|
|
80
|
+
if not (cred and cookies):
|
|
69
81
|
raise ValueError(
|
|
70
|
-
"Invalid dump format: must contain '
|
|
82
|
+
"Invalid dump format: must contain 'identifier', and 'cookies'"
|
|
71
83
|
)
|
|
72
84
|
|
|
73
85
|
cfg.client.cookies.clear()
|
|
@@ -80,13 +92,10 @@ class Login:
|
|
|
80
92
|
return instantiated
|
|
81
93
|
|
|
82
94
|
@classmethod
|
|
83
|
-
def from_saver(
|
|
84
|
-
cls, saver: SaverProtocol, cfg: Config, identifier: str
|
|
85
|
-
) -> Login:
|
|
95
|
+
def from_saver(cls, saver: SaverProtocol, cfg: Config, identifier: str) -> Login:
|
|
86
96
|
"""
|
|
87
97
|
Loads a session from a Saver Class.
|
|
88
|
-
|
|
89
|
-
Note: Kwargs are not used, make sure the defaults for the savers are what you want (or just implement this method yourself).
|
|
98
|
+
NOTE: Kwargs are not used, make sure the defaults for the savers are what you want (or just implement this method yourself).
|
|
90
99
|
"""
|
|
91
100
|
dump = saver.load(query={"identifier": identifier})
|
|
92
101
|
return cls.from_cookies(dump, cfg)
|
|
@@ -110,7 +119,7 @@ class Login:
|
|
|
110
119
|
|
|
111
120
|
if resp.fail:
|
|
112
121
|
raise LoginError("Could not get session", error=resp.error.string)
|
|
113
|
-
|
|
122
|
+
|
|
114
123
|
self.csrf_token = resp.raw.cookies.get("sp_sso_csrf_token")
|
|
115
124
|
self.flow_id = parse_json_string(resp.response, "flowCtx")
|
|
116
125
|
|
|
@@ -156,7 +165,7 @@ class Login:
|
|
|
156
165
|
# json_data will still be bad, but we know we are logged in now
|
|
157
166
|
return
|
|
158
167
|
|
|
159
|
-
if
|
|
168
|
+
if "error" not in json_data:
|
|
160
169
|
raise LoginError(f"Unexpected response format: {json_data}")
|
|
161
170
|
|
|
162
171
|
error_type = json_data["error"]
|
|
@@ -173,6 +182,9 @@ class Login:
|
|
|
173
182
|
|
|
174
183
|
def login(self) -> None:
|
|
175
184
|
"""Logins the user"""
|
|
185
|
+
if self.logged_in:
|
|
186
|
+
raise LoginError("User already logged in")
|
|
187
|
+
|
|
176
188
|
now = time.time()
|
|
177
189
|
self.__get_session()
|
|
178
190
|
|
|
@@ -219,7 +231,7 @@ class LoginChallenge:
|
|
|
219
231
|
"accounts/login",
|
|
220
232
|
"v3",
|
|
221
233
|
)
|
|
222
|
-
|
|
234
|
+
|
|
223
235
|
if not captcha_response:
|
|
224
236
|
raise LoginError("Could not solve captcha")
|
|
225
237
|
|
|
@@ -248,7 +260,6 @@ class LoginChallenge:
|
|
|
248
260
|
if resp.fail:
|
|
249
261
|
raise LoginError("Could not submit challenge", error=resp.error.string)
|
|
250
262
|
|
|
251
|
-
|
|
252
263
|
if not isinstance(resp.response, Mapping):
|
|
253
264
|
raise LoginError("Invalid JSON")
|
|
254
265
|
|
|
@@ -269,4 +280,4 @@ class LoginChallenge:
|
|
|
269
280
|
def defeat(self) -> None:
|
|
270
281
|
self.__get_challenge()
|
|
271
282
|
self.__submit_challenge()
|
|
272
|
-
self.__complete_challenge()
|
|
283
|
+
self.__complete_challenge()
|
|
@@ -31,7 +31,6 @@ class Password:
|
|
|
31
31
|
url = "https://accounts.spotify.com/en/password-reset"
|
|
32
32
|
resp = self.client.get(url)
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
if resp.fail:
|
|
36
35
|
raise PasswordError("Could not get session", error=resp.error.string)
|
|
37
36
|
|
|
@@ -51,7 +50,6 @@ class Password:
|
|
|
51
50
|
|
|
52
51
|
resp = self.client.post(url, data=payload, headers=headers)
|
|
53
52
|
|
|
54
|
-
|
|
55
53
|
if resp.fail:
|
|
56
54
|
raise PasswordError("Could not reset password", error=resp.error.string)
|
|
57
55
|
|
|
@@ -34,7 +34,7 @@ class PublicPlaylist:
|
|
|
34
34
|
self.playlist_link = f"https://open.spotify.com/playlist/{self.playlist_id}"
|
|
35
35
|
|
|
36
36
|
def get_playlist_info(
|
|
37
|
-
self, limit:
|
|
37
|
+
self, limit: int = 25, *, offset: int = 0
|
|
38
38
|
) -> Mapping[str, Any]:
|
|
39
39
|
"""Gets the public playlist information"""
|
|
40
40
|
if not self.playlist_id:
|
|
@@ -165,7 +165,6 @@ class PrivatePlaylist:
|
|
|
165
165
|
|
|
166
166
|
resp = self.login.client.post(url, json=payload, authenticate=True)
|
|
167
167
|
|
|
168
|
-
|
|
169
168
|
if resp.fail:
|
|
170
169
|
raise PlaylistError(
|
|
171
170
|
"Could not add playlist to library", error=resp.error.string
|
|
@@ -211,7 +210,7 @@ class PrivatePlaylist:
|
|
|
211
210
|
# They are the same requests
|
|
212
211
|
return self.remove_from_library()
|
|
213
212
|
|
|
214
|
-
def get_library(self, limit:
|
|
213
|
+
def get_library(self, limit: int = 50, /) -> Mapping[str, Any]:
|
|
215
214
|
"""Gets all the playlists in your library"""
|
|
216
215
|
url = "https://api-partner.spotify.com/pathfinder/v1/query"
|
|
217
216
|
params = {
|
|
@@ -12,10 +12,10 @@ class Capmonster:
|
|
|
12
12
|
def __init__(
|
|
13
13
|
self,
|
|
14
14
|
api_key: str,
|
|
15
|
-
client:
|
|
15
|
+
client: StdClient = StdClient(3),
|
|
16
16
|
*,
|
|
17
|
+
retries: int = 120,
|
|
17
18
|
proxy: Optional[str] = None,
|
|
18
|
-
retries: Optional[int] = 120,
|
|
19
19
|
) -> None:
|
|
20
20
|
self.api_key = api_key
|
|
21
21
|
self.client = client
|
|
@@ -27,7 +27,7 @@ class Capmonster:
|
|
|
27
27
|
self.client.authenticate = lambda kwargs: self._auth_rule(kwargs)
|
|
28
28
|
|
|
29
29
|
def _auth_rule(self, kwargs: dict) -> dict:
|
|
30
|
-
if
|
|
30
|
+
if "json" not in kwargs:
|
|
31
31
|
kwargs["json"] = {}
|
|
32
32
|
|
|
33
33
|
kwargs["json"]["clientKey"] = self.api_key
|
|
@@ -89,7 +89,7 @@ class Capmonster:
|
|
|
89
89
|
|
|
90
90
|
return str(resp["taskId"])
|
|
91
91
|
|
|
92
|
-
def _harvest_task(self, task_id: str, retries: int) -> str
|
|
92
|
+
def _harvest_task(self, task_id: str, retries: int) -> str:
|
|
93
93
|
for _ in range(retries):
|
|
94
94
|
payload = {"taskId": task_id}
|
|
95
95
|
endpoint = self.BaseURL + "getTaskResult"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import time
|
|
3
|
-
from typing import Literal, Optional
|
|
2
|
+
from typing import Literal, Optional, Dict, Any
|
|
4
3
|
|
|
5
4
|
from spotapi.exceptions import CaptchaException, SolverError
|
|
6
5
|
from spotapi.http.request import StdClient
|
|
@@ -12,10 +11,10 @@ class Capsolver:
|
|
|
12
11
|
def __init__(
|
|
13
12
|
self,
|
|
14
13
|
api_key: str,
|
|
15
|
-
client:
|
|
14
|
+
client: StdClient = StdClient(3),
|
|
16
15
|
*,
|
|
16
|
+
retries: int = 120,
|
|
17
17
|
proxy: Optional[str] = None,
|
|
18
|
-
retries: Optional[int] = 120,
|
|
19
18
|
) -> None:
|
|
20
19
|
self.api_key = api_key
|
|
21
20
|
self.client = client
|
|
@@ -25,7 +24,7 @@ class Capsolver:
|
|
|
25
24
|
self.client.authenticate = lambda kwargs: self._auth_rule(kwargs)
|
|
26
25
|
|
|
27
26
|
def _auth_rule(self, kwargs: dict) -> dict:
|
|
28
|
-
if
|
|
27
|
+
if "json" not in kwargs:
|
|
29
28
|
kwargs["json"] = {}
|
|
30
29
|
|
|
31
30
|
kwargs["json"]["clientKey"] = self.api_key
|
|
@@ -63,7 +62,7 @@ class Capsolver:
|
|
|
63
62
|
if proxy
|
|
64
63
|
else "ReCaptcha{}EnterpriseTaskProxyLess"
|
|
65
64
|
).format(task.upper())
|
|
66
|
-
payload = {
|
|
65
|
+
payload: Dict[str, Dict[str, Any]] = {
|
|
67
66
|
"task": {
|
|
68
67
|
"type": task_type,
|
|
69
68
|
"websiteURL": url,
|
|
@@ -92,7 +91,7 @@ class Capsolver:
|
|
|
92
91
|
|
|
93
92
|
return str(resp["taskId"])
|
|
94
93
|
|
|
95
|
-
def _harvest_task(self, task_id: str, retries: int) -> str
|
|
94
|
+
def _harvest_task(self, task_id: str, retries: int) -> str:
|
|
96
95
|
for _ in range(retries):
|
|
97
96
|
payload = {"taskId": task_id}
|
|
98
97
|
endpoint = self.BaseURL + "getTaskResult"
|
|
@@ -17,16 +17,17 @@ class Song:
|
|
|
17
17
|
self,
|
|
18
18
|
playlist: Optional[PrivatePlaylist] = None,
|
|
19
19
|
*,
|
|
20
|
-
client:
|
|
20
|
+
client: TLSClient = TLSClient("chrome_120", "", auto_retries=3),
|
|
21
21
|
) -> None:
|
|
22
22
|
self.playlist = playlist
|
|
23
|
-
self.base = BaseClient(client=playlist.client if (playlist is not None) else client)
|
|
23
|
+
self.base = BaseClient(client=playlist.client if (playlist is not None) else client) # type: ignore
|
|
24
24
|
|
|
25
25
|
def query_songs(
|
|
26
|
-
self, query: str, /, limit:
|
|
26
|
+
self, query: str, /, limit: int = 10, *, offset: int = 0
|
|
27
27
|
) -> Mapping[str, Any]:
|
|
28
28
|
"""
|
|
29
29
|
Searches for songs in the Spotify catalog.
|
|
30
|
+
NOTE: Returns the raw result unlike paginate_songs which only returns the songs.
|
|
30
31
|
"""
|
|
31
32
|
url = "https://api-partner.spotify.com/pathfinder/v1/query"
|
|
32
33
|
params = {
|
|
@@ -117,7 +118,7 @@ class Song:
|
|
|
117
118
|
def __stage_remove_song(self, uids: List[str]) -> None:
|
|
118
119
|
# If None, something internal went wrong
|
|
119
120
|
assert self.playlist is not None, "Playlist not set"
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
url = "https://api-partner.spotify.com/pathfinder/v1/query"
|
|
122
123
|
payload = {
|
|
123
124
|
"variables": {
|
|
@@ -143,9 +144,10 @@ class Song:
|
|
|
143
144
|
def __parse_playlist_items(
|
|
144
145
|
self,
|
|
145
146
|
items: List[Mapping[str, Any]],
|
|
147
|
+
*,
|
|
146
148
|
song_id: Optional[str] = None,
|
|
147
149
|
song_name: Optional[str] = None,
|
|
148
|
-
all_instances:
|
|
150
|
+
all_instances: bool = False,
|
|
149
151
|
) -> Tuple[List[str], bool]:
|
|
150
152
|
uids: List[str] = []
|
|
151
153
|
for item in items:
|
|
@@ -166,10 +168,10 @@ class Song:
|
|
|
166
168
|
def remove_song_from_playlist(
|
|
167
169
|
self,
|
|
168
170
|
*,
|
|
171
|
+
all_instances: bool = False,
|
|
169
172
|
uid: Optional[str] = None,
|
|
170
173
|
song_id: Optional[str] = None,
|
|
171
174
|
song_name: Optional[str] = None,
|
|
172
|
-
all_instances: Optional[bool] = False,
|
|
173
175
|
) -> None:
|
|
174
176
|
"""
|
|
175
177
|
Removes a song from the playlist.
|
|
@@ -2,21 +2,22 @@ from .data import Config
|
|
|
2
2
|
from .interfaces import CaptchaProtocol, LoggerProtocol, SaverProtocol
|
|
3
3
|
from typing import Any, Dict, Type, Set
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class FilterInheritedMeta(type):
|
|
6
7
|
def __new__(
|
|
7
|
-
cls: Type[
|
|
8
|
-
name: str,
|
|
9
|
-
bases: tuple,
|
|
10
|
-
class_dict: Dict[str, Any]
|
|
8
|
+
cls: Type["FilterInheritedMeta"],
|
|
9
|
+
name: str,
|
|
10
|
+
bases: tuple,
|
|
11
|
+
class_dict: Dict[str, Any],
|
|
11
12
|
) -> Type:
|
|
12
13
|
new_class_dict: Dict[str, Any] = {}
|
|
13
14
|
base_attrs: Set[str] = set()
|
|
14
15
|
|
|
15
16
|
for base in bases:
|
|
16
17
|
base_attrs.update(dir(base))
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
for key, value in class_dict.items():
|
|
19
20
|
if key not in base_attrs:
|
|
20
21
|
new_class_dict[key] = value
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
return super().__new__(cls, name, bases, new_class_dict)
|
|
@@ -8,13 +8,13 @@ class CaptchaProtocol(Protocol):
|
|
|
8
8
|
def __init__(
|
|
9
9
|
self: "CaptchaProtocol",
|
|
10
10
|
api_key: str,
|
|
11
|
-
client:
|
|
11
|
+
client: StdClient = StdClient(3),
|
|
12
12
|
*,
|
|
13
|
+
retries: int = 120,
|
|
13
14
|
proxy: Optional[str] = None,
|
|
14
|
-
retries: Optional[int] = 120,
|
|
15
15
|
) -> None:
|
|
16
16
|
...
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def get_balance(self: "CaptchaProtocol") -> float | None:
|
|
19
19
|
...
|
|
20
20
|
|
|
@@ -49,15 +49,15 @@ class LoggerProtocol(Protocol):
|
|
|
49
49
|
|
|
50
50
|
@runtime_checkable
|
|
51
51
|
class SaverProtocol(Protocol):
|
|
52
|
-
def __init__(
|
|
53
|
-
self: "SaverProtocol", *args, **kwargs
|
|
54
|
-
) -> None:
|
|
52
|
+
def __init__(self: "SaverProtocol", *args, **kwargs) -> None:
|
|
55
53
|
...
|
|
56
|
-
|
|
54
|
+
|
|
57
55
|
def save(self: "SaverProtocol", data: List[Mapping[str, Any]], **kwargs) -> None:
|
|
58
56
|
...
|
|
59
57
|
|
|
60
|
-
def load(
|
|
58
|
+
def load(
|
|
59
|
+
self: "SaverProtocol", query: Mapping[str, Any], **kwargs
|
|
60
|
+
) -> Mapping[str, Any]:
|
|
61
61
|
...
|
|
62
62
|
|
|
63
63
|
def delete(self: "SaverProtocol", query: Mapping[str, Any], **kwargs) -> None:
|
|
@@ -14,7 +14,7 @@ class User:
|
|
|
14
14
|
def __init__(self, login: Login) -> None:
|
|
15
15
|
if not login.logged_in:
|
|
16
16
|
raise ValueError("Must be logged in")
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
self.login = login
|
|
19
19
|
self._user_plan: Mapping[str, Any] | None = None
|
|
20
20
|
self._user_info: Mapping[str, Any] | None = None
|
|
@@ -29,7 +29,7 @@ class User:
|
|
|
29
29
|
@property
|
|
30
30
|
def username(self) -> str:
|
|
31
31
|
if self._user_info is None:
|
|
32
|
-
|
|
32
|
+
self._user_info = self.get_user_info()
|
|
33
33
|
|
|
34
34
|
return self._user_info["profile"]["username"]
|
|
35
35
|
|
|
@@ -38,7 +38,6 @@ class User:
|
|
|
38
38
|
url = "https://www.spotify.com/ca-en/api/account/v2/plan/"
|
|
39
39
|
resp = self.login.client.get(url)
|
|
40
40
|
|
|
41
|
-
|
|
42
41
|
if resp.fail:
|
|
43
42
|
raise UserError("Could not get user plan info", error=resp.error.string)
|
|
44
43
|
|
|
@@ -56,14 +55,12 @@ class User:
|
|
|
56
55
|
raise e
|
|
57
56
|
else:
|
|
58
57
|
return True
|
|
59
|
-
|
|
60
58
|
|
|
61
59
|
def get_user_info(self) -> Mapping[str, Any]:
|
|
62
60
|
"""Gets accounts user info."""
|
|
63
61
|
url = "https://www.spotify.com/api/account-settings/v1/profile"
|
|
64
62
|
resp = self.login.client.get(url)
|
|
65
63
|
|
|
66
|
-
|
|
67
64
|
if resp.fail:
|
|
68
65
|
raise UserError("Could not get user info", error=resp.error.string)
|
|
69
66
|
|
|
@@ -109,6 +106,5 @@ class User:
|
|
|
109
106
|
|
|
110
107
|
resp = self.login.client.put(url, json=dump, headers=headers)
|
|
111
108
|
|
|
112
|
-
|
|
113
109
|
if resp.fail:
|
|
114
110
|
raise UserError("Could not edit user info", error=resp.error.string)
|
|
@@ -277,9 +277,7 @@ class MongoSaver(SaverProtocol):
|
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
class RedisSaver(SaverProtocol):
|
|
280
|
-
def __init__(
|
|
281
|
-
self, host: str = "localhost", port: int = 6379, db: int = 0
|
|
282
|
-
) -> None:
|
|
280
|
+
def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0) -> None:
|
|
283
281
|
self.client = redis.StrictRedis(host=host, port=port, db=db)
|
|
284
282
|
atexit.register(self.client.close)
|
|
285
283
|
|
|
@@ -35,7 +35,7 @@ class WebsocketStreamer(BaseClient, Login):
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
self.rlock = threading.Lock()
|
|
38
|
-
self.ws_dump: dict| None = None
|
|
38
|
+
self.ws_dump: dict | None = None
|
|
39
39
|
|
|
40
40
|
if ignore_init_packet:
|
|
41
41
|
self.get_init_packet()
|
|
@@ -86,5 +86,4 @@ class WebsocketStreamer(BaseClient, Login):
|
|
|
86
86
|
def get_player_state(self) -> dict:
|
|
87
87
|
self.get_main_packet()
|
|
88
88
|
payload = self.ws_dump["payload"][0]
|
|
89
|
-
return payload["cluster"]["player_state"]
|
|
90
|
-
|
|
89
|
+
return payload["cluster"]["player_state"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spotapi
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: A sleek API wrapper for Spotify's private API
|
|
5
5
|
Home-page: UNKNOWN
|
|
6
6
|
Author: Aran
|
|
@@ -8,6 +8,9 @@ License: UNKNOWN
|
|
|
8
8
|
Keywords: Spotify,API,Spotify API,Spotify Private API,Follow,Like,Creator,Music,Music API,Streaming,Music Data,Track,Playlist,Album,Artist,Music Search,Music Metadata,SpotAPI,Python Spotify Wrapper,Music Automation,Web Scraping,Python Music API,Spotify Integration,Spotify Playlist,Spotify Tracks
|
|
9
9
|
Platform: UNKNOWN
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: websocket
|
|
12
|
+
Provides-Extra: redis
|
|
13
|
+
Provides-Extra: pymongo
|
|
11
14
|
License-File: LICENSE
|
|
12
15
|
|
|
13
16
|
# SpotAPI
|
|
@@ -16,6 +19,18 @@ Welcome to SpotAPI! This Python library is designed to interact with the private
|
|
|
16
19
|
|
|
17
20
|
**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
21
|
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
1. [Introduction](#spotapi)
|
|
25
|
+
2. [Features](#features)
|
|
26
|
+
3. [Installation](#installation)
|
|
27
|
+
4. [Import Cookies](#import-cookies)
|
|
28
|
+
5. [Quick Examples](#quick-examples)
|
|
29
|
+
6. [Contributing](#contributing)
|
|
30
|
+
7. [Roadmap](#roadmap)
|
|
31
|
+
8. [License](#license)
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
## Features
|
|
20
35
|
- **Public API Access**: Retrieve and manipulate public Spotify data such as playlists, albums, and tracks with ease.
|
|
21
36
|
- **Private API Access**: Explore private Spotify endpoints to tailor your application to your needs.
|
|
@@ -31,7 +46,27 @@ Everything you can do with Spotify, **SpotAPI** can do with just a user’s
|
|
|
31
46
|
pip install spotapi
|
|
32
47
|
```
|
|
33
48
|
|
|
34
|
-
##
|
|
49
|
+
## Import Cookies
|
|
50
|
+
If you prefer not to use a third party CAPTCHA solver, you can import cookies to manage your session.
|
|
51
|
+
|
|
52
|
+
### Steps to Import Cookies:
|
|
53
|
+
|
|
54
|
+
1. **Choose a Session Saver**:
|
|
55
|
+
- Select a session saver for storing your session data.
|
|
56
|
+
- For simplicity, you should use `JSONSaver`, especially if performance or quantity of sessions is not a big concern.
|
|
57
|
+
|
|
58
|
+
2. **Prepare Session Data**:
|
|
59
|
+
- Create an object with the following keys:
|
|
60
|
+
- **`identifier`**: This should be your email address or username.
|
|
61
|
+
- **`cookies`**: These are the cookies you obtain when logged in. To get these cookies, visit [Spotify](https://open.spotify.com/), log in, and copy the cookies from your browser.
|
|
62
|
+
- It can be a dict[str, str] or a string representation
|
|
63
|
+
|
|
64
|
+
3. **Load the Session**:
|
|
65
|
+
- Use your program to load the session manually with the saved data. This will enable you to use Spotify with a fully functional session without needing additional CAPTCHA solving.
|
|
66
|
+
|
|
67
|
+
## Quick Examples
|
|
68
|
+
|
|
69
|
+
### With User Authentication
|
|
35
70
|
```py
|
|
36
71
|
from spotapi import (
|
|
37
72
|
Login,
|
|
@@ -60,9 +95,51 @@ playlist.create_playlist("SpotAPI Showcase!")
|
|
|
60
95
|
instance.save(MongoSaver())
|
|
61
96
|
```
|
|
62
97
|
|
|
98
|
+
### Without User Authentication
|
|
99
|
+
```py
|
|
100
|
+
"""Here's the example from spotipy https://github.com/spotipy-dev/spotipy?tab=readme-ov-file#quick-start"""
|
|
101
|
+
from spotapi import Song
|
|
102
|
+
|
|
103
|
+
song = Song()
|
|
104
|
+
gen = song.paginate_songs("weezer")
|
|
105
|
+
|
|
106
|
+
# Paginates 100 songs at a time till there's no more
|
|
107
|
+
for batch in gen:
|
|
108
|
+
for idx, item in enumerate(batch):
|
|
109
|
+
print(idx, item['item']['data']['name'])
|
|
110
|
+
|
|
111
|
+
# ^ ONLY 6 LINES OF CODE
|
|
112
|
+
|
|
113
|
+
# Alternatively, you can query a specfic amount
|
|
114
|
+
songs = song.query_songs("weezer", limit=20)
|
|
115
|
+
data = songs["data"]["searchV2"]["tracksV2"]["items"]
|
|
116
|
+
for idx, item in enumerate(data):
|
|
117
|
+
print(idx, item['item']['data']['name'])
|
|
118
|
+
```
|
|
119
|
+
### Results
|
|
120
|
+
```
|
|
121
|
+
0 Island In The Sun
|
|
122
|
+
1 Say It Ain't So
|
|
123
|
+
2 Buddy Holly
|
|
124
|
+
.
|
|
125
|
+
.
|
|
126
|
+
.
|
|
127
|
+
18 Holiday
|
|
128
|
+
19 We Are All On d***s
|
|
129
|
+
```
|
|
130
|
+
|
|
63
131
|
## Contributing
|
|
64
132
|
Contributions are welcome! If you find any issues or have suggestions, please open an issue or submit a pull request.
|
|
65
133
|
|
|
134
|
+
## Roadmap
|
|
135
|
+
> I'll most likely do these if the project gains some traction
|
|
136
|
+
|
|
137
|
+
- [ ] No Captcha For Login (**100 Stars**)
|
|
138
|
+
- [ ] In Depth Documentation
|
|
139
|
+
- [ ] Websocket Listener (Is not working ATM)
|
|
140
|
+
- [ ] Player
|
|
141
|
+
- [ ] More wrappers around this project
|
|
142
|
+
|
|
66
143
|
## License
|
|
67
144
|
This project is licensed under the **GPL 3.0** License. See [LICENSE](https://choosealicense.com/licenses/gpl-3.0/) for details.
|
|
68
145
|
|
spotapi-1.0.3/README.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
|
|
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
|