yutipy 2.2.3__tar.gz → 2.2.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.

Potentially problematic release.


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

Files changed (61) hide show
  1. {yutipy-2.2.3 → yutipy-2.2.4}/PKG-INFO +1 -1
  2. {yutipy-2.2.3 → yutipy-2.2.4}/docs/api_reference.rst +1 -1
  3. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_spotify.py +1 -0
  4. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/deezer.py +14 -14
  5. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/exceptions.py +0 -5
  6. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/itunes.py +4 -5
  7. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/kkbox.py +4 -6
  8. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/lastfm.py +2 -1
  9. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/logger.py +1 -1
  10. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/models.py +3 -0
  11. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/musicyt.py +14 -6
  12. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/spotify.py +34 -25
  13. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/yutipy_music.py +22 -7
  14. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy.egg-info/PKG-INFO +1 -1
  15. {yutipy-2.2.3 → yutipy-2.2.4}/.gitattributes +0 -0
  16. {yutipy-2.2.3 → yutipy-2.2.4}/.github/FUNDING.yml +0 -0
  17. {yutipy-2.2.3 → yutipy-2.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {yutipy-2.2.3 → yutipy-2.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  19. {yutipy-2.2.3 → yutipy-2.2.4}/.github/dependabot.yml +0 -0
  20. {yutipy-2.2.3 → yutipy-2.2.4}/.github/workflows/pytest-unit-testing.yml +0 -0
  21. {yutipy-2.2.3 → yutipy-2.2.4}/.github/workflows/release.yml +0 -0
  22. {yutipy-2.2.3 → yutipy-2.2.4}/.gitignore +0 -0
  23. {yutipy-2.2.3 → yutipy-2.2.4}/.readthedocs.yaml +0 -0
  24. {yutipy-2.2.3 → yutipy-2.2.4}/LICENSE +0 -0
  25. {yutipy-2.2.3 → yutipy-2.2.4}/MANIFEST.in +0 -0
  26. {yutipy-2.2.3 → yutipy-2.2.4}/README.md +0 -0
  27. {yutipy-2.2.3 → yutipy-2.2.4}/docs/Makefile +0 -0
  28. {yutipy-2.2.3 → yutipy-2.2.4}/docs/_static/yutipy_header.png +0 -0
  29. {yutipy-2.2.3 → yutipy-2.2.4}/docs/_static/yutipy_logo.png +0 -0
  30. {yutipy-2.2.3 → yutipy-2.2.4}/docs/available_platforms.rst +0 -0
  31. {yutipy-2.2.3 → yutipy-2.2.4}/docs/cli.rst +0 -0
  32. {yutipy-2.2.3 → yutipy-2.2.4}/docs/conf.py +0 -0
  33. {yutipy-2.2.3 → yutipy-2.2.4}/docs/faq.rst +0 -0
  34. {yutipy-2.2.3 → yutipy-2.2.4}/docs/index.rst +0 -0
  35. {yutipy-2.2.3 → yutipy-2.2.4}/docs/installation.rst +0 -0
  36. {yutipy-2.2.3 → yutipy-2.2.4}/docs/make.bat +0 -0
  37. {yutipy-2.2.3 → yutipy-2.2.4}/docs/requirements.txt +0 -0
  38. {yutipy-2.2.3 → yutipy-2.2.4}/docs/usage_examples.rst +0 -0
  39. {yutipy-2.2.3 → yutipy-2.2.4}/pyproject.toml +0 -0
  40. {yutipy-2.2.3 → yutipy-2.2.4}/requirements-dev.txt +0 -0
  41. {yutipy-2.2.3 → yutipy-2.2.4}/requirements.txt +0 -0
  42. {yutipy-2.2.3 → yutipy-2.2.4}/setup.cfg +0 -0
  43. {yutipy-2.2.3 → yutipy-2.2.4}/tests/__init__.py +0 -0
  44. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_deezer.py +0 -0
  45. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_itunes.py +0 -0
  46. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_kkbox.py +0 -0
  47. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_lastfm.py +0 -0
  48. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_models.py +0 -0
  49. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_musicyt.py +0 -0
  50. {yutipy-2.2.3 → yutipy-2.2.4}/tests/test_utils.py +0 -0
  51. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/__init__.py +0 -0
  52. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/cli/__init__.py +0 -0
  53. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/cli/config.py +0 -0
  54. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/cli/search.py +0 -0
  55. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/utils/__init__.py +0 -0
  56. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy/utils/helpers.py +0 -0
  57. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy.egg-info/SOURCES.txt +0 -0
  58. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy.egg-info/dependency_links.txt +0 -0
  59. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy.egg-info/entry_points.txt +0 -0
  60. {yutipy-2.2.3 → yutipy-2.2.4}/yutipy.egg-info/requires.txt +0 -0
  61. {yutipy-2.2.3 → yutipy-2.2.4}/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.3
3
+ Version: 2.2.4
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>
@@ -97,7 +97,7 @@ UserPlaying
97
97
  .. autoclass:: yutipy.models.UserPlaying
98
98
  :members:
99
99
  :noindex:
100
- :exclude-members: album_art, album_art_source, album_title, album_type, artists, genre, id, isrc, lyrics, release_date, tempo, title, type, upc, url, is_playing
100
+ :exclude-members: album_art, album_art_source, album_title, album_type, artists, genre, id, isrc, lyrics, release_date, tempo, title, type, upc, url, timestamp, is_playing
101
101
 
102
102
  Exceptions
103
103
  =============
@@ -183,6 +183,7 @@ def test_get_currently_playing(spotify_auth, monkeypatch):
183
183
  @staticmethod
184
184
  def json():
185
185
  return {
186
+ "timestamp": 1745797530935,
186
187
  "is_playing": False,
187
188
  "item": {
188
189
  "album": {
@@ -9,11 +9,10 @@ from yutipy.exceptions import (
9
9
  DeezerException,
10
10
  InvalidResponseException,
11
11
  InvalidValueException,
12
- NetworkException,
13
12
  )
13
+ from yutipy.logger import logger
14
14
  from yutipy.models import MusicInfo
15
15
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
- from yutipy.logger import logger
17
16
 
18
17
 
19
18
  class Deezer:
@@ -97,17 +96,17 @@ class Deezer:
97
96
  logger.debug(f"Response status code: {response.status_code}")
98
97
  response.raise_for_status()
99
98
  except requests.RequestException as e:
100
- logger.error(f"Network error while fetching music info: {e}")
101
- raise NetworkException(f"Network error occurred: {e}")
99
+ logger.warning(f"Network error while fetching music info: {e}")
100
+ return None
102
101
  except Exception as e:
103
- logger.exception(f"Unexpected error while searching Deezer: {e}")
102
+ logger.warning(f"Unexpected error while searching Deezer: {e}")
104
103
  raise DeezerException(f"An error occurred while searching Deezer: {e}")
105
104
 
106
105
  try:
107
106
  logger.debug(f"Parsing response JSON: {response.json()}")
108
107
  result = response.json()["data"]
109
108
  except (IndexError, KeyError, ValueError) as e:
110
- logger.error(f"Invalid response structure from Deezer: {e}")
109
+ logger.warning(f"Invalid response structure from Deezer: {e}")
111
110
  raise InvalidResponseException(f"Invalid response received: {e}")
112
111
 
113
112
  music_info = self._parse_results(artist, song, result)
@@ -164,17 +163,17 @@ class Deezer:
164
163
  logger.debug(f"Response status code: {response.status_code}")
165
164
  response.raise_for_status()
166
165
  except requests.RequestException as e:
167
- logger.error(f"Error fetching track info: {e}")
168
- raise NetworkException(f"Network error occurred: {e}")
166
+ logger.warning(f"Error fetching track info: {e}")
167
+ return None
169
168
  except Exception as e:
170
- logger.error(f"Error fetching track info: {e}")
169
+ logger.warning(f"Error fetching track info: {e}")
171
170
  raise DeezerException(f"An error occurred while fetching track info: {e}")
172
171
 
173
172
  try:
174
173
  logger.debug(f"Response JSON: {response.json()}")
175
174
  result = response.json()
176
175
  except ValueError as e:
177
- logger.error(f"Invalid response received from Deezer: {e}")
176
+ logger.warning(f"Invalid response received from Deezer: {e}")
178
177
  raise InvalidResponseException(f"Invalid response received: {e}")
179
178
 
180
179
  return {
@@ -205,17 +204,17 @@ class Deezer:
205
204
  logger.info(f"Response status code: {response.status_code}")
206
205
  response.raise_for_status()
207
206
  except requests.RequestException as e:
208
- logger.error(f"Error fetching album info: {e}")
209
- raise NetworkException(f"Network error occurred: {e}")
207
+ logger.warning(f"Error fetching album info: {e}")
208
+ return None
210
209
  except Exception as e:
211
- logger.error(f"Error fetching album info: {e}")
210
+ logger.warning(f"Error fetching album info: {e}")
212
211
  raise DeezerException(f"An error occurred while fetching album info: {e}")
213
212
 
214
213
  try:
215
214
  logger.debug(f"Response JSON: {response.json()}")
216
215
  result = response.json()
217
216
  except ValueError as e:
218
- logger.error(f"Invalid response received from Deezer: {e}")
217
+ logger.warning(f"Invalid response received from Deezer: {e}")
219
218
  raise InvalidResponseException(f"Invalid response received: {e}")
220
219
 
221
220
  return {
@@ -323,6 +322,7 @@ class Deezer:
323
322
 
324
323
  if __name__ == "__main__":
325
324
  import logging
325
+
326
326
  from yutipy.logger import enable_logging
327
327
 
328
328
  enable_logging(level=logging.DEBUG)
@@ -2,7 +2,6 @@ __all__ = [
2
2
  "AuthenticationException",
3
3
  "InvalidResponseException",
4
4
  "InvalidValueException",
5
- "NetworkException",
6
5
  "YutipyException",
7
6
  ]
8
7
 
@@ -25,10 +24,6 @@ class InvalidValueException(YutipyException):
25
24
  """Exception raised for invalid values."""
26
25
 
27
26
 
28
- class NetworkException(YutipyException):
29
- """Exception raised for network-related errors."""
30
-
31
-
32
27
  # Service Exceptions
33
28
  class DeezerException(YutipyException):
34
29
  """Exception raised for errors related to the Deezer API."""
@@ -9,8 +9,7 @@ import requests
9
9
  from yutipy.exceptions import (
10
10
  InvalidResponseException,
11
11
  InvalidValueException,
12
- ItunesException,
13
- NetworkException,
12
+ ItunesException
14
13
  )
15
14
  from yutipy.models import MusicInfo
16
15
  from yutipy.utils.helpers import (
@@ -100,8 +99,8 @@ class Itunes:
100
99
  logger.debug(f"Response status code: {response.status_code}")
101
100
  response.raise_for_status()
102
101
  except requests.RequestException as e:
103
- logger.error(f"Network error while searching iTunes: {e}")
104
- raise NetworkException(f"Network error occurred: {e}")
102
+ logger.warning(f"Network error while searching iTunes: {e}")
103
+ return None
105
104
  except Exception as e:
106
105
  logger.exception(f"Unexpected error while searching iTunes: {e}")
107
106
  raise ItunesException(f"An error occurred while searching iTunes: {e}")
@@ -110,7 +109,7 @@ class Itunes:
110
109
  logger.debug(f"Parsing response JSON: {response.json()}")
111
110
  result = response.json()["results"]
112
111
  except (IndexError, KeyError, ValueError) as e:
113
- logger.error(f"Invalid response structure from iTunes: {e}")
112
+ logger.warning(f"Invalid response structure from iTunes: {e}")
114
113
  raise InvalidResponseException(f"Invalid response received: {e}")
115
114
 
116
115
  music_info = self._parse_result(artist, song, result)
@@ -12,10 +12,8 @@ from dotenv import load_dotenv
12
12
 
13
13
  from yutipy.exceptions import (
14
14
  AuthenticationException,
15
- InvalidResponseException,
16
15
  InvalidValueException,
17
16
  KKBoxException,
18
- NetworkException,
19
17
  )
20
18
  from yutipy.logger import logger
21
19
  from yutipy.models import MusicInfo
@@ -187,15 +185,15 @@ class KKBox:
187
185
  logger.debug(f"Authentication response status code: {response.status_code}")
188
186
  response.raise_for_status()
189
187
  except requests.RequestException as e:
190
- logger.error(f"Network error during KKBOX authentication: {e}")
191
- raise NetworkException(f"Network error occurred: {e}")
188
+ logger.warning(f"Network error during KKBOX authentication: {e}")
189
+ return None
192
190
 
193
191
  if response.status_code == 200:
194
192
  response_json = response.json()
195
193
  response_json["requested_at"] = time()
196
194
  return response_json
197
195
  else:
198
- raise InvalidResponseException(
196
+ raise AuthenticationException(
199
197
  f"Invalid response received: {response.json()}"
200
198
  )
201
199
 
@@ -319,7 +317,7 @@ class KKBox:
319
317
  logger.debug(f"Parsing response JSON: {response.json()}")
320
318
  response.raise_for_status()
321
319
  except requests.RequestException as e:
322
- raise NetworkException(f"Network error occurred: {e}")
320
+ return None
323
321
 
324
322
  if response.status_code != 200:
325
323
  raise KKBoxException(f"Failed to search for music: {response.json()}")
@@ -95,7 +95,7 @@ class LastFm:
95
95
  response = self.__session.get(query_url, timeout=30)
96
96
  response.raise_for_status()
97
97
  except requests.RequestException as e:
98
- logger.error(f"Failed to fetch user profile: {e}")
98
+ logger.warning(f"Failed to fetch user profile: {e}")
99
99
  return None
100
100
 
101
101
  response_json = response.json()
@@ -110,6 +110,7 @@ class LastFm:
110
110
  album_title=result.get("album", {}).get("#text"),
111
111
  artists=", ".join(separate_artists(result.get("artist", {}).get("#text"))),
112
112
  id=result.get("mbid"),
113
+ timestamp=result.get("date", {}).get("uts"),
113
114
  title=result.get("name"),
114
115
  url=result.get("url"),
115
116
  is_playing=result.get("@attr", {}).get("nowplaying", False),
@@ -2,7 +2,7 @@ import logging
2
2
 
3
3
  # Create a logger for the library
4
4
  logger = logging.getLogger("yutipy")
5
- logger.setLevel(logging.NOTSET)
5
+ logger.setLevel(logging.CRITICAL)
6
6
 
7
7
 
8
8
  def enable_logging(level=logging.INFO, handler=None):
@@ -74,8 +74,11 @@ class UserPlaying(MusicInfo):
74
74
 
75
75
  Attributes
76
76
  ----------
77
+ timetamp : Optional[int]
78
+ Unix Timestamp (in seconds) when playback was started.
77
79
  is_playing : Optional[bool]
78
80
  Whether the music is currently playing or paused.
79
81
  """
80
82
 
83
+ timestamp: Optional[int] = None
81
84
  is_playing: Optional[bool] = None
@@ -11,9 +11,9 @@ from yutipy.exceptions import (
11
11
  InvalidValueException,
12
12
  MusicYTException,
13
13
  )
14
+ from yutipy.logger import logger
14
15
  from yutipy.models import MusicInfo
15
16
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
- from yutipy.logger import logger
17
17
 
18
18
 
19
19
  class MusicYT:
@@ -87,8 +87,8 @@ class MusicYT:
87
87
  try:
88
88
  results = self.ytmusic.search(query=query, limit=limit)
89
89
  except exceptions.YTMusicServerError as e:
90
- logger.error(f"Something went wrong while searching YTMusic: {e}")
91
- raise MusicYTException(f"Something went wrong while searching YTMusic: {e}")
90
+ logger.warning(f"Something went wrong while searching YTMusic: {e}")
91
+ return None
92
92
 
93
93
  for result in results:
94
94
  if self._is_relevant_result(artist, song, result):
@@ -181,9 +181,15 @@ class MusicYT:
181
181
  The extracted music information.
182
182
  """
183
183
  if result["resultType"] in ["song", "video"]:
184
- return self._get_song(result)
184
+ try:
185
+ return self._get_song(result)
186
+ except InvalidResponseException:
187
+ return None
185
188
  else:
186
- return self._get_album(result)
189
+ try:
190
+ return self._get_album(result)
191
+ except InvalidResponseException:
192
+ return None
187
193
 
188
194
  def _get_song(self, result: dict) -> MusicInfo:
189
195
  """
@@ -200,7 +206,9 @@ class MusicYT:
200
206
  The extracted music information.
201
207
  """
202
208
  title = result.get("title")
203
- artist_names = ", ".join([artist.get("name") for artist in result.get("artists", [])])
209
+ artist_names = ", ".join(
210
+ [artist.get("name") for artist in result.get("artists", [])]
211
+ )
204
212
  video_id = result.get("videoId")
205
213
  song_url = f"https://music.youtube.com/watch?v={video_id}"
206
214
  lyrics_id = self.ytmusic.get_watch_playlist(video_id)
@@ -14,9 +14,7 @@ from dotenv import load_dotenv
14
14
 
15
15
  from yutipy.exceptions import (
16
16
  AuthenticationException,
17
- InvalidResponseException,
18
17
  InvalidValueException,
19
- NetworkException,
20
18
  SpotifyAuthException,
21
19
  SpotifyException,
22
20
  )
@@ -198,8 +196,8 @@ class Spotify:
198
196
  logger.debug(f"Authentication response status code: {response.status_code}")
199
197
  response.raise_for_status()
200
198
  except requests.RequestException as e:
201
- logger.error(f"Network error during Spotify authentication: {e}")
202
- raise NetworkException(f"Network error occurred: {e}")
199
+ logger.warning(f"Network error during Spotify authentication: {e}")
200
+ return None
203
201
 
204
202
  if response.status_code == 200:
205
203
  response_json = response.json()
@@ -212,19 +210,27 @@ class Spotify:
212
210
 
213
211
  def __refresh_access_token(self):
214
212
  """Refreshes the token if it has expired."""
215
- if time() - self.__token_requested_at >= self.__token_expires_in:
216
- token_info = self.__get_access_token()
213
+ try:
214
+ if time() - self.__token_requested_at >= self.__token_expires_in:
215
+ token_info = self.__get_access_token()
217
216
 
218
- try:
219
- self.save_access_token(token_info)
220
- except NotImplementedError as e:
221
- logger.warning(e)
217
+ try:
218
+ self.save_access_token(token_info)
219
+ except NotImplementedError as e:
220
+ logger.warning(e)
222
221
 
223
- self.__access_token = token_info.get("access_token")
224
- self.__token_expires_in = token_info.get("expires_in")
225
- self.__token_requested_at = token_info.get("requested_at")
222
+ self.__access_token = token_info.get("access_token")
223
+ self.__token_expires_in = token_info.get("expires_in")
224
+ self.__token_requested_at = token_info.get("requested_at")
226
225
 
227
- logger.info("The access token is still valid, no need to refresh.")
226
+ logger.info("The access token is still valid, no need to refresh.")
227
+ except TypeError:
228
+ logger.debug(
229
+ f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
230
+ )
231
+ logger.info(
232
+ "Something went wrong while trying to refresh the token. Set logging level to `DEBUG` to see the issue."
233
+ )
228
234
 
229
235
  def save_access_token(self, token_info: dict) -> None:
230
236
  """
@@ -335,7 +341,7 @@ class Spotify:
335
341
  )
336
342
  response.raise_for_status()
337
343
  except requests.RequestException as e:
338
- raise NetworkException(f"Network error occurred: {e}")
344
+ return None
339
345
 
340
346
  if response.status_code != 200:
341
347
  raise SpotifyException(f"Failed to search for music: {response.json()}")
@@ -402,7 +408,7 @@ class Spotify:
402
408
  )
403
409
  response.raise_for_status()
404
410
  except requests.RequestException as e:
405
- raise NetworkException(f"Network error occurred: {e}")
411
+ return None
406
412
 
407
413
  if response.status_code != 200:
408
414
  raise SpotifyException(
@@ -435,7 +441,7 @@ class Spotify:
435
441
  )
436
442
  response.raise_for_status()
437
443
  except requests.RequestException as e:
438
- raise NetworkException(f"Network error occurred: {e}")
444
+ return None
439
445
 
440
446
  if response.status_code != 200:
441
447
  return None
@@ -814,15 +820,15 @@ class SpotifyAuth:
814
820
  logger.debug(f"Authentication response status code: {response.status_code}")
815
821
  response.raise_for_status()
816
822
  except requests.RequestException as e:
817
- logger.error(f"Network error during Spotify authentication: {e}")
818
- raise NetworkException(f"Network error occurred: {e}")
823
+ logger.warning(f"Network error during Spotify authentication: {e}")
824
+ return None
819
825
 
820
826
  if response.status_code == 200:
821
827
  response_json = response.json()
822
828
  response_json["requested_at"] = time()
823
829
  return response_json
824
830
  else:
825
- raise InvalidResponseException(
831
+ raise AuthenticationException(
826
832
  f"Invalid response received: {response.json()}"
827
833
  )
828
834
 
@@ -1050,11 +1056,11 @@ class SpotifyAuth:
1050
1056
  response = self.__session.get(query_url, headers=header, timeout=30)
1051
1057
  response.raise_for_status()
1052
1058
  except requests.RequestException as e:
1053
- logger.error(f"Failed to fetch user profile: {e}")
1059
+ logger.warning(f"Failed to fetch user profile: {e}")
1054
1060
  return None
1055
1061
 
1056
1062
  if response.status_code != 200:
1057
- logger.error(f"Unexpected response: {response.json()}")
1063
+ logger.warning(f"Unexpected response: {response.json()}")
1058
1064
  return None
1059
1065
 
1060
1066
  response_json = response.json()
@@ -1099,16 +1105,16 @@ class SpotifyAuth:
1099
1105
  response = self.__session.get(query_url, headers=header, timeout=30)
1100
1106
  response.raise_for_status()
1101
1107
  except requests.RequestException as e:
1102
- raise NetworkException(f"Network error occurred: {e}")
1108
+ return None
1103
1109
 
1104
1110
  if response.status_code == 204:
1105
1111
  logger.info("Requested user is currently not listening to any music.")
1106
1112
  return None
1107
1113
  if response.status_code != 200:
1108
1114
  try:
1109
- logger.error(f"Unexpected response: {response.json()}")
1115
+ logger.warning(f"Unexpected response: {response.json()}")
1110
1116
  except requests.exceptions.JSONDecodeError:
1111
- logger.error(
1117
+ logger.warning(
1112
1118
  f"Response Code: {response.status_code}, Reason: {response.reason}"
1113
1119
  )
1114
1120
  return None
@@ -1122,6 +1128,8 @@ class SpotifyAuth:
1122
1128
  guess,
1123
1129
  use_translation=False,
1124
1130
  )
1131
+ # Spotify returns timestamp in milliseconds, so convert milliseconds to seconds:
1132
+ timestamp = response_json.get("timestamp") / 1000.0
1125
1133
  return UserPlaying(
1126
1134
  album_art=result.get("album", {}).get("images", [])[0].get("url"),
1127
1135
  album_title=result.get("album", {}).get("name"),
@@ -1138,6 +1146,7 @@ class SpotifyAuth:
1138
1146
  lyrics=None,
1139
1147
  release_date=result.get("album", {}).get("release_date"),
1140
1148
  tempo=None,
1149
+ timestamp=timestamp,
1141
1150
  title=result.get("name"),
1142
1151
  type=result.get("type"),
1143
1152
  upc=result.get("external_ids", {}).get("upc"),
@@ -22,8 +22,23 @@ class YutipyMusic:
22
22
  Instead of calling each service separately, you can use this class to get the information from all services at once.
23
23
  """
24
24
 
25
- def __init__(self) -> None:
26
- """Initializes the YutipyMusic class."""
25
+ def __init__(
26
+ self,
27
+ custom_kkbox_class = KKBox,
28
+ custom_spotify_class = Spotify,
29
+ ) -> None:
30
+ """
31
+ Initializes the YutipyMusic class.
32
+
33
+ Parameters
34
+ ----------
35
+ custom_kkbox_class : Optional[type], optional
36
+ A custom class inherited from ``KKBox`` to override the default KKBox implementation.
37
+ This class should implement ``load_access_token()`` and ``save_access_token()`` methods. Default is ``KKBox``.
38
+ custom_spotify_class : Optional[type], optional
39
+ A custom class inherited from ``Spotify`` to override the default Spotify implementation.
40
+ This class should implement ``load_access_token()`` and ``save_access_token()`` methods. Default is ``Spotify``.
41
+ """
27
42
  self.music_info = MusicInfos()
28
43
  self.normalize_non_english = True
29
44
  self.album_art_priority = ["deezer", "ytmusic", "itunes"]
@@ -34,8 +49,8 @@ class YutipyMusic:
34
49
  }
35
50
 
36
51
  try:
37
- self.services["kkbox"] = KKBox()
38
- except (KKBoxException) as e:
52
+ self.services["kkbox"] = custom_kkbox_class()
53
+ except KKBoxException as e:
39
54
  logger.warning(
40
55
  f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
41
56
  )
@@ -44,8 +59,8 @@ class YutipyMusic:
44
59
  self.album_art_priority.insert(idx, "kkbox")
45
60
 
46
61
  try:
47
- self.services["spotify"] = Spotify()
48
- except (SpotifyException) as e:
62
+ self.services["spotify"] = custom_spotify_class()
63
+ except SpotifyException as e:
49
64
  logger.warning(
50
65
  f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
51
66
  )
@@ -114,7 +129,7 @@ class YutipyMusic:
114
129
  result = future.result()
115
130
  self._combine_results(result, service_name)
116
131
  except Exception as e:
117
- logger.error(
132
+ logger.warning(
118
133
  f"Error occurred while searching with {service_name}: {e}"
119
134
  )
120
135
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 2.2.3
3
+ Version: 2.2.4
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>
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