spotapi 1.0.2__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.
Files changed (37) hide show
  1. {spotapi-1.0.2/spotapi.egg-info → spotapi-1.0.4}/PKG-INFO +79 -2
  2. spotapi-1.0.4/README.md +130 -0
  3. {spotapi-1.0.2 → spotapi-1.0.4}/setup.py +7 -4
  4. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/artist.py +4 -6
  5. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/client.py +6 -10
  6. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/creator.py +7 -14
  7. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/family.py +0 -5
  8. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/http/request.py +15 -17
  9. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/login.py +26 -15
  10. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/password.py +0 -2
  11. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/playlist.py +2 -3
  12. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/solvers/capmonster.py +4 -4
  13. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/solvers/capsolver.py +6 -7
  14. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/song.py +8 -6
  15. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/types/__init__.py +7 -6
  16. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/types/interfaces.py +8 -8
  17. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/user.py +2 -6
  18. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/utils/logger.py +1 -1
  19. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/utils/saver.py +1 -3
  20. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/websocket.py +2 -3
  21. {spotapi-1.0.2 → spotapi-1.0.4/spotapi.egg-info}/PKG-INFO +79 -2
  22. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi.egg-info/requires.txt +8 -2
  23. spotapi-1.0.2/README.md +0 -56
  24. {spotapi-1.0.2 → spotapi-1.0.4}/LICENSE +0 -0
  25. {spotapi-1.0.2 → spotapi-1.0.4}/setup.cfg +0 -0
  26. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/__init__.py +0 -0
  27. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/exceptions/__init__.py +0 -0
  28. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/exceptions/errors.py +0 -0
  29. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/http/__init__.py +0 -0
  30. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/http/data.py +0 -0
  31. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/solvers/__init__.py +0 -0
  32. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/types/data.py +0 -0
  33. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/utils/__init__.py +0 -0
  34. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi/utils/strings.py +0 -0
  35. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi.egg-info/SOURCES.txt +0 -0
  36. {spotapi-1.0.2 → spotapi-1.0.4}/spotapi.egg-info/dependency_links.txt +0 -0
  37. {spotapi-1.0.2 → 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.2
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
- ## Quick Example
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
 
@@ -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.2",
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: Optional[TLSClient] = TLSClient("chrome_120", "", auto_retries=3),
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) # type: ignore
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: Optional[int] = 10, *, offset: Optional[int] = 0
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: Optional[Literal["addToLibrary", "removeFromLibrary"]] = "addToLibrary",
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 not ("headers" in kwargs):
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: Optional[str] = random_email(),
24
- password: Optional[str] = random_string(10, True),
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": "Aran",
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 not ("success" in resp.response):
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, auto_retries: int = 0, auth_rule: Callable[[dict[Any, Any]], dict] | None = None
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
- ) -> Union[Response, None]:
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) # type: ignore
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(status_code=int(response.status_code), response=body, raw=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, Type
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: Mapping[str, Any] = dict(dump.get("cookies")) # type: ignore
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 (password and cred and cookies):
80
+ if not (cred and cookies):
69
81
  raise ValueError(
70
- "Invalid dump format: must contain 'password', 'identifier', and 'cookies'"
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 not ("error" in json_data):
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: Optional[int] = 25, *, offset: Optional[int] = 0
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: Optional[int] = 50) -> Mapping[str, Any]:
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: Optional[StdClient] = StdClient(3),
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 not ("json" in kwargs):
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 | None:
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: Optional[StdClient] = StdClient(3),
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 not ("json" in kwargs):
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 | None:
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: Optional[TLSClient] = TLSClient("chrome_120", "", auto_retries=3),
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) # type: ignore
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: Optional[int] = 10, *, offset: Optional[int] = 0
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: Optional[bool] = False,
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['FilterInheritedMeta'],
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: Optional[StdClient] = StdClient(3),
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(self: "SaverProtocol", query: Mapping[str, Any], **kwargs) -> Mapping[str, Any]:
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
- self._user_info = self.get_user_info()
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)
@@ -36,7 +36,7 @@ class Logger(LoggerProtocol):
36
36
  )
37
37
 
38
38
  @staticmethod
39
- def debug(s: str, **extra) -> None:
39
+ def attempt(s: str, **extra) -> None:
40
40
  with LOCK:
41
41
  fields = [
42
42
  f"{Style.BRIGHT}{Fore.LIGHTBLUE_EX}{k}={Fore.LIGHTYELLOW_EX}{v}{Style.RESET_ALL}"
@@ -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.2
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
- ## Quick Example
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
 
@@ -1,10 +1,16 @@
1
1
  requests
2
2
  colorama
3
3
  Pillow
4
- pymongo
5
4
  readerwriterlock
6
- redis
7
5
  tls_client
8
6
  typing_extensions
9
7
  validators
8
+
9
+ [pymongo]
10
+ pymongo
11
+
12
+ [redis]
13
+ redis
14
+
15
+ [websocket]
10
16
  websockets
spotapi-1.0.2/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