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 +2 -2
- yutipy/cli/__init__.py +0 -0
- yutipy/cli/config.py +86 -0
- yutipy/cli/search.py +104 -0
- yutipy/deezer.py +36 -5
- yutipy/itunes.py +18 -1
- yutipy/kkbox.py +16 -1
- yutipy/logging.py +3 -0
- yutipy/models.py +1 -1
- yutipy/musicyt.py +22 -1
- yutipy/spotify.py +17 -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.2.dist-info → yutipy-1.5.0.dist-info}/METADATA +39 -28
- yutipy-1.5.0.dist-info/RECORD +22 -0
- yutipy-1.5.0.dist-info/entry_points.txt +3 -0
- yutipy-1.4.2.dist-info/RECORD +0 -17
- {yutipy-1.4.2.dist-info → yutipy-1.5.0.dist-info}/WHEEL +0 -0
- {yutipy-1.4.2.dist-info → yutipy-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {yutipy-1.4.2.dist-info → yutipy-1.5.0.dist-info}/top_level.txt +0 -0
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.
|
|
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,
|
|
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/{
|
|
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,
|
|
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/{
|
|
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.
|
|
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.
|
|
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
yutipy/models.py
CHANGED
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.
|
|
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.
|
|
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
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, 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
|
-
|
|
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,39 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yutipy
|
|
3
|
-
Version: 1.
|
|
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
|
|
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
|
|
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,,
|
yutipy-1.4.2.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.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
|
|
File without changes
|
|
File without changes
|