yutipy 2.2.4__tar.gz → 2.2.6__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-2.2.4 → yutipy-2.2.6}/PKG-INFO +1 -1
  2. {yutipy-2.2.4 → yutipy-2.2.6}/docs/api_reference.rst +0 -6
  3. yutipy-2.2.6/tests/test_lastfm.py +118 -0
  4. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/lastfm.py +63 -17
  5. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/spotify.py +44 -20
  6. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/PKG-INFO +1 -1
  7. yutipy-2.2.4/tests/test_lastfm.py +0 -60
  8. {yutipy-2.2.4 → yutipy-2.2.6}/.gitattributes +0 -0
  9. {yutipy-2.2.4 → yutipy-2.2.6}/.github/FUNDING.yml +0 -0
  10. {yutipy-2.2.4 → yutipy-2.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {yutipy-2.2.4 → yutipy-2.2.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {yutipy-2.2.4 → yutipy-2.2.6}/.github/dependabot.yml +0 -0
  13. {yutipy-2.2.4 → yutipy-2.2.6}/.github/workflows/pytest-unit-testing.yml +0 -0
  14. {yutipy-2.2.4 → yutipy-2.2.6}/.github/workflows/release.yml +0 -0
  15. {yutipy-2.2.4 → yutipy-2.2.6}/.gitignore +0 -0
  16. {yutipy-2.2.4 → yutipy-2.2.6}/.readthedocs.yaml +0 -0
  17. {yutipy-2.2.4 → yutipy-2.2.6}/LICENSE +0 -0
  18. {yutipy-2.2.4 → yutipy-2.2.6}/MANIFEST.in +0 -0
  19. {yutipy-2.2.4 → yutipy-2.2.6}/README.md +0 -0
  20. {yutipy-2.2.4 → yutipy-2.2.6}/docs/Makefile +0 -0
  21. {yutipy-2.2.4 → yutipy-2.2.6}/docs/_static/yutipy_header.png +0 -0
  22. {yutipy-2.2.4 → yutipy-2.2.6}/docs/_static/yutipy_logo.png +0 -0
  23. {yutipy-2.2.4 → yutipy-2.2.6}/docs/available_platforms.rst +0 -0
  24. {yutipy-2.2.4 → yutipy-2.2.6}/docs/cli.rst +0 -0
  25. {yutipy-2.2.4 → yutipy-2.2.6}/docs/conf.py +0 -0
  26. {yutipy-2.2.4 → yutipy-2.2.6}/docs/faq.rst +0 -0
  27. {yutipy-2.2.4 → yutipy-2.2.6}/docs/index.rst +0 -0
  28. {yutipy-2.2.4 → yutipy-2.2.6}/docs/installation.rst +0 -0
  29. {yutipy-2.2.4 → yutipy-2.2.6}/docs/make.bat +0 -0
  30. {yutipy-2.2.4 → yutipy-2.2.6}/docs/requirements.txt +0 -0
  31. {yutipy-2.2.4 → yutipy-2.2.6}/docs/usage_examples.rst +0 -0
  32. {yutipy-2.2.4 → yutipy-2.2.6}/pyproject.toml +0 -0
  33. {yutipy-2.2.4 → yutipy-2.2.6}/requirements-dev.txt +0 -0
  34. {yutipy-2.2.4 → yutipy-2.2.6}/requirements.txt +0 -0
  35. {yutipy-2.2.4 → yutipy-2.2.6}/setup.cfg +0 -0
  36. {yutipy-2.2.4 → yutipy-2.2.6}/tests/__init__.py +0 -0
  37. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_deezer.py +0 -0
  38. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_itunes.py +0 -0
  39. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_kkbox.py +0 -0
  40. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_models.py +0 -0
  41. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_musicyt.py +0 -0
  42. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_spotify.py +0 -0
  43. {yutipy-2.2.4 → yutipy-2.2.6}/tests/test_utils.py +0 -0
  44. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/__init__.py +0 -0
  45. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/cli/__init__.py +0 -0
  46. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/cli/config.py +0 -0
  47. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/cli/search.py +0 -0
  48. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/deezer.py +0 -0
  49. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/exceptions.py +0 -0
  50. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/itunes.py +0 -0
  51. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/kkbox.py +0 -0
  52. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/logger.py +0 -0
  53. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/models.py +0 -0
  54. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/musicyt.py +0 -0
  55. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/utils/__init__.py +0 -0
  56. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/utils/helpers.py +0 -0
  57. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy/yutipy_music.py +0 -0
  58. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/SOURCES.txt +0 -0
  59. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/dependency_links.txt +0 -0
  60. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/entry_points.txt +0 -0
  61. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/requires.txt +0 -0
  62. {yutipy-2.2.4 → yutipy-2.2.6}/yutipy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 2.2.4
3
+ Version: 2.2.6
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>
@@ -132,12 +132,6 @@ Generic Exceptions
132
132
  :noindex:
133
133
  :exclude-members: add_note, args, with_traceback
134
134
 
135
- .. autoclass:: yutipy.exceptions.NetworkException
136
- :members:
137
- :inherited-members:
138
- :noindex:
139
- :exclude-members: add_note, args, with_traceback
140
-
141
135
  Service Exceptions
142
136
  ------------------
143
137
 
@@ -0,0 +1,118 @@
1
+ import pytest
2
+
3
+ from yutipy.lastfm import LastFm
4
+ from yutipy.models import UserPlaying
5
+ from tests import BaseResponse
6
+
7
+
8
+ @pytest.fixture
9
+ def lastfm():
10
+ return LastFm(api_key="test_api_key")
11
+
12
+
13
+ class MockResponseActivity(BaseResponse):
14
+ @staticmethod
15
+ def json():
16
+ return {
17
+ "recenttracks": {
18
+ "track": [
19
+ {
20
+ "artist": {"mbid": "", "#text": "Test Artist"},
21
+ "image": [
22
+ {
23
+ "size": "small",
24
+ "#text": "https://example.com/image/small.jpg",
25
+ },
26
+ {
27
+ "size": "extralarge",
28
+ "#text": "https://example.com/image/extralarge.jpg",
29
+ },
30
+ ],
31
+ "mbid": "",
32
+ "album": {
33
+ "mbid": "",
34
+ "#text": "Test Album",
35
+ },
36
+ "name": "Test Track",
37
+ "@attr": {"nowplaying": True},
38
+ "url": "https://www.last.fm/music/test+track",
39
+ }
40
+ ]
41
+ }
42
+ }
43
+
44
+
45
+ class MockResponseProfile(BaseResponse):
46
+ @staticmethod
47
+ def json():
48
+ return {
49
+ "user": {
50
+ "name": "john",
51
+ "realname": "Real John",
52
+ "image": [
53
+ {
54
+ "size": "small",
55
+ "#text": "https://example.com/image/john",
56
+ },
57
+ {
58
+ "size": "extralarge",
59
+ "#text": "https://example.com/image/john",
60
+ },
61
+ ],
62
+ "url": "https://example.com/john",
63
+ "type": "user",
64
+ }
65
+ }
66
+
67
+
68
+ @pytest.fixture
69
+ def mock_response_activity(lastfm, monkeypatch):
70
+ def mock_get(*args, **kwargs):
71
+ return MockResponseActivity()
72
+
73
+ monkeypatch.setattr(lastfm._LastFm__session, "get", mock_get)
74
+
75
+
76
+ @pytest.fixture
77
+ def mock_response_profile(lastfm, monkeypatch):
78
+ def mock_get(*args, **kwargs):
79
+ return MockResponseProfile()
80
+
81
+ monkeypatch.setattr(lastfm._LastFm__session, "get", mock_get)
82
+
83
+
84
+ def test_get_currently_playing(lastfm, mock_response_activity):
85
+ username = "bob"
86
+ currently_playing = lastfm.get_currently_playing(username=username)
87
+ assert currently_playing is not None
88
+ assert isinstance(currently_playing, UserPlaying)
89
+ assert currently_playing.title == "Test Track"
90
+ assert currently_playing.album_title == "Test Album"
91
+ assert "extralarge" in currently_playing.album_art
92
+ assert currently_playing.is_playing is True
93
+
94
+
95
+ def test_get_user_profile(lastfm, mock_response_profile):
96
+ username = "john"
97
+ profile = lastfm.get_user_profile(username=username)
98
+ assert profile is not None
99
+ assert profile["username"] == username
100
+ assert profile["name"] == "Real John"
101
+ assert profile["type"] == "user"
102
+
103
+
104
+ def test_invalid_username(lastfm, monkeypatch):
105
+ def mock_get(*args, **kwargs):
106
+ class MockResponse(BaseResponse):
107
+ @staticmethod
108
+ def json():
109
+ return {"message": "User not found", "error": 6}
110
+
111
+ return MockResponse()
112
+
113
+ monkeypatch.setattr(lastfm._LastFm__session, "get", mock_get)
114
+
115
+ username = "alksdjfalsjdfweurppqoweiuwu"
116
+ profile = lastfm.get_user_profile(username=username)
117
+ assert profile is not None
118
+ assert "error" in profile
@@ -2,6 +2,7 @@ __all__ = ["LastFm", "LastFmException"]
2
2
 
3
3
  import os
4
4
  from dataclasses import asdict
5
+ from time import time
5
6
  from pprint import pprint
6
7
  from typing import Optional
7
8
 
@@ -73,6 +74,46 @@ class LastFm:
73
74
  """Checks if the session is closed."""
74
75
  return self._is_session_closed
75
76
 
77
+ def get_user_profile(self, username: str):
78
+ """
79
+ Fetches the user profile information for the provided username.
80
+
81
+ Returns
82
+ -------
83
+ dict
84
+ A dictionary containing the user's profile information or error is username does not exist.
85
+ """
86
+ query = (
87
+ f"?method=user.getinfo&user={username}&api_key={self.api_key}&format=json"
88
+ )
89
+ query_url = self.__api_url + query
90
+
91
+ try:
92
+ response = self.__session.get(query_url, timeout=30)
93
+ except requests.RequestException as e:
94
+ logger.warning(f"Failed to fetch user profile: {e}")
95
+ return None
96
+
97
+ response_json = response.json()
98
+ result = response_json.get("user")
99
+ error = response_json.get("message")
100
+ if result:
101
+ images = [
102
+ {"size": image.get("size"), "url": image.get("#text")}
103
+ for image in result.get("image", [])
104
+ ]
105
+ return {
106
+ "name": result.get("realname"),
107
+ "username": result.get("name"),
108
+ "type": result.get("type"),
109
+ "url": result.get("url"),
110
+ "images": images,
111
+ }
112
+ elif error:
113
+ return {"error": error}
114
+ else:
115
+ return None
116
+
76
117
  def get_currently_playing(self, username: str) -> Optional[UserPlaying]:
77
118
  """
78
119
  Fetches information about the currently playing or most recent track for a user.
@@ -100,29 +141,34 @@ class LastFm:
100
141
 
101
142
  response_json = response.json()
102
143
  result = response_json.get("recenttracks", {}).get("track", [])[0]
103
- album_art = [
104
- img.get("#text")
105
- for img in result.get("image", [])
106
- if img.get("size") == "extralarge"
107
- ]
108
- return UserPlaying(
109
- album_art="".join(album_art),
110
- album_title=result.get("album", {}).get("#text"),
111
- artists=", ".join(separate_artists(result.get("artist", {}).get("#text"))),
112
- id=result.get("mbid"),
113
- timestamp=result.get("date", {}).get("uts"),
114
- title=result.get("name"),
115
- url=result.get("url"),
116
- is_playing=result.get("@attr", {}).get("nowplaying", False),
117
- )
144
+ is_playing = result.get("@attr", {}).get("nowplaying", False)
145
+ if result and is_playing:
146
+ album_art = [
147
+ img.get("#text")
148
+ for img in result.get("image", [])
149
+ if img.get("size") == "extralarge"
150
+ ]
151
+ return UserPlaying(
152
+ album_art="".join(album_art),
153
+ album_title=result.get("album", {}).get("#text"),
154
+ artists=", ".join(
155
+ separate_artists(result.get("artist", {}).get("#text"))
156
+ ),
157
+ id=result.get("mbid"),
158
+ timestamp=result.get("date", {}).get("uts") or time(),
159
+ title=result.get("name"),
160
+ url=result.get("url"),
161
+ is_playing=is_playing,
162
+ )
163
+ return None
118
164
 
119
165
 
120
166
  if __name__ == "__main__":
121
167
  with LastFm() as lastfm:
122
168
  username = input("Enter Lasfm Username: ").strip()
123
- result = lastfm.get_currently_playing(username=username, limit=5)
169
+ result = lastfm.get_user_profile(username=username)
124
170
 
125
171
  if result:
126
- pprint(asdict(result))
172
+ pprint(result)
127
173
  else:
128
174
  print("No result was found. Make sure the username is correct!")
@@ -196,8 +196,9 @@ class Spotify:
196
196
  logger.debug(f"Authentication response status code: {response.status_code}")
197
197
  response.raise_for_status()
198
198
  except requests.RequestException as e:
199
- logger.warning(f"Network error during Spotify authentication: {e}")
200
- return None
199
+ raise requests.RequestException(
200
+ f"Network error during Spotify authentication: {e}"
201
+ )
201
202
 
202
203
  if response.status_code == 200:
203
204
  response_json = response.json()
@@ -210,6 +211,9 @@ class Spotify:
210
211
 
211
212
  def __refresh_access_token(self):
212
213
  """Refreshes the token if it has expired."""
214
+ if not self.__access_token:
215
+ raise SpotifyAuthException("No access token was found.")
216
+
213
217
  try:
214
218
  if time() - self.__token_requested_at >= self.__token_expires_in:
215
219
  token_info = self.__get_access_token()
@@ -224,12 +228,16 @@ class Spotify:
224
228
  self.__token_requested_at = token_info.get("requested_at")
225
229
 
226
230
  logger.info("The access token is still valid, no need to refresh.")
231
+ except (AuthenticationException, requests.RequestException) as e:
232
+ logger.warning(
233
+ f"Failed to refresh the access toke due to following error: {e}"
234
+ )
227
235
  except TypeError:
228
236
  logger.debug(
229
237
  f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
230
238
  )
231
239
  logger.info(
232
- "Something went wrong while trying to refresh the token. Set logging level to `DEBUG` to see the issue."
240
+ "Something went wrong while trying to refresh the access token. Set logging level to `DEBUG` to see the issue."
233
241
  )
234
242
 
235
243
  def save_access_token(self, token_info: dict) -> None:
@@ -341,6 +349,7 @@ class Spotify:
341
349
  )
342
350
  response.raise_for_status()
343
351
  except requests.RequestException as e:
352
+ logger.warning(f"Network error during Spotify search: {e}")
344
353
  return None
345
354
 
346
355
  if response.status_code != 200:
@@ -408,6 +417,7 @@ class Spotify:
408
417
  )
409
418
  response.raise_for_status()
410
419
  except requests.RequestException as e:
420
+ logger.warning(f"Network error during Spotify search (advanced): {e}")
411
421
  return None
412
422
 
413
423
  if response.status_code != 200:
@@ -441,6 +451,7 @@ class Spotify:
441
451
  )
442
452
  response.raise_for_status()
443
453
  except requests.RequestException as e:
454
+ logger.warning(f"Network error during Spotify get artist ids: {e}")
444
455
  return None
445
456
 
446
457
  if response.status_code != 200:
@@ -820,8 +831,9 @@ class SpotifyAuth:
820
831
  logger.debug(f"Authentication response status code: {response.status_code}")
821
832
  response.raise_for_status()
822
833
  except requests.RequestException as e:
823
- logger.warning(f"Network error during Spotify authentication: {e}")
824
- return None
834
+ raise requests.RequestException(
835
+ f"Network error during Spotify authentication: {e}"
836
+ )
825
837
 
826
838
  if response.status_code == 200:
827
839
  response_json = response.json()
@@ -837,20 +849,30 @@ class SpotifyAuth:
837
849
  if not self.__access_token:
838
850
  raise SpotifyAuthException("No access token was found.")
839
851
 
840
- if time() - self.__token_requested_at >= self.__token_expires_in:
841
- token_info = self.__get_access_token(refresh_token=self.__refresh_token)
852
+ try:
853
+ if time() - self.__token_requested_at >= self.__token_expires_in:
854
+ token_info = self.__get_access_token(refresh_token=self.__refresh_token)
842
855
 
843
- try:
844
- self.save_access_token(token_info)
845
- except NotImplementedError as e:
846
- logger.warning(e)
856
+ try:
857
+ self.save_access_token(token_info)
858
+ except NotImplementedError as e:
859
+ logger.warning(e)
847
860
 
848
- self.__access_token = token_info.get("access_token")
849
- self.__refresh_token = token_info.get("refresh_token")
850
- self.__token_expires_in = token_info.get("expires_in")
851
- self.__token_requested_at = token_info.get("requested_at")
861
+ self.__access_token = token_info.get("access_token")
862
+ self.__refresh_token = token_info.get("refresh_token")
863
+ self.__token_expires_in = token_info.get("expires_in")
864
+ self.__token_requested_at = token_info.get("requested_at")
852
865
 
853
- logger.info("The access token is still valid, no need to refresh.")
866
+ logger.info("The access token is still valid, no need to refresh.")
867
+ except (AuthenticationException, requests.RequestException) as e:
868
+ logger.warning(f"Failed to refresh the access toke due to following error: {e}")
869
+ except TypeError:
870
+ logger.debug(
871
+ f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
872
+ )
873
+ logger.warning(
874
+ "Something went wrong while trying to refresh the access token. Set logging level to `DEBUG` to see the issue."
875
+ )
854
876
 
855
877
  @staticmethod
856
878
  def generate_state() -> str:
@@ -1028,7 +1050,7 @@ class SpotifyAuth:
1028
1050
  except NotImplementedError as e:
1029
1051
  logger.warning(e)
1030
1052
 
1031
- def get_user_profile(self):
1053
+ def get_user_profile(self) -> Optional[dict]:
1032
1054
  """
1033
1055
  Fetches the user's display name and profile images.
1034
1056
 
@@ -1063,10 +1085,11 @@ class SpotifyAuth:
1063
1085
  logger.warning(f"Unexpected response: {response.json()}")
1064
1086
  return None
1065
1087
 
1066
- response_json = response.json()
1088
+ result = response.json()
1067
1089
  return {
1068
- "display_name": response_json.get("display_name"),
1069
- "images": response_json.get("images", []),
1090
+ "display_name": result.get("display_name"),
1091
+ "images": result.get("images", []),
1092
+ "url": result.get("external_urls", {}).get("spotify")
1070
1093
  }
1071
1094
 
1072
1095
  def get_currently_playing(self) -> Optional[UserPlaying]:
@@ -1105,6 +1128,7 @@ class SpotifyAuth:
1105
1128
  response = self.__session.get(query_url, headers=header, timeout=30)
1106
1129
  response.raise_for_status()
1107
1130
  except requests.RequestException as e:
1131
+ logger.warning(f"Error while getting Spotify user activity: {e}")
1108
1132
  return None
1109
1133
 
1110
1134
  if response.status_code == 204:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 2.2.4
3
+ Version: 2.2.6
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>
@@ -1,60 +0,0 @@
1
- import pytest
2
-
3
- from yutipy.lastfm import LastFm
4
- from yutipy.models import UserPlaying
5
- from tests import BaseResponse
6
-
7
-
8
- @pytest.fixture
9
- def lastfm():
10
- return LastFm(api_key="test_api_key")
11
-
12
-
13
- class MockResponse(BaseResponse):
14
- @staticmethod
15
- def json():
16
- return {
17
- "recenttracks": {
18
- "track": [
19
- {
20
- "artist": {"mbid": "", "#text": "Test Artist"},
21
- "image": [
22
- {
23
- "size": "small",
24
- "#text": "https://example.com/image/small.jpg",
25
- },
26
- {
27
- "size": "extralarge",
28
- "#text": "https://example.com/image/extralarge.jpg",
29
- },
30
- ],
31
- "mbid": "",
32
- "album": {
33
- "mbid": "",
34
- "#text": "Test Album",
35
- },
36
- "name": "Test Track",
37
- "url": "https://www.last.fm/music/test+track",
38
- }
39
- ]
40
- }
41
- }
42
-
43
-
44
- @pytest.fixture
45
- def mock_response(lastfm, monkeypatch):
46
- def mock_get(*args, **kwargs):
47
- return MockResponse()
48
-
49
- monkeypatch.setattr(lastfm._LastFm__session, "get", mock_get)
50
-
51
-
52
- def test_get_currently_playing(lastfm, mock_response):
53
- username = "bob"
54
- currently_playing = lastfm.get_currently_playing(username=username)
55
- assert currently_playing is not None
56
- assert isinstance(currently_playing, UserPlaying)
57
- assert currently_playing.title == "Test Track"
58
- assert currently_playing.album_title == "Test Album"
59
- assert "extralarge" in currently_playing.album_art
60
- assert currently_playing.is_playing is False
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
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
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