yutipy 1.4.2__py3-none-any.whl → 1.5.0__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,17 +1,17 @@
1
1
  from .deezer import Deezer
2
2
  from .itunes import Itunes
3
3
  from .kkbox import KKBox
4
- from .models import MusicInfo
5
4
  from .musicyt import MusicYT
6
5
  from .spotify import Spotify
7
6
  from .yutipy_music import YutipyMusic
7
+ from . import exceptions
8
8
 
9
9
  __all__ = [
10
10
  "Deezer",
11
11
  "Itunes",
12
12
  "KKBox",
13
- "MusicInfo",
14
13
  "MusicYT",
15
14
  "Spotify",
16
15
  "YutipyMusic",
16
+ "exceptions"
17
17
  ]
yutipy/cli/__init__.py ADDED
File without changes
yutipy/cli/config.py ADDED
@@ -0,0 +1,86 @@
1
+ import os
2
+ import webbrowser
3
+ from dotenv import load_dotenv, set_key
4
+
5
+
6
+ def run_config_wizard():
7
+ """Interactive configuration wizard for setting up API keys."""
8
+ print("Welcome to the yutipy Configuration Wizard!")
9
+ print("This wizard will help you set up your API keys for various services.\n")
10
+
11
+ # Load existing .env file if it exists
12
+ env_file = ".env"
13
+ load_dotenv(env_file)
14
+
15
+ # List of required environment variables and their instructions
16
+ required_vars = {
17
+ "SPOTIFY_CLIENT_ID": {
18
+ "description": "Spotify Client ID",
19
+ "url": "https://developer.spotify.com/dashboard",
20
+ "instructions": """
21
+ 1. Go to your Spotify Developer Dashboard: https://developer.spotify.com/dashboard
22
+ 2. Create a new app and fill in the required details.
23
+ 3. Copy the "Client ID" and "Client Secret" from the app's settings.
24
+ 4. Paste them here when prompted.
25
+ """,
26
+ },
27
+ "SPOTIFY_CLIENT_SECRET": {
28
+ "description": "Spotify Client Secret",
29
+ "url": "https://developer.spotify.com/dashboard",
30
+ "instructions": "See the steps above for Spotify Client ID.",
31
+ },
32
+ "KKBOX_CLIENT_ID": {
33
+ "description": "KKBox Client ID",
34
+ "url": "https://developer.kkbox.com/",
35
+ "instructions": """
36
+ 1. Go to the KKBOX Developer Portal: https://developer.kkbox.com/
37
+ 2. Log in and create a new application.
38
+ 3. Copy the "Client ID" and "Client Secret" from the app's settings.
39
+ 4. Paste them here when prompted.
40
+ """,
41
+ },
42
+ "KKBOX_CLIENT_SECRET": {
43
+ "description": "KKBox Client Secret",
44
+ "url": "https://developer.kkbox.com/",
45
+ "instructions": "See the steps above for KKBox Client ID.",
46
+ },
47
+ }
48
+
49
+ # Track whether the browser has already been opened for a service
50
+ browser_opened = set()
51
+
52
+ # Prompt the user for each variable
53
+ for var, details in required_vars.items():
54
+ current_value = os.getenv(var)
55
+ if current_value:
56
+ print(f"{details['description']} is already set.")
57
+ continue
58
+
59
+ print(f"\n{details['description']} is missing.")
60
+ print(details["instructions"])
61
+
62
+ # Check if the browser has already been opened for this service
63
+ if details["url"] not in browser_opened:
64
+ open_browser = (
65
+ input(
66
+ f"Do you want to open the website to get your {details['description']}? (y/N): "
67
+ )
68
+ .strip()
69
+ .lower()
70
+ )
71
+ if open_browser == "y":
72
+ webbrowser.open(details["url"])
73
+ print(f"The website has been opened in your browser: {details['url']}")
74
+ browser_opened.add(details["url"]) # Mark this URL as opened
75
+
76
+ # Prompt the user to enter the value
77
+ new_value = input(f"Enter your {details['description']}: ").strip()
78
+ if new_value:
79
+ set_key(env_file, var, new_value)
80
+ print(f"{details['description']} has been saved to the .env file.")
81
+
82
+ print("\nConfiguration complete! Your API keys have been saved to the .env file.")
83
+
84
+
85
+ if __name__ == "__main__":
86
+ run_config_wizard()
yutipy/cli/search.py ADDED
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ from dataclasses import asdict
3
+ from pprint import pprint
4
+
5
+ from importlib.metadata import version, PackageNotFoundError
6
+
7
+ try:
8
+ __version__ = version("yutipy")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
11
+
12
+ from yutipy.deezer import Deezer
13
+ from yutipy.itunes import Itunes
14
+ from yutipy.kkbox import KKBox
15
+ from yutipy.musicyt import MusicYT
16
+ from yutipy.spotify import Spotify
17
+ from yutipy.utils.logger import disable_logging, enable_logging
18
+ from yutipy.yutipy_music import YutipyMusic
19
+
20
+
21
+ def main():
22
+ disable_logging()
23
+
24
+ # Create the argument parser
25
+ parser = argparse.ArgumentParser(
26
+ description="Search for music information across multiple platforms using yutipy."
27
+ )
28
+ parser.add_argument("artist", type=str, help="The name of the artist.")
29
+ parser.add_argument("song", type=str, help="The title of the song.")
30
+ parser.add_argument(
31
+ "--limit",
32
+ type=int,
33
+ default=5,
34
+ help="The number of results to retrieve (default: 5).",
35
+ )
36
+ parser.add_argument(
37
+ "--normalize",
38
+ action="store_true",
39
+ help="Normalize non-English characters.",
40
+ default=False,
41
+ )
42
+ parser.add_argument(
43
+ "--verbose",
44
+ action="store_true",
45
+ help="Enable logging in terminal",
46
+ default=False,
47
+ )
48
+ parser.add_argument(
49
+ "--version",
50
+ action="version",
51
+ version=f"yutipy v{__version__}",
52
+ help="Show the version of the yutipy and exit.",
53
+ )
54
+ parser.add_argument(
55
+ "--service",
56
+ type=str,
57
+ choices=["deezer", "itunes", "kkbox", "spotify", "ytmusic"],
58
+ help="Specify a single service to search (e.g., deezer, itunes, kkbox, spotify, ytmusic).",
59
+ )
60
+
61
+ # Parse the arguments
62
+ args = parser.parse_args()
63
+
64
+ if args.verbose:
65
+ enable_logging()
66
+
67
+ # Use the specified service or default to YutipyMusic
68
+ try:
69
+ if args.service:
70
+ service_map = {
71
+ "deezer": Deezer,
72
+ "itunes": Itunes,
73
+ "kkbox": KKBox,
74
+ "spotify": Spotify,
75
+ "ytmusic": MusicYT,
76
+ }
77
+ service_class = service_map[args.service]
78
+ with service_class() as service:
79
+ result = service.search(
80
+ artist=args.artist,
81
+ song=args.song,
82
+ limit=args.limit,
83
+ normalize_non_english=args.normalize,
84
+ )
85
+ else:
86
+ with YutipyMusic() as yutipy_music:
87
+ result = yutipy_music.search(
88
+ artist=args.artist,
89
+ song=args.song,
90
+ limit=args.limit,
91
+ normalize_non_english=args.normalize,
92
+ )
93
+
94
+ if result:
95
+ print("\nSEARCH RESULTS:\n")
96
+ pprint(asdict(result))
97
+ else:
98
+ print("No results found.")
99
+ except Exception as e:
100
+ print(f"An error occurred: {e}")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
yutipy/deezer.py CHANGED
@@ -10,7 +10,8 @@ from yutipy.exceptions import (
10
10
  NetworkException,
11
11
  )
12
12
  from yutipy.models import MusicInfo
13
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
13
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
14
+ from yutipy.utils.logger import logger
14
15
 
15
16
 
16
17
  class Deezer:
@@ -86,22 +87,34 @@ class Deezer:
86
87
  query_url = endpoint + query
87
88
 
88
89
  try:
90
+ logger.info(
91
+ f'Searching music info for `artist="{artist}"` and `song="{song}"`'
92
+ )
93
+ logger.debug(f"Query URL: {query_url}")
89
94
  response = self._session.get(query_url, timeout=30)
95
+ logger.debug(f"Response status code: {response.status_code}")
90
96
  response.raise_for_status()
91
97
  except requests.RequestException as e:
98
+ logger.error(f"Network error while fetching music info: {e}")
92
99
  raise NetworkException(f"Network error occurred: {e}")
93
100
  except Exception as e:
101
+ logger.exception(f"Unexpected error while searching Deezer: {e}")
94
102
  raise DeezerException(f"An error occurred while searching Deezer: {e}")
95
103
 
96
104
  try:
105
+ logger.debug(f"Parsing response JSON: {response.json()}")
97
106
  result = response.json()["data"]
98
107
  except (IndexError, KeyError, ValueError) as e:
108
+ logger.error(f"Invalid response structure from Deezer: {e}")
99
109
  raise InvalidResponseException(f"Invalid response received: {e}")
100
110
 
101
111
  music_info = self._parse_results(artist, song, result)
102
112
  if music_info:
103
113
  return music_info
104
114
 
115
+ logger.warning(
116
+ f"No matching results found for artist='{artist}' and song='{song}'"
117
+ )
105
118
  return None
106
119
 
107
120
  def _get_upc_isrc(self, music_id: int, music_type: str) -> Optional[Dict]:
@@ -127,7 +140,7 @@ class Deezer:
127
140
  else:
128
141
  raise DeezerException(f"Invalid music type: {music_type}")
129
142
 
130
- def _get_track_info(self, music_id: int) -> Optional[Dict]:
143
+ def _get_track_info(self, track_id: int) -> Optional[Dict]:
131
144
  """
132
145
  Retrieves track information for a given track ID.
133
146
 
@@ -141,18 +154,25 @@ class Deezer:
141
154
  Optional[Dict]
142
155
  A dictionary containing track information.
143
156
  """
144
- query_url = f"{self.api_url}/track/{music_id}"
157
+ query_url = f"{self.api_url}/track/{track_id}"
145
158
  try:
159
+ logger.info(f"Fetching track info for track_id: {track_id}")
160
+ logger.debug(f"Query URL: {query_url}")
146
161
  response = self._session.get(query_url, timeout=30)
162
+ logger.debug(f"Response status code: {response.status_code}")
147
163
  response.raise_for_status()
148
164
  except requests.RequestException as e:
165
+ logger.error(f"Error fetching track info: {e}")
149
166
  raise NetworkException(f"Network error occurred: {e}")
150
167
  except Exception as e:
168
+ logger.error(f"Error fetching track info: {e}")
151
169
  raise DeezerException(f"An error occurred while fetching track info: {e}")
152
170
 
153
171
  try:
172
+ logger.debug(f"Response JSON: {response.json()}")
154
173
  result = response.json()
155
174
  except ValueError as e:
175
+ logger.error(f"Invalid response received from Deezer: {e}")
156
176
  raise InvalidResponseException(f"Invalid response received: {e}")
157
177
 
158
178
  return {
@@ -161,7 +181,7 @@ class Deezer:
161
181
  "tempo": result.get("bpm"),
162
182
  }
163
183
 
164
- def _get_album_info(self, music_id: int) -> Optional[Dict]:
184
+ def _get_album_info(self, album_id: int) -> Optional[Dict]:
165
185
  """
166
186
  Retrieves album information for a given album ID.
167
187
 
@@ -175,18 +195,25 @@ class Deezer:
175
195
  Optional[Dict]
176
196
  A dictionary containing album information.
177
197
  """
178
- query_url = f"{self.api_url}/album/{music_id}"
198
+ query_url = f"{self.api_url}/album/{album_id}"
179
199
  try:
200
+ logger.info(f"Fetching album info for album_id: {album_id}")
201
+ logger.debug(f"Query URL: {query_url}")
180
202
  response = self._session.get(query_url, timeout=30)
203
+ logger.info(f"Response status code: {response.status_code}")
181
204
  response.raise_for_status()
182
205
  except requests.RequestException as e:
206
+ logger.error(f"Error fetching album info: {e}")
183
207
  raise NetworkException(f"Network error occurred: {e}")
184
208
  except Exception as e:
209
+ logger.error(f"Error fetching album info: {e}")
185
210
  raise DeezerException(f"An error occurred while fetching album info: {e}")
186
211
 
187
212
  try:
213
+ logger.debug(f"Response JSON: {response.json()}")
188
214
  result = response.json()
189
215
  except ValueError as e:
216
+ logger.error(f"Invalid response received from Deezer: {e}")
190
217
  raise InvalidResponseException(f"Invalid response received: {e}")
191
218
 
192
219
  return {
@@ -293,6 +320,10 @@ class Deezer:
293
320
 
294
321
 
295
322
  if __name__ == "__main__":
323
+ import logging
324
+ from yutipy.utils.logger import enable_logging
325
+
326
+ enable_logging(level=logging.DEBUG)
296
327
  deezer = Deezer()
297
328
  try:
298
329
  artist_name = input("Artist Name: ")
yutipy/itunes.py CHANGED
@@ -11,11 +11,12 @@ from yutipy.exceptions import (
11
11
  NetworkException,
12
12
  )
13
13
  from yutipy.models import MusicInfo
14
- from yutipy.utils.cheap_utils import (
14
+ from yutipy.utils.helpers import (
15
15
  are_strings_similar,
16
16
  guess_album_type,
17
17
  is_valid_string,
18
18
  )
19
+ from yutipy.utils.logger import logger
19
20
 
20
21
 
21
22
  class Itunes:
@@ -89,22 +90,34 @@ class Itunes:
89
90
  query_url = endpoint + query
90
91
 
91
92
  try:
93
+ logger.info(
94
+ f'Searching iTunes for `artist="{artist}"` and `song="{song}"`'
95
+ )
96
+ logger.debug(f"Query URL: {query_url}")
92
97
  response = self._session.get(query_url, timeout=30)
98
+ logger.debug(f"Response status code: {response.status_code}")
93
99
  response.raise_for_status()
94
100
  except requests.RequestException as e:
101
+ logger.error(f"Network error while searching iTunes: {e}")
95
102
  raise NetworkException(f"Network error occurred: {e}")
96
103
  except Exception as e:
104
+ logger.exception(f"Unexpected error while searching iTunes: {e}")
97
105
  raise ItunesException(f"An error occurred while searching iTunes: {e}")
98
106
 
99
107
  try:
108
+ logger.debug(f"Parsing response JSON: {response.json()}")
100
109
  result = response.json()["results"]
101
110
  except (IndexError, KeyError, ValueError) as e:
111
+ logger.error(f"Invalid response structure from iTunes: {e}")
102
112
  raise InvalidResponseException(f"Invalid response received: {e}")
103
113
 
104
114
  music_info = self._parse_result(artist, song, result)
105
115
  if music_info:
106
116
  return music_info
107
117
 
118
+ logger.warning(
119
+ f"No matching results found for artist='{artist}' and song='{song}'"
120
+ )
108
121
  return None
109
122
 
110
123
  def _parse_result(
@@ -212,6 +225,10 @@ class Itunes:
212
225
 
213
226
 
214
227
  if __name__ == "__main__":
228
+ import logging
229
+ from yutipy.utils.logger import enable_logging
230
+
231
+ enable_logging(level=logging.DEBUG)
215
232
  itunes = Itunes()
216
233
 
217
234
  try:
yutipy/kkbox.py CHANGED
@@ -15,7 +15,8 @@ from yutipy.exceptions import (
15
15
  NetworkException,
16
16
  )
17
17
  from yutipy.models import MusicInfo
18
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
18
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
19
+ from yutipy.utils.logger import logger
19
20
 
20
21
  load_dotenv()
21
22
 
@@ -117,11 +118,14 @@ class KKBox:
117
118
  data = {"grant_type": "client_credentials"}
118
119
 
119
120
  try:
121
+ logger.info("Authenticating with KKBOX Open API")
120
122
  response = self._session.post(
121
123
  url=url, headers=headers, data=data, timeout=30
122
124
  )
125
+ logger.debug(f"Authentication response status code: {response.status_code}")
123
126
  response.raise_for_status()
124
127
  except requests.RequestException as e:
128
+ logger.error(f"Network error during KKBOX authentication: {e}")
125
129
  raise NetworkException(f"Network error occurred: {e}")
126
130
 
127
131
  try:
@@ -180,8 +184,12 @@ class KKBox:
180
184
  )
181
185
  query_url = f"{self.api_url}/search{query}"
182
186
 
187
+ logger.info(f"Searching KKBOX for `artist='{artist}'` and `song='{song}'`")
188
+ logger.debug(f"Query URL: {query_url}")
189
+
183
190
  try:
184
191
  response = self._session.get(query_url, headers=self.__header, timeout=30)
192
+ logger.debug(f"Parsing response JSON: {response.json()}")
185
193
  response.raise_for_status()
186
194
  except requests.RequestException as e:
187
195
  raise NetworkException(f"Network error occurred: {e}")
@@ -279,6 +287,9 @@ class KKBox:
279
287
  except KeyError:
280
288
  pass
281
289
 
290
+ logger.warning(
291
+ f"No matching results found for artist='{artist}' and song='{song}'"
292
+ )
282
293
  return None
283
294
 
284
295
  def _find_track(self, song: str, artist: str, track: dict) -> Optional[MusicInfo]:
@@ -403,6 +414,10 @@ class KKBox:
403
414
 
404
415
 
405
416
  if __name__ == "__main__":
417
+ import logging
418
+ from yutipy.utils.logger import enable_logging
419
+
420
+ enable_logging(level=logging.DEBUG)
406
421
  kkbox = KKBox(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
407
422
 
408
423
  try:
yutipy/logging.py ADDED
@@ -0,0 +1,3 @@
1
+ from .utils.logger import enable_logging, disable_logging
2
+
3
+ __all__ = ["enable_logging", "disable_logging"]
yutipy/models.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Optional, Dict, Union
2
+ from typing import Dict, Optional, Union
3
3
 
4
4
 
5
5
  @dataclass
yutipy/musicyt.py CHANGED
@@ -11,7 +11,8 @@ from yutipy.exceptions import (
11
11
  NetworkException,
12
12
  )
13
13
  from yutipy.models import MusicInfo
14
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
14
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
15
+ from yutipy.utils.logger import logger
15
16
 
16
17
 
17
18
  class MusicYT:
@@ -24,6 +25,14 @@ class MusicYT:
24
25
  self.normalize_non_english = True
25
26
  self._translation_session = requests.Session()
26
27
 
28
+ def __enter__(self) -> "MusicYT":
29
+ """Enters the runtime context related to this object."""
30
+ return self
31
+
32
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
33
+ """Exits the runtime context related to this object."""
34
+ self.close_session()
35
+
27
36
  def close_session(self) -> None:
28
37
  """Closes the current session(s)."""
29
38
  if not self.is_session_closed:
@@ -70,15 +79,23 @@ class MusicYT:
70
79
 
71
80
  query = f"{artist} - {song}"
72
81
 
82
+ logger.info(
83
+ f"Searching YouTube Music for `artist='{artist}'` and `song='{song}'`"
84
+ )
85
+
73
86
  try:
74
87
  results = self.ytmusic.search(query=query, limit=limit)
75
88
  except exceptions.YTMusicServerError as e:
89
+ logger.error(f"Network error while searching YouTube Music: {e}")
76
90
  raise NetworkException(f"Network error occurred: {e}")
77
91
 
78
92
  for result in results:
79
93
  if self._is_relevant_result(artist, song, result):
80
94
  return self._process_result(result)
81
95
 
96
+ logger.warning(
97
+ f"No matching results found for artist='{artist}' and song='{song}'"
98
+ )
82
99
  return None
83
100
 
84
101
  def _is_relevant_result(self, artist: str, song: str, result: dict) -> bool:
@@ -276,6 +293,10 @@ class MusicYT:
276
293
 
277
294
 
278
295
  if __name__ == "__main__":
296
+ import logging
297
+ from yutipy.utils.logger import enable_logging
298
+
299
+ enable_logging(level=logging.DEBUG)
279
300
  music_yt = MusicYT()
280
301
 
281
302
  artist_name = input("Artist Name: ")
yutipy/spotify.py CHANGED
@@ -15,12 +15,13 @@ from yutipy.exceptions import (
15
15
  SpotifyException,
16
16
  )
17
17
  from yutipy.models import MusicInfo
18
- from yutipy.utils.cheap_utils import (
18
+ from yutipy.utils.helpers import (
19
19
  are_strings_similar,
20
20
  guess_album_type,
21
21
  is_valid_string,
22
22
  separate_artists,
23
23
  )
24
+ from yutipy.utils.logger import logger
24
25
 
25
26
  load_dotenv()
26
27
 
@@ -123,11 +124,14 @@ class Spotify:
123
124
  data = {"grant_type": "client_credentials"}
124
125
 
125
126
  try:
127
+ logger.info("Authenticating with Spotify API")
126
128
  response = self._session.post(
127
129
  url=url, headers=headers, data=data, timeout=30
128
130
  )
131
+ logger.debug(f"Authentication response status code: {response.status_code}")
129
132
  response.raise_for_status()
130
133
  except requests.RequestException as e:
134
+ logger.error(f"Network error during Spotify authentication: {e}")
131
135
  raise NetworkException(f"Network error occurred: {e}")
132
136
 
133
137
  try:
@@ -190,6 +194,11 @@ class Spotify:
190
194
 
191
195
  query_url = f"{self.api_url}/search{query}"
192
196
 
197
+ logger.info(
198
+ f"Searching Spotify for `artist='{artist}'` and `song='{song}'`"
199
+ )
200
+ logger.debug(f"Query URL: {query_url}")
201
+
193
202
  try:
194
203
  response = self._session.get(
195
204
  query_url, headers=self.__header, timeout=30
@@ -342,6 +351,9 @@ class Spotify:
342
351
  except KeyError:
343
352
  pass
344
353
 
354
+ logger.warning(
355
+ f"No matching results found for artist='{artist}' and song='{song}'"
356
+ )
345
357
  return None
346
358
 
347
359
  def _find_track(
@@ -477,6 +489,10 @@ class Spotify:
477
489
 
478
490
 
479
491
  if __name__ == "__main__":
492
+ import logging
493
+ from yutipy.utils.logger import enable_logging
494
+
495
+ enable_logging(level=logging.DEBUG)
480
496
  spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
481
497
 
482
498
  try:
yutipy/utils/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .cheap_utils import (
1
+ from .helpers import (
2
2
  guess_album_type,
3
3
  are_strings_similar,
4
4
  is_valid_string,
@@ -139,7 +139,3 @@ def guess_album_type(total_tracks: int):
139
139
  return "ep"
140
140
  if total_tracks >= 7:
141
141
  return "album"
142
-
143
-
144
- if __name__ == "__main__":
145
- separate_artists("Artist A ft. Artist B")
yutipy/utils/logger.py CHANGED
@@ -1,4 +1,39 @@
1
1
  import logging
2
2
 
3
- logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
4
- logger = logging.getLogger(__name__)
3
+ # Create a logger for the library
4
+ logger = logging.getLogger("yutipy")
5
+ logger.setLevel(logging.WARNING)
6
+
7
+
8
+ def enable_logging(level=logging.INFO, handler=None):
9
+ """Enable logging for the library.
10
+
11
+ Parameters
12
+ ----------
13
+ level : int, optional
14
+ The logging level to set, by default logging.INFO.
15
+ handler : logging.Handler, optional
16
+ A custom logging handler to add, by default None (uses console handler).
17
+ """
18
+ logger.setLevel(level)
19
+
20
+ # If no handler is provided, use the default console handler
21
+ if handler is None:
22
+ console_handler = logging.StreamHandler()
23
+ formatter = logging.Formatter(
24
+ "[%(asctime)s] | %(name)s | [%(levelname)s] → %(module)s : line %(lineno)d : %(message)s",
25
+ datefmt="%Y-%m-%d %H:%M:%S %Z",
26
+ )
27
+ console_handler.setFormatter(formatter)
28
+ handler = console_handler
29
+
30
+ # Add the handler if not already added
31
+ if not any(isinstance(h, type(handler)) for h in logger.handlers):
32
+ logger.addHandler(handler)
33
+
34
+
35
+ def disable_logging():
36
+ """Disable logging for the library."""
37
+ logger.setLevel(logging.CRITICAL)
38
+ for handler in logger.handlers[:]: # Remove all handlers
39
+ logger.removeHandler(handler)
yutipy/yutipy_music.py CHANGED
@@ -2,8 +2,6 @@ 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
-
7
5
  from yutipy.deezer import Deezer
8
6
  from yutipy.exceptions import InvalidValueException, KKBoxException, SpotifyException
9
7
  from yutipy.itunes import Itunes
@@ -11,7 +9,7 @@ from yutipy.kkbox import KKBox
11
9
  from yutipy.models import MusicInfo, MusicInfos
12
10
  from yutipy.musicyt import MusicYT
13
11
  from yutipy.spotify import Spotify
14
- from yutipy.utils.cheap_utils import is_valid_string
12
+ from yutipy.utils.helpers import is_valid_string
15
13
  from yutipy.utils.logger import logger
16
14
 
17
15
 
@@ -25,6 +23,7 @@ class YutipyMusic:
25
23
  def __init__(self) -> None:
26
24
  """Initializes the YutipyMusic class."""
27
25
  self.music_info = MusicInfos()
26
+ self.normalize_non_english = True
28
27
  self.album_art_priority = ["deezer", "ytmusic", "itunes"]
29
28
  self.services = {
30
29
  "deezer": Deezer(),
@@ -34,7 +33,7 @@ class YutipyMusic:
34
33
 
35
34
  try:
36
35
  self.services["kkbox"] = KKBox()
37
- except KKBoxException as e:
36
+ except (KKBoxException, Exception) as e:
38
37
  logger.warning(
39
38
  f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
40
39
  )
@@ -44,20 +43,13 @@ class YutipyMusic:
44
43
 
45
44
  try:
46
45
  self.services["spotify"] = Spotify()
47
- except SpotifyException as e:
46
+ except (SpotifyException, Exception) as e:
48
47
  logger.warning(
49
48
  f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
50
49
  )
51
50
  else:
52
51
  idx = self.album_art_priority.index("ytmusic")
53
52
  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
53
 
62
54
  def __enter__(self) -> "YutipyMusic":
63
55
  return self
@@ -98,19 +90,9 @@ class YutipyMusic:
98
90
 
99
91
  self.normalize_non_english = normalize_non_english
100
92
 
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
- ]
93
+ logger.info(
94
+ f"Searching all platforms for `artist='{artist}'` and `song='{song}'`"
95
+ )
114
96
 
115
97
  with ThreadPoolExecutor() as executor:
116
98
  futures = {
@@ -126,17 +108,23 @@ class YutipyMusic:
126
108
 
127
109
  for future in as_completed(futures):
128
110
  service_name = futures[future]
129
- result = future.result()
130
- self._combine_results(result, service_name, attributes)
111
+ try:
112
+ result = future.result()
113
+ self._combine_results(result, service_name)
114
+ except Exception as e:
115
+ logger.error(
116
+ f"Error occurred while searching with {service_name}: {e}"
117
+ )
131
118
 
132
119
  if len(self.music_info.url) == 0:
120
+ logger.warning(
121
+ f"No matching results found across all platforms for artist='{artist}' and song='{song}'"
122
+ )
133
123
  return None
134
124
 
135
125
  return self.music_info
136
126
 
137
- def _combine_results(
138
- self, result: Optional[MusicInfo], service_name: str, attributes: list
139
- ) -> None:
127
+ def _combine_results(self, result: Optional[MusicInfo], service_name: str) -> None:
140
128
  """
141
129
  Combines the results from different services.
142
130
 
@@ -150,16 +138,26 @@ class YutipyMusic:
150
138
  if not result:
151
139
  return
152
140
 
141
+ attributes = [
142
+ "album_title",
143
+ "album_type",
144
+ "artists",
145
+ "genre",
146
+ "isrc",
147
+ "lyrics",
148
+ "release_date",
149
+ "tempo",
150
+ "title",
151
+ "type",
152
+ "upc",
153
+ ]
154
+
153
155
  for attr in attributes:
154
156
  if getattr(result, attr) and (
155
157
  not getattr(self.music_info, attr)
156
158
  or (attr in ["genre", "album_type"] and service_name == "itunes")
157
159
  ):
158
- setattr(
159
- self.music_info,
160
- attr,
161
- getattr(result, attributes.pop(attributes.index(attr))),
162
- )
160
+ setattr(self.music_info, attr, getattr(result, attr))
163
161
 
164
162
  if result.album_art:
165
163
  current_priority = self.album_art_priority.index(service_name)
@@ -183,8 +181,14 @@ class YutipyMusic:
183
181
 
184
182
 
185
183
  if __name__ == "__main__":
184
+ import logging
185
+ from yutipy.utils.logger import enable_logging
186
+
187
+ enable_logging(level=logging.DEBUG)
186
188
  yutipy_music = YutipyMusic()
189
+
187
190
  artist_name = input("Artist Name: ")
188
191
  song_name = input("Song Name: ")
192
+
189
193
  pprint(yutipy_music.search(artist_name, song_name))
190
194
  yutipy_music.close_sessions()
@@ -1,39 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 1.4.2
3
+ Version: 1.5.0
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>
7
7
  Maintainer-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
8
- License: MIT License
9
-
10
- Copyright (c) 2025 Cheap Nightbot
11
-
12
- Permission is hereby granted, free of charge, to any person obtaining a copy
13
- of this software and associated documentation files (the "Software"), to deal
14
- in the Software without restriction, including without limitation the rights
15
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
- copies of the Software, and to permit persons to whom the Software is
17
- furnished to do so, subject to the following conditions:
18
-
19
- The above copyright notice and this permission notice shall be included in all
20
- copies or substantial portions of the Software.
21
-
22
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
- SOFTWARE.
29
-
8
+ License-Expression: MIT
30
9
  Project-URL: Homepage, https://github.com/CheapNightbot/yutipy
31
10
  Project-URL: Documentation, https://yutipy.readthedocs.io/
32
11
  Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
33
12
  Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
34
13
  Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
35
14
  Project-URL: funding, https://ko-fi.com/cheapnightbot
36
- Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify,KKBox
15
+ Keywords: music,API,Deezer,iTunes,KKBox,Spotify,YouTube Music,search,retrieve,information,yutify
37
16
  Classifier: Development Status :: 4 - Beta
38
17
  Classifier: Intended Audience :: Developers
39
18
  Classifier: Topic :: Software Development :: Libraries
@@ -43,11 +22,11 @@ Classifier: Programming Language :: Python :: 3.9
43
22
  Classifier: Programming Language :: Python :: 3.10
44
23
  Classifier: Programming Language :: Python :: 3.11
45
24
  Classifier: Programming Language :: Python :: 3.12
46
- Classifier: License :: OSI Approved :: MIT License
47
25
  Classifier: Operating System :: OS Independent
48
26
  Requires-Python: >=3.8
49
27
  Description-Content-Type: text/markdown
50
28
  License-File: LICENSE
29
+ Requires-Dist: pykakasi==2.3.0
51
30
  Requires-Dist: python-dotenv==1.0.1
52
31
  Requires-Dist: rapidfuzz==3.12.1
53
32
  Requires-Dist: requests==2.32.3
@@ -89,6 +68,10 @@ A _**simple**_ Python package for searching and retrieving music information fro
89
68
  - [Available Music Platforms](#available-music-platforms)
90
69
  - [Installation](#installation)
91
70
  - [Usage Example](#usage-example)
71
+ - [Command-Line Interface (CLI)](#command-line-interface-cli)
72
+ - [Search for Music](#search-for-music)
73
+ - [Options](#options)
74
+ - [Configuration Wizard](#configuration-wizard)
92
75
  - [Contributing](#contributing)
93
76
  - [License](#license)
94
77
 
@@ -120,9 +103,7 @@ pip install -U yutipy
120
103
 
121
104
  ## Usage Example
122
105
 
123
- Here's a quick example of how to use the `yutipy` package to search for a song:
124
-
125
- ### Deezer
106
+ Here's a quick example of how to use the `yutipy` package to search for a song on **Deezer**:
126
107
 
127
108
  ```python
128
109
  from yutipy.deezer import Deezer
@@ -134,6 +115,36 @@ with Deezer() as deezer:
134
115
 
135
116
  For more usage examples, see the [Usage Examples](https://yutipy.readthedocs.io/en/latest/usage_examples.html) page in docs.
136
117
 
118
+ ## Command-Line Interface (CLI)
119
+
120
+ The `yutipy` package includes a CLI tool that allows you to search for music directly from the command line and configure API keys interactively.
121
+
122
+ ### Search for Music
123
+
124
+ You can use the CLI tool to search for music across multiple platforms:
125
+
126
+ ```bash
127
+ yutipy-cli "Rick Astley" "Never Gonna Give You Up" --limit 3 --normalize
128
+ ```
129
+
130
+ #### Options:
131
+ - `artist` (required): The name of the artist.
132
+ - `song` (required): The title of the song.
133
+ - `--limit`: The number of results to retrieve (default: 5).
134
+ - `--normalize`: Normalize non-English characters for comparison.
135
+ - `--verbose`: Enable logging in the terminal.
136
+ - `--service`: Specify a single service to search (e.g., `deezer`, `spotify`, `itunes`).
137
+
138
+ ### Configuration Wizard
139
+
140
+ To set up your API keys interactively, use the configuration wizard:
141
+
142
+ ```bash
143
+ yutipy-config
144
+ ```
145
+
146
+ The wizard will guide you through obtaining and setting up API keys for supported services like Spotify and KKBOX. If the required environment variables are already set, the wizard will skip those steps.
147
+
137
148
  ## Contributing
138
149
 
139
150
  Contributions are welcome! Please follow these steps:
@@ -0,0 +1,22 @@
1
+ yutipy/__init__.py,sha256=wCSrxrAzsRZ0qElyyws5P6x-1FQnc9PeyPVjvy2I9rk,322
2
+ yutipy/deezer.py,sha256=nPw4aUhr4RSgbholsvV_oVYvGyK5SXQF4pKixwweIfg,11368
3
+ yutipy/exceptions.py,sha256=4L0Oe1PwFP34LoFTy-Fruipk7uB-JkaackRmkjlaZJU,1138
4
+ yutipy/itunes.py,sha256=sG-qXi8FDahG2NDdGdkwa3dsgwWUzbuEbRZ8wzzzsO0,7893
5
+ yutipy/kkbox.py,sha256=ueIrske7yfmcNX_VFNK7v7OQJk6UGBfPNpQRYMZdGu4,14157
6
+ yutipy/logging.py,sha256=CcnaAZGClTUgfYqRXuVfFEhfMhlXF86di4r3O7aSHDA,107
7
+ yutipy/models.py,sha256=vvWIA3MwCOOM2CBHSabqmFXz4NdVHaQtObU6zhGpJOM,1931
8
+ yutipy/musicyt.py,sha256=iel4-iYpo24-BcknwgO7hb_nu0E016pa_NsgFL2BybI,9181
9
+ yutipy/spotify.py,sha256=OUYDSEYMAqg1a9XSAyrE2eTJRRhq_KorAL2Cd_nbGkw,16280
10
+ yutipy/yutipy_music.py,sha256=2MInRKG5HkbLcVafua6Vkg9u66XvBctV2dnRdS3jX7M,6565
11
+ yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ yutipy/cli/config.py,sha256=6p69ZlT3ebUH5wzhd0iisLYKOnYX6xTSzoyrdBwGNo0,3230
13
+ yutipy/cli/search.py,sha256=i2GkiKYnBewjJNu0WCqSfF7IVd0JtKwglUNk2tCLv2w,3031
14
+ yutipy/utils/__init__.py,sha256=S28uLH8-WlytWHNabgeLNHx1emZVryBz0BDwF5wgum4,228
15
+ yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
16
+ yutipy/utils/logger.py,sha256=cHCjpDslVsBOnp7jluqrOOi4ekDIggPhbSfqHeIfT-U,1263
17
+ yutipy-1.5.0.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
18
+ yutipy-1.5.0.dist-info/METADATA,sha256=JYM-LSbrHsmVetyNWf8rzW-hDBmNnGZeVX2XVM2VWy0,6498
19
+ yutipy-1.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
20
+ yutipy-1.5.0.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
21
+ yutipy-1.5.0.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
22
+ yutipy-1.5.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ yutipy-cli = yutipy.cli.search:main
3
+ yutipy-config = yutipy.cli.config:run_config_wizard
@@ -1,17 +0,0 @@
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=SZOCShlIyutIWQ2KI9WLqxc4gN4Cu2gilqxYwuXugUU,7059
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=P-MCOny1ZXK75_jdQKRI_jgUrK7B9xbBFOvRu6NOjxk,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=ttNu566ybTQ3BzQ2trBgpvpNthB6Da-KnkBbFiK3pw4,5282
12
- yutipy/utils/logger.py,sha256=2_b2FlDwUVpdPdqiwweR8Xr2tZOq0qGUGcekC5lXq2M,130
13
- yutipy-1.4.2.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
14
- yutipy-1.4.2.dist-info/METADATA,sha256=nxBwtRJFWpM7gsgH9-RZIP239hna-PirfWJ2WIBxXB4,6495
15
- yutipy-1.4.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
16
- yutipy-1.4.2.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
17
- yutipy-1.4.2.dist-info/RECORD,,
File without changes