yutipy 2.0.0__py3-none-any.whl → 2.1.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/deezer.py +5 -5
- yutipy/itunes.py +7 -7
- yutipy/kkbox.py +181 -72
- yutipy/models.py +13 -0
- yutipy/musicyt.py +13 -12
- yutipy/spotify.py +211 -40
- yutipy/yutipy_music.py +2 -0
- {yutipy-2.0.0.dist-info → yutipy-2.1.1.dist-info}/METADATA +1 -1
- yutipy-2.1.1.dist-info/RECORD +21 -0
- yutipy-2.0.0.dist-info/RECORD +0 -21
- {yutipy-2.0.0.dist-info → yutipy-2.1.1.dist-info}/WHEEL +0 -0
- {yutipy-2.0.0.dist-info → yutipy-2.1.1.dist-info}/entry_points.txt +0 -0
- {yutipy-2.0.0.dist-info → yutipy-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {yutipy-2.0.0.dist-info → yutipy-2.1.1.dist-info}/top_level.txt +0 -0
yutipy/deezer.py
CHANGED
|
@@ -21,10 +21,10 @@ class Deezer:
|
|
|
21
21
|
|
|
22
22
|
def __init__(self) -> None:
|
|
23
23
|
"""Initializes the Deezer class and sets up the session."""
|
|
24
|
-
self._session = requests.Session()
|
|
25
24
|
self.api_url = "https://api.deezer.com"
|
|
26
25
|
self._is_session_closed = False
|
|
27
26
|
self.normalize_non_english = True
|
|
27
|
+
self.__session = requests.Session()
|
|
28
28
|
self._translation_session = requests.Session()
|
|
29
29
|
|
|
30
30
|
def __enter__(self) -> "Deezer":
|
|
@@ -38,7 +38,7 @@ class Deezer:
|
|
|
38
38
|
def close_session(self) -> None:
|
|
39
39
|
"""Closes the current session."""
|
|
40
40
|
if not self.is_session_closed:
|
|
41
|
-
self.
|
|
41
|
+
self.__session.close()
|
|
42
42
|
self._translation_session.close()
|
|
43
43
|
self._is_session_closed = True
|
|
44
44
|
|
|
@@ -93,7 +93,7 @@ class Deezer:
|
|
|
93
93
|
f'Searching music info for `artist="{artist}"` and `song="{song}"`'
|
|
94
94
|
)
|
|
95
95
|
logger.debug(f"Query URL: {query_url}")
|
|
96
|
-
response = self.
|
|
96
|
+
response = self.__session.get(query_url, timeout=30)
|
|
97
97
|
logger.debug(f"Response status code: {response.status_code}")
|
|
98
98
|
response.raise_for_status()
|
|
99
99
|
except requests.RequestException as e:
|
|
@@ -160,7 +160,7 @@ class Deezer:
|
|
|
160
160
|
try:
|
|
161
161
|
logger.info(f"Fetching track info for track_id: {track_id}")
|
|
162
162
|
logger.debug(f"Query URL: {query_url}")
|
|
163
|
-
response = self.
|
|
163
|
+
response = self.__session.get(query_url, timeout=30)
|
|
164
164
|
logger.debug(f"Response status code: {response.status_code}")
|
|
165
165
|
response.raise_for_status()
|
|
166
166
|
except requests.RequestException as e:
|
|
@@ -201,7 +201,7 @@ class Deezer:
|
|
|
201
201
|
try:
|
|
202
202
|
logger.info(f"Fetching album info for album_id: {album_id}")
|
|
203
203
|
logger.debug(f"Query URL: {query_url}")
|
|
204
|
-
response = self.
|
|
204
|
+
response = self.__session.get(query_url, timeout=30)
|
|
205
205
|
logger.info(f"Response status code: {response.status_code}")
|
|
206
206
|
response.raise_for_status()
|
|
207
207
|
except requests.RequestException as e:
|
yutipy/itunes.py
CHANGED
|
@@ -26,11 +26,11 @@ class Itunes:
|
|
|
26
26
|
|
|
27
27
|
def __init__(self) -> None:
|
|
28
28
|
"""Initializes the iTunes class and sets up the session."""
|
|
29
|
-
self.
|
|
29
|
+
self.__session = requests.Session()
|
|
30
30
|
self.api_url = "https://itunes.apple.com"
|
|
31
31
|
self._is_session_closed = False
|
|
32
32
|
self.normalize_non_english = True
|
|
33
|
-
self.
|
|
33
|
+
self.__translation_session = requests.Session()
|
|
34
34
|
|
|
35
35
|
def __enter__(self) -> "Itunes":
|
|
36
36
|
"""Enters the runtime context related to this object."""
|
|
@@ -43,8 +43,8 @@ class Itunes:
|
|
|
43
43
|
def close_session(self) -> None:
|
|
44
44
|
"""Closes the current session."""
|
|
45
45
|
if not self.is_session_closed:
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
46
|
+
self.__session.close()
|
|
47
|
+
self.__translation_session.close()
|
|
48
48
|
self._is_session_closed = True
|
|
49
49
|
|
|
50
50
|
@property
|
|
@@ -96,7 +96,7 @@ class Itunes:
|
|
|
96
96
|
f'Searching iTunes for `artist="{artist}"` and `song="{song}"`'
|
|
97
97
|
)
|
|
98
98
|
logger.debug(f"Query URL: {query_url}")
|
|
99
|
-
response = self.
|
|
99
|
+
response = self.__session.get(query_url, timeout=30)
|
|
100
100
|
logger.debug(f"Response status code: {response.status_code}")
|
|
101
101
|
response.raise_for_status()
|
|
102
102
|
except requests.RequestException as e:
|
|
@@ -148,13 +148,13 @@ class Itunes:
|
|
|
148
148
|
result.get("trackName", result["collectionName"]),
|
|
149
149
|
song,
|
|
150
150
|
use_translation=self.normalize_non_english,
|
|
151
|
-
translation_session=self.
|
|
151
|
+
translation_session=self.__translation_session,
|
|
152
152
|
)
|
|
153
153
|
and are_strings_similar(
|
|
154
154
|
result["artistName"],
|
|
155
155
|
artist,
|
|
156
156
|
use_translation=self.normalize_non_english,
|
|
157
|
-
translation_session=self.
|
|
157
|
+
translation_session=self.__translation_session,
|
|
158
158
|
)
|
|
159
159
|
):
|
|
160
160
|
continue
|
yutipy/kkbox.py
CHANGED
|
@@ -2,8 +2,9 @@ __all__ = ["KKBox", "KKBoxException"]
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import os
|
|
5
|
-
import
|
|
5
|
+
from dataclasses import asdict
|
|
6
6
|
from pprint import pprint
|
|
7
|
+
from time import time
|
|
7
8
|
from typing import Optional, Union
|
|
8
9
|
|
|
9
10
|
import requests
|
|
@@ -16,9 +17,9 @@ from yutipy.exceptions import (
|
|
|
16
17
|
KKBoxException,
|
|
17
18
|
NetworkException,
|
|
18
19
|
)
|
|
20
|
+
from yutipy.logger import logger
|
|
19
21
|
from yutipy.models import MusicInfo
|
|
20
22
|
from yutipy.utils.helpers import are_strings_similar, is_valid_string
|
|
21
|
-
from yutipy.logger import logger
|
|
22
23
|
|
|
23
24
|
load_dotenv()
|
|
24
25
|
|
|
@@ -35,7 +36,7 @@ class KKBox:
|
|
|
35
36
|
"""
|
|
36
37
|
|
|
37
38
|
def __init__(
|
|
38
|
-
self, client_id: str =
|
|
39
|
+
self, client_id: str = None, client_secret: str = None, defer_load: bool = False
|
|
39
40
|
) -> None:
|
|
40
41
|
"""
|
|
41
42
|
Initializes the KKBox class and sets up the session.
|
|
@@ -46,22 +47,54 @@ class KKBox:
|
|
|
46
47
|
The Client ID for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_ID`` from .env file.
|
|
47
48
|
client_secret : str, optional
|
|
48
49
|
The Client secret for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_SECRET`` from .env file.
|
|
50
|
+
defer_load : bool, optional
|
|
51
|
+
Whether to defer loading the access token during initialization. Default is ``False``.
|
|
49
52
|
"""
|
|
50
|
-
|
|
53
|
+
self.client_id = client_id or KKBOX_CLIENT_ID
|
|
54
|
+
self.client_secret = client_secret or KKBOX_CLIENT_SECRET
|
|
55
|
+
|
|
56
|
+
if not self.client_id:
|
|
51
57
|
raise KKBoxException(
|
|
52
|
-
"
|
|
58
|
+
"Client ID was not found. Set it in environment variable or directly pass it when creating object."
|
|
53
59
|
)
|
|
54
60
|
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
self.
|
|
61
|
+
if not self.client_secret:
|
|
62
|
+
raise KKBoxException(
|
|
63
|
+
"Client Secret was not found. Set it in environment variable or directly pass it when creating object."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.defer_load = defer_load
|
|
67
|
+
|
|
61
68
|
self._is_session_closed = False
|
|
62
|
-
self.
|
|
63
|
-
self.
|
|
64
|
-
|
|
69
|
+
self._normalize_non_english = True
|
|
70
|
+
self._valid_territories = ["HK", "JP", "MY", "SG", "TW"]
|
|
71
|
+
|
|
72
|
+
self.api_url = "https://api.kkbox.com/v1.1"
|
|
73
|
+
self.__access_token = None
|
|
74
|
+
self.__token_expires_in = None
|
|
75
|
+
self.__token_requested_at = None
|
|
76
|
+
self.__session = requests.Session()
|
|
77
|
+
self.__translation_session = requests.Session()
|
|
78
|
+
|
|
79
|
+
if not defer_load:
|
|
80
|
+
# Attempt to load access token during initialization if not deferred
|
|
81
|
+
token_info = None
|
|
82
|
+
try:
|
|
83
|
+
token_info = self.load_access_token()
|
|
84
|
+
except NotImplementedError:
|
|
85
|
+
logger.warning(
|
|
86
|
+
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
87
|
+
)
|
|
88
|
+
finally:
|
|
89
|
+
if not token_info:
|
|
90
|
+
token_info = self.__get_access_token()
|
|
91
|
+
self.__access_token = token_info.get("access_token")
|
|
92
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
93
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
94
|
+
else:
|
|
95
|
+
logger.warning(
|
|
96
|
+
"`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
|
|
97
|
+
)
|
|
65
98
|
|
|
66
99
|
def __enter__(self):
|
|
67
100
|
"""Enters the runtime context related to this object."""
|
|
@@ -74,8 +107,8 @@ class KKBox:
|
|
|
74
107
|
def close_session(self) -> None:
|
|
75
108
|
"""Closes the current session."""
|
|
76
109
|
if not self.is_session_closed:
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
110
|
+
self.__session.close()
|
|
111
|
+
self.__translation_session.close()
|
|
79
112
|
self._is_session_closed = True
|
|
80
113
|
|
|
81
114
|
@property
|
|
@@ -83,31 +116,44 @@ class KKBox:
|
|
|
83
116
|
"""Checks if the session is closed."""
|
|
84
117
|
return self._is_session_closed
|
|
85
118
|
|
|
86
|
-
def
|
|
119
|
+
def load_token_after_init(self):
|
|
120
|
+
"""
|
|
121
|
+
Explicitly load the access token after initialization.
|
|
122
|
+
This is useful when ``defer_load`` is set to ``True`` during initialization.
|
|
123
|
+
"""
|
|
124
|
+
token_info = None
|
|
125
|
+
try:
|
|
126
|
+
token_info = self.load_access_token()
|
|
127
|
+
except NotImplementedError:
|
|
128
|
+
logger.warning(
|
|
129
|
+
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
130
|
+
)
|
|
131
|
+
finally:
|
|
132
|
+
if not token_info:
|
|
133
|
+
token_info = self.__get_access_token()
|
|
134
|
+
self.__access_token = token_info.get("access_token")
|
|
135
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
136
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
137
|
+
|
|
138
|
+
def __authorization_header(self) -> dict:
|
|
87
139
|
"""
|
|
88
|
-
|
|
140
|
+
Generates the authorization header for Spotify API requests.
|
|
89
141
|
|
|
90
142
|
Returns
|
|
91
143
|
-------
|
|
92
144
|
dict
|
|
93
|
-
|
|
145
|
+
A dictionary containing the Bearer token for authentication.
|
|
94
146
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
except Exception as e:
|
|
99
|
-
raise AuthenticationException(
|
|
100
|
-
"Failed to authenticate with KKBOX Open API"
|
|
101
|
-
) from e
|
|
102
|
-
|
|
103
|
-
def __get_access_token(self) -> tuple:
|
|
147
|
+
return {"Authorization": f"Bearer {self.__access_token}"}
|
|
148
|
+
|
|
149
|
+
def __get_access_token(self) -> dict:
|
|
104
150
|
"""
|
|
105
|
-
Gets the KKBOX Open API token.
|
|
151
|
+
Gets the KKBOX Open API access token information.
|
|
106
152
|
|
|
107
153
|
Returns
|
|
108
154
|
-------
|
|
109
155
|
str
|
|
110
|
-
The KKBOX Open API token.
|
|
156
|
+
The KKBOX Open API access token, with additional information such as expires in, etc.
|
|
111
157
|
"""
|
|
112
158
|
auth_string = f"{self.client_id}:{self.client_secret}"
|
|
113
159
|
auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
@@ -121,7 +167,7 @@ class KKBox:
|
|
|
121
167
|
|
|
122
168
|
try:
|
|
123
169
|
logger.info("Authenticating with KKBOX Open API")
|
|
124
|
-
response = self.
|
|
170
|
+
response = self.__session.post(
|
|
125
171
|
url=url, headers=headers, data=data, timeout=30
|
|
126
172
|
)
|
|
127
173
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
@@ -130,17 +176,80 @@ class KKBox:
|
|
|
130
176
|
logger.error(f"Network error during KKBOX authentication: {e}")
|
|
131
177
|
raise NetworkException(f"Network error occurred: {e}")
|
|
132
178
|
|
|
133
|
-
|
|
179
|
+
if response.status_code == 200:
|
|
134
180
|
response_json = response.json()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
181
|
+
response_json["requested_at"] = time()
|
|
182
|
+
return response_json
|
|
183
|
+
else:
|
|
184
|
+
raise InvalidResponseException(
|
|
185
|
+
f"Invalid response received: {response.json()}"
|
|
186
|
+
)
|
|
138
187
|
|
|
139
|
-
def
|
|
188
|
+
def __refresh_access_token(self):
|
|
140
189
|
"""Refreshes the token if it has expired."""
|
|
141
|
-
if time
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
191
|
+
token_info = self.__get_access_token()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
self.save_access_token(token_info)
|
|
195
|
+
except NotImplementedError as e:
|
|
196
|
+
logger.warning(e)
|
|
197
|
+
|
|
198
|
+
self.__access_token = token_info.get("access_token")
|
|
199
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
200
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
201
|
+
|
|
202
|
+
logger.info("The access token is still valid, no need to refresh.")
|
|
203
|
+
|
|
204
|
+
def save_access_token(self, token_info: dict) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Saves the access token and related information.
|
|
207
|
+
|
|
208
|
+
This method must be overridden in a subclass to persist the access token and other
|
|
209
|
+
related information (e.g., expiration time). If not implemented,
|
|
210
|
+
the access token will not be saved, and it will be requested each time the
|
|
211
|
+
application restarts.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
token_info : dict
|
|
216
|
+
A dictionary containing the access token and related information, such as
|
|
217
|
+
refresh token, expiration time, etc.
|
|
218
|
+
|
|
219
|
+
Raises
|
|
220
|
+
------
|
|
221
|
+
NotImplementedError
|
|
222
|
+
If the method is not overridden in a subclass.
|
|
223
|
+
"""
|
|
224
|
+
raise NotImplementedError(
|
|
225
|
+
"The `save_access_token` method must be overridden in a subclass to save the access token and related information. "
|
|
226
|
+
"If not implemented, access token information will not be persisted, and users will need to re-authenticate after application restarts."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def load_access_token(self) -> Union[dict, None]:
|
|
230
|
+
"""
|
|
231
|
+
Loads the access token and related information.
|
|
232
|
+
|
|
233
|
+
This method must be overridden in a subclass to retrieve the access token and other
|
|
234
|
+
related information (e.g., expiration time) from persistent storage.
|
|
235
|
+
If not implemented, the access token will not be loaded, and it will be requested
|
|
236
|
+
each time the application restarts.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
dict | None
|
|
241
|
+
A dictionary containing the access token and related information, such as
|
|
242
|
+
refresh token, expiration time, etc., or None if no token is found.
|
|
243
|
+
|
|
244
|
+
Raises
|
|
245
|
+
------
|
|
246
|
+
NotImplementedError
|
|
247
|
+
If the method is not overridden in a subclass.
|
|
248
|
+
"""
|
|
249
|
+
raise NotImplementedError(
|
|
250
|
+
"The `load_access_token` method must be overridden in a subclass to load access token and related information. "
|
|
251
|
+
"If not implemented, access token information will not be loaded, and users will need to re-authenticate after application restarts."
|
|
252
|
+
)
|
|
144
253
|
|
|
145
254
|
def search(
|
|
146
255
|
self,
|
|
@@ -177,9 +286,9 @@ class KKBox:
|
|
|
177
286
|
"Artist and song names must be valid strings and can't be empty."
|
|
178
287
|
)
|
|
179
288
|
|
|
180
|
-
self.
|
|
289
|
+
self._normalize_non_english = normalize_non_english
|
|
181
290
|
|
|
182
|
-
self.
|
|
291
|
+
self.__refresh_access_token()
|
|
183
292
|
|
|
184
293
|
query = (
|
|
185
294
|
f"?q={artist} - {song}&type=track,album&territory={territory}&limit={limit}"
|
|
@@ -190,7 +299,9 @@ class KKBox:
|
|
|
190
299
|
logger.debug(f"Query URL: {query_url}")
|
|
191
300
|
|
|
192
301
|
try:
|
|
193
|
-
response = self.
|
|
302
|
+
response = self.__session.get(
|
|
303
|
+
query_url, headers=self.__authorization_header(), timeout=30
|
|
304
|
+
)
|
|
194
305
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
195
306
|
response.raise_for_status()
|
|
196
307
|
except requests.RequestException as e:
|
|
@@ -241,9 +352,9 @@ class KKBox:
|
|
|
241
352
|
f"`content_type` must be one of these: {valid_content_types} !"
|
|
242
353
|
)
|
|
243
354
|
|
|
244
|
-
if territory not in self.
|
|
355
|
+
if territory not in self._valid_territories:
|
|
245
356
|
raise InvalidValueException(
|
|
246
|
-
f"`territory` must be one of these: {self.
|
|
357
|
+
f"`territory` must be one of these: {self._valid_territories} !"
|
|
247
358
|
)
|
|
248
359
|
|
|
249
360
|
if widget_lang not in valid_widget_langs:
|
|
@@ -306,8 +417,6 @@ class KKBox:
|
|
|
306
417
|
The name of the artist.
|
|
307
418
|
track : dict
|
|
308
419
|
A single track from the search results.
|
|
309
|
-
artist_ids : list
|
|
310
|
-
A list of artist IDs.
|
|
311
420
|
|
|
312
421
|
Returns
|
|
313
422
|
-------
|
|
@@ -317,39 +426,39 @@ class KKBox:
|
|
|
317
426
|
if not are_strings_similar(
|
|
318
427
|
track["name"],
|
|
319
428
|
song,
|
|
320
|
-
use_translation=self.
|
|
321
|
-
translation_session=self.
|
|
429
|
+
use_translation=self._normalize_non_english,
|
|
430
|
+
translation_session=self.__translation_session,
|
|
322
431
|
):
|
|
323
432
|
return None
|
|
324
433
|
|
|
325
|
-
artists_name = track
|
|
434
|
+
artists_name = track.get("album", {}).get("artist", {}).get("name")
|
|
326
435
|
matching_artists = (
|
|
327
436
|
artists_name
|
|
328
437
|
if are_strings_similar(
|
|
329
438
|
artists_name,
|
|
330
439
|
artist,
|
|
331
|
-
use_translation=self.
|
|
332
|
-
translation_session=self.
|
|
440
|
+
use_translation=self._normalize_non_english,
|
|
441
|
+
translation_session=self.__translation_session,
|
|
333
442
|
)
|
|
334
443
|
else None
|
|
335
444
|
)
|
|
336
445
|
|
|
337
446
|
if matching_artists:
|
|
338
447
|
return MusicInfo(
|
|
339
|
-
album_art=track
|
|
340
|
-
album_title=track
|
|
448
|
+
album_art=track.get("album", {}).get("images", [])[2]["url"],
|
|
449
|
+
album_title=track.get("album", {}).get("name"),
|
|
341
450
|
album_type=None,
|
|
342
451
|
artists=artists_name,
|
|
343
452
|
genre=None,
|
|
344
|
-
id=track
|
|
345
|
-
isrc=track
|
|
453
|
+
id=track.get("id"),
|
|
454
|
+
isrc=track.get("isrc"),
|
|
346
455
|
lyrics=None,
|
|
347
|
-
release_date=track
|
|
456
|
+
release_date=track.get("album", {}).get("release_date"),
|
|
348
457
|
tempo=None,
|
|
349
|
-
title=track
|
|
458
|
+
title=track.get("name"),
|
|
350
459
|
type="track",
|
|
351
460
|
upc=None,
|
|
352
|
-
url=track
|
|
461
|
+
url=track.get("url"),
|
|
353
462
|
)
|
|
354
463
|
|
|
355
464
|
return None
|
|
@@ -366,8 +475,6 @@ class KKBox:
|
|
|
366
475
|
The name of the artist.
|
|
367
476
|
album : dict
|
|
368
477
|
A single album from the search results.
|
|
369
|
-
artist_ids : list
|
|
370
|
-
A list of artist IDs.
|
|
371
478
|
|
|
372
479
|
Returns
|
|
373
480
|
-------
|
|
@@ -377,39 +484,39 @@ class KKBox:
|
|
|
377
484
|
if not are_strings_similar(
|
|
378
485
|
album["name"],
|
|
379
486
|
song,
|
|
380
|
-
use_translation=self.
|
|
381
|
-
translation_session=self.
|
|
487
|
+
use_translation=self._normalize_non_english,
|
|
488
|
+
translation_session=self.__translation_session,
|
|
382
489
|
):
|
|
383
490
|
return None
|
|
384
491
|
|
|
385
|
-
artists_name = album
|
|
492
|
+
artists_name = album.get("artist", {}).get("name")
|
|
386
493
|
matching_artists = (
|
|
387
494
|
artists_name
|
|
388
495
|
if are_strings_similar(
|
|
389
496
|
artists_name,
|
|
390
497
|
artist,
|
|
391
|
-
use_translation=self.
|
|
392
|
-
translation_session=self.
|
|
498
|
+
use_translation=self._normalize_non_english,
|
|
499
|
+
translation_session=self.__translation_session,
|
|
393
500
|
)
|
|
394
501
|
else None
|
|
395
502
|
)
|
|
396
503
|
|
|
397
504
|
if matching_artists:
|
|
398
505
|
return MusicInfo(
|
|
399
|
-
album_art=album
|
|
400
|
-
album_title=album
|
|
506
|
+
album_art=album.get("images", [])[2]["url"],
|
|
507
|
+
album_title=album.get("name"),
|
|
401
508
|
album_type=None,
|
|
402
509
|
artists=artists_name,
|
|
403
510
|
genre=None,
|
|
404
|
-
id=album
|
|
511
|
+
id=album.get("id"),
|
|
405
512
|
isrc=None,
|
|
406
513
|
lyrics=None,
|
|
407
|
-
release_date=album
|
|
514
|
+
release_date=album.get("release_date"),
|
|
408
515
|
tempo=None,
|
|
409
|
-
title=album
|
|
516
|
+
title=album.get("name"),
|
|
410
517
|
type="album",
|
|
411
518
|
upc=None,
|
|
412
|
-
url=album
|
|
519
|
+
url=album.get("url"),
|
|
413
520
|
)
|
|
414
521
|
|
|
415
522
|
return None
|
|
@@ -417,6 +524,7 @@ class KKBox:
|
|
|
417
524
|
|
|
418
525
|
if __name__ == "__main__":
|
|
419
526
|
import logging
|
|
527
|
+
|
|
420
528
|
from yutipy.logger import enable_logging
|
|
421
529
|
|
|
422
530
|
enable_logging(level=logging.DEBUG)
|
|
@@ -425,6 +533,7 @@ if __name__ == "__main__":
|
|
|
425
533
|
try:
|
|
426
534
|
artist_name = input("Artist Name: ")
|
|
427
535
|
song_name = input("Song Name: ")
|
|
428
|
-
|
|
536
|
+
result = kkbox.search(artist_name, song_name)
|
|
537
|
+
pprint(asdict(result))
|
|
429
538
|
finally:
|
|
430
539
|
kkbox.close_session()
|
yutipy/models.py
CHANGED
|
@@ -66,3 +66,16 @@ class MusicInfos(MusicInfo):
|
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
album_art_source: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class UserPlaying(MusicInfo):
|
|
73
|
+
"""A data class to store users' currently playing music information.
|
|
74
|
+
|
|
75
|
+
Attributes
|
|
76
|
+
----------
|
|
77
|
+
is_playing : Optional[bool]
|
|
78
|
+
Whether the music is currently playing or paused.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
is_playing: Optional[bool] = None
|
yutipy/musicyt.py
CHANGED
|
@@ -24,7 +24,7 @@ class MusicYT:
|
|
|
24
24
|
self.ytmusic = YTMusic()
|
|
25
25
|
self._is_session_closed = False
|
|
26
26
|
self.normalize_non_english = True
|
|
27
|
-
self.
|
|
27
|
+
self.__translation_session = requests.Session()
|
|
28
28
|
|
|
29
29
|
def __enter__(self) -> "MusicYT":
|
|
30
30
|
"""Enters the runtime context related to this object."""
|
|
@@ -37,7 +37,7 @@ class MusicYT:
|
|
|
37
37
|
def close_session(self) -> None:
|
|
38
38
|
"""Closes the current session(s)."""
|
|
39
39
|
if not self.is_session_closed:
|
|
40
|
-
self.
|
|
40
|
+
self.__translation_session.close()
|
|
41
41
|
self._is_session_closed = True
|
|
42
42
|
|
|
43
43
|
@property
|
|
@@ -125,13 +125,13 @@ class MusicYT:
|
|
|
125
125
|
result.get("title"),
|
|
126
126
|
song,
|
|
127
127
|
use_translation=self.normalize_non_english,
|
|
128
|
-
translation_session=self.
|
|
128
|
+
translation_session=self.__translation_session,
|
|
129
129
|
)
|
|
130
130
|
and are_strings_similar(
|
|
131
131
|
_artist.get("name"),
|
|
132
132
|
artist,
|
|
133
133
|
use_translation=self.normalize_non_english,
|
|
134
|
-
translation_session=self.
|
|
134
|
+
translation_session=self.__translation_session,
|
|
135
135
|
)
|
|
136
136
|
for _artist in result.get("artists", [])
|
|
137
137
|
)
|
|
@@ -199,9 +199,9 @@ class MusicYT:
|
|
|
199
199
|
MusicInfo
|
|
200
200
|
The extracted music information.
|
|
201
201
|
"""
|
|
202
|
-
title = result
|
|
203
|
-
artist_names = ", ".join([artist
|
|
204
|
-
video_id = result
|
|
202
|
+
title = result.get("title")
|
|
203
|
+
artist_names = ", ".join([artist.get("name") for artist in result.get("artists", [])])
|
|
204
|
+
video_id = result.get("videoId")
|
|
205
205
|
song_url = f"https://music.youtube.com/watch?v={video_id}"
|
|
206
206
|
lyrics_id = self.ytmusic.get_watch_playlist(video_id)
|
|
207
207
|
|
|
@@ -300,8 +300,9 @@ if __name__ == "__main__":
|
|
|
300
300
|
|
|
301
301
|
enable_logging(level=logging.DEBUG)
|
|
302
302
|
music_yt = MusicYT()
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
try:
|
|
304
|
+
artist_name = input("Artist Name: ")
|
|
305
|
+
song_name = input("Song Name: ")
|
|
306
|
+
pprint(music_yt.search(artist_name, song_name))
|
|
307
|
+
finally:
|
|
308
|
+
music_yt.close_session()
|
yutipy/spotify.py
CHANGED
|
@@ -21,7 +21,7 @@ from yutipy.exceptions import (
|
|
|
21
21
|
SpotifyException,
|
|
22
22
|
)
|
|
23
23
|
from yutipy.logger import logger
|
|
24
|
-
from yutipy.models import MusicInfo
|
|
24
|
+
from yutipy.models import MusicInfo, UserPlaying
|
|
25
25
|
from yutipy.utils.helpers import (
|
|
26
26
|
are_strings_similar,
|
|
27
27
|
guess_album_type,
|
|
@@ -45,9 +45,7 @@ class Spotify:
|
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
def __init__(
|
|
48
|
-
self,
|
|
49
|
-
client_id: str = None,
|
|
50
|
-
client_secret: str = None,
|
|
48
|
+
self, client_id: str = None, client_secret: str = None, defer_load: bool = False
|
|
51
49
|
) -> None:
|
|
52
50
|
"""
|
|
53
51
|
Initializes the Spotify class (using Client Credentials grant type/flow) and sets up the session.
|
|
@@ -58,6 +56,8 @@ class Spotify:
|
|
|
58
56
|
The Client ID for the Spotify API. Defaults to ``SPOTIFY_CLIENT_ID`` from environment variable or the ``.env`` file.
|
|
59
57
|
client_secret : str, optional
|
|
60
58
|
The Client secret for the Spotify API. Defaults to ``SPOTIFY_CLIENT_SECRET`` from environment variable or the ``.env`` file.
|
|
59
|
+
defer_load : bool, optional
|
|
60
|
+
Whether to defer loading the access token during initialization. Default is ``False``.
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
self.client_id = client_id or SPOTIFY_CLIENT_ID
|
|
@@ -73,14 +73,37 @@ class Spotify:
|
|
|
73
73
|
"Client Secret was not found. Set it in environment variable or directly pass it when creating object."
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
+
self.defer_load = defer_load
|
|
77
|
+
|
|
76
78
|
self._is_session_closed = False
|
|
77
79
|
self._normalize_non_english = True
|
|
78
80
|
|
|
79
81
|
self.__api_url = "https://api.spotify.com/v1"
|
|
82
|
+
self.__access_token = None
|
|
83
|
+
self.__token_expires_in = None
|
|
84
|
+
self.__token_requested_at = None
|
|
80
85
|
self.__session = requests.Session()
|
|
81
86
|
self.__translation_session = requests.Session()
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
|
|
88
|
+
if not defer_load:
|
|
89
|
+
# Attempt to load access token during initialization if not deferred
|
|
90
|
+
token_info = None
|
|
91
|
+
try:
|
|
92
|
+
token_info = self.load_access_token()
|
|
93
|
+
except NotImplementedError:
|
|
94
|
+
logger.warning(
|
|
95
|
+
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
96
|
+
)
|
|
97
|
+
finally:
|
|
98
|
+
if not token_info:
|
|
99
|
+
token_info = self.__get_access_token()
|
|
100
|
+
self.__access_token = token_info.get("access_token")
|
|
101
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
102
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
103
|
+
else:
|
|
104
|
+
logger.warning(
|
|
105
|
+
"`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
|
|
106
|
+
)
|
|
84
107
|
|
|
85
108
|
def __enter__(self):
|
|
86
109
|
"""Enters the runtime context related to this object."""
|
|
@@ -102,31 +125,44 @@ class Spotify:
|
|
|
102
125
|
"""Checks if the session is closed."""
|
|
103
126
|
return self._is_session_closed
|
|
104
127
|
|
|
105
|
-
def
|
|
128
|
+
def load_token_after_init(self):
|
|
129
|
+
"""
|
|
130
|
+
Explicitly load the access token after initialization.
|
|
131
|
+
This is useful when ``defer_load`` is set to ``True`` during initialization.
|
|
106
132
|
"""
|
|
107
|
-
|
|
133
|
+
token_info = None
|
|
134
|
+
try:
|
|
135
|
+
token_info = self.load_access_token()
|
|
136
|
+
except NotImplementedError:
|
|
137
|
+
logger.warning(
|
|
138
|
+
"`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
|
|
139
|
+
)
|
|
140
|
+
finally:
|
|
141
|
+
if not token_info:
|
|
142
|
+
token_info = self.__get_access_token()
|
|
143
|
+
self.__access_token = token_info.get("access_token")
|
|
144
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
145
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
146
|
+
|
|
147
|
+
def __authorization_header(self) -> dict:
|
|
148
|
+
"""
|
|
149
|
+
Generates the authorization header for Spotify API requests.
|
|
108
150
|
|
|
109
151
|
Returns
|
|
110
152
|
-------
|
|
111
153
|
dict
|
|
112
|
-
|
|
154
|
+
A dictionary containing the Bearer token for authentication.
|
|
113
155
|
"""
|
|
114
|
-
|
|
115
|
-
token, expires_in = self.__get_spotify_token()
|
|
116
|
-
return {"Authorization": f"Bearer {token}"}, expires_in
|
|
117
|
-
except Exception as e:
|
|
118
|
-
raise AuthenticationException(
|
|
119
|
-
"Failed to authenticate with Spotify API"
|
|
120
|
-
) from e
|
|
156
|
+
return {"Authorization": f"Bearer {self.__access_token}"}
|
|
121
157
|
|
|
122
|
-
def
|
|
158
|
+
def __get_access_token(self) -> dict:
|
|
123
159
|
"""
|
|
124
|
-
Gets the Spotify API token.
|
|
160
|
+
Gets the Spotify API access token information.
|
|
125
161
|
|
|
126
162
|
Returns
|
|
127
163
|
-------
|
|
128
|
-
|
|
129
|
-
The Spotify API token.
|
|
164
|
+
dict
|
|
165
|
+
The Spotify API access token, with additional information such as expires in, etc.
|
|
130
166
|
"""
|
|
131
167
|
auth_string = f"{self.client_id}:{self.client_secret}"
|
|
132
168
|
auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
@@ -139,7 +175,9 @@ class Spotify:
|
|
|
139
175
|
data = {"grant_type": "client_credentials"}
|
|
140
176
|
|
|
141
177
|
try:
|
|
142
|
-
logger.info(
|
|
178
|
+
logger.info(
|
|
179
|
+
"Authenticating with Spotify API using Client Credentials grant type."
|
|
180
|
+
)
|
|
143
181
|
response = self.__session.post(
|
|
144
182
|
url=url, headers=headers, data=data, timeout=30
|
|
145
183
|
)
|
|
@@ -149,17 +187,80 @@ class Spotify:
|
|
|
149
187
|
logger.error(f"Network error during Spotify authentication: {e}")
|
|
150
188
|
raise NetworkException(f"Network error occurred: {e}")
|
|
151
189
|
|
|
152
|
-
|
|
190
|
+
if response.status_code == 200:
|
|
153
191
|
response_json = response.json()
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
192
|
+
response_json["requested_at"] = time()
|
|
193
|
+
return response_json
|
|
194
|
+
else:
|
|
195
|
+
raise InvalidResponseException(
|
|
196
|
+
f"Invalid response received: {response.json()}"
|
|
197
|
+
)
|
|
157
198
|
|
|
158
|
-
def
|
|
199
|
+
def __refresh_access_token(self):
|
|
159
200
|
"""Refreshes the token if it has expired."""
|
|
160
|
-
if time() - self.
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
202
|
+
token_info = self.__get_access_token()
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
self.save_access_token(token_info)
|
|
206
|
+
except NotImplementedError as e:
|
|
207
|
+
logger.warning(e)
|
|
208
|
+
|
|
209
|
+
self.__access_token = token_info.get("access_token")
|
|
210
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
211
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
212
|
+
|
|
213
|
+
logger.info("The access token is still valid, no need to refresh.")
|
|
214
|
+
|
|
215
|
+
def save_access_token(self, token_info: dict) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Saves the access token and related information.
|
|
218
|
+
|
|
219
|
+
This method must be overridden in a subclass to persist the access token and other
|
|
220
|
+
related information (e.g., expiration time). If not implemented,
|
|
221
|
+
the access token will not be saved, and it will be requested each time the
|
|
222
|
+
application restarts.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
token_info : dict
|
|
227
|
+
A dictionary containing the access token and related information, such as
|
|
228
|
+
refresh token, expiration time, etc.
|
|
229
|
+
|
|
230
|
+
Raises
|
|
231
|
+
------
|
|
232
|
+
NotImplementedError
|
|
233
|
+
If the method is not overridden in a subclass.
|
|
234
|
+
"""
|
|
235
|
+
raise NotImplementedError(
|
|
236
|
+
"The `save_access_token` method must be overridden in a subclass to save the access token and related information. "
|
|
237
|
+
"If not implemented, access token information will not be persisted, and users will need to re-authenticate after application restarts."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def load_access_token(self) -> Union[dict, None]:
|
|
241
|
+
"""
|
|
242
|
+
Loads the access token and related information.
|
|
243
|
+
|
|
244
|
+
This method must be overridden in a subclass to retrieve the access token and other
|
|
245
|
+
related information (e.g., expiration time) from persistent storage.
|
|
246
|
+
If not implemented, the access token will not be loaded, and it will be requested
|
|
247
|
+
each time the application restarts.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
dict | None
|
|
252
|
+
A dictionary containing the access token and related information, such as
|
|
253
|
+
refresh token, expiration time, etc., or None if no token is found.
|
|
254
|
+
|
|
255
|
+
Raises
|
|
256
|
+
------
|
|
257
|
+
NotImplementedError
|
|
258
|
+
If the method is not overridden in a subclass.
|
|
259
|
+
"""
|
|
260
|
+
raise NotImplementedError(
|
|
261
|
+
"The `load_access_token` method must be overridden in a subclass to load access token and related information. "
|
|
262
|
+
"If not implemented, access token information will not be loaded, and users will need to re-authenticate after application restarts."
|
|
263
|
+
)
|
|
163
264
|
|
|
164
265
|
def search(
|
|
165
266
|
self,
|
|
@@ -205,7 +306,7 @@ class Spotify:
|
|
|
205
306
|
if music_info:
|
|
206
307
|
return music_info
|
|
207
308
|
|
|
208
|
-
self.
|
|
309
|
+
self.__refresh_access_token()
|
|
209
310
|
|
|
210
311
|
query_url = f"{self.__api_url}/search{query}"
|
|
211
312
|
|
|
@@ -216,7 +317,7 @@ class Spotify:
|
|
|
216
317
|
|
|
217
318
|
try:
|
|
218
319
|
response = self.__session.get(
|
|
219
|
-
query_url, headers=self.
|
|
320
|
+
query_url, headers=self.__authorization_header(), timeout=30
|
|
220
321
|
)
|
|
221
322
|
response.raise_for_status()
|
|
222
323
|
except requests.RequestException as e:
|
|
@@ -271,7 +372,7 @@ class Spotify:
|
|
|
271
372
|
|
|
272
373
|
self._normalize_non_english = normalize_non_english
|
|
273
374
|
|
|
274
|
-
self.
|
|
375
|
+
self.__refresh_access_token()
|
|
275
376
|
|
|
276
377
|
if isrc:
|
|
277
378
|
query = f"?q={artist} {song} isrc:{isrc}&type=track&limit={limit}"
|
|
@@ -282,7 +383,9 @@ class Spotify:
|
|
|
282
383
|
|
|
283
384
|
query_url = f"{self.__api_url}/search{query}"
|
|
284
385
|
try:
|
|
285
|
-
response = self.__session.get(
|
|
386
|
+
response = self.__session.get(
|
|
387
|
+
query_url, headers=self.__authorization_header(), timeout=30
|
|
388
|
+
)
|
|
286
389
|
response.raise_for_status()
|
|
287
390
|
except requests.RequestException as e:
|
|
288
391
|
raise NetworkException(f"Network error occurred: {e}")
|
|
@@ -314,7 +417,7 @@ class Spotify:
|
|
|
314
417
|
query_url = f"{self.__api_url}/search?q={name}&type=artist&limit=5"
|
|
315
418
|
try:
|
|
316
419
|
response = self.__session.get(
|
|
317
|
-
query_url, headers=self.
|
|
420
|
+
query_url, headers=self.__authorization_header(), timeout=30
|
|
318
421
|
)
|
|
319
422
|
response.raise_for_status()
|
|
320
423
|
except requests.RequestException as e:
|
|
@@ -560,6 +663,7 @@ class SpotifyAuth:
|
|
|
560
663
|
|
|
561
664
|
self._is_session_closed = False
|
|
562
665
|
|
|
666
|
+
self.__api_url = "https://api.spotify.com/v1/me"
|
|
563
667
|
self.__access_token = None
|
|
564
668
|
self.__refresh_token = None
|
|
565
669
|
self.__token_expires_in = None
|
|
@@ -608,7 +712,6 @@ class SpotifyAuth:
|
|
|
608
712
|
"""Closes the current session(s)."""
|
|
609
713
|
if not self.is_session_closed:
|
|
610
714
|
self.__session.close()
|
|
611
|
-
self.__translation_session.close()
|
|
612
715
|
self._is_session_closed = True
|
|
613
716
|
|
|
614
717
|
@property
|
|
@@ -709,7 +812,7 @@ class SpotifyAuth:
|
|
|
709
812
|
f"Invalid response received: {response.json()}"
|
|
710
813
|
)
|
|
711
814
|
|
|
712
|
-
def
|
|
815
|
+
def __refresh_access_token(self):
|
|
713
816
|
"""Refreshes the token if it has expired."""
|
|
714
817
|
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
715
818
|
token_info = self.__get_access_token(refresh_token=self.__refresh_token)
|
|
@@ -717,7 +820,7 @@ class SpotifyAuth:
|
|
|
717
820
|
try:
|
|
718
821
|
self.save_access_token(token_info)
|
|
719
822
|
except NotImplementedError as e:
|
|
720
|
-
|
|
823
|
+
logger.warning(e)
|
|
721
824
|
|
|
722
825
|
self.__access_token = token_info.get("access_token")
|
|
723
826
|
self.__refresh_token = token_info.get("refresh_token")
|
|
@@ -909,11 +1012,12 @@ class SpotifyAuth:
|
|
|
909
1012
|
dict
|
|
910
1013
|
A dictionary containing the user's display name and profile images.
|
|
911
1014
|
"""
|
|
912
|
-
|
|
1015
|
+
self.__refresh_access_token()
|
|
1016
|
+
query_url = self.__api_url
|
|
913
1017
|
header = self.__authorization_header()
|
|
914
1018
|
|
|
915
1019
|
try:
|
|
916
|
-
response = self.__session.get(
|
|
1020
|
+
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
917
1021
|
response.raise_for_status()
|
|
918
1022
|
except requests.RequestException as e:
|
|
919
1023
|
logger.error(f"Failed to fetch user profile: {e}")
|
|
@@ -929,6 +1033,72 @@ class SpotifyAuth:
|
|
|
929
1033
|
"images": response_json.get("images", []),
|
|
930
1034
|
}
|
|
931
1035
|
|
|
1036
|
+
def get_currently_playing(self) -> Optional[UserPlaying]:
|
|
1037
|
+
"""
|
|
1038
|
+
Fetches information about the currently playing track for the authenticated user.
|
|
1039
|
+
|
|
1040
|
+
This method interacts with the Spotify API to retrieve details about the track
|
|
1041
|
+
the user is currently listening to. It includes information such as the track's
|
|
1042
|
+
title, album, artists, release date, and more.
|
|
1043
|
+
|
|
1044
|
+
Returns
|
|
1045
|
+
-------
|
|
1046
|
+
Optional[UserPlaying_]
|
|
1047
|
+
An instance of the ``UserPlaying`` model containing details about the currently
|
|
1048
|
+
playing track if available, or ``None`` if no track is currently playing or an
|
|
1049
|
+
error occurs.
|
|
1050
|
+
|
|
1051
|
+
Notes
|
|
1052
|
+
-----
|
|
1053
|
+
- The user must have granted the necessary permissions (e.g., `user-read-currently-playing` scope) for this method to work.
|
|
1054
|
+
- If the API response does not contain the expected data, the method will return `None`.
|
|
1055
|
+
|
|
1056
|
+
"""
|
|
1057
|
+
self.__refresh_access_token()
|
|
1058
|
+
query_url = f"{self.__api_url}/player/currently-playing"
|
|
1059
|
+
header = self.__authorization_header()
|
|
1060
|
+
|
|
1061
|
+
try:
|
|
1062
|
+
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
1063
|
+
response.raise_for_status()
|
|
1064
|
+
except requests.RequestException as e:
|
|
1065
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
1066
|
+
|
|
1067
|
+
if response.status_code != 200:
|
|
1068
|
+
logger.error(f"Unexpected response: {response.json()}")
|
|
1069
|
+
return None
|
|
1070
|
+
|
|
1071
|
+
response_json = response.json()
|
|
1072
|
+
result = response_json.get("item")
|
|
1073
|
+
if result:
|
|
1074
|
+
guess = guess_album_type(result.get("album", {}).get("total_tracks", 1))
|
|
1075
|
+
guessed_right = are_strings_similar(
|
|
1076
|
+
result.get("album", {}).get("album_type", "x"),
|
|
1077
|
+
guess,
|
|
1078
|
+
use_translation=False,
|
|
1079
|
+
)
|
|
1080
|
+
return UserPlaying(
|
|
1081
|
+
album_art=result.get("album", {}).get("images", [])[0].get("url"),
|
|
1082
|
+
album_title=result.get("album", {}).get("name"),
|
|
1083
|
+
album_type=(
|
|
1084
|
+
result.get("album", {}).get("album_type")
|
|
1085
|
+
if guessed_right
|
|
1086
|
+
else guess
|
|
1087
|
+
),
|
|
1088
|
+
artists=", ".join([x["name"] for x in result.get("artists", [])]),
|
|
1089
|
+
genre=None,
|
|
1090
|
+
id=result.get("id"),
|
|
1091
|
+
isrc=result.get("external_ids", {}).get("isrc"),
|
|
1092
|
+
is_playing=response_json.get("is_playing"),
|
|
1093
|
+
lyrics=None,
|
|
1094
|
+
release_date=result.get("album", {}).get("release_date"),
|
|
1095
|
+
tempo=None,
|
|
1096
|
+
title=result.get("name"),
|
|
1097
|
+
type=result.get("type"),
|
|
1098
|
+
upc=result.get("external_ids", {}).get("upc"),
|
|
1099
|
+
url=result.get("external_urls", {}).get("spotify"),
|
|
1100
|
+
)
|
|
1101
|
+
|
|
932
1102
|
|
|
933
1103
|
if __name__ == "__main__":
|
|
934
1104
|
import logging
|
|
@@ -949,7 +1119,8 @@ if __name__ == "__main__":
|
|
|
949
1119
|
try:
|
|
950
1120
|
artist_name = input("Artist Name: ")
|
|
951
1121
|
song_name = input("Song Name: ")
|
|
952
|
-
|
|
1122
|
+
result = spotify.search(artist_name, song_name)
|
|
1123
|
+
pprint(asdict(result))
|
|
953
1124
|
finally:
|
|
954
1125
|
spotify.close_session()
|
|
955
1126
|
|
yutipy/yutipy_music.py
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
yutipy/__init__.py,sha256=eURfdAFBvA9xF6cuUJUMWLf44LHxRQuXONfIPD-CkIQ,268
|
|
2
|
+
yutipy/deezer.py,sha256=PTTTfeORh1HZ_ta7_Uu4YARouSknUnAxO9AQJPFm4v0,11402
|
|
3
|
+
yutipy/exceptions.py,sha256=fur945x1Ibu7yeIPDRsOcujfVRRa5JHQw27dsOUreK4,1393
|
|
4
|
+
yutipy/itunes.py,sha256=fV7KLsXWvfM_97KwVwn_KfnWM7j0cVGE7RytvnDGlZM,7929
|
|
5
|
+
yutipy/kkbox.py,sha256=MuYpR_UTZQxeitn2rc0UUgiMVikIXcVWmns3eSVjd_g,18847
|
|
6
|
+
yutipy/logger.py,sha256=cHCjpDslVsBOnp7jluqrOOi4ekDIggPhbSfqHeIfT-U,1263
|
|
7
|
+
yutipy/models.py,sha256=_92e54uXXCw53oWZiNLBBai6C0InOZMJL7r8GJ5smbM,2215
|
|
8
|
+
yutipy/musicyt.py,sha256=6Vz8bI8hDNFoDKRh6GK90dGMRbn_d5d6eGPsaYogb_Y,9315
|
|
9
|
+
yutipy/spotify.py,sha256=m9GV3xZ94jRKtajMo34BARuR4l2gP6sUfZ8CKsJQDWo,42135
|
|
10
|
+
yutipy/yutipy_music.py,sha256=cHJ95HxGILweVrnEacj8tTlU0NPxMpuDVMpngdX0mZQ,6558
|
|
11
|
+
yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
yutipy/cli/config.py,sha256=6p69ZlT3ebUH5wzhd0iisLYKOnYX6xTSzoyrdBwGNo0,3230
|
|
13
|
+
yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
|
|
14
|
+
yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
|
|
15
|
+
yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
|
|
16
|
+
yutipy-2.1.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
17
|
+
yutipy-2.1.1.dist-info/METADATA,sha256=86Xbqgk9pHIiGIQkZAMzaOKkUu4vmo8sadsoH1hAlQ8,6494
|
|
18
|
+
yutipy-2.1.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
19
|
+
yutipy-2.1.1.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
|
|
20
|
+
yutipy-2.1.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
21
|
+
yutipy-2.1.1.dist-info/RECORD,,
|
yutipy-2.0.0.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
yutipy/__init__.py,sha256=eURfdAFBvA9xF6cuUJUMWLf44LHxRQuXONfIPD-CkIQ,268
|
|
2
|
-
yutipy/deezer.py,sha256=YZs2ilHL3pwc3AB9CmctKlyZ4GfD6qcMjZ6dgjYVxXo,11397
|
|
3
|
-
yutipy/exceptions.py,sha256=fur945x1Ibu7yeIPDRsOcujfVRRa5JHQw27dsOUreK4,1393
|
|
4
|
-
yutipy/itunes.py,sha256=hXMybI_lVWgAZ6QE4q_F1jOmWqIBShYQnn9nZUa8XW8,7922
|
|
5
|
-
yutipy/kkbox.py,sha256=tS5LqIMlExvW1tUxMEhFXs9d9t5iFMxPp1mgaf9l7uk,14184
|
|
6
|
-
yutipy/logger.py,sha256=cHCjpDslVsBOnp7jluqrOOi4ekDIggPhbSfqHeIfT-U,1263
|
|
7
|
-
yutipy/models.py,sha256=vvWIA3MwCOOM2CBHSabqmFXz4NdVHaQtObU6zhGpJOM,1931
|
|
8
|
-
yutipy/musicyt.py,sha256=6BvHM6SK8kSdFlGlWZIXThljIVhpwqEGUYZ7LMmXqbE,9226
|
|
9
|
-
yutipy/spotify.py,sha256=7BG9iv42mQk5CGAd7879fvUwo5ImpQlbFf3yfAzhnQI,34722
|
|
10
|
-
yutipy/yutipy_music.py,sha256=3ry1KHukTInWV7xxzigYB-zWsAupBUXiAoL0F61Bmis,6531
|
|
11
|
-
yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
yutipy/cli/config.py,sha256=6p69ZlT3ebUH5wzhd0iisLYKOnYX6xTSzoyrdBwGNo0,3230
|
|
13
|
-
yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
|
|
14
|
-
yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
|
|
15
|
-
yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
|
|
16
|
-
yutipy-2.0.0.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
17
|
-
yutipy-2.0.0.dist-info/METADATA,sha256=3KzURL_aKX4g6PHVo0RYO_cMxTnQ8cCHNeXVZsMxvRE,6494
|
|
18
|
-
yutipy-2.0.0.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
19
|
-
yutipy-2.0.0.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
|
|
20
|
-
yutipy-2.0.0.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
21
|
-
yutipy-2.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|