yutipy 1.0.0__py3-none-any.whl → 1.1.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/exceptions.py +4 -0
- yutipy/kkbox.py +320 -0
- yutipy/spotify.py +36 -19
- {yutipy-1.0.0.dist-info → yutipy-1.1.0.dist-info}/METADATA +13 -1
- {yutipy-1.0.0.dist-info → yutipy-1.1.0.dist-info}/RECORD +8 -7
- {yutipy-1.0.0.dist-info → yutipy-1.1.0.dist-info}/LICENSE +0 -0
- {yutipy-1.0.0.dist-info → yutipy-1.1.0.dist-info}/WHEEL +0 -0
- {yutipy-1.0.0.dist-info → yutipy-1.1.0.dist-info}/top_level.txt +0 -0
yutipy/exceptions.py
CHANGED
yutipy/kkbox.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from pprint import pprint
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from yutipy.exceptions import (
|
|
11
|
+
AuthenticationException,
|
|
12
|
+
InvalidResponseException,
|
|
13
|
+
InvalidValueException,
|
|
14
|
+
KKBoxException,
|
|
15
|
+
NetworkException,
|
|
16
|
+
)
|
|
17
|
+
from yutipy.models import MusicInfo
|
|
18
|
+
from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
|
|
19
|
+
|
|
20
|
+
load_dotenv()
|
|
21
|
+
|
|
22
|
+
KKBOX_CLIENT_ID = os.getenv("KKBOX_CLIENT_ID")
|
|
23
|
+
KKBOX_CLIENT_SECRET = os.getenv("KKBOX_CLIENT_SECRET")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KKBox:
|
|
27
|
+
"""
|
|
28
|
+
A class to interact with KKBOX Open API.
|
|
29
|
+
|
|
30
|
+
This class reads the ``KKBOX_CLIENT_ID`` and ``KKBOX_CLIENT_SECRET`` from environment variables or the ``.env`` file by default.
|
|
31
|
+
Alternatively, you can manually provide these values when creating an object.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, client_id: str = KKBOX_CLIENT_ID, client_secret: str = KKBOX_CLIENT_SECRET
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initializes the KKBox class and sets up the session.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
client_id : str, optional
|
|
43
|
+
The Client ID for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_ID`` from .env file.
|
|
44
|
+
client_secret : str, optional
|
|
45
|
+
The Client secret for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_SECRET`` from .env file.
|
|
46
|
+
"""
|
|
47
|
+
if not client_id or not client_secret:
|
|
48
|
+
raise KKBoxException(
|
|
49
|
+
"Failed to read `KKBOX_CLIENT_ID` and/or `KKBOX_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.client_id = client_id
|
|
53
|
+
self.client_secret = client_secret
|
|
54
|
+
self._session = requests.Session()
|
|
55
|
+
self.api_url = "https://api.kkbox.com/v1.1"
|
|
56
|
+
self.__header, self.__expires_in = self.__authenticate()
|
|
57
|
+
self.__start_time = time.time()
|
|
58
|
+
self._is_session_closed = False
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
"""Enters the runtime context related to this object."""
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
65
|
+
"""Exits the runtime context related to this object."""
|
|
66
|
+
self._close_session()
|
|
67
|
+
|
|
68
|
+
def close_session(self) -> None:
|
|
69
|
+
"""Closes the current session."""
|
|
70
|
+
if not self.is_session_closed:
|
|
71
|
+
self._session.close()
|
|
72
|
+
self._is_session_closed = True
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_session_closed(self) -> bool:
|
|
76
|
+
"""Checks if the session is closed."""
|
|
77
|
+
return self._is_session_closed
|
|
78
|
+
|
|
79
|
+
def __authenticate(self) -> tuple:
|
|
80
|
+
"""
|
|
81
|
+
Authenticates with the KKBOX Open API and returns the authorization header.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
dict
|
|
86
|
+
The authorization header.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
token, expires_in = self.__get_access_token()
|
|
90
|
+
return {"Authorization": f"Bearer {token}"}, expires_in
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise AuthenticationException(
|
|
93
|
+
"Failed to authenticate with KKBOX Open API"
|
|
94
|
+
) from e
|
|
95
|
+
|
|
96
|
+
def __get_access_token(self) -> tuple:
|
|
97
|
+
"""
|
|
98
|
+
Gets the KKBOX Open API token.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
str
|
|
103
|
+
The KKBOX Open API token.
|
|
104
|
+
"""
|
|
105
|
+
auth_string = f"{self.client_id}:{self.client_secret}"
|
|
106
|
+
auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
107
|
+
|
|
108
|
+
url = " https://account.kkbox.com/oauth2/token"
|
|
109
|
+
headers = {
|
|
110
|
+
"Authorization": f"Basic {auth_base64}",
|
|
111
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
112
|
+
}
|
|
113
|
+
data = {"grant_type": "client_credentials"}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = self._session.post(
|
|
117
|
+
url=url, headers=headers, data=data, timeout=30
|
|
118
|
+
)
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
except requests.RequestException as e:
|
|
121
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
response_json = response.json()
|
|
125
|
+
return response_json.get("access_token"), response_json.get("expires_in")
|
|
126
|
+
except (KeyError, ValueError) as e:
|
|
127
|
+
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
128
|
+
|
|
129
|
+
def __refresh_token_if_expired(self):
|
|
130
|
+
"""Refreshes the token if it has expired."""
|
|
131
|
+
if time.time() - self.__start_time >= self.__expires_in:
|
|
132
|
+
self.__header, self.__expires_in = self.__authenticate()
|
|
133
|
+
self.__start_time = time.time()
|
|
134
|
+
|
|
135
|
+
def search(
|
|
136
|
+
self, artist: str, song: str, territory: str = "TW"
|
|
137
|
+
) -> Optional[MusicInfo]:
|
|
138
|
+
"""
|
|
139
|
+
Searches for a song by artist and title.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
artist : str
|
|
144
|
+
The name of the artist.
|
|
145
|
+
song : str
|
|
146
|
+
The title of the song.
|
|
147
|
+
territory : str
|
|
148
|
+
Two-letter country codes from ISO 3166-1 alpha-2.
|
|
149
|
+
Allowed values: ``HK``, ``JP``, ``MY``, ``SG``, ``TW``.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
Optional[MusicInfo_]
|
|
154
|
+
The music information if found, otherwise None.
|
|
155
|
+
"""
|
|
156
|
+
if not is_valid_string(artist) or not is_valid_string(song):
|
|
157
|
+
raise InvalidValueException(
|
|
158
|
+
"Artist and song names must be valid strings and can't be empty."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self.__refresh_token_if_expired()
|
|
162
|
+
|
|
163
|
+
query = f"?q={artist} - {song}&type=track,album&territory={territory}&limit=10"
|
|
164
|
+
query_url = f"{self.api_url}/search{query}"
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
response = self._session.get(query_url, headers=self.__header, timeout=30)
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
except requests.RequestException as e:
|
|
170
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
171
|
+
|
|
172
|
+
if response.status_code != 200:
|
|
173
|
+
raise KKBoxException(f"Failed to search for music: {response.json()}")
|
|
174
|
+
|
|
175
|
+
return self._find_music_info(artist, song, response.json())
|
|
176
|
+
|
|
177
|
+
def _find_music_info(
|
|
178
|
+
self, artist: str, song: str, response_json: dict
|
|
179
|
+
) -> Optional[MusicInfo]:
|
|
180
|
+
"""
|
|
181
|
+
Finds the music information from the search results.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
artist : str
|
|
186
|
+
The name of the artist.
|
|
187
|
+
song : str
|
|
188
|
+
The title of the song.
|
|
189
|
+
response_json : dict
|
|
190
|
+
The JSON response from the API.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
Optional[MusicInfo]
|
|
195
|
+
The music information if found, otherwise None.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
for track in response_json["tracks"]["data"]:
|
|
199
|
+
music_info = self._find_track(song, artist, track)
|
|
200
|
+
if music_info:
|
|
201
|
+
return music_info
|
|
202
|
+
except KeyError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
for album in response_json["albums"]["data"]:
|
|
207
|
+
music_info = self._find_album(song, artist, album)
|
|
208
|
+
if music_info:
|
|
209
|
+
return music_info
|
|
210
|
+
except KeyError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def _find_track(self, song: str, artist: str, track: dict) -> Optional[MusicInfo]:
|
|
216
|
+
"""
|
|
217
|
+
Finds the track information from the search results.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
song : str
|
|
222
|
+
The title of the song.
|
|
223
|
+
artist : str
|
|
224
|
+
The name of the artist.
|
|
225
|
+
track : dict
|
|
226
|
+
A single track from the search results.
|
|
227
|
+
artist_ids : list
|
|
228
|
+
A list of artist IDs.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
Optional[MusicInfo]
|
|
233
|
+
The music information if found, otherwise None.
|
|
234
|
+
"""
|
|
235
|
+
if not are_strings_similar(track["name"], song):
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
artists_name = track["album"]["artist"]["name"]
|
|
239
|
+
matching_artists = (
|
|
240
|
+
artists_name if are_strings_similar(artists_name, artist) else None
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if matching_artists:
|
|
244
|
+
return MusicInfo(
|
|
245
|
+
album_art=track["album"]["images"][2]["url"],
|
|
246
|
+
album_title=track["album"]["name"],
|
|
247
|
+
album_type=None,
|
|
248
|
+
artists=artists_name,
|
|
249
|
+
genre=None,
|
|
250
|
+
id=track["id"],
|
|
251
|
+
isrc=track["isrc"],
|
|
252
|
+
lyrics=None,
|
|
253
|
+
release_date=track["album"]["release_date"],
|
|
254
|
+
tempo=None,
|
|
255
|
+
title=track["name"],
|
|
256
|
+
type="track",
|
|
257
|
+
upc=None,
|
|
258
|
+
url=track["url"],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def _find_album(self, song: str, artist: str, album: dict) -> Optional[MusicInfo]:
|
|
264
|
+
"""
|
|
265
|
+
Finds the album information from the search results.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
song : str
|
|
270
|
+
The title of the song.
|
|
271
|
+
artist : str
|
|
272
|
+
The name of the artist.
|
|
273
|
+
album : dict
|
|
274
|
+
A single album from the search results.
|
|
275
|
+
artist_ids : list
|
|
276
|
+
A list of artist IDs.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
Optional[MusicInfo]
|
|
281
|
+
The music information if found, otherwise None.
|
|
282
|
+
"""
|
|
283
|
+
if not are_strings_similar(album["name"], song):
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
artists_name = album["artist"]["name"]
|
|
287
|
+
matching_artists = (
|
|
288
|
+
artists_name if are_strings_similar(artists_name, artist) else None
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if matching_artists:
|
|
292
|
+
return MusicInfo(
|
|
293
|
+
album_art=album["images"][2]["url"],
|
|
294
|
+
album_title=album["name"],
|
|
295
|
+
album_type=None,
|
|
296
|
+
artists=artists_name,
|
|
297
|
+
genre=None,
|
|
298
|
+
id=album["id"],
|
|
299
|
+
isrc=None,
|
|
300
|
+
lyrics=None,
|
|
301
|
+
release_date=album["release_date"],
|
|
302
|
+
tempo=None,
|
|
303
|
+
title=album["name"],
|
|
304
|
+
type="album",
|
|
305
|
+
upc=None,
|
|
306
|
+
url=album["url"],
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
kkbox = KKBox(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
artist_name = input("Artist Name: ")
|
|
317
|
+
song_name = input("Song Name: ")
|
|
318
|
+
pprint(kkbox.search(artist_name, song_name))
|
|
319
|
+
finally:
|
|
320
|
+
kkbox.close_session()
|
yutipy/spotify.py
CHANGED
|
@@ -36,7 +36,9 @@ class Spotify:
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
def __init__(
|
|
39
|
-
self,
|
|
39
|
+
self,
|
|
40
|
+
client_id: str = SPOTIFY_CLIENT_ID,
|
|
41
|
+
client_secret: str = SPOTIFY_CLIENT_SECRET,
|
|
40
42
|
) -> None:
|
|
41
43
|
"""
|
|
42
44
|
Initializes the Spotify class and sets up the session.
|
|
@@ -50,7 +52,7 @@ class Spotify:
|
|
|
50
52
|
"""
|
|
51
53
|
if not client_id or not client_secret:
|
|
52
54
|
raise SpotifyException(
|
|
53
|
-
"Failed to read `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
|
|
55
|
+
"Failed to read `SPOTIFY_CLIENT_ID` and/or `SPOTIFY_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
self.client_id = client_id
|
|
@@ -157,22 +159,37 @@ class Spotify:
|
|
|
157
159
|
"Artist and song names must be valid strings and can't be empty."
|
|
158
160
|
)
|
|
159
161
|
|
|
160
|
-
|
|
162
|
+
music_info = None
|
|
163
|
+
queries = [
|
|
164
|
+
f"?q=artist:{artist} track:{song}&type=track&limit=10",
|
|
165
|
+
f"?q=artist:{artist} album:{song}&type=album&limit=10",
|
|
166
|
+
]
|
|
161
167
|
|
|
162
|
-
query
|
|
163
|
-
|
|
168
|
+
for query in queries:
|
|
169
|
+
if music_info:
|
|
170
|
+
return music_info
|
|
164
171
|
|
|
165
|
-
|
|
166
|
-
response = self._session.get(query_url, headers=self.__header, timeout=30)
|
|
167
|
-
response.raise_for_status()
|
|
168
|
-
except requests.RequestException as e:
|
|
169
|
-
raise NetworkException(f"Network error occurred: {e}")
|
|
172
|
+
self.__refresh_token_if_expired()
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
raise SpotifyException(f"Failed to search for music: {response.json()}")
|
|
174
|
+
query_url = f"{self.api_url}/search{query}"
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
try:
|
|
177
|
+
response = self._session.get(
|
|
178
|
+
query_url, headers=self.__header, timeout=30
|
|
179
|
+
)
|
|
180
|
+
response.raise_for_status()
|
|
181
|
+
except requests.RequestException as e:
|
|
182
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
183
|
+
|
|
184
|
+
if response.status_code != 200:
|
|
185
|
+
raise SpotifyException(f"Failed to search for music: {response.json()}")
|
|
186
|
+
|
|
187
|
+
artist_ids = self._get_artists_ids(artist)
|
|
188
|
+
music_info = self._find_music_info(
|
|
189
|
+
artist, song, response.json(), artist_ids
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return music_info
|
|
176
193
|
|
|
177
194
|
def search_advanced(
|
|
178
195
|
self, artist: str, song: str, isrc: str = None, upc: str = None
|
|
@@ -282,7 +299,7 @@ class Spotify:
|
|
|
282
299
|
"""
|
|
283
300
|
try:
|
|
284
301
|
for track in response_json["tracks"]["items"]:
|
|
285
|
-
music_info = self.
|
|
302
|
+
music_info = self._find_track(song, artist, track, artist_ids)
|
|
286
303
|
if music_info:
|
|
287
304
|
return music_info
|
|
288
305
|
except KeyError:
|
|
@@ -298,7 +315,7 @@ class Spotify:
|
|
|
298
315
|
|
|
299
316
|
return None
|
|
300
317
|
|
|
301
|
-
def
|
|
318
|
+
def _find_track(
|
|
302
319
|
self, song: str, artist: str, track: dict, artist_ids: list
|
|
303
320
|
) -> Optional[MusicInfo]:
|
|
304
321
|
"""
|
|
@@ -404,11 +421,11 @@ class Spotify:
|
|
|
404
421
|
|
|
405
422
|
|
|
406
423
|
if __name__ == "__main__":
|
|
407
|
-
|
|
424
|
+
spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
|
|
408
425
|
|
|
409
426
|
try:
|
|
410
427
|
artist_name = input("Artist Name: ")
|
|
411
428
|
song_name = input("Song Name: ")
|
|
412
|
-
pprint(
|
|
429
|
+
pprint(spotify.search(artist_name, song_name))
|
|
413
430
|
finally:
|
|
414
|
-
|
|
431
|
+
spotify.close_session()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: yutipy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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>
|
|
@@ -85,6 +85,7 @@ A _**simple**_ Python package for searching and retrieving music information fro
|
|
|
85
85
|
## Table of Contents
|
|
86
86
|
|
|
87
87
|
- [Features](#features)
|
|
88
|
+
- [Available Music Platforms](#available-music-platforms)
|
|
88
89
|
- [Installation](#installation)
|
|
89
90
|
- [Usage Example](#usage-example)
|
|
90
91
|
- [Contributing](#contributing)
|
|
@@ -97,6 +98,17 @@ A _**simple**_ Python package for searching and retrieving music information fro
|
|
|
97
98
|
- It uses `RapidFuzz` to compare & return the best match so that you can be sure you got what you asked for without having to worry and doing all that work by yourself.
|
|
98
99
|
- Retrieve detailed music information, including album art, release dates, lyrics, ISRC, and UPC codes.
|
|
99
100
|
|
|
101
|
+
### Available Music Platforms
|
|
102
|
+
|
|
103
|
+
Right now, the following music platforms are available in yutipy for searching music. New platforms will be added in the future.
|
|
104
|
+
Feel free to request any music platform you would like me to add by opening an issue on [GitHub](https://github.com/CheapNightbot/yutipy/issues) or by emailing me.
|
|
105
|
+
|
|
106
|
+
- `Deezer`: https://www.deezer.com
|
|
107
|
+
- `iTunes`: https://music.apple.com
|
|
108
|
+
- `KKBOX`: https://www.kkbox.com
|
|
109
|
+
- `Spotify`: https://spotify.com
|
|
110
|
+
- `YouTube Music`: https://music.youtube.com
|
|
111
|
+
|
|
100
112
|
## Installation
|
|
101
113
|
|
|
102
114
|
You can install the package using pip. Make sure you have Python 3.8 or higher installed.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
yutipy/__init__.py,sha256=t0w-pRLjIGD3UtDyd-JpeL-RrMona7YTV_0lZCpGLEc,232
|
|
2
2
|
yutipy/deezer.py,sha256=TbPk6f1ytrtk9GHXOoRZ948fcgOi0619-rQC9xfTYu0,8641
|
|
3
|
-
yutipy/exceptions.py,sha256=
|
|
3
|
+
yutipy/exceptions.py,sha256=4L0Oe1PwFP34LoFTy-Fruipk7uB-JkaackRmkjlaZJU,1138
|
|
4
4
|
yutipy/itunes.py,sha256=NhrllA3U0PUY3dNoMyiG1oZuyvdMQKYRBHzYSkt8ikM,5843
|
|
5
|
+
yutipy/kkbox.py,sha256=l-I1FZjKzLLqjN0hO2hvz6AdNuYvD1B4XI36m3sWmfM,10123
|
|
5
6
|
yutipy/models.py,sha256=RnfWjoNO1UoNsRvNRYd-4TpkU-4jHKu5t394vIqOOgw,1360
|
|
6
7
|
yutipy/musicyt.py,sha256=kvRYuhlNVuvy-WahJq895T3JAzmGTTiKB89r_uTMTAI,7155
|
|
7
|
-
yutipy/spotify.py,sha256=
|
|
8
|
+
yutipy/spotify.py,sha256=CZA9XF0EV5wSrLrN9cURtiFKSEHsIvqVlXLKkvb4JEM,13609
|
|
8
9
|
yutipy/utils/__init__.py,sha256=7UFcFZ7fBtNXOTngjnRD3MeobT3x5UT2Gag94TXVgLk,169
|
|
9
10
|
yutipy/utils/cheap_utils.py,sha256=LIuEHib_97NuLahXxdHJUD9v-ccXNUc3NrLYk8EQ52A,1652
|
|
10
|
-
yutipy-1.
|
|
11
|
-
yutipy-1.
|
|
12
|
-
yutipy-1.
|
|
13
|
-
yutipy-1.
|
|
14
|
-
yutipy-1.
|
|
11
|
+
yutipy-1.1.0.dist-info/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
12
|
+
yutipy-1.1.0.dist-info/METADATA,sha256=2EY4cfuWnLA1fH4E1G1qp8x3o53M8bzAYSX6QPdxAms,6467
|
|
13
|
+
yutipy-1.1.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
14
|
+
yutipy-1.1.0.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
15
|
+
yutipy-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|