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 +0 -2
- yutipy/cli/__init__.py +0 -0
- yutipy/cli/config.py +86 -0
- yutipy/cli/search.py +104 -0
- yutipy/deezer.py +38 -5
- yutipy/exceptions.py +28 -14
- yutipy/itunes.py +20 -1
- yutipy/kkbox.py +18 -1
- yutipy/logging.py +3 -0
- yutipy/models.py +1 -1
- yutipy/musicyt.py +27 -4
- yutipy/spotify.py +20 -1
- yutipy/utils/__init__.py +1 -1
- yutipy/utils/{cheap_utils.py → helpers.py} +0 -4
- yutipy/utils/logger.py +37 -2
- yutipy/yutipy_music.py +39 -35
- {yutipy-1.4.22.dist-info → yutipy-1.5.1.dist-info}/METADATA +37 -27
- yutipy-1.5.1.dist-info/RECORD +22 -0
- yutipy-1.5.1.dist-info/entry_points.txt +3 -0
- yutipy-1.4.22.dist-info/RECORD +0 -17
- {yutipy-1.4.22.dist-info → yutipy-1.5.1.dist-info}/WHEEL +0 -0
- {yutipy-1.4.22.dist-info → yutipy-1.5.1.dist-info}/licenses/LICENSE +0 -0
- {yutipy-1.4.22.dist-info → yutipy-1.5.1.dist-info}/top_level.txt +0 -0
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.
|
|
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,
|
|
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/{
|
|
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,
|
|
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/{
|
|
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
|
-
|
|
2
|
-
""
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
__all__ = [
|
|
2
|
+
"AuthenticationException",
|
|
3
|
+
"InvalidResponseException",
|
|
4
|
+
"InvalidValueException",
|
|
5
|
+
"NetworkException",
|
|
6
|
+
"YutipyException",
|
|
7
|
+
]
|
|
5
8
|
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
26
|
-
"""Exception raised for
|
|
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
|
|
38
|
-
"""Exception raised for
|
|
42
|
+
class SpotifyException(YutipyException):
|
|
43
|
+
"""Exception raised for errors related to the Spotify API."""
|
|
39
44
|
|
|
40
45
|
pass
|
|
41
46
|
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
56
|
-
"""Exception raised for
|
|
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.
|
|
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.
|
|
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
yutipy/models.py
CHANGED
yutipy/musicyt.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
12
|
+
MusicYTException,
|
|
12
13
|
)
|
|
13
14
|
from yutipy.models import MusicInfo
|
|
14
|
-
from yutipy.utils.
|
|
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
|
-
|
|
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.
|
|
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
yutipy/utils/logger.py
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
logger = logging.getLogger(
|
|
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.
|
|
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
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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.
|
|
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
|
|
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,,
|
yutipy-1.4.22.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|