yutipy 1.4.22__py3-none-any.whl → 1.5.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,7 +1,6 @@
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
@@ -10,7 +9,6 @@ __all__ = [
10
9
  "Deezer",
11
10
  "Itunes",
12
11
  "KKBox",
13
- "MusicInfo",
14
12
  "MusicYT",
15
13
  "Spotify",
16
14
  "YutipyMusic",
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
@@ -1,3 +1,5 @@
1
+ __all__ = ["Deezer", "DeezerException"]
2
+
1
3
  from pprint import pprint
2
4
  from typing import Dict, List, Optional
3
5
 
@@ -10,7 +12,8 @@ from yutipy.exceptions import (
10
12
  NetworkException,
11
13
  )
12
14
  from yutipy.models import MusicInfo
13
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
15
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
+ from yutipy.utils.logger import logger
14
17
 
15
18
 
16
19
  class Deezer:
@@ -86,22 +89,34 @@ class Deezer:
86
89
  query_url = endpoint + query
87
90
 
88
91
  try:
92
+ logger.info(
93
+ f'Searching music info for `artist="{artist}"` and `song="{song}"`'
94
+ )
95
+ logger.debug(f"Query URL: {query_url}")
89
96
  response = self._session.get(query_url, timeout=30)
97
+ logger.debug(f"Response status code: {response.status_code}")
90
98
  response.raise_for_status()
91
99
  except requests.RequestException as e:
100
+ logger.error(f"Network error while fetching music info: {e}")
92
101
  raise NetworkException(f"Network error occurred: {e}")
93
102
  except Exception as e:
103
+ logger.exception(f"Unexpected error while searching Deezer: {e}")
94
104
  raise DeezerException(f"An error occurred while searching Deezer: {e}")
95
105
 
96
106
  try:
107
+ logger.debug(f"Parsing response JSON: {response.json()}")
97
108
  result = response.json()["data"]
98
109
  except (IndexError, KeyError, ValueError) as e:
110
+ logger.error(f"Invalid response structure from Deezer: {e}")
99
111
  raise InvalidResponseException(f"Invalid response received: {e}")
100
112
 
101
113
  music_info = self._parse_results(artist, song, result)
102
114
  if music_info:
103
115
  return music_info
104
116
 
117
+ logger.warning(
118
+ f"No matching results found for artist='{artist}' and song='{song}'"
119
+ )
105
120
  return None
106
121
 
107
122
  def _get_upc_isrc(self, music_id: int, music_type: str) -> Optional[Dict]:
@@ -127,7 +142,7 @@ class Deezer:
127
142
  else:
128
143
  raise DeezerException(f"Invalid music type: {music_type}")
129
144
 
130
- def _get_track_info(self, music_id: int) -> Optional[Dict]:
145
+ def _get_track_info(self, track_id: int) -> Optional[Dict]:
131
146
  """
132
147
  Retrieves track information for a given track ID.
133
148
 
@@ -141,18 +156,25 @@ class Deezer:
141
156
  Optional[Dict]
142
157
  A dictionary containing track information.
143
158
  """
144
- query_url = f"{self.api_url}/track/{music_id}"
159
+ query_url = f"{self.api_url}/track/{track_id}"
145
160
  try:
161
+ logger.info(f"Fetching track info for track_id: {track_id}")
162
+ logger.debug(f"Query URL: {query_url}")
146
163
  response = self._session.get(query_url, timeout=30)
164
+ logger.debug(f"Response status code: {response.status_code}")
147
165
  response.raise_for_status()
148
166
  except requests.RequestException as e:
167
+ logger.error(f"Error fetching track info: {e}")
149
168
  raise NetworkException(f"Network error occurred: {e}")
150
169
  except Exception as e:
170
+ logger.error(f"Error fetching track info: {e}")
151
171
  raise DeezerException(f"An error occurred while fetching track info: {e}")
152
172
 
153
173
  try:
174
+ logger.debug(f"Response JSON: {response.json()}")
154
175
  result = response.json()
155
176
  except ValueError as e:
177
+ logger.error(f"Invalid response received from Deezer: {e}")
156
178
  raise InvalidResponseException(f"Invalid response received: {e}")
157
179
 
158
180
  return {
@@ -161,7 +183,7 @@ class Deezer:
161
183
  "tempo": result.get("bpm"),
162
184
  }
163
185
 
164
- def _get_album_info(self, music_id: int) -> Optional[Dict]:
186
+ def _get_album_info(self, album_id: int) -> Optional[Dict]:
165
187
  """
166
188
  Retrieves album information for a given album ID.
167
189
 
@@ -175,18 +197,25 @@ class Deezer:
175
197
  Optional[Dict]
176
198
  A dictionary containing album information.
177
199
  """
178
- query_url = f"{self.api_url}/album/{music_id}"
200
+ query_url = f"{self.api_url}/album/{album_id}"
179
201
  try:
202
+ logger.info(f"Fetching album info for album_id: {album_id}")
203
+ logger.debug(f"Query URL: {query_url}")
180
204
  response = self._session.get(query_url, timeout=30)
205
+ logger.info(f"Response status code: {response.status_code}")
181
206
  response.raise_for_status()
182
207
  except requests.RequestException as e:
208
+ logger.error(f"Error fetching album info: {e}")
183
209
  raise NetworkException(f"Network error occurred: {e}")
184
210
  except Exception as e:
211
+ logger.error(f"Error fetching album info: {e}")
185
212
  raise DeezerException(f"An error occurred while fetching album info: {e}")
186
213
 
187
214
  try:
215
+ logger.debug(f"Response JSON: {response.json()}")
188
216
  result = response.json()
189
217
  except ValueError as e:
218
+ logger.error(f"Invalid response received from Deezer: {e}")
190
219
  raise InvalidResponseException(f"Invalid response received: {e}")
191
220
 
192
221
  return {
@@ -293,6 +322,10 @@ class Deezer:
293
322
 
294
323
 
295
324
  if __name__ == "__main__":
325
+ import logging
326
+ from yutipy.utils.logger import enable_logging
327
+
328
+ enable_logging(level=logging.DEBUG)
296
329
  deezer = Deezer()
297
330
  try:
298
331
  artist_name = input("Artist Name: ")
yutipy/exceptions.py CHANGED
@@ -1,15 +1,20 @@
1
- class YutipyException(Exception):
2
- """Base class for exceptions in the Yutipy package."""
3
-
4
- pass
1
+ __all__ = [
2
+ "AuthenticationException",
3
+ "InvalidResponseException",
4
+ "InvalidValueException",
5
+ "NetworkException",
6
+ "YutipyException",
7
+ ]
5
8
 
6
9
 
7
- class InvalidValueException(YutipyException):
8
- """Exception raised for invalid values."""
10
+ # Base Exception
11
+ class YutipyException(Exception):
12
+ """Base class for exceptions in the Yutipy package."""
9
13
 
10
14
  pass
11
15
 
12
16
 
17
+ # Service Exceptions
13
18
  class DeezerException(YutipyException):
14
19
  """Exception raised for errors related to the Deezer API."""
15
20
 
@@ -22,8 +27,8 @@ class ItunesException(YutipyException):
22
27
  pass
23
28
 
24
29
 
25
- class SpotifyException(YutipyException):
26
- """Exception raised for errors related to the Spotify API."""
30
+ class KKBoxException(YutipyException):
31
+ """Exception raised for erros related to the KKBOX Open API."""
27
32
 
28
33
  pass
29
34
 
@@ -34,14 +39,15 @@ class MusicYTException(YutipyException):
34
39
  pass
35
40
 
36
41
 
37
- class AuthenticationException(YutipyException):
38
- """Exception raised for authentication errors."""
42
+ class SpotifyException(YutipyException):
43
+ """Exception raised for errors related to the Spotify API."""
39
44
 
40
45
  pass
41
46
 
42
47
 
43
- class NetworkException(YutipyException):
44
- """Exception raised for network-related errors."""
48
+ # Generic Exceptions
49
+ class AuthenticationException(YutipyException):
50
+ """Exception raised for authentication errors."""
45
51
 
46
52
  pass
47
53
 
@@ -52,5 +58,13 @@ class InvalidResponseException(YutipyException):
52
58
  pass
53
59
 
54
60
 
55
- class KKBoxException(YutipyException):
56
- """Exception raised for erros related to the KKBOX Open API."""
61
+ class InvalidValueException(YutipyException):
62
+ """Exception raised for invalid values."""
63
+
64
+ pass
65
+
66
+
67
+ class NetworkException(YutipyException):
68
+ """Exception raised for network-related errors."""
69
+
70
+ pass
yutipy/itunes.py CHANGED
@@ -1,3 +1,5 @@
1
+ __all__ = ["Itunes", "ItunesException"]
2
+
1
3
  from datetime import datetime
2
4
  from pprint import pprint
3
5
  from typing import Dict, Optional
@@ -11,11 +13,12 @@ from yutipy.exceptions import (
11
13
  NetworkException,
12
14
  )
13
15
  from yutipy.models import MusicInfo
14
- from yutipy.utils.cheap_utils import (
16
+ from yutipy.utils.helpers import (
15
17
  are_strings_similar,
16
18
  guess_album_type,
17
19
  is_valid_string,
18
20
  )
21
+ from yutipy.utils.logger import logger
19
22
 
20
23
 
21
24
  class Itunes:
@@ -89,22 +92,34 @@ class Itunes:
89
92
  query_url = endpoint + query
90
93
 
91
94
  try:
95
+ logger.info(
96
+ f'Searching iTunes for `artist="{artist}"` and `song="{song}"`'
97
+ )
98
+ logger.debug(f"Query URL: {query_url}")
92
99
  response = self._session.get(query_url, timeout=30)
100
+ logger.debug(f"Response status code: {response.status_code}")
93
101
  response.raise_for_status()
94
102
  except requests.RequestException as e:
103
+ logger.error(f"Network error while searching iTunes: {e}")
95
104
  raise NetworkException(f"Network error occurred: {e}")
96
105
  except Exception as e:
106
+ logger.exception(f"Unexpected error while searching iTunes: {e}")
97
107
  raise ItunesException(f"An error occurred while searching iTunes: {e}")
98
108
 
99
109
  try:
110
+ logger.debug(f"Parsing response JSON: {response.json()}")
100
111
  result = response.json()["results"]
101
112
  except (IndexError, KeyError, ValueError) as e:
113
+ logger.error(f"Invalid response structure from iTunes: {e}")
102
114
  raise InvalidResponseException(f"Invalid response received: {e}")
103
115
 
104
116
  music_info = self._parse_result(artist, song, result)
105
117
  if music_info:
106
118
  return music_info
107
119
 
120
+ logger.warning(
121
+ f"No matching results found for artist='{artist}' and song='{song}'"
122
+ )
108
123
  return None
109
124
 
110
125
  def _parse_result(
@@ -212,6 +227,10 @@ class Itunes:
212
227
 
213
228
 
214
229
  if __name__ == "__main__":
230
+ import logging
231
+ from yutipy.utils.logger import enable_logging
232
+
233
+ enable_logging(level=logging.DEBUG)
215
234
  itunes = Itunes()
216
235
 
217
236
  try:
yutipy/kkbox.py CHANGED
@@ -1,3 +1,5 @@
1
+ __all__ = ["KKBox", "KKBoxException"]
2
+
1
3
  import base64
2
4
  import os
3
5
  import time
@@ -15,7 +17,8 @@ from yutipy.exceptions import (
15
17
  NetworkException,
16
18
  )
17
19
  from yutipy.models import MusicInfo
18
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
20
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
21
+ from yutipy.utils.logger import logger
19
22
 
20
23
  load_dotenv()
21
24
 
@@ -117,11 +120,14 @@ class KKBox:
117
120
  data = {"grant_type": "client_credentials"}
118
121
 
119
122
  try:
123
+ logger.info("Authenticating with KKBOX Open API")
120
124
  response = self._session.post(
121
125
  url=url, headers=headers, data=data, timeout=30
122
126
  )
127
+ logger.debug(f"Authentication response status code: {response.status_code}")
123
128
  response.raise_for_status()
124
129
  except requests.RequestException as e:
130
+ logger.error(f"Network error during KKBOX authentication: {e}")
125
131
  raise NetworkException(f"Network error occurred: {e}")
126
132
 
127
133
  try:
@@ -180,8 +186,12 @@ class KKBox:
180
186
  )
181
187
  query_url = f"{self.api_url}/search{query}"
182
188
 
189
+ logger.info(f"Searching KKBOX for `artist='{artist}'` and `song='{song}'`")
190
+ logger.debug(f"Query URL: {query_url}")
191
+
183
192
  try:
184
193
  response = self._session.get(query_url, headers=self.__header, timeout=30)
194
+ logger.debug(f"Parsing response JSON: {response.json()}")
185
195
  response.raise_for_status()
186
196
  except requests.RequestException as e:
187
197
  raise NetworkException(f"Network error occurred: {e}")
@@ -279,6 +289,9 @@ class KKBox:
279
289
  except KeyError:
280
290
  pass
281
291
 
292
+ logger.warning(
293
+ f"No matching results found for artist='{artist}' and song='{song}'"
294
+ )
282
295
  return None
283
296
 
284
297
  def _find_track(self, song: str, artist: str, track: dict) -> Optional[MusicInfo]:
@@ -403,6 +416,10 @@ class KKBox:
403
416
 
404
417
 
405
418
  if __name__ == "__main__":
419
+ import logging
420
+ from yutipy.utils.logger import enable_logging
421
+
422
+ enable_logging(level=logging.DEBUG)
406
423
  kkbox = KKBox(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
407
424
 
408
425
  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
@@ -1,4 +1,5 @@
1
- import os
1
+ __all__ = ["MusicYT", "MusicYTException"]
2
+
2
3
  from pprint import pprint
3
4
  from typing import Optional
4
5
 
@@ -8,10 +9,11 @@ from ytmusicapi import YTMusic, exceptions
8
9
  from yutipy.exceptions import (
9
10
  InvalidResponseException,
10
11
  InvalidValueException,
11
- NetworkException,
12
+ MusicYTException,
12
13
  )
13
14
  from yutipy.models import MusicInfo
14
- from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
15
+ from yutipy.utils.helpers import are_strings_similar, is_valid_string
16
+ from yutipy.utils.logger import logger
15
17
 
16
18
 
17
19
  class MusicYT:
@@ -24,6 +26,14 @@ class MusicYT:
24
26
  self.normalize_non_english = True
25
27
  self._translation_session = requests.Session()
26
28
 
29
+ def __enter__(self) -> "MusicYT":
30
+ """Enters the runtime context related to this object."""
31
+ return self
32
+
33
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
34
+ """Exits the runtime context related to this object."""
35
+ self.close_session()
36
+
27
37
  def close_session(self) -> None:
28
38
  """Closes the current session(s)."""
29
39
  if not self.is_session_closed:
@@ -70,15 +80,23 @@ class MusicYT:
70
80
 
71
81
  query = f"{artist} - {song}"
72
82
 
83
+ logger.info(
84
+ f"Searching YouTube Music for `artist='{artist}'` and `song='{song}'`"
85
+ )
86
+
73
87
  try:
74
88
  results = self.ytmusic.search(query=query, limit=limit)
75
89
  except exceptions.YTMusicServerError as e:
76
- raise NetworkException(f"Network error occurred: {e}")
90
+ logger.error(f"Something went wrong while searching YTMusic: {e}")
91
+ raise MusicYTException(f"Something went wrong while searching YTMusic: {e}")
77
92
 
78
93
  for result in results:
79
94
  if self._is_relevant_result(artist, song, result):
80
95
  return self._process_result(result)
81
96
 
97
+ logger.warning(
98
+ f"No matching results found for artist='{artist}' and song='{song}'"
99
+ )
82
100
  return None
83
101
 
84
102
  def _is_relevant_result(self, artist: str, song: str, result: dict) -> bool:
@@ -276,6 +294,11 @@ class MusicYT:
276
294
 
277
295
 
278
296
  if __name__ == "__main__":
297
+ import logging
298
+
299
+ from yutipy.utils.logger import enable_logging
300
+
301
+ enable_logging(level=logging.DEBUG)
279
302
  music_yt = MusicYT()
280
303
 
281
304
  artist_name = input("Artist Name: ")
yutipy/spotify.py CHANGED
@@ -1,3 +1,5 @@
1
+ __all__ = ["Spotify", "SpotifyException"]
2
+
1
3
  import base64
2
4
  import os
3
5
  import time
@@ -15,12 +17,13 @@ from yutipy.exceptions import (
15
17
  SpotifyException,
16
18
  )
17
19
  from yutipy.models import MusicInfo
18
- from yutipy.utils.cheap_utils import (
20
+ from yutipy.utils.helpers import (
19
21
  are_strings_similar,
20
22
  guess_album_type,
21
23
  is_valid_string,
22
24
  separate_artists,
23
25
  )
26
+ from yutipy.utils.logger import logger
24
27
 
25
28
  load_dotenv()
26
29
 
@@ -123,11 +126,14 @@ class Spotify:
123
126
  data = {"grant_type": "client_credentials"}
124
127
 
125
128
  try:
129
+ logger.info("Authenticating with Spotify API")
126
130
  response = self._session.post(
127
131
  url=url, headers=headers, data=data, timeout=30
128
132
  )
133
+ logger.debug(f"Authentication response status code: {response.status_code}")
129
134
  response.raise_for_status()
130
135
  except requests.RequestException as e:
136
+ logger.error(f"Network error during Spotify authentication: {e}")
131
137
  raise NetworkException(f"Network error occurred: {e}")
132
138
 
133
139
  try:
@@ -190,6 +196,11 @@ class Spotify:
190
196
 
191
197
  query_url = f"{self.api_url}/search{query}"
192
198
 
199
+ logger.info(
200
+ f"Searching Spotify for `artist='{artist}'` and `song='{song}'`"
201
+ )
202
+ logger.debug(f"Query URL: {query_url}")
203
+
193
204
  try:
194
205
  response = self._session.get(
195
206
  query_url, headers=self.__header, timeout=30
@@ -342,6 +353,9 @@ class Spotify:
342
353
  except KeyError:
343
354
  pass
344
355
 
356
+ logger.warning(
357
+ f"No matching results found for artist='{artist}' and song='{song}'"
358
+ )
345
359
  return None
346
360
 
347
361
  def _find_track(
@@ -477,6 +491,11 @@ class Spotify:
477
491
 
478
492
 
479
493
  if __name__ == "__main__":
494
+ import logging
495
+
496
+ from yutipy.utils.logger import enable_logging
497
+
498
+ enable_logging(level=logging.DEBUG)
480
499
  spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
481
500
 
482
501
  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) 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) 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,32 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 1.4.22
3
+ Version: 1.5.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>
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
@@ -43,7 +22,6 @@ 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
@@ -90,6 +68,10 @@ A _**simple**_ Python package for searching and retrieving music information fro
90
68
  - [Available Music Platforms](#available-music-platforms)
91
69
  - [Installation](#installation)
92
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)
93
75
  - [Contributing](#contributing)
94
76
  - [License](#license)
95
77
 
@@ -121,9 +103,7 @@ pip install -U yutipy
121
103
 
122
104
  ## Usage Example
123
105
 
124
- Here's a quick example of how to use the `yutipy` package to search for a song:
125
-
126
- ### Deezer
106
+ Here's a quick example of how to use the `yutipy` package to search for a song on **Deezer**:
127
107
 
128
108
  ```python
129
109
  from yutipy.deezer import Deezer
@@ -135,6 +115,36 @@ with Deezer() as deezer:
135
115
 
136
116
  For more usage examples, see the [Usage Examples](https://yutipy.readthedocs.io/en/latest/usage_examples.html) page in docs.
137
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
+
138
148
  ## Contributing
139
149
 
140
150
  Contributions are welcome! Please follow these steps:
@@ -0,0 +1,22 @@
1
+ yutipy/__init__.py,sha256=5_naWng1pAIg9e98eEdjsvom69cV3QXfmccVDSNZKCc,280
2
+ yutipy/deezer.py,sha256=_-UtSq32FYqmMpmM4Y3S8KYjsxSC1tuU0jLyWZo7bBs,11409
3
+ yutipy/exceptions.py,sha256=LdVYtmQLpX5is9iMsbjECxZYoBUdbtWR3nFN4SBVJOM,1362
4
+ yutipy/itunes.py,sha256=IfJmAhd1OF-LOPYz4jsYOYrWqGV2hhBq8Y-ynOnohas,7934
5
+ yutipy/kkbox.py,sha256=v21qcFzfH2Kg_dYGoEwtaBrWS3JejqpZdddeOqm2F_4,14196
6
+ yutipy/logging.py,sha256=CcnaAZGClTUgfYqRXuVfFEhfMhlXF86di4r3O7aSHDA,107
7
+ yutipy/models.py,sha256=vvWIA3MwCOOM2CBHSabqmFXz4NdVHaQtObU6zhGpJOM,1931
8
+ yutipy/musicyt.py,sha256=gPbmhAaFa4PZ7xNNJkKnzcwnkqieOsTUcXgIenp41OM,9238
9
+ yutipy/spotify.py,sha256=cZw7X4lVWNE_0IDN7yztNxUy33vU7TrWnz82kK2cUHE,16324
10
+ yutipy/yutipy_music.py,sha256=ec_a5QcygaSISV7OeOeYkMpwzE5YBeJjfobnQ4cb-Cw,6543
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.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
18
+ yutipy-1.5.1.dist-info/METADATA,sha256=h7Ty2ILxjPwSUg8TjWLCwU3MBdyC4xBHOmc0VwAJgy8,6498
19
+ yutipy-1.5.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
20
+ yutipy-1.5.1.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
21
+ yutipy-1.5.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
22
+ yutipy-1.5.1.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.22.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
14
- yutipy-1.4.22.dist-info/METADATA,sha256=7MST1bW-oYnZA9bZzRFPyJvW9RI86Ck5c0MVeGhQ7U0,6527
15
- yutipy-1.4.22.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
16
- yutipy-1.4.22.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
17
- yutipy-1.4.22.dist-info/RECORD,,