yutipy 2.2.6__py3-none-any.whl → 2.2.7__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/base_clients.py +582 -0
- yutipy/deezer.py +7 -21
- yutipy/itunes.py +10 -20
- yutipy/kkbox.py +22 -222
- yutipy/lastfm.py +1 -1
- yutipy/musicyt.py +3 -5
- yutipy/spotify.py +46 -636
- {yutipy-2.2.6.dist-info → yutipy-2.2.7.dist-info}/METADATA +4 -4
- yutipy-2.2.7.dist-info/RECORD +23 -0
- {yutipy-2.2.6.dist-info → yutipy-2.2.7.dist-info}/WHEEL +1 -1
- yutipy-2.2.6.dist-info/RECORD +0 -22
- {yutipy-2.2.6.dist-info → yutipy-2.2.7.dist-info}/entry_points.txt +0 -0
- {yutipy-2.2.6.dist-info → yutipy-2.2.7.dist-info}/licenses/LICENSE +0 -0
- {yutipy-2.2.6.dist-info → yutipy-2.2.7.dist-info}/top_level.txt +0 -0
yutipy/itunes.py
CHANGED
|
@@ -2,22 +2,14 @@ __all__ = ["Itunes", "ItunesException"]
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pprint import pprint
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
|
-
from yutipy.exceptions import
|
|
10
|
-
InvalidResponseException,
|
|
11
|
-
InvalidValueException,
|
|
12
|
-
ItunesException
|
|
13
|
-
)
|
|
14
|
-
from yutipy.models import MusicInfo
|
|
15
|
-
from yutipy.utils.helpers import (
|
|
16
|
-
are_strings_similar,
|
|
17
|
-
guess_album_type,
|
|
18
|
-
is_valid_string,
|
|
19
|
-
)
|
|
9
|
+
from yutipy.exceptions import InvalidValueException, ItunesException
|
|
20
10
|
from yutipy.logger import logger
|
|
11
|
+
from yutipy.models import MusicInfo
|
|
12
|
+
from yutipy.utils.helpers import are_strings_similar, guess_album_type, is_valid_string
|
|
21
13
|
|
|
22
14
|
|
|
23
15
|
class Itunes:
|
|
@@ -25,13 +17,13 @@ class Itunes:
|
|
|
25
17
|
|
|
26
18
|
def __init__(self) -> None:
|
|
27
19
|
"""Initializes the iTunes class and sets up the session."""
|
|
28
|
-
self.__session = requests.Session()
|
|
29
20
|
self.api_url = "https://itunes.apple.com"
|
|
30
|
-
self._is_session_closed = False
|
|
31
21
|
self.normalize_non_english = True
|
|
22
|
+
self._is_session_closed = False
|
|
23
|
+
self.__session = requests.Session()
|
|
32
24
|
self.__translation_session = requests.Session()
|
|
33
25
|
|
|
34
|
-
def __enter__(self)
|
|
26
|
+
def __enter__(self):
|
|
35
27
|
"""Enters the runtime context related to this object."""
|
|
36
28
|
return self
|
|
37
29
|
|
|
@@ -99,18 +91,15 @@ class Itunes:
|
|
|
99
91
|
logger.debug(f"Response status code: {response.status_code}")
|
|
100
92
|
response.raise_for_status()
|
|
101
93
|
except requests.RequestException as e:
|
|
102
|
-
logger.warning(f"
|
|
94
|
+
logger.warning(f"Unexpected error while searching iTunes: {e}")
|
|
103
95
|
return None
|
|
104
|
-
except Exception as e:
|
|
105
|
-
logger.exception(f"Unexpected error while searching iTunes: {e}")
|
|
106
|
-
raise ItunesException(f"An error occurred while searching iTunes: {e}")
|
|
107
96
|
|
|
108
97
|
try:
|
|
109
98
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
110
99
|
result = response.json()["results"]
|
|
111
100
|
except (IndexError, KeyError, ValueError) as e:
|
|
112
101
|
logger.warning(f"Invalid response structure from iTunes: {e}")
|
|
113
|
-
|
|
102
|
+
return None
|
|
114
103
|
|
|
115
104
|
music_info = self._parse_result(artist, song, result)
|
|
116
105
|
if music_info:
|
|
@@ -227,6 +216,7 @@ class Itunes:
|
|
|
227
216
|
|
|
228
217
|
if __name__ == "__main__":
|
|
229
218
|
import logging
|
|
219
|
+
|
|
230
220
|
from yutipy.logger import enable_logging
|
|
231
221
|
|
|
232
222
|
enable_logging(level=logging.DEBUG)
|
yutipy/kkbox.py
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
__all__ = ["KKBox", "KKBoxException"]
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import os
|
|
5
4
|
from dataclasses import asdict
|
|
6
5
|
from pprint import pprint
|
|
7
|
-
from
|
|
8
|
-
from typing import Optional, Union
|
|
6
|
+
from typing import Optional
|
|
9
7
|
|
|
10
8
|
import requests
|
|
11
9
|
from dotenv import load_dotenv
|
|
12
10
|
|
|
13
|
-
from yutipy.
|
|
14
|
-
|
|
15
|
-
InvalidValueException,
|
|
16
|
-
KKBoxException,
|
|
17
|
-
)
|
|
11
|
+
from yutipy.base_clients import BaseClient
|
|
12
|
+
from yutipy.exceptions import InvalidValueException, KKBoxException
|
|
18
13
|
from yutipy.logger import logger
|
|
19
14
|
from yutipy.models import MusicInfo
|
|
20
15
|
from yutipy.utils.helpers import are_strings_similar, is_valid_string
|
|
@@ -25,7 +20,7 @@ KKBOX_CLIENT_ID = os.getenv("KKBOX_CLIENT_ID")
|
|
|
25
20
|
KKBOX_CLIENT_SECRET = os.getenv("KKBOX_CLIENT_SECRET")
|
|
26
21
|
|
|
27
22
|
|
|
28
|
-
class KKBox:
|
|
23
|
+
class KKBox(BaseClient):
|
|
29
24
|
"""
|
|
30
25
|
A class to interact with KKBOX Open API.
|
|
31
26
|
|
|
@@ -61,207 +56,16 @@ class KKBox:
|
|
|
61
56
|
"Client Secret was not found. Set it in environment variable or directly pass it when creating object."
|
|
62
57
|
)
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.api_url = "https://api.kkbox.com/v1.1"
|
|
71
|
-
self.__access_token = None
|
|
72
|
-
self.__token_expires_in = None
|
|
73
|
-
self.__token_requested_at = None
|
|
74
|
-
self.__session = requests.Session()
|
|
75
|
-
self.__translation_session = requests.Session()
|
|
76
|
-
|
|
77
|
-
if not defer_load:
|
|
78
|
-
# Attempt to load access token during initialization if not deferred
|
|
79
|
-
token_info = None
|
|
80
|
-
try:
|
|
81
|
-
token_info = self.load_access_token()
|
|
82
|
-
except NotImplementedError:
|
|
83
|
-
logger.warning(
|
|
84
|
-
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
85
|
-
)
|
|
86
|
-
finally:
|
|
87
|
-
if not token_info:
|
|
88
|
-
token_info = self.__get_access_token()
|
|
89
|
-
self.__access_token = token_info.get("access_token")
|
|
90
|
-
self.__token_expires_in = token_info.get("expires_in")
|
|
91
|
-
self.__token_requested_at = token_info.get("requested_at")
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
self.save_access_token(token_info)
|
|
95
|
-
except NotImplementedError:
|
|
96
|
-
logger.warning(
|
|
97
|
-
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
logger.warning(
|
|
101
|
-
"`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
def __enter__(self):
|
|
105
|
-
"""Enters the runtime context related to this object."""
|
|
106
|
-
return self
|
|
107
|
-
|
|
108
|
-
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
109
|
-
"""Exits the runtime context related to this object."""
|
|
110
|
-
self.close_session()
|
|
111
|
-
|
|
112
|
-
def close_session(self) -> None:
|
|
113
|
-
"""Closes the current session."""
|
|
114
|
-
if not self.is_session_closed:
|
|
115
|
-
self.__session.close()
|
|
116
|
-
self.__translation_session.close()
|
|
117
|
-
self._is_session_closed = True
|
|
118
|
-
|
|
119
|
-
@property
|
|
120
|
-
def is_session_closed(self) -> bool:
|
|
121
|
-
"""Checks if the session is closed."""
|
|
122
|
-
return self._is_session_closed
|
|
123
|
-
|
|
124
|
-
def load_token_after_init(self):
|
|
125
|
-
"""
|
|
126
|
-
Explicitly load the access token after initialization.
|
|
127
|
-
This is useful when ``defer_load`` is set to ``True`` during initialization.
|
|
128
|
-
"""
|
|
129
|
-
token_info = None
|
|
130
|
-
try:
|
|
131
|
-
token_info = self.load_access_token()
|
|
132
|
-
except NotImplementedError:
|
|
133
|
-
logger.warning(
|
|
134
|
-
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
135
|
-
)
|
|
136
|
-
finally:
|
|
137
|
-
if not token_info:
|
|
138
|
-
token_info = self.__get_access_token()
|
|
139
|
-
self.__access_token = token_info.get("access_token")
|
|
140
|
-
self.__token_expires_in = token_info.get("expires_in")
|
|
141
|
-
self.__token_requested_at = token_info.get("requested_at")
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
self.save_access_token(token_info)
|
|
145
|
-
except NotImplementedError:
|
|
146
|
-
logger.warning(
|
|
147
|
-
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
def __authorization_header(self) -> dict:
|
|
151
|
-
"""
|
|
152
|
-
Generates the authorization header for Spotify API requests.
|
|
153
|
-
|
|
154
|
-
Returns
|
|
155
|
-
-------
|
|
156
|
-
dict
|
|
157
|
-
A dictionary containing the Bearer token for authentication.
|
|
158
|
-
"""
|
|
159
|
-
return {"Authorization": f"Bearer {self.__access_token}"}
|
|
160
|
-
|
|
161
|
-
def __get_access_token(self) -> dict:
|
|
162
|
-
"""
|
|
163
|
-
Gets the KKBOX Open API access token information.
|
|
164
|
-
|
|
165
|
-
Returns
|
|
166
|
-
-------
|
|
167
|
-
str
|
|
168
|
-
The KKBOX Open API access token, with additional information such as expires in, etc.
|
|
169
|
-
"""
|
|
170
|
-
auth_string = f"{self.client_id}:{self.client_secret}"
|
|
171
|
-
auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
172
|
-
|
|
173
|
-
url = " https://account.kkbox.com/oauth2/token"
|
|
174
|
-
headers = {
|
|
175
|
-
"Authorization": f"Basic {auth_base64}",
|
|
176
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
177
|
-
}
|
|
178
|
-
data = {"grant_type": "client_credentials"}
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
logger.info("Authenticating with KKBOX Open API")
|
|
182
|
-
response = self.__session.post(
|
|
183
|
-
url=url, headers=headers, data=data, timeout=30
|
|
184
|
-
)
|
|
185
|
-
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
186
|
-
response.raise_for_status()
|
|
187
|
-
except requests.RequestException as e:
|
|
188
|
-
logger.warning(f"Network error during KKBOX authentication: {e}")
|
|
189
|
-
return None
|
|
190
|
-
|
|
191
|
-
if response.status_code == 200:
|
|
192
|
-
response_json = response.json()
|
|
193
|
-
response_json["requested_at"] = time()
|
|
194
|
-
return response_json
|
|
195
|
-
else:
|
|
196
|
-
raise AuthenticationException(
|
|
197
|
-
f"Invalid response received: {response.json()}"
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
def __refresh_access_token(self):
|
|
201
|
-
"""Refreshes the token if it has expired."""
|
|
202
|
-
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
203
|
-
token_info = self.__get_access_token()
|
|
204
|
-
|
|
205
|
-
try:
|
|
206
|
-
self.save_access_token(token_info)
|
|
207
|
-
except NotImplementedError as e:
|
|
208
|
-
logger.warning(e)
|
|
209
|
-
|
|
210
|
-
self.__access_token = token_info.get("access_token")
|
|
211
|
-
self.__token_expires_in = token_info.get("expires_in")
|
|
212
|
-
self.__token_requested_at = token_info.get("requested_at")
|
|
213
|
-
|
|
214
|
-
logger.info("The access token is still valid, no need to refresh.")
|
|
215
|
-
|
|
216
|
-
def save_access_token(self, token_info: dict) -> None:
|
|
217
|
-
"""
|
|
218
|
-
Saves the access token and related information.
|
|
219
|
-
|
|
220
|
-
This method must be overridden in a subclass to persist the access token and other
|
|
221
|
-
related information (e.g., expiration time). If not implemented,
|
|
222
|
-
the access token will not be saved, and it will be requested each time the
|
|
223
|
-
application restarts.
|
|
224
|
-
|
|
225
|
-
Parameters
|
|
226
|
-
----------
|
|
227
|
-
token_info : dict
|
|
228
|
-
A dictionary containing the access token and related information, such as
|
|
229
|
-
refresh token, expiration time, etc.
|
|
230
|
-
|
|
231
|
-
Raises
|
|
232
|
-
------
|
|
233
|
-
NotImplementedError
|
|
234
|
-
If the method is not overridden in a subclass.
|
|
235
|
-
"""
|
|
236
|
-
raise NotImplementedError(
|
|
237
|
-
"The `save_access_token` method must be overridden in a subclass to save the access token and related information. "
|
|
238
|
-
"If not implemented, access token information will not be persisted, and users will need to re-authenticate after application restarts."
|
|
59
|
+
super().__init__(
|
|
60
|
+
service_name="KKBox",
|
|
61
|
+
access_token_url="https://account.kkbox.com/oauth2/token",
|
|
62
|
+
client_id=self.client_id,
|
|
63
|
+
client_secret=self.client_secret,
|
|
64
|
+
defer_load=defer_load,
|
|
239
65
|
)
|
|
240
66
|
|
|
241
|
-
|
|
242
|
-
"""
|
|
243
|
-
Loads the access token and related information.
|
|
244
|
-
|
|
245
|
-
This method must be overridden in a subclass to retrieve the access token and other
|
|
246
|
-
related information (e.g., expiration time) from persistent storage.
|
|
247
|
-
If not implemented, the access token will not be loaded, and it will be requested
|
|
248
|
-
each time the application restarts.
|
|
249
|
-
|
|
250
|
-
Returns
|
|
251
|
-
-------
|
|
252
|
-
dict | None
|
|
253
|
-
A dictionary containing the access token and related information, such as
|
|
254
|
-
refresh token, expiration time, etc., or None if no token is found.
|
|
255
|
-
|
|
256
|
-
Raises
|
|
257
|
-
------
|
|
258
|
-
NotImplementedError
|
|
259
|
-
If the method is not overridden in a subclass.
|
|
260
|
-
"""
|
|
261
|
-
raise NotImplementedError(
|
|
262
|
-
"The `load_access_token` method must be overridden in a subclass to load access token and related information. "
|
|
263
|
-
"If not implemented, access token information will not be loaded, and users will need to re-authenticate after application restarts."
|
|
264
|
-
)
|
|
67
|
+
self.__api_url = "https://api.kkbox.com/v1.1"
|
|
68
|
+
self._valid_territories = ["HK", "JP", "MY", "SG", "TW"]
|
|
265
69
|
|
|
266
70
|
def search(
|
|
267
71
|
self,
|
|
@@ -299,29 +103,25 @@ class KKBox:
|
|
|
299
103
|
)
|
|
300
104
|
|
|
301
105
|
self._normalize_non_english = normalize_non_english
|
|
302
|
-
|
|
303
|
-
self.__refresh_access_token()
|
|
106
|
+
self._refresh_access_token()
|
|
304
107
|
|
|
305
108
|
query = (
|
|
306
109
|
f"?q={artist} - {song}&type=track,album&territory={territory}&limit={limit}"
|
|
307
110
|
)
|
|
308
|
-
query_url = f"{self.
|
|
111
|
+
query_url = f"{self.__api_url}/search{query}"
|
|
309
112
|
|
|
310
113
|
logger.info(f"Searching KKBOX for `artist='{artist}'` and `song='{song}'`")
|
|
311
114
|
logger.debug(f"Query URL: {query_url}")
|
|
312
115
|
|
|
313
116
|
try:
|
|
314
|
-
response = self.
|
|
315
|
-
query_url, headers=self.
|
|
117
|
+
response = self._session.get(
|
|
118
|
+
query_url, headers=self._authorization_header(), timeout=30
|
|
316
119
|
)
|
|
317
|
-
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
318
120
|
response.raise_for_status()
|
|
319
121
|
except requests.RequestException as e:
|
|
122
|
+
logger.warning(f"Unexpected error while searching KKBox: {e}")
|
|
320
123
|
return None
|
|
321
124
|
|
|
322
|
-
if response.status_code != 200:
|
|
323
|
-
raise KKBoxException(f"Failed to search for music: {response.json()}")
|
|
324
|
-
|
|
325
125
|
return self._find_music_info(artist, song, response.json())
|
|
326
126
|
|
|
327
127
|
def get_html_widget(
|
|
@@ -439,7 +239,7 @@ class KKBox:
|
|
|
439
239
|
track["name"],
|
|
440
240
|
song,
|
|
441
241
|
use_translation=self._normalize_non_english,
|
|
442
|
-
translation_session=self.
|
|
242
|
+
translation_session=self._translation_session,
|
|
443
243
|
):
|
|
444
244
|
return None
|
|
445
245
|
|
|
@@ -450,7 +250,7 @@ class KKBox:
|
|
|
450
250
|
artists_name,
|
|
451
251
|
artist,
|
|
452
252
|
use_translation=self._normalize_non_english,
|
|
453
|
-
translation_session=self.
|
|
253
|
+
translation_session=self._translation_session,
|
|
454
254
|
)
|
|
455
255
|
else None
|
|
456
256
|
)
|
|
@@ -497,7 +297,7 @@ class KKBox:
|
|
|
497
297
|
album["name"],
|
|
498
298
|
song,
|
|
499
299
|
use_translation=self._normalize_non_english,
|
|
500
|
-
translation_session=self.
|
|
300
|
+
translation_session=self._translation_session,
|
|
501
301
|
):
|
|
502
302
|
return None
|
|
503
303
|
|
|
@@ -508,7 +308,7 @@ class KKBox:
|
|
|
508
308
|
artists_name,
|
|
509
309
|
artist,
|
|
510
310
|
use_translation=self._normalize_non_english,
|
|
511
|
-
translation_session=self.
|
|
311
|
+
translation_session=self._translation_session,
|
|
512
312
|
)
|
|
513
313
|
else None
|
|
514
314
|
)
|
|
@@ -540,7 +340,7 @@ if __name__ == "__main__":
|
|
|
540
340
|
from yutipy.logger import enable_logging
|
|
541
341
|
|
|
542
342
|
enable_logging(level=logging.DEBUG)
|
|
543
|
-
kkbox = KKBox(
|
|
343
|
+
kkbox = KKBox()
|
|
544
344
|
|
|
545
345
|
try:
|
|
546
346
|
artist_name = input("Artist Name: ")
|
yutipy/lastfm.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
__all__ = ["LastFm", "LastFmException"]
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from dataclasses import asdict
|
|
5
4
|
from time import time
|
|
6
5
|
from pprint import pprint
|
|
7
6
|
from typing import Optional
|
|
@@ -142,6 +141,7 @@ class LastFm:
|
|
|
142
141
|
response_json = response.json()
|
|
143
142
|
result = response_json.get("recenttracks", {}).get("track", [])[0]
|
|
144
143
|
is_playing = result.get("@attr", {}).get("nowplaying", False)
|
|
144
|
+
is_playing = True if isinstance(is_playing, str) and is_playing == "true" else False
|
|
145
145
|
if result and is_playing:
|
|
146
146
|
album_art = [
|
|
147
147
|
img.get("#text")
|
yutipy/musicyt.py
CHANGED
|
@@ -79,12 +79,10 @@ class MusicYT:
|
|
|
79
79
|
self.normalize_non_english = normalize_non_english
|
|
80
80
|
|
|
81
81
|
query = f"{artist} - {song}"
|
|
82
|
-
|
|
83
|
-
logger.info(
|
|
84
|
-
f"Searching YouTube Music for `artist='{artist}'` and `song='{song}'`"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
82
|
try:
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Searching YouTube Music for `artist='{artist}'` and `song='{song}'`"
|
|
85
|
+
)
|
|
88
86
|
results = self.ytmusic.search(query=query, limit=limit)
|
|
89
87
|
except exceptions.YTMusicServerError as e:
|
|
90
88
|
logger.warning(f"Something went wrong while searching YTMusic: {e}")
|