yutipy 2.2.6__py3-none-any.whl → 2.2.8__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/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 Dict, Optional
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) -> "Itunes":
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"Network error while searching iTunes: {e}")
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
- raise InvalidResponseException(f"Invalid response received: {e}")
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 time import time
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.exceptions import (
14
- AuthenticationException,
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
- self.defer_load = defer_load
65
-
66
- self._is_session_closed = False
67
- self._normalize_non_english = True
68
- self._valid_territories = ["HK", "JP", "MY", "SG", "TW"]
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
- def load_access_token(self) -> Union[dict, None]:
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.api_url}/search{query}"
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.__session.get(
315
- query_url, headers=self.__authorization_header(), timeout=30
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.__translation_session,
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.__translation_session,
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.__translation_session,
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.__translation_session,
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(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
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}")