yutipy 1.5.1__tar.gz → 1.6.1__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.

Potentially problematic release.


This version of yutipy might be problematic. Click here for more details.

Files changed (62) hide show
  1. {yutipy-1.5.1 → yutipy-1.6.1}/PKG-INFO +1 -1
  2. {yutipy-1.5.1 → yutipy-1.6.1}/docs/api_reference.rst +6 -0
  3. {yutipy-1.5.1 → yutipy-1.6.1}/docs/usage_examples.rst +89 -2
  4. yutipy-1.6.1/tests/test_spotify.py +130 -0
  5. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/__init__.py +3 -0
  6. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/cli/search.py +1 -1
  7. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/deezer.py +2 -2
  8. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/exceptions.py +18 -34
  9. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/itunes.py +2 -2
  10. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/kkbox.py +2 -2
  11. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/musicyt.py +2 -2
  12. yutipy-1.6.1/yutipy/spotify.py +981 -0
  13. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/utils/__init__.py +4 -1
  14. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/yutipy_music.py +2 -2
  15. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/PKG-INFO +1 -1
  16. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/SOURCES.txt +2 -3
  17. yutipy-1.5.1/tests/test_spotify.py +0 -53
  18. yutipy-1.5.1/yutipy/logging.py +0 -3
  19. yutipy-1.5.1/yutipy/spotify.py +0 -506
  20. {yutipy-1.5.1 → yutipy-1.6.1}/.gitattributes +0 -0
  21. {yutipy-1.5.1 → yutipy-1.6.1}/.github/FUNDING.yml +0 -0
  22. {yutipy-1.5.1 → yutipy-1.6.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {yutipy-1.5.1 → yutipy-1.6.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {yutipy-1.5.1 → yutipy-1.6.1}/.github/workflows/pytest-unit-testing.yml +0 -0
  25. {yutipy-1.5.1 → yutipy-1.6.1}/.github/workflows/release.yml +0 -0
  26. {yutipy-1.5.1 → yutipy-1.6.1}/.gitignore +0 -0
  27. {yutipy-1.5.1 → yutipy-1.6.1}/.readthedocs.yaml +0 -0
  28. {yutipy-1.5.1 → yutipy-1.6.1}/LICENSE +0 -0
  29. {yutipy-1.5.1 → yutipy-1.6.1}/MANIFEST.in +0 -0
  30. {yutipy-1.5.1 → yutipy-1.6.1}/README.md +0 -0
  31. {yutipy-1.5.1 → yutipy-1.6.1}/docs/Makefile +0 -0
  32. {yutipy-1.5.1 → yutipy-1.6.1}/docs/_static/yutipy_header.png +0 -0
  33. {yutipy-1.5.1 → yutipy-1.6.1}/docs/_static/yutipy_logo.png +0 -0
  34. {yutipy-1.5.1 → yutipy-1.6.1}/docs/available_platforms.rst +0 -0
  35. {yutipy-1.5.1 → yutipy-1.6.1}/docs/cli.rst +0 -0
  36. {yutipy-1.5.1 → yutipy-1.6.1}/docs/conf.py +0 -0
  37. {yutipy-1.5.1 → yutipy-1.6.1}/docs/faq.rst +0 -0
  38. {yutipy-1.5.1 → yutipy-1.6.1}/docs/index.rst +0 -0
  39. {yutipy-1.5.1 → yutipy-1.6.1}/docs/installation.rst +0 -0
  40. {yutipy-1.5.1 → yutipy-1.6.1}/docs/make.bat +0 -0
  41. {yutipy-1.5.1 → yutipy-1.6.1}/docs/requirements.txt +0 -0
  42. {yutipy-1.5.1 → yutipy-1.6.1}/pyproject.toml +0 -0
  43. {yutipy-1.5.1 → yutipy-1.6.1}/requirements-dev.txt +0 -0
  44. {yutipy-1.5.1 → yutipy-1.6.1}/requirements.txt +0 -0
  45. {yutipy-1.5.1 → yutipy-1.6.1}/setup.cfg +0 -0
  46. {yutipy-1.5.1 → yutipy-1.6.1}/tests/__init__.py +0 -0
  47. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_deezer.py +0 -0
  48. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_itunes.py +0 -0
  49. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_kkbox.py +0 -0
  50. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_models.py +0 -0
  51. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_musicyt.py +0 -0
  52. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_utils.py +0 -0
  53. {yutipy-1.5.1 → yutipy-1.6.1}/tests/test_yutipy_music.py +0 -0
  54. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/cli/__init__.py +0 -0
  55. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/cli/config.py +0 -0
  56. {yutipy-1.5.1/yutipy/utils → yutipy-1.6.1/yutipy}/logger.py +0 -0
  57. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/models.py +0 -0
  58. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy/utils/helpers.py +0 -0
  59. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/dependency_links.txt +0 -0
  60. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/entry_points.txt +0 -0
  61. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/requires.txt +0 -0
  62. {yutipy-1.5.1 → yutipy-1.6.1}/yutipy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 1.5.1
3
+ Version: 1.6.1
4
4
  Summary: A simple package for retrieving music information from various music platforms APIs.
5
5
  Author: Cheap Nightbot
6
6
  Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
@@ -41,6 +41,12 @@ Spotify
41
41
  :noindex:
42
42
  :exclude-members: is_session_closed
43
43
 
44
+ .. autoclass:: yutipy.spotify.SpotifyAuth
45
+ :members:
46
+ :inherited-members:
47
+ :noindex:
48
+ :exclude-members: is_session_closed
49
+
44
50
  YouTube Music
45
51
  -------------
46
52
 
@@ -73,7 +73,11 @@ Alternatively, you can manually provide these values when creating an object of
73
73
  Spotify
74
74
  -------
75
75
 
76
- To use the Spotify API, you need to set the ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` for Spotify. You can do this by creating a ``.env`` file in the root directory of your project with the following content:
76
+ Client Credentials Grant Type
77
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78
+
79
+ To use the Spotify API with Client Credentials flow, you need to set the ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` for Spotify.
80
+ You can do this by creating a ``.env`` file in the root directory of your project with the following content:
77
81
 
78
82
  .. admonition:: .env
79
83
 
@@ -82,7 +86,7 @@ To use the Spotify API, you need to set the ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_
82
86
  SPOTIFY_CLIENT_ID=<your_spotify_client_id>
83
87
  SPOTIFY_CLIENT_SECRET=<your_spotify_client_secret>
84
88
 
85
- Alternatively, you can manually provide these values when creating an object of the `Spotify` class:
89
+ Alternatively, you can manually provide these values when creating an object of the ``Spotify`` class:
86
90
 
87
91
  .. code-block:: python
88
92
 
@@ -109,6 +113,89 @@ OR, if you have the ":abbr:`ISRC (International Standard Recording Code)`" or ":
109
113
  result = spotify.search_advanced("Artist Name", "Song Title", isrc="USAT29900609", upc="00602517078194")
110
114
  print(result)
111
115
 
116
+ Authorization Code Flow
117
+ ~~~~~~~~~~~~~~~~~~~~~~~
118
+
119
+ To use the Spotify API with the Authorization Code Flow, you need to set the ``SPOTIFY_CLIENT_ID``, ``SPOTIFY_CLIENT_SECRET``, and ``SPOTIFY_REDIRECT_URI``.
120
+ You can do this by creating a ``.env`` file in the root directory of your project with the following content:
121
+
122
+ .. admonition:: .env
123
+
124
+ .. code-block:: bash
125
+
126
+ SPOTIFY_CLIENT_ID=<your_spotify_client_id>
127
+ SPOTIFY_CLIENT_SECRET=<your_spotify_client_secret>
128
+ SPOTIFY_REDIRECT_URI=<your_redirect_uri>
129
+
130
+ Alternatively, you can manually provide these values when creating an object of the ``SpotifyAuth`` class.
131
+
132
+ Here’s an example of how to use the ``SpotifyAuth`` class in a web application:
133
+
134
+ .. code-block:: python
135
+
136
+ from flask import Flask, request, session, redirect, url_for
137
+ from yutipy.spotify import SpotifyAuth, SpotifyAuthException
138
+
139
+ app = Flask(__name__)
140
+ app.secret_key = "your_secret_key" # Replace with a secure secret key
141
+
142
+ # Initialize SpotifyAuth with your credentials
143
+ SPOTIFY_CLIENT_ID = "your_spotify_client_id"
144
+ SPOTIFY_CLIENT_SECRET = "your_spotify_client_secret"
145
+ SPOTIFY_REDIRECT_URI = "http://localhost:5000/callback"
146
+
147
+ spotify_auth = SpotifyAuth(
148
+ client_id=SPOTIFY_CLIENT_ID,
149
+ client_secret=SPOTIFY_CLIENT_SECRET,
150
+ redirect_uri=SPOTIFY_REDIRECT_URI,
151
+ scopes=["user-read-email", "user-read-private"],
152
+ )
153
+
154
+ @app.route("/")
155
+ def home():
156
+ """Home route to start the authorization process."""
157
+ state = spotify_auth.generate_state()
158
+ session["state"] = state # Save the state in the session for validation
159
+ auth_url = spotify_auth.get_authorization_url(state=state)
160
+ return redirect(auth_url)
161
+
162
+ @app.route("/callback")
163
+ def callback():
164
+ """Callback route to handle Spotify's response after user authorization."""
165
+ code = request.args.get("code")
166
+ state = request.args.get("state")
167
+ expected_state = session.get("state")
168
+
169
+ if not code or not state:
170
+ return "Authorization failed: Missing code or state.", 400
171
+
172
+ try:
173
+ spotify_auth.callback_handler(code, state, expected_state)
174
+ user_profile = spotify_auth.get_user_profile()
175
+ if user_profile:
176
+ return f"Successfully authenticated! Welcome, {user_profile['display_name']}."
177
+ else:
178
+ return "Authentication successful, but failed to fetch user profile."
179
+ except SpotifyAuthException as e:
180
+ return f"Authorization failed: {e}", 400
181
+
182
+ @app.route("/logout")
183
+ def logout():
184
+ """Logout route to close the session."""
185
+ spotify_auth.close_session()
186
+ session.clear()
187
+ return "Logged out successfully."
188
+
189
+ if __name__ == "__main__":
190
+ app.run(debug=True)
191
+
192
+ .. note::
193
+
194
+ - Avoid hard-coding your credentials in the code. Instead, define them in a ``.env`` file, which will be automatically read by the library.
195
+ - Ensure that the redirect URI matches the one configured in your Spotify Developer Dashboard.
196
+ - This example uses Flask, but it can be adapted to other web frameworks as needed.
197
+
198
+
112
199
  YouTube Music
113
200
  -------------
114
201
 
@@ -0,0 +1,130 @@
1
+ import pytest
2
+
3
+ from yutipy.exceptions import SpotifyException
4
+ from yutipy.models import MusicInfo
5
+ from yutipy.spotify import Spotify, SpotifyAuth
6
+
7
+
8
+ @pytest.fixture(scope="module")
9
+ def spotify():
10
+ try:
11
+ return Spotify()
12
+ except SpotifyException:
13
+ pytest.skip("Spotify credentials not found")
14
+
15
+
16
+ @pytest.fixture(scope="module")
17
+ def spotify_auth():
18
+ return SpotifyAuth(
19
+ client_id="test_client_id",
20
+ client_secret="test_client_secret",
21
+ redirect_uri="http://localhost/callback",
22
+ scopes=["user-read-email", "user-read-private"],
23
+ )
24
+
25
+
26
+ def test_search(spotify):
27
+ artist = "Adele"
28
+ song = "Hello"
29
+ result = spotify.search(artist, song)
30
+ assert result is not None
31
+ assert isinstance(result, MusicInfo)
32
+ assert result.title == song
33
+ assert artist in result.artists
34
+
35
+
36
+ def test_search_advanced_with_isrc(spotify):
37
+ artist = "Adele"
38
+ song = "Hello"
39
+ isrc = "GBBKS1500214"
40
+ result = spotify.search_advanced(artist, song, isrc=isrc)
41
+ assert result is not None
42
+ assert result.isrc == isrc
43
+
44
+
45
+ def test_search_advanced_with_upc(spotify):
46
+ artist = "Miles Davis"
47
+ album = "Kind Of Blue (Legacy Edition)"
48
+ upc = "888880696069"
49
+ result = spotify.search_advanced(artist, album, upc=upc)
50
+ print(result)
51
+ assert result is not None
52
+
53
+
54
+ def test_get_artists_ids(spotify):
55
+ artist = "Adele"
56
+ artist_ids = spotify._get_artists_ids(artist)
57
+ assert isinstance(artist_ids, list)
58
+ assert len(artist_ids) > 0
59
+
60
+
61
+ def test_close_session(spotify):
62
+ spotify.close_session()
63
+ assert spotify.is_session_closed
64
+
65
+
66
+ def test_get_authorization_url(spotify_auth):
67
+ state = spotify_auth.generate_state()
68
+ auth_url = spotify_auth.get_authorization_url(state=state)
69
+ assert "https://accounts.spotify.com/authorize" in auth_url
70
+ assert "response_type=code" in auth_url
71
+ assert f"client_id={spotify_auth.client_id}" in auth_url
72
+
73
+
74
+ def test_get_user_profile(spotify_auth, monkeypatch):
75
+ def mock_authorization_header():
76
+ return {"Authorization": "Bearer test_token"}
77
+
78
+ def mock_get(*args, **kwargs):
79
+ class MockResponse:
80
+ status_code = 200
81
+
82
+ @staticmethod
83
+ def raise_for_status():
84
+ pass # Simulates a successful response with no exceptions raised
85
+
86
+ @staticmethod
87
+ def json():
88
+ return {
89
+ "display_name": "Test User",
90
+ "images": [
91
+ {
92
+ "url": "https://example.com/image.jpg",
93
+ "height": 300,
94
+ "width": 300,
95
+ }
96
+ ],
97
+ }
98
+
99
+ return MockResponse()
100
+
101
+ monkeypatch.setattr(
102
+ spotify_auth, "_SpotifyAuth__authorization_header", mock_authorization_header
103
+ )
104
+ monkeypatch.setattr(spotify_auth._SpotifyAuth__session, "get", mock_get)
105
+
106
+ user_profile = spotify_auth.get_user_profile()
107
+ assert user_profile is not None
108
+ assert user_profile["display_name"] == "Test User"
109
+ assert len(user_profile["images"]) == 1
110
+ assert user_profile["images"][0]["url"] == "https://example.com/image.jpg"
111
+
112
+
113
+ def test_callback_handler(spotify_auth, monkeypatch):
114
+ def mock_get_access_token(*args, **kwargs):
115
+ return {
116
+ "access_token": "test_access_token",
117
+ "refresh_token": "test_refresh_token",
118
+ "expires_in": 3600,
119
+ "requested_at": 1234567890,
120
+ }
121
+
122
+ monkeypatch.setattr(
123
+ spotify_auth, "_SpotifyAuth__get_access_token", mock_get_access_token
124
+ )
125
+
126
+ spotify_auth.callback_handler("test_code", "test_state", "test_state")
127
+ assert spotify_auth._SpotifyAuth__access_token == "test_access_token"
128
+ assert spotify_auth._SpotifyAuth__refresh_token == "test_refresh_token"
129
+ assert spotify_auth._SpotifyAuth__token_expires_in == 3600
130
+ assert spotify_auth._SpotifyAuth__token_requested_at == 1234567890
@@ -3,6 +3,7 @@ from .itunes import Itunes
3
3
  from .kkbox import KKBox
4
4
  from .musicyt import MusicYT
5
5
  from .spotify import Spotify
6
+ from .utils import disable_logging, enable_logging
6
7
  from .yutipy_music import YutipyMusic
7
8
 
8
9
  __all__ = [
@@ -12,4 +13,6 @@ __all__ = [
12
13
  "MusicYT",
13
14
  "Spotify",
14
15
  "YutipyMusic",
16
+ "enable_logging",
17
+ "disable_logging",
15
18
  ]
@@ -14,7 +14,7 @@ from yutipy.itunes import Itunes
14
14
  from yutipy.kkbox import KKBox
15
15
  from yutipy.musicyt import MusicYT
16
16
  from yutipy.spotify import Spotify
17
- from yutipy.utils.logger import disable_logging, enable_logging
17
+ from yutipy.logger import disable_logging, enable_logging
18
18
  from yutipy.yutipy_music import YutipyMusic
19
19
 
20
20
 
@@ -13,7 +13,7 @@ from yutipy.exceptions import (
13
13
  )
14
14
  from yutipy.models import MusicInfo
15
15
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
- from yutipy.utils.logger import logger
16
+ from yutipy.logger import logger
17
17
 
18
18
 
19
19
  class Deezer:
@@ -323,7 +323,7 @@ class Deezer:
323
323
 
324
324
  if __name__ == "__main__":
325
325
  import logging
326
- from yutipy.utils.logger import enable_logging
326
+ from yutipy.logger import enable_logging
327
327
 
328
328
  enable_logging(level=logging.DEBUG)
329
329
  deezer = Deezer()
@@ -11,60 +11,44 @@ __all__ = [
11
11
  class YutipyException(Exception):
12
12
  """Base class for exceptions in the Yutipy package."""
13
13
 
14
- pass
14
+
15
+ # Generic Exceptions
16
+ class AuthenticationException(YutipyException):
17
+ """Exception raised for authentication errors."""
18
+
19
+
20
+ class InvalidResponseException(YutipyException):
21
+ """Exception raised for invalid responses from APIs."""
22
+
23
+
24
+ class InvalidValueException(YutipyException):
25
+ """Exception raised for invalid values."""
26
+
27
+
28
+ class NetworkException(YutipyException):
29
+ """Exception raised for network-related errors."""
15
30
 
16
31
 
17
32
  # Service Exceptions
18
33
  class DeezerException(YutipyException):
19
34
  """Exception raised for errors related to the Deezer API."""
20
35
 
21
- pass
22
-
23
36
 
24
37
  class ItunesException(YutipyException):
25
38
  """Exception raised for errors related to the iTunes API."""
26
39
 
27
- pass
28
-
29
40
 
30
41
  class KKBoxException(YutipyException):
31
42
  """Exception raised for erros related to the KKBOX Open API."""
32
43
 
33
- pass
34
-
35
44
 
36
45
  class MusicYTException(YutipyException):
37
46
  """Exception raised for errors related to the YouTube Music API."""
38
47
 
39
- pass
40
-
41
48
 
42
49
  class SpotifyException(YutipyException):
43
50
  """Exception raised for errors related to the Spotify API."""
44
51
 
45
- pass
46
-
47
-
48
- # Generic Exceptions
49
- class AuthenticationException(YutipyException):
50
- """Exception raised for authentication errors."""
51
-
52
- pass
53
-
54
-
55
- class InvalidResponseException(YutipyException):
56
- """Exception raised for invalid responses from APIs."""
57
-
58
- pass
59
-
60
-
61
- class InvalidValueException(YutipyException):
62
- """Exception raised for invalid values."""
63
-
64
- pass
65
-
66
-
67
- class NetworkException(YutipyException):
68
- """Exception raised for network-related errors."""
69
52
 
70
- pass
53
+ class SpotifyAuthException(AuthenticationException):
54
+ """Exception raised for Spotify authorization code grant type / flow"""
@@ -18,7 +18,7 @@ from yutipy.utils.helpers import (
18
18
  guess_album_type,
19
19
  is_valid_string,
20
20
  )
21
- from yutipy.utils.logger import logger
21
+ from yutipy.logger import logger
22
22
 
23
23
 
24
24
  class Itunes:
@@ -228,7 +228,7 @@ class Itunes:
228
228
 
229
229
  if __name__ == "__main__":
230
230
  import logging
231
- from yutipy.utils.logger import enable_logging
231
+ from yutipy.logger import enable_logging
232
232
 
233
233
  enable_logging(level=logging.DEBUG)
234
234
  itunes = Itunes()
@@ -18,7 +18,7 @@ from yutipy.exceptions import (
18
18
  )
19
19
  from yutipy.models import MusicInfo
20
20
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
21
- from yutipy.utils.logger import logger
21
+ from yutipy.logger import logger
22
22
 
23
23
  load_dotenv()
24
24
 
@@ -417,7 +417,7 @@ class KKBox:
417
417
 
418
418
  if __name__ == "__main__":
419
419
  import logging
420
- from yutipy.utils.logger import enable_logging
420
+ from yutipy.logger import enable_logging
421
421
 
422
422
  enable_logging(level=logging.DEBUG)
423
423
  kkbox = KKBox(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
@@ -13,7 +13,7 @@ from yutipy.exceptions import (
13
13
  )
14
14
  from yutipy.models import MusicInfo
15
15
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
- from yutipy.utils.logger import logger
16
+ from yutipy.logger import logger
17
17
 
18
18
 
19
19
  class MusicYT:
@@ -296,7 +296,7 @@ class MusicYT:
296
296
  if __name__ == "__main__":
297
297
  import logging
298
298
 
299
- from yutipy.utils.logger import enable_logging
299
+ from yutipy.logger import enable_logging
300
300
 
301
301
  enable_logging(level=logging.DEBUG)
302
302
  music_yt = MusicYT()