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 +2 -0
- yutipy/base_clients.py +11 -0
- yutipy/deezer.py +19 -4
- yutipy/itunes.py +26 -5
- yutipy/kkbox.py +18 -4
- yutipy/lastfm.py +4 -8
- yutipy/lrclib.py +166 -0
- yutipy/musicyt.py +23 -5
- yutipy/spotify.py +33 -11
- yutipy/yutipy_music.py +26 -14
- {yutipy-2.2.15.dist-info → yutipy-2.3.1.dist-info}/METADATA +4 -1
- yutipy-2.3.1.dist-info/RECORD +24 -0
- yutipy-2.2.15.dist-info/RECORD +0 -23
- {yutipy-2.2.15.dist-info → yutipy-2.3.1.dist-info}/WHEEL +0 -0
- {yutipy-2.2.15.dist-info → yutipy-2.3.1.dist-info}/entry_points.txt +0 -0
- {yutipy-2.2.15.dist-info → yutipy-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {yutipy-2.2.15.dist-info → yutipy-2.3.1.dist-info}/top_level.txt +0 -0
yutipy/__init__.py
CHANGED
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
|
-
"""
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
"""
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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,
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
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,,
|
yutipy-2.2.15.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|