yutipy 2.2.15__py3-none-any.whl → 2.3.1__py3-none-any.whl

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.

yutipy/__init__.py CHANGED
@@ -5,6 +5,7 @@ from yutipy import (
5
5
  kkbox,
6
6
  lastfm,
7
7
  logger,
8
+ lrclib,
8
9
  musicyt,
9
10
  spotify,
10
11
  yutipy_music,
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "kkbox",
18
19
  "lastfm",
19
20
  "logger",
21
+ "lrclib",
20
22
  "musicyt",
21
23
  "spotify",
22
24
  "yutipy_music",
yutipy/base_clients.py CHANGED
@@ -384,6 +384,11 @@ class BaseAuthClient:
384
384
  "refresh_token": self._refresh_token,
385
385
  }
386
386
 
387
+ if not data:
388
+ raise AuthenticationException(
389
+ "Either `authorization_code` or `refresh_token` must be provided to get access token."
390
+ )
391
+
387
392
  try:
388
393
  logger.info(
389
394
  f"Authenticating with {self.SERVICE_NAME} API using Authorization Code grant type."
@@ -404,6 +409,12 @@ class BaseAuthClient:
404
409
 
405
410
  def _refresh_access_token(self):
406
411
  """Refreshes the token if it has expired."""
412
+ if not self._access_token or not self._refresh_token:
413
+ logger.warning(
414
+ "No access token or refresh token found. You must authenticate to obtain a new token."
415
+ )
416
+ return
417
+
407
418
  try:
408
419
  if time() - self._token_requested_at >= self._token_expires_in:
409
420
  token_info = self._get_access_token(refresh_token=self._refresh_token)
yutipy/deezer.py CHANGED
@@ -9,16 +9,23 @@ from yutipy.exceptions import DeezerException, InvalidValueException
9
9
  from yutipy.logger import logger
10
10
  from yutipy.models import MusicInfo
11
11
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
12
+ from yutipy.lrclib import LrcLib
12
13
 
13
14
 
14
15
  class Deezer:
15
16
  """A class to interact with the Deezer API."""
16
17
 
17
- def __init__(self) -> None:
18
- """Initializes the Deezer class and sets up the session."""
18
+ def __init__(self, fetch_lyrics: bool = True) -> None:
19
+ """
20
+ Parameters
21
+ ----------
22
+ fetch_lyrics : bool, optional
23
+ Whether to fetch lyrics (using `LRCLIB <https://lrclib.net>`__) if the music platform does not provide lyrics (default is True).
24
+ """
19
25
  self.api_url = "https://api.deezer.com"
20
26
  self._is_session_closed = False
21
27
  self.normalize_non_english = True
28
+ self.fetch_lyrics = fetch_lyrics
22
29
  self.__session = requests.Session()
23
30
  self._translation_session = requests.Session()
24
31
 
@@ -95,7 +102,7 @@ class Deezer:
95
102
  return None
96
103
 
97
104
  try:
98
- logger.debug(f"Parsing response JSON.")
105
+ logger.debug("Parsing response JSON.")
99
106
  result = response.json()["data"]
100
107
  except (IndexError, KeyError, ValueError) as e:
101
108
  logger.warning(f"Invalid response structure from Deezer: {e}")
@@ -159,7 +166,7 @@ class Deezer:
159
166
  return None
160
167
 
161
168
  try:
162
- logger.debug(f"Parsing Response JSON.")
169
+ logger.debug("Parsing Response JSON.")
163
170
  result = response.json()
164
171
  except ValueError as e:
165
172
  logger.warning(f"Invalid response received from Deezer: {e}")
@@ -303,6 +310,14 @@ class Deezer:
303
310
  music_info.release_date = album_info.get("release_date")
304
311
  music_info.genre = album_info.get("genre")
305
312
 
313
+ if self.fetch_lyrics:
314
+ with LrcLib() as lrc_lib:
315
+ lyrics = lrc_lib.get_lyrics(
316
+ artist=music_info.artists, song=music_info.title
317
+ )
318
+ if lyrics:
319
+ music_info.lyrics = lyrics.get("plainLyrics")
320
+
306
321
  return music_info
307
322
 
308
323
 
yutipy/itunes.py CHANGED
@@ -8,18 +8,30 @@ import requests
8
8
 
9
9
  from yutipy.exceptions import InvalidValueException, ItunesException
10
10
  from yutipy.logger import logger
11
+ from yutipy.lrclib import LrcLib
11
12
  from yutipy.models import MusicInfo
12
- from yutipy.utils.helpers import are_strings_similar, guess_album_type, is_valid_string, separate_artists
13
+ from yutipy.utils.helpers import (
14
+ are_strings_similar,
15
+ guess_album_type,
16
+ is_valid_string,
17
+ separate_artists,
18
+ )
13
19
 
14
20
 
15
21
  class Itunes:
16
22
  """A class to interact with the iTunes API."""
17
23
 
18
- def __init__(self) -> None:
19
- """Initializes the iTunes class and sets up the session."""
24
+ def __init__(self, fetch_lyrics: bool = True) -> None:
25
+ """
26
+ Parameters
27
+ ----------
28
+ fetch_lyrics : bool, optional
29
+ Whether to fetch lyrics (using `LRCLIB <https://lrclib.net>`__) if the music platform does not provide lyrics (default is True).
30
+ """
20
31
  self.api_url = "https://itunes.apple.com"
21
32
  self.normalize_non_english = True
22
33
  self._is_session_closed = False
34
+ self.fetch_lyrics = fetch_lyrics
23
35
  self.__session = requests.Session()
24
36
  self.__translation_session = requests.Session()
25
37
 
@@ -95,7 +107,7 @@ class Itunes:
95
107
  return None
96
108
 
97
109
  try:
98
- logger.debug(f"Parsing response JSON.")
110
+ logger.debug("Parsing response JSON.")
99
111
  result = response.json()["results"]
100
112
  except (IndexError, KeyError, ValueError) as e:
101
113
  logger.warning(f"Invalid response structure from iTunes: {e}")
@@ -151,7 +163,7 @@ class Itunes:
151
163
  release_date = self._format_release_date(result["releaseDate"])
152
164
  artists = separate_artists(result["artistName"])
153
165
 
154
- return MusicInfo(
166
+ music_info = MusicInfo(
155
167
  album_art=result["artworkUrl100"],
156
168
  album_title=album_title,
157
169
  album_type=album_type.lower(),
@@ -168,6 +180,15 @@ class Itunes:
168
180
  url=result.get("trackViewUrl", result["collectionViewUrl"]),
169
181
  )
170
182
 
183
+ if self.fetch_lyrics:
184
+ with LrcLib() as lrc_lib:
185
+ lyrics = lrc_lib.get_lyrics(
186
+ artist=music_info.artists, song=music_info.title
187
+ )
188
+ if lyrics:
189
+ music_info.lyrics = lyrics.get("plainLyrics")
190
+
191
+ return music_info
171
192
  return None
172
193
 
173
194
  def _extract_album_info(self, result: dict) -> tuple:
yutipy/kkbox.py CHANGED
@@ -11,6 +11,7 @@ from dotenv import load_dotenv
11
11
  from yutipy.base_clients import BaseClient
12
12
  from yutipy.exceptions import InvalidValueException, KKBoxException
13
13
  from yutipy.logger import logger
14
+ from yutipy.lrclib import LrcLib
14
15
  from yutipy.models import MusicInfo
15
16
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
17
 
@@ -29,11 +30,13 @@ class KKBox(BaseClient):
29
30
  """
30
31
 
31
32
  def __init__(
32
- self, client_id: str = None, client_secret: str = None, defer_load: bool = False
33
+ self,
34
+ client_id: str = None,
35
+ client_secret: str = None,
36
+ defer_load: bool = False,
37
+ fetch_lyrics: bool = True,
33
38
  ) -> None:
34
39
  """
35
- Initializes the KKBox class and sets up the session.
36
-
37
40
  Parameters
38
41
  ----------
39
42
  client_id : str, optional
@@ -42,9 +45,12 @@ class KKBox(BaseClient):
42
45
  The Client secret for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_SECRET`` from .env file.
43
46
  defer_load : bool, optional
44
47
  Whether to defer loading the access token during initialization. Default is ``False``.
48
+ fetch_lyrics : bool, optional
49
+ Whether to fetch lyrics (using `LRCLIB <https://lrclib.net>`__) if the music platform does not provide lyrics (default is True).
45
50
  """
46
51
  self.client_id = client_id or KKBOX_CLIENT_ID
47
52
  self.client_secret = client_secret or KKBOX_CLIENT_SECRET
53
+ self.fetch_lyrics = fetch_lyrics
48
54
 
49
55
  if not self.client_id:
50
56
  raise KKBoxException(
@@ -256,7 +262,7 @@ class KKBox(BaseClient):
256
262
  )
257
263
 
258
264
  if matching_artists:
259
- return MusicInfo(
265
+ music_info = MusicInfo(
260
266
  album_art=track.get("album", {}).get("images", [])[2]["url"],
261
267
  album_title=track.get("album", {}).get("name"),
262
268
  album_type=None,
@@ -273,6 +279,14 @@ class KKBox(BaseClient):
273
279
  url=track.get("url"),
274
280
  )
275
281
 
282
+ if self.fetch_lyrics:
283
+ with LrcLib() as lrc_lib:
284
+ lyrics = lrc_lib.get_lyrics(
285
+ artist=music_info.artists, song=music_info.title
286
+ )
287
+ if lyrics:
288
+ music_info.lyrics = lyrics.get("plainLyrics")
289
+ return music_info
276
290
  return None
277
291
 
278
292
  def _find_album(self, song: str, artist: str, album: dict) -> Optional[MusicInfo]:
yutipy/lastfm.py CHANGED
@@ -1,8 +1,8 @@
1
1
  __all__ = ["LastFm", "LastFmException"]
2
2
 
3
3
  import os
4
- from time import time
5
4
  from pprint import pprint
5
+ from time import time
6
6
  from typing import Optional
7
7
 
8
8
  import requests
@@ -28,12 +28,6 @@ class LastFm:
28
28
 
29
29
  def __init__(self, api_key: str = None):
30
30
  """
31
- Initializes the LastFm class.
32
-
33
- Args:
34
- lastfm_api_key (str, optional): The Last.fm API key. If not provided,
35
- it will be fetched from the environment variable `LASTFM_API_KEY`.
36
-
37
31
  Parameters
38
32
  ----------
39
33
  lastfm_api_key : str, optional
@@ -141,7 +135,9 @@ class LastFm:
141
135
  response_json = response.json()
142
136
  result = response_json.get("recenttracks", {}).get("track", [])[0]
143
137
  is_playing = result.get("@attr", {}).get("nowplaying", False)
144
- is_playing = True if isinstance(is_playing, str) and is_playing == "true" else False
138
+ is_playing = (
139
+ True if isinstance(is_playing, str) and is_playing == "true" else False
140
+ )
145
141
  if result and is_playing:
146
142
  album_art = [
147
143
  img.get("#text")
yutipy/lrclib.py ADDED
@@ -0,0 +1,166 @@
1
+ __all__ = ["LrcLib"]
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+ from typing import Optional
5
+
6
+ import requests
7
+
8
+ from yutipy.exceptions import InvalidValueException
9
+ from yutipy.logger import logger
10
+ from yutipy.utils import are_strings_similar, is_valid_string
11
+
12
+
13
+ class LrcLib:
14
+ """
15
+ A class to interact with the `LRCLIB <lrclib.net>`_ API for fetching lyrics.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ app_name: str = "yutipy",
21
+ app_version: str = None,
22
+ app_url: str = "https://github.com/CheapNightbot/yutipy",
23
+ ) -> None:
24
+ """
25
+ Parameters
26
+ ----------
27
+ app_name : str
28
+ The name of the application.
29
+ app_version : str, optional
30
+ The version of the application.
31
+ app_url : str, optional
32
+ The URL of the application.
33
+
34
+ Notes
35
+ -----
36
+ These are used to set the User-Agent header for requests made to the API as suggested by the API documentation of `LRCLIB <lrclib.net>`_.
37
+ """
38
+ self.api_url = "https://lrclib.net/api"
39
+ self.app_name = app_name
40
+ self.app_url = app_url
41
+ if not app_version:
42
+ try:
43
+ self.app_version = f"v{version('yutipy')}"
44
+ except PackageNotFoundError:
45
+ self.app_version = "N/A"
46
+ else:
47
+ self.app_version = app_version
48
+
49
+ self._is_session_closed = False
50
+ self.__session = requests.Session()
51
+ self.__session.headers.update(
52
+ {"User-Agent": f"{self.app_name} {self.app_version} ({self.app_url})"}
53
+ )
54
+ self._translation_session = requests.Session()
55
+
56
+ def __enter__(self):
57
+ return self
58
+
59
+ def __exit__(self, exc_type, exc_value, traceback):
60
+ self.close_session()
61
+
62
+ def close_session(self):
63
+ """
64
+ Closes the session if it is not already closed.
65
+ """
66
+ if not self._is_session_closed:
67
+ self.__session.close()
68
+ self._translation_session.close()
69
+ self._is_session_closed = True
70
+
71
+ @property
72
+ def is_session_closed(self) -> bool:
73
+ return self._is_session_closed
74
+
75
+ def get_lyrics(
76
+ self,
77
+ artist: str,
78
+ song: str,
79
+ album: str = None,
80
+ normalize_non_english: bool = True,
81
+ ) -> Optional[dict]:
82
+ """
83
+ Fetches lyrics for a given artist and song.
84
+
85
+ Parameters
86
+ ----------
87
+ artist : str
88
+ The name of the artist.
89
+ song : str
90
+ The title of the song.
91
+ album : str, optional
92
+ The title of the album.
93
+ normalize_non_english : bool, optional
94
+ Whether to normalize non-English characters for comparison (default is True).
95
+
96
+ Returns
97
+ -------
98
+ Optional[dict]
99
+ The lyrics information if found, otherwise None.
100
+ """
101
+
102
+ if not is_valid_string(artist) or not is_valid_string(song):
103
+ raise InvalidValueException(
104
+ "Artist and song names must be valid strings and can't be empty."
105
+ )
106
+
107
+ endpoint = f"{self.api_url}/search"
108
+ query = f"?artist_name={artist}&track_name={song}"
109
+ query += f"&album_name={album}" if album else ""
110
+ query_url = endpoint + query
111
+
112
+ try:
113
+ logger.info(
114
+ f"Fetching lyrics for artist: {artist}, song: {song}, album: {album}"
115
+ )
116
+ response = self.__session.get(query_url, timeout=30)
117
+ logger.debug(f"Response status code: {response.status_code}")
118
+ response.raise_for_status()
119
+ except requests.RequestException as e:
120
+ logger.warning(f"Unexpected error while fetching lyrics: {e}")
121
+ return
122
+
123
+ results = response.json()
124
+
125
+ for result in results:
126
+ if are_strings_similar(
127
+ result.get("trackName"),
128
+ song,
129
+ use_translation=normalize_non_english,
130
+ translation_session=self._translation_session,
131
+ ) and are_strings_similar(
132
+ result.get("artistName"),
133
+ artist,
134
+ use_translation=normalize_non_english,
135
+ translation_session=self._translation_session,
136
+ ):
137
+ if album and not are_strings_similar(
138
+ result.get("albumName"),
139
+ album,
140
+ use_translation=normalize_non_english,
141
+ translation_session=self._translation_session,
142
+ ):
143
+ continue
144
+ return result
145
+ return None
146
+
147
+
148
+ if __name__ == "__main__":
149
+ import logging
150
+
151
+ from yutipy.logger import enable_logging
152
+
153
+ enable_logging(level=logging.DEBUG)
154
+
155
+ with LrcLib() as lyric_lib:
156
+ artist_name = input("Artist Name: ").strip()
157
+ song_title = input("Song Title: ").strip()
158
+ lyrics = lyric_lib.get_lyrics(artist_name, song_title)
159
+ print(f"\nLyrics for '{song_title}' by {artist_name}:\n{'-' * 40}\n")
160
+
161
+ if lyrics:
162
+ print(lyrics.get("plainLyrics"))
163
+ else:
164
+ print(
165
+ "It seems that the lyrics were not found! You might have to guess them..."
166
+ )
yutipy/musicyt.py CHANGED
@@ -14,17 +14,24 @@ from yutipy.exceptions import (
14
14
  from yutipy.logger import logger
15
15
  from yutipy.models import MusicInfo
16
16
  from yutipy.utils.helpers import are_strings_similar, is_valid_string
17
+ from yutipy.lrclib import LrcLib
17
18
 
18
19
 
19
20
  class MusicYT:
20
21
  """A class to interact with the YouTube Music API."""
21
22
 
22
- def __init__(self) -> None:
23
- """Initializes the YouTube Music class and sets up the session."""
23
+ def __init__(self, fetch_lyrics: bool = True) -> None:
24
+ """
25
+ Parameters
26
+ ----------
27
+ fetch_lyrics : bool, optional
28
+ Whether to fetch lyrics (using `LRCLIB <https://lrclib.net>`__) if the music platform does not provide lyrics (default is True).
29
+ """
24
30
  self.ytmusic = YTMusic()
25
31
  self._is_session_closed = False
26
32
  self.normalize_non_english = True
27
33
  self.__translation_session = requests.Session()
34
+ self.fetch_lyrics = fetch_lyrics
28
35
 
29
36
  def __enter__(self) -> "MusicYT":
30
37
  """Enters the runtime context related to this object."""
@@ -224,12 +231,13 @@ class MusicYT:
224
231
 
225
232
  try:
226
233
  lyrics = self.ytmusic.get_lyrics(lyrics_id.get("lyrics"))
234
+ lyrics = lyrics.get("lyrics")
227
235
  except exceptions.YTMusicUserError:
228
- lyrics = {}
236
+ lyrics = None
229
237
 
230
238
  album_art = result.get("thumbnails", [{}])[-1].get("url", None)
231
239
 
232
- return MusicInfo(
240
+ music_info = MusicInfo(
233
241
  album_art=album_art,
234
242
  album_title=None,
235
243
  album_type="single",
@@ -237,7 +245,7 @@ class MusicYT:
237
245
  genre=None,
238
246
  id=video_id,
239
247
  isrc=None,
240
- lyrics=lyrics.get("lyrics"),
248
+ lyrics=lyrics,
241
249
  release_date=release_date,
242
250
  tempo=None,
243
251
  title=title,
@@ -246,6 +254,16 @@ class MusicYT:
246
254
  url=song_url,
247
255
  )
248
256
 
257
+ if not music_info.lyrics and self.fetch_lyrics:
258
+ with LrcLib() as lrc_lib:
259
+ lyrics = lrc_lib.get_lyrics(
260
+ artist=music_info.artists, song=music_info.title
261
+ )
262
+ if lyrics:
263
+ music_info.lyrics = lyrics.get("plainLyrics")
264
+
265
+ return music_info
266
+
249
267
  def _get_album(self, result: dict) -> MusicInfo:
250
268
  """
251
269
  Return album info as a `MusicInfo` object.
yutipy/spotify.py CHANGED
@@ -15,6 +15,7 @@ from yutipy.exceptions import (
15
15
  SpotifyException,
16
16
  )
17
17
  from yutipy.logger import logger
18
+ from yutipy.lrclib import LrcLib
18
19
  from yutipy.models import MusicInfo, UserPlaying
19
20
  from yutipy.utils.helpers import (
20
21
  are_strings_similar,
@@ -39,11 +40,13 @@ class Spotify(BaseClient):
39
40
  """
40
41
 
41
42
  def __init__(
42
- self, client_id: str = None, client_secret: str = None, defer_load: bool = False
43
+ self,
44
+ client_id: str = None,
45
+ client_secret: str = None,
46
+ defer_load: bool = False,
47
+ fetch_lyrics: bool = True,
43
48
  ) -> None:
44
49
  """
45
- Initializes the Spotify class (using Client Credentials grant type/flow) and sets up the session.
46
-
47
50
  Parameters
48
51
  ----------
49
52
  client_id : str, optional
@@ -52,9 +55,12 @@ class Spotify(BaseClient):
52
55
  The Client secret for the Spotify API. Defaults to ``SPOTIFY_CLIENT_SECRET`` from environment variable or the ``.env`` file.
53
56
  defer_load : bool, optional
54
57
  Whether to defer loading the access token during initialization, by default ``False``
58
+ fetch_lyrics : bool, optional
59
+ Whether to fetch lyrics (using `LRCLIB <https://lrclib.net>`__) if the music platform does not provide lyrics (default is True).
55
60
  """
56
61
  self.client_id = client_id or SPOTIFY_CLIENT_ID
57
62
  self.client_secret = client_secret or SPOTIFY_CLIENT_SECRET
63
+ self.fetch_lyrics = fetch_lyrics
58
64
 
59
65
  if not self.client_id:
60
66
  raise SpotifyException(
@@ -135,7 +141,7 @@ class Spotify(BaseClient):
135
141
  )
136
142
  response.raise_for_status()
137
143
  except requests.RequestException as e:
138
- logger.warning(f"Failed to search for music: {response.json()}")
144
+ logger.warning(f"Failed to search for music: {e}")
139
145
  return None
140
146
 
141
147
  artist_ids = artist_ids if artist_ids else self._get_artists_ids(artist)
@@ -199,9 +205,7 @@ class Spotify(BaseClient):
199
205
  )
200
206
  response.raise_for_status()
201
207
  except requests.RequestException as e:
202
- raise logger.warning(
203
- f"Failed to search music with ISRC/UPC: {response.json()}"
204
- )
208
+ raise logger.warning(f"Failed to search music with ISRC/UPC: {e}")
205
209
  return None
206
210
 
207
211
  artist_ids = self._get_artists_ids(artist)
@@ -328,7 +332,7 @@ class Spotify(BaseClient):
328
332
  ]
329
333
 
330
334
  if matching_artists:
331
- return MusicInfo(
335
+ music_info = MusicInfo(
332
336
  album_art=track["album"]["images"][0]["url"],
333
337
  album_title=track["album"]["name"],
334
338
  album_type=track["album"]["album_type"],
@@ -345,6 +349,14 @@ class Spotify(BaseClient):
345
349
  url=track["external_urls"]["spotify"],
346
350
  )
347
351
 
352
+ if self.fetch_lyrics:
353
+ with LrcLib() as lrc_lib:
354
+ lyrics = lrc_lib.get_lyrics(
355
+ artist=music_info.artists, song=music_info.title
356
+ )
357
+ if lyrics:
358
+ music_info.lyrics = lyrics.get("plainLyrics")
359
+ return music_info
348
360
  return None
349
361
 
350
362
  def _find_album(
@@ -432,10 +444,9 @@ class SpotifyAuth(BaseAuthClient):
432
444
  redirect_uri: str = None,
433
445
  scopes: list[str] = None,
434
446
  defer_load: bool = False,
447
+ fetch_lyrics: bool = True,
435
448
  ):
436
449
  """
437
- Initializes the SpotifyAuth class (using Authorization Code grant type/flow) and sets up the session.
438
-
439
450
  Parameters
440
451
  ----------
441
452
  client_id : str, optional
@@ -448,11 +459,14 @@ class SpotifyAuth(BaseAuthClient):
448
459
  A list of scopes for the Spotify API. For example: `['user-read-email', 'user-read-private']`.
449
460
  defer_load : bool, optional
450
461
  Whether to defer loading the access token during initialization. Default is ``False``.
462
+ fetch_lyrics : bool, optional
463
+ Whether to fetch lyrics using `LRCLIB <https://lrclib.net>`__ if the music platform does not provide lyrics (default is True).
451
464
  """
452
465
  self.client_id = client_id or os.getenv("SPOTIFY_CLIENT_ID")
453
466
  self.client_secret = client_secret or os.getenv("SPOTIFY_CLIENT_SECRET")
454
467
  self.redirect_uri = redirect_uri or os.getenv("SPOTIFY_REDIRECT_URI")
455
468
  self.scopes = scopes
469
+ self.fetch_lyrics = fetch_lyrics
456
470
 
457
471
  if not self.client_id:
458
472
  raise SpotifyAuthException(
@@ -571,7 +585,7 @@ class SpotifyAuth(BaseAuthClient):
571
585
  )
572
586
  # Spotify returns timestamp in milliseconds, so convert milliseconds to seconds:
573
587
  timestamp = response_json.get("timestamp") / 1000.0
574
- return UserPlaying(
588
+ user_playing = UserPlaying(
575
589
  album_art=result.get("album", {}).get("images", [])[0].get("url"),
576
590
  album_title=result.get("album", {}).get("name"),
577
591
  album_type=(
@@ -594,6 +608,14 @@ class SpotifyAuth(BaseAuthClient):
594
608
  url=result.get("external_urls", {}).get("spotify"),
595
609
  )
596
610
 
611
+ if self.fetch_lyrics:
612
+ with LrcLib() as lrc_lib:
613
+ lyrics = lrc_lib.get_lyrics(
614
+ artist=user_playing.artists, song=user_playing.title
615
+ )
616
+ if lyrics:
617
+ user_playing.lyrics = lyrics.get("plainLyrics")
618
+ return user_playing
597
619
  return None
598
620
 
599
621
 
yutipy/yutipy_music.py CHANGED
@@ -8,11 +8,12 @@ from yutipy.deezer import Deezer
8
8
  from yutipy.exceptions import InvalidValueException, KKBoxException, SpotifyException
9
9
  from yutipy.itunes import Itunes
10
10
  from yutipy.kkbox import KKBox
11
+ from yutipy.logger import logger
12
+ from yutipy.lrclib import LrcLib
11
13
  from yutipy.models import MusicInfo, MusicInfos
12
14
  from yutipy.musicyt import MusicYT
13
15
  from yutipy.spotify import Spotify
14
16
  from yutipy.utils.helpers import is_valid_string
15
- from yutipy.logger import logger
16
17
 
17
18
 
18
19
  class YutipyMusic:
@@ -23,13 +24,11 @@ class YutipyMusic:
23
24
  """
24
25
 
25
26
  def __init__(
26
- self,
27
- custom_kkbox_class = KKBox,
28
- custom_spotify_class = Spotify,
29
- ) -> None:
27
+ self,
28
+ custom_kkbox_class=KKBox,
29
+ custom_spotify_class=Spotify,
30
+ ) -> None:
30
31
  """
31
- Initializes the YutipyMusic class.
32
-
33
32
  Parameters
34
33
  ----------
35
34
  custom_kkbox_class : Optional[type], optional
@@ -43,13 +42,15 @@ class YutipyMusic:
43
42
  self.normalize_non_english = True
44
43
  self.album_art_priority = ["deezer", "ytmusic", "itunes"]
45
44
  self.services = {
46
- "deezer": Deezer(),
47
- "itunes": Itunes(),
48
- "ytmusic": MusicYT(),
45
+ "deezer": Deezer(fetch_lyrics=False),
46
+ "itunes": Itunes(fetch_lyrics=False),
47
+ "ytmusic": MusicYT(fetch_lyrics=False),
49
48
  }
50
49
 
51
50
  try:
52
- self.services["kkbox"] = custom_kkbox_class()
51
+ self.services["kkbox"] = custom_kkbox_class(
52
+ defer_load=True, fetch_lyrics=False
53
+ )
53
54
  except KKBoxException as e:
54
55
  logger.warning(
55
56
  f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
@@ -59,7 +60,9 @@ class YutipyMusic:
59
60
  self.album_art_priority.insert(idx, "kkbox")
60
61
 
61
62
  try:
62
- self.services["spotify"] = custom_spotify_class()
63
+ self.services["spotify"] = custom_spotify_class(
64
+ defer_load=True, fetch_lyrics=False
65
+ )
63
66
  except SpotifyException as e:
64
67
  logger.warning(
65
68
  f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
@@ -134,11 +137,18 @@ class YutipyMusic:
134
137
  )
135
138
 
136
139
  if len(self.music_info.url) == 0:
137
- logger.warning(
140
+ logger.info(
138
141
  f"No matching results found across all platforms for artist='{artist}' and song='{song}'"
139
142
  )
140
143
  return None
141
144
 
145
+ # Fetch lyrics only once using LrcLib if not already present
146
+ if not self.music_info.lyrics:
147
+ with LrcLib() as lrc_lib:
148
+ lyrics_result = lrc_lib.get_lyrics(artist, song)
149
+ if lyrics_result:
150
+ self.music_info.lyrics = lyrics_result.get("plainLyrics")
151
+
142
152
  return self.music_info
143
153
 
144
154
  def _combine_results(self, result: Optional[MusicInfo], service_name: str) -> None:
@@ -199,6 +209,8 @@ class YutipyMusic:
199
209
 
200
210
  if __name__ == "__main__":
201
211
  import logging
212
+ from dataclasses import asdict
213
+
202
214
  from yutipy.logger import enable_logging
203
215
 
204
216
  enable_logging(level=logging.DEBUG)
@@ -207,5 +219,5 @@ if __name__ == "__main__":
207
219
  artist_name = input("Artist Name: ")
208
220
  song_name = input("Song Name: ")
209
221
 
210
- pprint(yutipy_music.search(artist_name, song_name))
222
+ pprint(asdict(yutipy_music.search(artist_name, song_name)))
211
223
  yutipy_music.close_sessions()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 2.2.15
3
+ Version: 2.3.1
4
4
  Summary: A simple Python package to interact with various music platforms APIs.
5
5
  Author: Cheap Nightbot
6
6
  Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
@@ -57,6 +57,9 @@ Dynamic: license-file
57
57
  </a>
58
58
  </p>
59
59
 
60
+ > **Looking for an easy-to-use API or GUI to search for music, instead of using the CLI or building your own integration?**
61
+ > Check out [yutify](https://yutify.cheapnightbot.me) — it’s powered by yutipy!
62
+
60
63
  A _**simple**_ Python package to interact with various music platforms APIs.
61
64
 
62
65
  ## Table of Contents
@@ -0,0 +1,24 @@
1
+ yutipy/__init__.py,sha256=DjHnnzikF6QIvCVqpelYUGQalYjovs6hSf6vbfPnEqM,320
2
+ yutipy/base_clients.py,sha256=sIIgeFLzKKMcGdVU1jOZzdZhrD0NwxonBZJ4Jc3eDCE,22096
3
+ yutipy/deezer.py,sha256=WzqC4ylZo7VmbyAfVZarvaqmnlamdEge3ceY_OJ9h4Y,11021
4
+ yutipy/exceptions.py,sha256=zz0XyyZr5xRcmRyw3hdTGaVRcwRn_RSYZdmwmuO0sEM,1379
5
+ yutipy/itunes.py,sha256=_iWq1COXNAiLIrUtwfI1jNey9dXe3-gCzoRF0eGgTs8,8038
6
+ yutipy/kkbox.py,sha256=nvclRgGqQHUzIS_zhbJp_PJHiWmkU3nzwLAUhdaPCLI,12103
7
+ yutipy/lastfm.py,sha256=2CQJfHtJyrg0qQ_kVdG2v18n6K8nbnBEKgrLagLtIf8,5775
8
+ yutipy/logger.py,sha256=GyLBlfQZ6pLNJ5MbyQSvcD_PkxmFdX41DPq5aeG1z68,1316
9
+ yutipy/lrclib.py,sha256=7EFoCZ7kF2blbwA81tzyCrmIIvuuWsE2aO3SfNyyWEc,5220
10
+ yutipy/models.py,sha256=45M-bNHusaAan_Ta_E9DyvsWujsT-ivbJqIfy2-i3R8,2343
11
+ yutipy/musicyt.py,sha256=T_tUCU_jHTi22dSUyNqMYdtnAS0sVuM1km7bM9R1llY,10114
12
+ yutipy/spotify.py,sha256=666HD2SkV_RCLqNqKAbtlHWCFEIxlmm7qirk1dxydiE,23614
13
+ yutipy/yutipy_music.py,sha256=F3MD6iS_arAGCDkZjtEOgNFQhcJjTQaNEMQ50638mMo,7814
14
+ yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ yutipy/cli/config.py,sha256=e5RIq6RxVxxzx30nKVMa06gwyQ258s7U0WA1xvJuR_0,4543
16
+ yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
17
+ yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
18
+ yutipy/utils/helpers.py,sha256=-iH0bx_sxW3Y3jjl6eTbY6QOBoG5t4obRcp7GGyw3ro,7476
19
+ yutipy-2.3.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
20
+ yutipy-2.3.1.dist-info/METADATA,sha256=qFEWzEP9BwX147dwfm0X2xlXXD4EblNxuqhSeRZgC2U,6577
21
+ yutipy-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ yutipy-2.3.1.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
23
+ yutipy-2.3.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
24
+ yutipy-2.3.1.dist-info/RECORD,,
@@ -1,23 +0,0 @@
1
- yutipy/__init__.py,sha256=Zrw3cr_6khXp1IgQdZxGcUM9A64GYgPs-6rlqSukW5Q,294
2
- yutipy/base_clients.py,sha256=FHCyCUQ-qE2Jo5JH-DZCxupoZTlb5ADs8XKDbHDVHwA,21687
3
- yutipy/deezer.py,sha256=ZI1C5gam8NiNznyyagn5r0Potpg25MXja8UXg-9i9ug,10463
4
- yutipy/exceptions.py,sha256=zz0XyyZr5xRcmRyw3hdTGaVRcwRn_RSYZdmwmuO0sEM,1379
5
- yutipy/itunes.py,sha256=dOkz7RqUCIaGggrNWOyxJebrv0f-mF1s9VOG2PVuFbY,7394
6
- yutipy/kkbox.py,sha256=Pfx-ZgAI9F1cbxjr7MCsMi-QulNt67t60L7y9lNmo5g,11503
7
- yutipy/lastfm.py,sha256=hOFQOZdf51Gp2m02b4NjKRmQ9yQZ9Yas6MaM78c-oCg,5970
8
- yutipy/logger.py,sha256=GyLBlfQZ6pLNJ5MbyQSvcD_PkxmFdX41DPq5aeG1z68,1316
9
- yutipy/models.py,sha256=45M-bNHusaAan_Ta_E9DyvsWujsT-ivbJqIfy2-i3R8,2343
10
- yutipy/musicyt.py,sha256=n3yaH9qyGlsW2HKPAmqYQNGzhuDH4s5Gh0R5v4JPoeg,9472
11
- yutipy/spotify.py,sha256=RQvzP62-bIXCLhMocRNFgst0xwDliFLYxU8nzdeWxRo,22616
12
- yutipy/yutipy_music.py,sha256=MNNh2WT-7GTAykAabLF6p4-0uXiIIbuogswmb-_QqtQ,7272
13
- yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- yutipy/cli/config.py,sha256=e5RIq6RxVxxzx30nKVMa06gwyQ258s7U0WA1xvJuR_0,4543
15
- yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
16
- yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
17
- yutipy/utils/helpers.py,sha256=-iH0bx_sxW3Y3jjl6eTbY6QOBoG5t4obRcp7GGyw3ro,7476
18
- yutipy-2.2.15.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
19
- yutipy-2.2.15.dist-info/METADATA,sha256=jUEpFLxQnWbxKn4FVOFNF4hs2avjkoVRQhemNHNYl_4,6369
20
- yutipy-2.2.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- yutipy-2.2.15.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
22
- yutipy-2.2.15.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
23
- yutipy-2.2.15.dist-info/RECORD,,