yutipy 1.3.2__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of yutipy might be problematic. Click here for more details.

yutipy/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from .deezer import Deezer
2
2
  from .itunes import Itunes
3
+ from .kkbox import KKBox
3
4
  from .models import MusicInfo
4
5
  from .musicyt import MusicYT
5
6
  from .spotify import Spotify
@@ -8,6 +9,7 @@ from .yutipy_music import YutipyMusic
8
9
  __all__ = [
9
10
  "Deezer",
10
11
  "Itunes",
12
+ "KKBox",
11
13
  "MusicInfo",
12
14
  "MusicYT",
13
15
  "Spotify",
yutipy/deezer.py CHANGED
@@ -21,6 +21,8 @@ class Deezer:
21
21
  self._session = requests.Session()
22
22
  self.api_url = "https://api.deezer.com"
23
23
  self._is_session_closed = False
24
+ self.normalize_non_english = True
25
+ self._translation_session = requests.Session()
24
26
 
25
27
  def __enter__(self) -> "Deezer":
26
28
  """Enters the runtime context related to this object."""
@@ -34,6 +36,7 @@ class Deezer:
34
36
  """Closes the current session."""
35
37
  if not self.is_session_closed:
36
38
  self._session.close()
39
+ self._translation_session.close()
37
40
  self._is_session_closed = True
38
41
 
39
42
  @property
@@ -41,7 +44,13 @@ class Deezer:
41
44
  """Checks if the session is closed."""
42
45
  return self._is_session_closed
43
46
 
44
- def search(self, artist: str, song: str, limit: int = 10) -> Optional[MusicInfo]:
47
+ def search(
48
+ self,
49
+ artist: str,
50
+ song: str,
51
+ limit: int = 10,
52
+ normalize_non_english: bool = True,
53
+ ) -> Optional[MusicInfo]:
45
54
  """
46
55
  Searches for a song by artist and title.
47
56
 
@@ -52,8 +61,10 @@ class Deezer:
52
61
  song : str
53
62
  The title of the song.
54
63
  limit: int, optional
55
- The number of items to retrieve from API.
56
- ``limit >=1 and <= 50``. Default is ``10``.
64
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
65
+ normalize_non_english : bool, optional
66
+ Whether to normalize non-English characters for comparison. Default is ``True``.
67
+
57
68
 
58
69
  Returns
59
70
  -------
@@ -65,6 +76,8 @@ class Deezer:
65
76
  "Artist and song names must be valid strings and can't be empty."
66
77
  )
67
78
 
79
+ self.normalize_non_english = normalize_non_english
80
+
68
81
  search_types = ["track", "album"]
69
82
 
70
83
  for search_type in search_types:
@@ -208,8 +221,18 @@ class Deezer:
208
221
  """
209
222
  for result in results:
210
223
  if not (
211
- are_strings_similar(result["title"], song)
212
- and are_strings_similar(result["artist"]["name"], artist)
224
+ are_strings_similar(
225
+ result["title"],
226
+ song,
227
+ use_translation=self.normalize_non_english,
228
+ translation_session=self._translation_session,
229
+ )
230
+ and are_strings_similar(
231
+ result["artist"]["name"],
232
+ artist,
233
+ use_translation=self.normalize_non_english,
234
+ translation_session=self._translation_session,
235
+ )
213
236
  ):
214
237
  continue
215
238
 
yutipy/itunes.py CHANGED
@@ -22,6 +22,8 @@ class Itunes:
22
22
  self._session = requests.Session()
23
23
  self.api_url = "https://itunes.apple.com"
24
24
  self._is_session_closed = False
25
+ self.normalize_non_english = True
26
+ self._translation_session = requests.Session()
25
27
 
26
28
  def __enter__(self) -> "Itunes":
27
29
  """Enters the runtime context related to this object."""
@@ -35,6 +37,7 @@ class Itunes:
35
37
  """Closes the current session."""
36
38
  if not self.is_session_closed:
37
39
  self._session.close()
40
+ self._translation_session.close()
38
41
  self._is_session_closed = True
39
42
 
40
43
  @property
@@ -42,7 +45,13 @@ class Itunes:
42
45
  """Checks if the session is closed."""
43
46
  return self._is_session_closed
44
47
 
45
- def search(self, artist: str, song: str, limit: int = 10) -> Optional[MusicInfo]:
48
+ def search(
49
+ self,
50
+ artist: str,
51
+ song: str,
52
+ limit: int = 10,
53
+ normalize_non_english: bool = True,
54
+ ) -> Optional[MusicInfo]:
46
55
  """
47
56
  Searches for a song by artist and title.
48
57
 
@@ -53,8 +62,9 @@ class Itunes:
53
62
  song : str
54
63
  The title of the song.
55
64
  limit: int, optional
56
- The number of items to retrieve from API.
57
- ``limit >=1 and <= 50``. Default is ``10``.
65
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
66
+ normalize_non_english : bool, optional
67
+ Whether to normalize non-English characters for comparison. Default is ``True``.
58
68
 
59
69
  Returns
60
70
  -------
@@ -66,6 +76,8 @@ class Itunes:
66
76
  "Artist and song names must be valid strings and can't be empty."
67
77
  )
68
78
 
79
+ self.normalize_non_english = normalize_non_english
80
+
69
81
  entities = ["song", "album"]
70
82
  for entity in entities:
71
83
  endpoint = f"{self.api_url}/search"
@@ -114,9 +126,17 @@ class Itunes:
114
126
  for result in results:
115
127
  if not (
116
128
  are_strings_similar(
117
- result.get("trackName", result["collectionName"]), song
129
+ result.get("trackName", result["collectionName"]),
130
+ song,
131
+ use_translation=self.normalize_non_english,
132
+ translation_session=self._translation_session,
133
+ )
134
+ and are_strings_similar(
135
+ result["artistName"],
136
+ artist,
137
+ use_translation=self.normalize_non_english,
138
+ translation_session=self._translation_session,
118
139
  )
119
- and are_strings_similar(result["artistName"], artist)
120
140
  ):
121
141
  continue
122
142
 
yutipy/kkbox.py CHANGED
@@ -57,6 +57,8 @@ class KKBox:
57
57
  self.__start_time = time.time()
58
58
  self._is_session_closed = False
59
59
  self.valid_territories = ["HK", "JP", "MY", "SG", "TW"]
60
+ self.normalize_non_english = True
61
+ self._translation_session = requests.Session()
60
62
 
61
63
  def __enter__(self):
62
64
  """Enters the runtime context related to this object."""
@@ -70,6 +72,7 @@ class KKBox:
70
72
  """Closes the current session."""
71
73
  if not self.is_session_closed:
72
74
  self._session.close()
75
+ self._translation_session.close()
73
76
  self._is_session_closed = True
74
77
 
75
78
  @property
@@ -134,7 +137,12 @@ class KKBox:
134
137
  self.__start_time = time.time()
135
138
 
136
139
  def search(
137
- self, artist: str, song: str, territory: str = "TW", limit: int = 10,
140
+ self,
141
+ artist: str,
142
+ song: str,
143
+ territory: str = "TW",
144
+ limit: int = 10,
145
+ normalize_non_english: bool = True,
138
146
  ) -> Optional[MusicInfo]:
139
147
  """
140
148
  Searches for a song by artist and title.
@@ -149,8 +157,9 @@ class KKBox:
149
157
  Two-letter country codes from ISO 3166-1 alpha-2.
150
158
  Allowed values: ``HK``, ``JP``, ``MY``, ``SG``, ``TW``.
151
159
  limit: int, optional
152
- The number of items to retrieve from API.
153
- ``limit >=1 and <= 50``. Default is ``10``.
160
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
161
+ normalize_non_english : bool, optional
162
+ Whether to normalize non-English characters for comparison. Default is ``True``.
154
163
 
155
164
  Returns
156
165
  -------
@@ -162,9 +171,13 @@ class KKBox:
162
171
  "Artist and song names must be valid strings and can't be empty."
163
172
  )
164
173
 
174
+ self.normalize_non_english = normalize_non_english
175
+
165
176
  self.__refresh_token_if_expired()
166
177
 
167
- query = f"?q={artist} - {song}&type=track,album&territory={territory}&limit={limit}"
178
+ query = (
179
+ f"?q={artist} - {song}&type=track,album&territory={territory}&limit={limit}"
180
+ )
168
181
  query_url = f"{self.api_url}/search{query}"
169
182
 
170
183
  try:
@@ -288,12 +301,24 @@ class KKBox:
288
301
  Optional[MusicInfo]
289
302
  The music information if found, otherwise None.
290
303
  """
291
- if not are_strings_similar(track["name"], song):
304
+ if not are_strings_similar(
305
+ track["name"],
306
+ song,
307
+ use_translation=self.normalize_non_english,
308
+ translation_session=self._translation_session,
309
+ ):
292
310
  return None
293
311
 
294
312
  artists_name = track["album"]["artist"]["name"]
295
313
  matching_artists = (
296
- artists_name if are_strings_similar(artists_name, artist) else None
314
+ artists_name
315
+ if are_strings_similar(
316
+ artists_name,
317
+ artist,
318
+ use_translation=self.normalize_non_english,
319
+ translation_session=self._translation_session,
320
+ )
321
+ else None
297
322
  )
298
323
 
299
324
  if matching_artists:
@@ -336,12 +361,24 @@ class KKBox:
336
361
  Optional[MusicInfo]
337
362
  The music information if found, otherwise None.
338
363
  """
339
- if not are_strings_similar(album["name"], song):
364
+ if not are_strings_similar(
365
+ album["name"],
366
+ song,
367
+ use_translation=self.normalize_non_english,
368
+ translation_session=self._translation_session,
369
+ ):
340
370
  return None
341
371
 
342
372
  artists_name = album["artist"]["name"]
343
373
  matching_artists = (
344
- artists_name if are_strings_similar(artists_name, artist) else None
374
+ artists_name
375
+ if are_strings_similar(
376
+ artists_name,
377
+ artist,
378
+ use_translation=self.normalize_non_english,
379
+ translation_session=self._translation_session,
380
+ )
381
+ else None
345
382
  )
346
383
 
347
384
  if matching_artists:
yutipy/musicyt.py CHANGED
@@ -2,6 +2,7 @@ import os
2
2
  from pprint import pprint
3
3
  from typing import Optional
4
4
 
5
+ import requests
5
6
  from ytmusicapi import YTMusic, exceptions
6
7
 
7
8
  from yutipy.exceptions import (
@@ -19,8 +20,28 @@ class MusicYT:
19
20
  def __init__(self) -> None:
20
21
  """Initializes the YouTube Music class and sets up the session."""
21
22
  self.ytmusic = YTMusic()
22
-
23
- def search(self, artist: str, song: str, limit: int = 10) -> Optional[MusicInfo]:
23
+ self._is_session_closed = False
24
+ self.normalize_non_english = True
25
+ self._translation_session = requests.Session()
26
+
27
+ def close_session(self) -> None:
28
+ """Closes the current session(s)."""
29
+ if not self.is_session_closed:
30
+ self._translation_session.close()
31
+ self._is_session_closed = True
32
+
33
+ @property
34
+ def is_session_closed(self) -> bool:
35
+ """Checks if the session is closed."""
36
+ return self._is_session_closed
37
+
38
+ def search(
39
+ self,
40
+ artist: str,
41
+ song: str,
42
+ limit: int = 10,
43
+ normalize_non_english: bool = True,
44
+ ) -> Optional[MusicInfo]:
24
45
  """
25
46
  Searches for a song by artist and title.
26
47
 
@@ -31,8 +52,9 @@ class MusicYT:
31
52
  song : str
32
53
  The title of the song.
33
54
  limit: int, optional
34
- The number of items to retrieve from API.
35
- ``limit >=1 and <= 50``. Default is ``10``.
55
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
56
+ normalize_non_english : bool, optional
57
+ Whether to normalize non-English characters for comparison. Default is ``True``.
36
58
 
37
59
  Returns
38
60
  -------
@@ -44,6 +66,8 @@ class MusicYT:
44
66
  "Artist and song names must be valid strings and can't be empty."
45
67
  )
46
68
 
69
+ self.normalize_non_english = normalize_non_english
70
+
47
71
  query = f"{artist} - {song}"
48
72
 
49
73
  try:
@@ -79,8 +103,18 @@ class MusicYT:
79
103
  return False
80
104
 
81
105
  return any(
82
- are_strings_similar(result.get("title"), song)
83
- and are_strings_similar(_artist.get("name"), artist)
106
+ are_strings_similar(
107
+ result.get("title"),
108
+ song,
109
+ use_translation=self.normalize_non_english,
110
+ translation_session=self._translation_session,
111
+ )
112
+ and are_strings_similar(
113
+ _artist.get("name"),
114
+ artist,
115
+ use_translation=self.normalize_non_english,
116
+ translation_session=self._translation_session,
117
+ )
84
118
  for _artist in result.get("artists", [])
85
119
  )
86
120
 
yutipy/spotify.py CHANGED
@@ -16,6 +16,7 @@ from yutipy.exceptions import (
16
16
  )
17
17
  from yutipy.models import MusicInfo
18
18
  from yutipy.utils.cheap_utils import (
19
+ guess_album_type,
19
20
  are_strings_similar,
20
21
  is_valid_string,
21
22
  separate_artists,
@@ -62,6 +63,8 @@ class Spotify:
62
63
  self.__header, self.__expires_in = self.__authenticate()
63
64
  self.__start_time = time.time()
64
65
  self._is_session_closed = False
66
+ self.normalize_non_english = True
67
+ self._translation_session = requests.Session()
65
68
 
66
69
  def __enter__(self):
67
70
  """Enters the runtime context related to this object."""
@@ -72,9 +75,10 @@ class Spotify:
72
75
  self.close_session()
73
76
 
74
77
  def close_session(self) -> None:
75
- """Closes the current session."""
78
+ """Closes the current session(s)."""
76
79
  if not self.is_session_closed:
77
80
  self._session.close()
81
+ self._translation_session.close()
78
82
  self._is_session_closed = True
79
83
 
80
84
  @property
@@ -138,7 +142,13 @@ class Spotify:
138
142
  self.__header, self.__expires_in = self.__authenticate()
139
143
  self.__start_time = time.time()
140
144
 
141
- def search(self, artist: str, song: str, limit: int = 10) -> Optional[MusicInfo]:
145
+ def search(
146
+ self,
147
+ artist: str,
148
+ song: str,
149
+ limit: int = 10,
150
+ normalize_non_english: bool = True,
151
+ ) -> Optional[MusicInfo]:
142
152
  """
143
153
  Searches for a song by artist and title.
144
154
 
@@ -149,8 +159,9 @@ class Spotify:
149
159
  song : str
150
160
  The title of the song.
151
161
  limit: int, optional
152
- The number of items to retrieve from API.
153
- ``limit >=1 and <= 50``. Default is ``10``.
162
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
163
+ normalize_non_english : bool, optional
164
+ Whether to normalize non-English characters for comparison. Default is ``True``.
154
165
 
155
166
  Returns
156
167
  -------
@@ -162,7 +173,10 @@ class Spotify:
162
173
  "Artist and song names must be valid strings and can't be empty."
163
174
  )
164
175
 
176
+ self.normalize_non_english = normalize_non_english
177
+
165
178
  music_info = None
179
+ artist_ids = None
166
180
  queries = [
167
181
  f"?q=artist:{artist} track:{song}&type=track&limit={limit}",
168
182
  f"?q=artist:{artist} album:{song}&type=album&limit={limit}",
@@ -187,7 +201,7 @@ class Spotify:
187
201
  if response.status_code != 200:
188
202
  raise SpotifyException(f"Failed to search for music: {response.json()}")
189
203
 
190
- artist_ids = self._get_artists_ids(artist)
204
+ artist_ids = artist_ids if artist_ids else self._get_artists_ids(artist)
191
205
  music_info = self._find_music_info(
192
206
  artist, song, response.json(), artist_ids
193
207
  )
@@ -195,7 +209,13 @@ class Spotify:
195
209
  return music_info
196
210
 
197
211
  def search_advanced(
198
- self, artist: str, song: str, isrc: str = None, upc: str = None
212
+ self,
213
+ artist: str,
214
+ song: str,
215
+ isrc: str = None,
216
+ upc: str = None,
217
+ limit: int = 1,
218
+ normalize_non_english: bool = True,
199
219
  ) -> Optional[MusicInfo]:
200
220
  """
201
221
  Searches for a song by artist, title, ISRC, or UPC.
@@ -210,6 +230,10 @@ class Spotify:
210
230
  The ISRC of the track.
211
231
  upc : str, optional
212
232
  The UPC of the album.
233
+ limit: int, optional
234
+ The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``1``.
235
+ normalize_non_english : bool, optional
236
+ Whether to normalize non-English characters for comparison. Default is ``True``.
213
237
 
214
238
  Returns
215
239
  -------
@@ -221,12 +245,14 @@ class Spotify:
221
245
  "Artist and song names must be valid strings and can't be empty."
222
246
  )
223
247
 
248
+ self.normalize_non_english = normalize_non_english
249
+
224
250
  self.__refresh_token_if_expired()
225
251
 
226
252
  if isrc:
227
- query = f"?q={artist} {song} isrc:{isrc}&type=track&limit=1"
253
+ query = f"?q={artist} {song} isrc:{isrc}&type=track&limit={limit}"
228
254
  elif upc:
229
- query = f"?q={artist} {song} upc:{upc}&type=album&limit=1"
255
+ query = f"?q={artist} {song} upc:{upc}&type=album&limit={limit}"
230
256
  else:
231
257
  raise InvalidValueException("ISRC or UPC must be provided.")
232
258
 
@@ -340,14 +366,25 @@ class Spotify:
340
366
  Optional[MusicInfo]
341
367
  The music information if found, otherwise None.
342
368
  """
343
- if not are_strings_similar(track["name"], song):
369
+ if not are_strings_similar(
370
+ track["name"],
371
+ song,
372
+ use_translation=self.normalize_non_english,
373
+ translation_session=self._translation_session,
374
+ ):
344
375
  return None
345
376
 
346
377
  artists_name = [x["name"] for x in track["artists"]]
347
378
  matching_artists = [
348
379
  x["name"]
349
380
  for x in track["artists"]
350
- if are_strings_similar(x["name"], artist) or x["id"] in artist_ids
381
+ if are_strings_similar(
382
+ x["name"],
383
+ artist,
384
+ use_translation=self.normalize_non_english,
385
+ translation_session=self._translation_session,
386
+ )
387
+ or x["id"] in artist_ids
351
388
  ]
352
389
 
353
390
  if matching_artists:
@@ -392,21 +429,37 @@ class Spotify:
392
429
  Optional[MusicInfo]
393
430
  The music information if found, otherwise None.
394
431
  """
395
- if not are_strings_similar(album["name"], song):
432
+ if not are_strings_similar(
433
+ album["name"],
434
+ song,
435
+ use_translation=self.normalize_non_english,
436
+ translation_session=self._translation_session,
437
+ ):
396
438
  return None
397
439
 
398
440
  artists_name = [x["name"] for x in album["artists"]]
399
441
  matching_artists = [
400
442
  x["name"]
401
443
  for x in album["artists"]
402
- if are_strings_similar(x["name"], artist) or x["id"] in artist_ids
444
+ if are_strings_similar(
445
+ x["name"],
446
+ artist,
447
+ use_translation=self.normalize_non_english,
448
+ translation_session=self._translation_session,
449
+ )
450
+ or x["id"] in artist_ids
403
451
  ]
404
452
 
405
453
  if matching_artists:
454
+ guess = guess_album_type(album.get("total_tracks", 1))
455
+ guessed_right = are_strings_similar(
456
+ album.get("album_type", "x"), guess, use_translation=False
457
+ )
458
+
406
459
  return MusicInfo(
407
460
  album_art=album["images"][0]["url"],
408
461
  album_title=album["name"],
409
- album_type=album["album_type"],
462
+ album_type=album.get("alnum_type") if guessed_right else guess,
410
463
  artists=", ".join(artists_name),
411
464
  genre=None,
412
465
  id=album["id"],
@@ -415,7 +468,7 @@ class Spotify:
415
468
  release_date=album["release_date"],
416
469
  tempo=None,
417
470
  title=album["name"],
418
- type="album",
471
+ type=album.get("type"),
419
472
  upc=None,
420
473
  url=album["external_urls"]["spotify"],
421
474
  )
yutipy/utils/__init__.py CHANGED
@@ -1,6 +1,12 @@
1
- from .cheap_utils import are_strings_similar, is_valid_string, separate_artists
1
+ from .cheap_utils import (
2
+ guess_album_type,
3
+ are_strings_similar,
4
+ is_valid_string,
5
+ separate_artists,
6
+ )
2
7
 
3
8
  __all__ = [
9
+ "guess_album_type",
4
10
  "are_strings_similar",
5
11
  "is_valid_string",
6
12
  "separate_artists",
@@ -7,6 +7,7 @@ def translate_text(
7
7
  text: str,
8
8
  sl: str = None,
9
9
  dl: str = "en",
10
+ session: requests.Session = None,
10
11
  ) -> dict:
11
12
  """
12
13
  Translate text from one language to another.
@@ -15,7 +16,8 @@ def translate_text(
15
16
  text (str): The text to be translated.
16
17
  sl (str, optional): The source language code (e.g., 'en' for English, 'es' for Spanish). If not provided, the API will attempt to detect the source language.
17
18
  dl (str, optional): The destination language code (default is 'en' for English).
18
-
19
+ session (requests.Session, optional): A `requests.Session` object to use for making the API request. If not provided, a new session will be created and closed within the function.
20
+ Providing your own session can improve performance by reusing the same session for multiple requests. Don't forget to close the session afterwards.
19
21
 
20
22
  Returns:
21
23
  dict: A dictionary containing the following keys:
@@ -24,12 +26,17 @@ def translate_text(
24
26
  - 'destination-text': The translated text.
25
27
  - 'destination-language': The destination language code.
26
28
  """
29
+ default_session = False
30
+ if session is None:
31
+ default_session = True
32
+ session = requests.Session()
33
+
27
34
  if sl:
28
35
  url = f"https://ftapi.pythonanywhere.com/translate?sl={sl}&dl={dl}&text={text}"
29
36
  else:
30
37
  url = f"https://ftapi.pythonanywhere.com/translate?dl={dl}&text={text}"
31
38
 
32
- response = requests.get(url)
39
+ response = session.get(url)
33
40
  response_json = response.json()
34
41
  result = {
35
42
  "source-text": response_json["source-text"],
@@ -37,10 +44,20 @@ def translate_text(
37
44
  "destination-text": response_json["destination-text"],
38
45
  "destination-language": response_json["destination-language"],
39
46
  }
47
+
48
+ if default_session:
49
+ session.close()
50
+
40
51
  return result
41
52
 
42
53
 
43
- def are_strings_similar(str1: str, str2: str, threshold: int = 80) -> bool:
54
+ def are_strings_similar(
55
+ str1: str,
56
+ str2: str,
57
+ threshold: int = 80,
58
+ use_translation: bool = True,
59
+ translation_session: requests.Session = None,
60
+ ) -> bool:
44
61
  """
45
62
  Determine if two strings are similar based on a given threshold.
46
63
 
@@ -48,12 +65,24 @@ def are_strings_similar(str1: str, str2: str, threshold: int = 80) -> bool:
48
65
  str1 (str): First string to compare.
49
66
  str2 (str): Second string to compare.
50
67
  threshold (int, optional): Similarity threshold. Defaults to 80.
68
+ use_translation (bool, optional): Use translations to compare strings. Defaults to ``True``
69
+ translation_session (requests.Session, optional): A `requests.Session` object to use for making the API request. If not provided, a new session will be created and closed within the function.
70
+ Providing your own session can improve performance by reusing the same session for multiple requests. Don't forget to close the session afterwards.
51
71
 
52
72
  Returns:
53
73
  bool: True if the strings are similar, otherwise False.
54
74
  """
55
- str1 = translate_text(str1)["destination-text"]
56
- str2 = translate_text(str2)["destination-text"]
75
+ if use_translation:
76
+ str1 = (
77
+ translate_text(str1, session=translation_session)["destination-text"]
78
+ if translation_session
79
+ else translate_text(str1)["destination-text"]
80
+ )
81
+ str2 = (
82
+ translate_text(str2, session=translation_session)["destination-text"]
83
+ if translation_session
84
+ else translate_text(str2)["destination-text"]
85
+ )
57
86
 
58
87
  similarity_score = fuzz.WRatio(str1, str2, processor=default_process)
59
88
  return similarity_score > threshold
@@ -88,5 +117,15 @@ def is_valid_string(string: str) -> bool:
88
117
  return bool(string and (string.isalnum() or not string.isspace()))
89
118
 
90
119
 
120
+ def guess_album_type(total_tracks: int):
121
+ """Just guessing the album type (i.e. single, ep or album) by total track counts."""
122
+ if total_tracks == 1:
123
+ return "single"
124
+ if 3 <= total_tracks <= 5:
125
+ return "ep"
126
+ if total_tracks >= 7:
127
+ return "album"
128
+
129
+
91
130
  if __name__ == "__main__":
92
131
  separate_artists("Artist A ft. Artist B")
yutipy/utils/logger.py ADDED
@@ -0,0 +1,4 @@
1
+ import logging
2
+
3
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
4
+ logger = logging.getLogger(__name__)
yutipy/yutipy_music.py CHANGED
@@ -2,14 +2,17 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
2
2
  from pprint import pprint
3
3
  from typing import Optional
4
4
 
5
+ import requests
6
+
5
7
  from yutipy.deezer import Deezer
6
- from yutipy.exceptions import InvalidValueException
8
+ from yutipy.exceptions import InvalidValueException, KKBoxException, SpotifyException
7
9
  from yutipy.itunes import Itunes
8
10
  from yutipy.kkbox import KKBox
9
11
  from yutipy.models import MusicInfo, MusicInfos
10
12
  from yutipy.musicyt import MusicYT
11
13
  from yutipy.spotify import Spotify
12
14
  from yutipy.utils.cheap_utils import is_valid_string
15
+ from yutipy.utils.logger import logger
13
16
 
14
17
 
15
18
  class YutipyMusic:
@@ -22,22 +25,53 @@ class YutipyMusic:
22
25
  def __init__(self) -> None:
23
26
  """Initializes the YutipyMusic class."""
24
27
  self.music_info = MusicInfos()
25
- self.album_art_priority = ["deezer", "kkbox", "spotify", "ytmusic", "itunes"]
28
+ self.album_art_priority = ["deezer", "ytmusic", "itunes"]
26
29
  self.services = {
27
30
  "deezer": Deezer(),
28
31
  "itunes": Itunes(),
29
- "kkbox": KKBox(),
30
32
  "ytmusic": MusicYT(),
31
- "spotify": Spotify(),
32
33
  }
33
34
 
35
+ try:
36
+ self.services["kkbox"] = KKBox()
37
+ except KKBoxException as e:
38
+ logger.warning(
39
+ f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
40
+ )
41
+ else:
42
+ idx = self.album_art_priority.index("ytmusic")
43
+ self.album_art_priority.insert(idx, "kkbox")
44
+
45
+ try:
46
+ self.services["spotify"] = Spotify()
47
+ except SpotifyException as e:
48
+ logger.warning(
49
+ f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
50
+ )
51
+ else:
52
+ idx = self.album_art_priority.index("ytmusic")
53
+ self.album_art_priority.insert(idx, "spotify")
54
+ self.normalize_non_english = True
55
+ self._translation_session = requests.Session()
56
+
57
+ # Assign the translation session to each service
58
+ for service in self.services.values():
59
+ if hasattr(service, "_translation_session"):
60
+ service._translation_session = self._translation_session
61
+
34
62
  def __enter__(self) -> "YutipyMusic":
35
63
  return self
36
64
 
37
65
  def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
38
66
  self.close_sessions()
39
67
 
40
- def search(self, artist: str, song: str, limit: int = 5) -> Optional[MusicInfos]:
68
+ def search(
69
+ self,
70
+ artist: str,
71
+ song: str,
72
+ limit: int = 5,
73
+ normalize_non_english: bool = True,
74
+ ) -> Optional[MusicInfos]:
41
75
  """
42
76
  Searches for a song by artist and title.
43
77
 
@@ -48,8 +82,9 @@ class YutipyMusic:
48
82
  song : str
49
83
  The title of the song.
50
84
  limit: int, optional
51
- The number of items to retrieve from all APIs.
52
- ``limit >=1 and <= 50``. Default is ``5``.
85
+ The number of items to retrieve from all APIs. ``limit >=1 and <= 50``. Default is ``5``.
86
+ normalize_non_english : bool, optional
87
+ Whether to normalize non-English characters for comparison. Default is ``True``.
53
88
 
54
89
  Returns
55
90
  -------
@@ -61,10 +96,30 @@ class YutipyMusic:
61
96
  "Artist and song names must be valid strings and can't be empty."
62
97
  )
63
98
 
99
+ self.normalize_non_english = normalize_non_english
100
+
101
+ attributes = [
102
+ "album_title",
103
+ "album_type",
104
+ "artists",
105
+ "genre",
106
+ "isrc",
107
+ "lyrics",
108
+ "release_date",
109
+ "tempo",
110
+ "title",
111
+ "type",
112
+ "upc",
113
+ ]
114
+
64
115
  with ThreadPoolExecutor() as executor:
65
116
  futures = {
66
117
  executor.submit(
67
- service.search, artist=artist, song=song, limit=limit
118
+ service.search,
119
+ artist=artist,
120
+ song=song,
121
+ limit=limit,
122
+ normalize_non_english=self.normalize_non_english,
68
123
  ): name
69
124
  for name, service in self.services.items()
70
125
  }
@@ -72,14 +127,16 @@ class YutipyMusic:
72
127
  for future in as_completed(futures):
73
128
  service_name = futures[future]
74
129
  result = future.result()
75
- self._combine_results(result, service_name)
130
+ self._combine_results(result, service_name, attributes)
76
131
 
77
132
  if len(self.music_info.url) == 0:
78
133
  return None
79
134
 
80
135
  return self.music_info
81
136
 
82
- def _combine_results(self, result: Optional[MusicInfo], service_name: str) -> None:
137
+ def _combine_results(
138
+ self, result: Optional[MusicInfo], service_name: str, attributes: list
139
+ ) -> None:
83
140
  """
84
141
  Combines the results from different services.
85
142
 
@@ -93,25 +150,16 @@ class YutipyMusic:
93
150
  if not result:
94
151
  return
95
152
 
96
- attributes = [
97
- "album_title",
98
- "album_type",
99
- "artists",
100
- "genre",
101
- "isrc",
102
- "lyrics",
103
- "release_date",
104
- "tempo",
105
- "title",
106
- "type",
107
- "upc",
108
- ]
109
-
110
153
  for attr in attributes:
111
154
  if getattr(result, attr) and (
112
- not getattr(self.music_info, attr) or service_name == "spotify"
155
+ not getattr(self.music_info, attr)
156
+ or (attr in ["genre", "album_type"] and service_name == "itunes")
113
157
  ):
114
- setattr(self.music_info, attr, getattr(result, attr))
158
+ setattr(
159
+ self.music_info,
160
+ attr,
161
+ getattr(result, attributes.pop(attributes.index(attr))),
162
+ )
115
163
 
116
164
  if result.album_art:
117
165
  current_priority = self.album_art_priority.index(service_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 1.3.2
3
+ Version: 1.4.1
4
4
  Summary: A simple package for retrieving music information from various music platforms APIs.
5
5
  Author: Cheap Nightbot
6
6
  Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
@@ -33,7 +33,7 @@ Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
33
33
  Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
34
34
  Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
35
35
  Project-URL: funding, https://ko-fi.com/cheapnightbot
36
- Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify
36
+ Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify,KKBox
37
37
  Classifier: Development Status :: 4 - Beta
38
38
  Classifier: Intended Audience :: Developers
39
39
  Classifier: Topic :: Software Development :: Libraries
@@ -0,0 +1,17 @@
1
+ yutipy/__init__.py,sha256=ucXbhRgNpLo-G0TeDGCaKyCK9Ftc13hUqB8POWdUY1c,327
2
+ yutipy/deezer.py,sha256=cTQpnOgS81mUw_EJ0b2Z1r9CG9ircHCxl9vL-oAJ9Lg,9622
3
+ yutipy/exceptions.py,sha256=4L0Oe1PwFP34LoFTy-Fruipk7uB-JkaackRmkjlaZJU,1138
4
+ yutipy/itunes.py,sha256=9VOcIfjezj8WAVpsne9bbBPoIUbn0GO0UWrCrXzbrE4,6758
5
+ yutipy/kkbox.py,sha256=V-A7nFa1oNrTBOEu7DFzMHAHFZ5cPz55GInEuYfTSfg,13466
6
+ yutipy/models.py,sha256=si_qgaApAYDfSyE8cl_Yg4IfWOtxk1I5JCT8bZsmV4U,1931
7
+ yutipy/musicyt.py,sha256=igqz99bJYGtUGyZAmEJoQbdGIF-1YFmeaV5HmlhwqkA,8441
8
+ yutipy/spotify.py,sha256=462mHo2GCurRNb0RKxu2ZlbmX9ZggPg_ZHN7fb0p9t4,15620
9
+ yutipy/yutipy_music.py,sha256=I4581MvcTTclSwVtAfMEsOujxmhZzqKyG7eX8zuEsEY,6424
10
+ yutipy/utils/__init__.py,sha256=o6lk01FHwhFmNHV0HjGG0qe2azTaQT_eviiLgNV5fHw,232
11
+ yutipy/utils/cheap_utils.py,sha256=MChJ29x-yj_LrdJfWu7qQT3-gSHDdPyPEGo5ITfbJSI,4824
12
+ yutipy/utils/logger.py,sha256=2_b2FlDwUVpdPdqiwweR8Xr2tZOq0qGUGcekC5lXq2M,130
13
+ yutipy-1.4.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
14
+ yutipy-1.4.1.dist-info/METADATA,sha256=-DlkGS8lGIpG7lSewxaOR6hxikj3VdfT1avz23o2YKM,6495
15
+ yutipy-1.4.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
16
+ yutipy-1.4.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
17
+ yutipy-1.4.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- yutipy/__init__.py,sha256=UIymi9iT8s1nGuqcQcFudZYNsJDR4RzXe6LUaN9V8l0,289
2
- yutipy/deezer.py,sha256=JNG9iy5xtjXwEjH-mlA0b75-MN0tswh0E3ytFskA5jI,8827
3
- yutipy/exceptions.py,sha256=4L0Oe1PwFP34LoFTy-Fruipk7uB-JkaackRmkjlaZJU,1138
4
- yutipy/itunes.py,sha256=cyddyZJscz7nD0mnJEimyBenZKlyjTOaMtcWKcW347w,5994
5
- yutipy/kkbox.py,sha256=8rW9jzfM_RIaf0I0cOUCP_w_We25jFntQeoDl0vvbeg,12347
6
- yutipy/models.py,sha256=si_qgaApAYDfSyE8cl_Yg4IfWOtxk1I5JCT8bZsmV4U,1931
7
- yutipy/musicyt.py,sha256=drlAKIp5yNfEIlhp7eRJj42cJX2YxLvNashliDkdSNc,7324
8
- yutipy/spotify.py,sha256=0B0SLWY1HRkC05zPMRTkSJ9bMogSRBsN9Wq7G0QW7lY,13774
9
- yutipy/yutipy_music.py,sha256=jRMMbjxrlBcTI7AN0TZytLKt01ezUwzApP_byiEo_Dw,4752
10
- yutipy/utils/__init__.py,sha256=7UFcFZ7fBtNXOTngjnRD3MeobT3x5UT2Gag94TXVgLk,169
11
- yutipy/utils/cheap_utils.py,sha256=Yl0ssVbyvrdVeSPF7PqpGJpwTYLVa4CMgLtr6uk67v0,3104
12
- yutipy-1.3.2.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
13
- yutipy-1.3.2.dist-info/METADATA,sha256=Hzj9UM3ExumUN-k2r2VGVymMhbdCTHXMub5ITeXXTUA,6489
14
- yutipy-1.3.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
15
- yutipy-1.3.2.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
16
- yutipy-1.3.2.dist-info/RECORD,,
File without changes