yutipy 0.1.1__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 CHANGED
@@ -50,3 +50,7 @@ class InvalidResponseException(YutipyException):
50
50
  """Exception raised for invalid responses from APIs."""
51
51
 
52
52
  pass
53
+
54
+
55
+ class KKBoxException(YutipyException):
56
+ """Exception raised for erros related to the KKBOX Open API."""
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/models.py CHANGED
@@ -1,55 +1,55 @@
1
- from dataclasses import dataclass
2
- from typing import Optional
3
-
4
-
5
- @dataclass
6
- class MusicInfo:
7
- """
8
- A data class to store music information.
9
-
10
- Attributes
11
- ----------
12
- album_art : Optional[str]
13
- URL to the album art.
14
- album_title : Optional[str]
15
- Title of the album.
16
- album_type : Optional[str]
17
- Type of the album (e.g., album, single).
18
- artists : str
19
- Name(s) of the artist(s).
20
- genre : Optional[str]
21
- Genre of the music.
22
- id : str
23
- Unique identifier for the music.
24
- isrc : Optional[str]
25
- International Standard Recording Code.
26
- lyrics : Optional[str]
27
- Lyrics of the song.
28
- release_date : Optional[str]
29
- Release date of the music.
30
- tempo : Optional[float]
31
- Tempo of the music in BPM.
32
- title : str
33
- Title of the music.
34
- type : Optional[str]
35
- Type of the music (e.g., track, album).
36
- upc : Optional[str]
37
- Universal Product Code.
38
- url : str
39
- URL to the music on the platform.
40
- """
41
-
42
- album_art: Optional[str]
43
- album_title: Optional[str]
44
- album_type: Optional[str]
45
- artists: str
46
- genre: Optional[str]
47
- id: str
48
- isrc: Optional[str]
49
- lyrics: Optional[str]
50
- release_date: Optional[str]
51
- tempo: Optional[float]
52
- title: str
53
- type: Optional[str]
54
- upc: Optional[str]
55
- url: str
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class MusicInfo:
7
+ """
8
+ A data class to store music information.
9
+
10
+ Attributes
11
+ ----------
12
+ album_art : Optional[str]
13
+ URL to the album art.
14
+ album_title : Optional[str]
15
+ Title of the album.
16
+ album_type : Optional[str]
17
+ Type of the album (e.g., album, single).
18
+ artists : str
19
+ Name(s) of the artist(s).
20
+ genre : Optional[str]
21
+ Genre of the music.
22
+ id : str
23
+ Unique identifier for the music.
24
+ isrc : Optional[str]
25
+ International Standard Recording Code.
26
+ lyrics : Optional[str]
27
+ Lyrics of the song.
28
+ release_date : Optional[str]
29
+ Release date of the music.
30
+ tempo : Optional[float]
31
+ Tempo of the music in BPM.
32
+ title : str
33
+ Title of the music.
34
+ type : Optional[str]
35
+ Type of the music (e.g., track, album).
36
+ upc : Optional[str]
37
+ Universal Product Code.
38
+ url : str
39
+ URL to the music on the platform.
40
+ """
41
+
42
+ album_art: Optional[str]
43
+ album_title: Optional[str]
44
+ album_type: Optional[str]
45
+ artists: str
46
+ genre: Optional[str]
47
+ id: str
48
+ isrc: Optional[str]
49
+ lyrics: Optional[str]
50
+ release_date: Optional[str]
51
+ tempo: Optional[float]
52
+ title: str
53
+ type: Optional[str]
54
+ upc: Optional[str]
55
+ url: str
yutipy/spotify.py CHANGED
@@ -23,20 +23,22 @@ from yutipy.utils.cheap_utils import (
23
23
 
24
24
  load_dotenv()
25
25
 
26
- CLIENT_ID = os.getenv("CLIENT_ID")
27
- CLIENT_SECRET = os.getenv("CLIENT_SECRET")
26
+ SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
27
+ SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
28
28
 
29
29
 
30
30
  class Spotify:
31
31
  """
32
32
  A class to interact with the Spotify API.
33
33
 
34
- This class reads the ``CLIENT_ID`` and ``CLIENT_SECRET`` from environment variables or the ``.env`` file by default.
35
- Alternatively, users can manually provide these values when creating an object.
34
+ This class reads the ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` from environment variables or the ``.env`` file by default.
35
+ Alternatively, you can manually provide these values when creating an object.
36
36
  """
37
37
 
38
38
  def __init__(
39
- self, client_id: str = CLIENT_ID, client_secret: str = CLIENT_SECRET
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.
@@ -44,20 +46,20 @@ class Spotify:
44
46
  Parameters
45
47
  ----------
46
48
  client_id : str, optional
47
- The client ID for the Spotify API. Defaults to CLIENT_ID from .env file.
49
+ The Client ID for the Spotify API. Defaults to ``SPOTIFY_CLIENT_ID`` from .env file.
48
50
  client_secret : str, optional
49
- The client secret for the Spotify API. Defaults to CLIENT_SECRET from .env file.
51
+ The Client secret for the Spotify API. Defaults to ``SPOTIFY_CLIENT_SECRET`` from .env file.
50
52
  """
51
53
  if not client_id or not client_secret:
52
54
  raise SpotifyException(
53
- "Failed to read CLIENT_ID and 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
57
59
  self.client_secret = client_secret
58
60
  self._session = requests.Session()
59
61
  self.api_url = "https://api.spotify.com/v1"
60
- self.__header = self.__authenticate()
62
+ self.__header, self.__expires_in = self.__authenticate()
61
63
  self.__start_time = time.time()
62
64
  self._is_session_closed = False
63
65
 
@@ -80,7 +82,7 @@ class Spotify:
80
82
  """Checks if the session is closed."""
81
83
  return self._is_session_closed
82
84
 
83
- def __authenticate(self) -> dict:
85
+ def __authenticate(self) -> tuple:
84
86
  """
85
87
  Authenticates with the Spotify API and returns the authorization header.
86
88
 
@@ -90,14 +92,14 @@ class Spotify:
90
92
  The authorization header.
91
93
  """
92
94
  try:
93
- token = self.__get_spotify_token()
94
- return {"Authorization": f"Bearer {token}"}
95
+ token, expires_in = self.__get_spotify_token()
96
+ return {"Authorization": f"Bearer {token}"}, expires_in
95
97
  except Exception as e:
96
98
  raise AuthenticationException(
97
99
  "Failed to authenticate with Spotify API"
98
100
  ) from e
99
101
 
100
- def __get_spotify_token(self) -> str:
102
+ def __get_spotify_token(self) -> tuple:
101
103
  """
102
104
  Gets the Spotify API token.
103
105
 
@@ -125,14 +127,16 @@ class Spotify:
125
127
  raise NetworkException(f"Network error occurred: {e}")
126
128
 
127
129
  try:
128
- return response.json().get("access_token")
130
+ response_json = response.json()
131
+ return response_json.get("access_token"), response_json.get("expires_in")
129
132
  except (KeyError, ValueError) as e:
130
133
  raise InvalidResponseException(f"Invalid response received: {e}")
131
134
 
132
135
  def __refresh_token_if_expired(self):
133
136
  """Refreshes the token if it has expired."""
134
- if time.time() - self.__start_time >= 3600:
135
- self.__header = self.__authenticate()
137
+ if time.time() - self.__start_time >= self.__expires_in:
138
+ self.__header, self.__expires_in = self.__authenticate()
139
+ self.__start_time = time.time()
136
140
 
137
141
  def search(self, artist: str, song: str) -> Optional[MusicInfo]:
138
142
  """
@@ -155,23 +159,37 @@ class Spotify:
155
159
  "Artist and song names must be valid strings and can't be empty."
156
160
  )
157
161
 
158
- self.__refresh_token_if_expired()
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
+ ]
159
167
 
160
- query = f"?q=artist:{artist} track:{song}&type=track&limit=10"
161
- query_url = f"{self.api_url}/search{query}"
168
+ for query in queries:
169
+ if music_info:
170
+ return music_info
162
171
 
163
- try:
164
- response = self._session.get(query_url, headers=self.__header, timeout=30)
165
- response.raise_for_status()
166
- except requests.RequestException as e:
167
- raise NetworkException(f"Network error occurred: {e}")
172
+ self.__refresh_token_if_expired()
168
173
 
169
- try:
170
- result = response.json()["tracks"]["items"]
171
- except (KeyError, ValueError) as e:
172
- raise InvalidResponseException(f"Invalid response received: {e}")
174
+ query_url = f"{self.api_url}/search{query}"
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
+ )
173
191
 
174
- return self._parse_results(artist, song, result)
192
+ return music_info
175
193
 
176
194
  def search_advanced(
177
195
  self, artist: str, song: str, isrc: str = None, upc: str = None
@@ -281,7 +299,7 @@ class Spotify:
281
299
  """
282
300
  try:
283
301
  for track in response_json["tracks"]["items"]:
284
- music_info = self._find_tracks(song, artist, track, artist_ids)
302
+ music_info = self._find_track(song, artist, track, artist_ids)
285
303
  if music_info:
286
304
  return music_info
287
305
  except KeyError:
@@ -297,7 +315,7 @@ class Spotify:
297
315
 
298
316
  return None
299
317
 
300
- def _find_tracks(
318
+ def _find_track(
301
319
  self, song: str, artist: str, track: dict, artist_ids: list
302
320
  ) -> Optional[MusicInfo]:
303
321
  """
@@ -403,11 +421,11 @@ class Spotify:
403
421
 
404
422
 
405
423
  if __name__ == "__main__":
406
- Spotify = Spotify(CLIENT_ID, CLIENT_SECRET)
424
+ spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
407
425
 
408
426
  try:
409
427
  artist_name = input("Artist Name: ")
410
428
  song_name = input("Song Name: ")
411
- pprint(Spotify.search(artist_name, song_name))
429
+ pprint(spotify.search(artist_name, song_name))
412
430
  finally:
413
- Spotify._close_session()
431
+ spotify.close_session()
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Cheap Nightbot
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Cheap Nightbot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,140 +1,152 @@
1
- Metadata-Version: 2.2
2
- Name: yutipy
3
- Version: 0.1.1
4
- Summary: A simple package for retrieving music information from various music platforms APIs.
5
- Author: Cheap Nightbot
6
- Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
7
- Maintainer-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
8
- License: MIT License
9
-
10
- Copyright (c) 2025 Cheap Nightbot
11
-
12
- Permission is hereby granted, free of charge, to any person obtaining a copy
13
- of this software and associated documentation files (the "Software"), to deal
14
- in the Software without restriction, including without limitation the rights
15
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
- copies of the Software, and to permit persons to whom the Software is
17
- furnished to do so, subject to the following conditions:
18
-
19
- The above copyright notice and this permission notice shall be included in all
20
- copies or substantial portions of the Software.
21
-
22
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
- SOFTWARE.
29
-
30
- Project-URL: Homepage, https://github.com/CheapNightbot/yutipy
31
- Project-URL: Documentation, https://yutipy.readthedocs.io/
32
- Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
33
- Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
34
- Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
35
- Project-URL: funding, https://ko-fi.com/cheapnightbot
36
- Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify
37
- Classifier: Development Status :: 4 - Beta
38
- Classifier: Intended Audience :: Developers
39
- Classifier: Topic :: Software Development :: Libraries
40
- Classifier: Programming Language :: Python :: 3
41
- Classifier: Programming Language :: Python :: 3.8
42
- Classifier: Programming Language :: Python :: 3.9
43
- Classifier: Programming Language :: Python :: 3.10
44
- Classifier: Programming Language :: Python :: 3.11
45
- Classifier: Programming Language :: Python :: 3.12
46
- Classifier: License :: OSI Approved :: MIT License
47
- Classifier: Operating System :: OS Independent
48
- Requires-Python: >=3.8
49
- Description-Content-Type: text/markdown
50
- License-File: LICENSE
51
- Requires-Dist: python-dotenv==1.0.1
52
- Requires-Dist: rapidfuzz==3.12.1
53
- Requires-Dist: requests==2.32.3
54
- Requires-Dist: ytmusicapi==1.10.1
55
- Provides-Extra: dev
56
- Requires-Dist: pytest; extra == "dev"
57
-
58
- <p align="center">
59
- <img src="https://raw.githubusercontent.com/CheapNightbot/yutipy/main/docs/_static/yutipy_header.png" alt="yutipy" />
60
- </p>
61
-
62
- <p align="center">
63
- <a href="https://github.com/CheapNightbot/yutipy/actions/workflows/tests.yml">
64
- <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/cheapnightbot/yutipy/pytest-unit-testing.yml?style=for-the-badge&label=Pytest">
65
- </a>
66
-
67
- <a href="https://pypi.org/project/yutipy/">
68
- <img src="https://img.shields.io/pypi/v/yutipy?style=for-the-badge" alt="PyPI" />
69
- </a>
70
- <a href="https://yutipy.readthedocs.io/en/latest/">
71
- <img src="https://img.shields.io/readthedocs/yutipy?style=for-the-badge" alt="Documentation Status" />
72
- </a>
73
- <a href="https://github.com/CheapNightbot/yutipy/blob/master/LICENSE">
74
- <img src="https://img.shields.io/github/license/CheapNightbot/yutipy?style=for-the-badge" alt="License" />
75
- </a>
76
- <a href="https://github.com/CheapNightbot/yutipy/stargazers">
77
- <img src="https://img.shields.io/github/stars/CheapNightbot/yutipy?style=for-the-badge" alt="Stars" />
78
- </a>
79
- <a href="https://github.com/CheapNightbot/yutipy/issues">
80
- <img src="https://img.shields.io/github/issues/CheapNightbot/yutipy?style=for-the-badge" alt="Issues" />
81
- </a>
82
- </p>
83
-
84
- A _**simple**_ Python package for searching and retrieving music information from various music platforms APIs, including Deezer, iTunes, Spotify, and YouTube Music.
85
-
86
- ## Table of Contents
87
-
88
- - [Features](#features)
89
- - [Installation](#installation)
90
- - [Usage Example](#usage-example)
91
- - [Contributing](#contributing)
92
- - [License](#license)
93
-
94
- ## Features
95
-
96
- - Simple & Easy integration with popular music APIs.
97
- - Search for music by artist and song title across multiple platforms.
98
- - Retrieve detailed music information, including album art, release dates, ISRC, and UPC codes.
99
-
100
- ## Installation
101
-
102
- You can install the package using pip. Make sure you have Python 3.8 or higher installed.
103
-
104
- ```bash
105
- pip install -U yutipy
106
- ```
107
-
108
- ## Usage Example
109
-
110
- Here's a quick example of how to use the `yutipy` package to search for a song:
111
-
112
- ### Deezer
113
-
114
- ```python
115
- from yutipy.deezer import Deezer
116
-
117
- with Deezer() as deezer:
118
- result = deezer.search("Artist Name", "Song Title")
119
- print(result)
120
- ```
121
-
122
- For more usage examples, see the [Usage Examples](https://yutipy.readthedocs.io/en/latest/usage_examples.html) page in docs.
123
-
124
- ## Contributing
125
-
126
- Contributions are welcome! Please follow these steps:
127
-
128
- 1. Fork the repository.
129
- 2. Optionally, create an issue to discuss the changes you plan to make.
130
- 3. Create a new branch linked to that issue.
131
- 4. Make your changes in the new branch.
132
- 5. Write tests if you add new functionality.
133
- 6. Ensure all tests pass before opening a pull request.
134
- 7. Open a pull request for review.
135
-
136
- Thank you for your contributions!
137
-
138
- ## License
139
-
140
- This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
1
+ Metadata-Version: 2.2
2
+ Name: yutipy
3
+ Version: 1.1.0
4
+ Summary: A simple package for retrieving music information from various music platforms APIs.
5
+ Author: Cheap Nightbot
6
+ Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
7
+ Maintainer-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Cheap Nightbot
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/CheapNightbot/yutipy
31
+ Project-URL: Documentation, https://yutipy.readthedocs.io/
32
+ Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
33
+ Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
34
+ Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
35
+ Project-URL: funding, https://ko-fi.com/cheapnightbot
36
+ Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify
37
+ Classifier: Development Status :: 4 - Beta
38
+ Classifier: Intended Audience :: Developers
39
+ Classifier: Topic :: Software Development :: Libraries
40
+ Classifier: Programming Language :: Python :: 3
41
+ Classifier: Programming Language :: Python :: 3.8
42
+ Classifier: Programming Language :: Python :: 3.9
43
+ Classifier: Programming Language :: Python :: 3.10
44
+ Classifier: Programming Language :: Python :: 3.11
45
+ Classifier: Programming Language :: Python :: 3.12
46
+ Classifier: License :: OSI Approved :: MIT License
47
+ Classifier: Operating System :: OS Independent
48
+ Requires-Python: >=3.8
49
+ Description-Content-Type: text/markdown
50
+ License-File: LICENSE
51
+ Requires-Dist: python-dotenv==1.0.1
52
+ Requires-Dist: rapidfuzz==3.12.1
53
+ Requires-Dist: requests==2.32.3
54
+ Requires-Dist: ytmusicapi==1.10.1
55
+ Provides-Extra: dev
56
+ Requires-Dist: pytest; extra == "dev"
57
+
58
+ <p align="center">
59
+ <img src="https://raw.githubusercontent.com/CheapNightbot/yutipy/main/docs/_static/yutipy_header.png" alt="yutipy" />
60
+ </p>
61
+
62
+ <p align="center">
63
+ <a href="https://github.com/CheapNightbot/yutipy/actions/workflows/tests.yml">
64
+ <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/cheapnightbot/yutipy/pytest-unit-testing.yml?style=for-the-badge&label=Pytest">
65
+ </a>
66
+ <a href="https://pypi.org/project/yutipy/">
67
+ <img src="https://img.shields.io/pypi/v/yutipy?style=for-the-badge" alt="PyPI" />
68
+ </a>
69
+ <a href="https://yutipy.readthedocs.io/en/latest/">
70
+ <img src="https://img.shields.io/readthedocs/yutipy?style=for-the-badge" alt="Documentation Status" />
71
+ </a>
72
+ <a href="https://github.com/CheapNightbot/yutipy/blob/master/LICENSE">
73
+ <img src="https://img.shields.io/github/license/CheapNightbot/yutipy?style=for-the-badge" alt="License" />
74
+ </a>
75
+ <a href="https://github.com/CheapNightbot/yutipy/stargazers">
76
+ <img src="https://img.shields.io/github/stars/CheapNightbot/yutipy?style=for-the-badge" alt="Stars" />
77
+ </a>
78
+ <a href="https://github.com/CheapNightbot/yutipy/issues">
79
+ <img src="https://img.shields.io/github/issues/CheapNightbot/yutipy?style=for-the-badge" alt="Issues" />
80
+ </a>
81
+ </p>
82
+
83
+ A _**simple**_ Python package for searching and retrieving music information from various music platforms APIs, including Deezer, iTunes, Spotify, and YouTube Music.
84
+
85
+ ## Table of Contents
86
+
87
+ - [Features](#features)
88
+ - [Available Music Platforms](#available-music-platforms)
89
+ - [Installation](#installation)
90
+ - [Usage Example](#usage-example)
91
+ - [Contributing](#contributing)
92
+ - [License](#license)
93
+
94
+ ## Features
95
+
96
+ - Simple & Easy integration with popular music APIs.
97
+ - Search for music by artist and song title across multiple platforms.
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.
99
+ - Retrieve detailed music information, including album art, release dates, lyrics, ISRC, and UPC codes.
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
+
112
+ ## Installation
113
+
114
+ You can install the package using pip. Make sure you have Python 3.8 or higher installed.
115
+
116
+ ```bash
117
+ pip install -U yutipy
118
+ ```
119
+
120
+ ## Usage Example
121
+
122
+ Here's a quick example of how to use the `yutipy` package to search for a song:
123
+
124
+ ### Deezer
125
+
126
+ ```python
127
+ from yutipy.deezer import Deezer
128
+
129
+ with Deezer() as deezer:
130
+ result = deezer.search("Artist Name", "Song Title")
131
+ print(result)
132
+ ```
133
+
134
+ For more usage examples, see the [Usage Examples](https://yutipy.readthedocs.io/en/latest/usage_examples.html) page in docs.
135
+
136
+ ## Contributing
137
+
138
+ Contributions are welcome! Please follow these steps:
139
+
140
+ 1. Fork the repository.
141
+ 2. Optionally, create an issue to discuss the changes you plan to make.
142
+ 3. Create a new branch linked to that issue.
143
+ 4. Make your changes in the new branch.
144
+ 5. Write tests if you add new functionality.
145
+ 6. Ensure all tests pass before opening a pull request.
146
+ 7. Open a pull request for review.
147
+
148
+ Thank you for your contributions!
149
+
150
+ ## License
151
+
152
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,15 @@
1
+ yutipy/__init__.py,sha256=t0w-pRLjIGD3UtDyd-JpeL-RrMona7YTV_0lZCpGLEc,232
2
+ yutipy/deezer.py,sha256=TbPk6f1ytrtk9GHXOoRZ948fcgOi0619-rQC9xfTYu0,8641
3
+ yutipy/exceptions.py,sha256=4L0Oe1PwFP34LoFTy-Fruipk7uB-JkaackRmkjlaZJU,1138
4
+ yutipy/itunes.py,sha256=NhrllA3U0PUY3dNoMyiG1oZuyvdMQKYRBHzYSkt8ikM,5843
5
+ yutipy/kkbox.py,sha256=l-I1FZjKzLLqjN0hO2hvz6AdNuYvD1B4XI36m3sWmfM,10123
6
+ yutipy/models.py,sha256=RnfWjoNO1UoNsRvNRYd-4TpkU-4jHKu5t394vIqOOgw,1360
7
+ yutipy/musicyt.py,sha256=kvRYuhlNVuvy-WahJq895T3JAzmGTTiKB89r_uTMTAI,7155
8
+ yutipy/spotify.py,sha256=CZA9XF0EV5wSrLrN9cURtiFKSEHsIvqVlXLKkvb4JEM,13609
9
+ yutipy/utils/__init__.py,sha256=7UFcFZ7fBtNXOTngjnRD3MeobT3x5UT2Gag94TXVgLk,169
10
+ yutipy/utils/cheap_utils.py,sha256=LIuEHib_97NuLahXxdHJUD9v-ccXNUc3NrLYk8EQ52A,1652
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,23 +0,0 @@
1
- yutipy/__init__.py,sha256=t0w-pRLjIGD3UtDyd-JpeL-RrMona7YTV_0lZCpGLEc,232
2
- yutipy/deezer.py,sha256=TbPk6f1ytrtk9GHXOoRZ948fcgOi0619-rQC9xfTYu0,8641
3
- yutipy/exceptions.py,sha256=QDfOkaeZtSNg1lhXZ872yNJwbof68eVHDezZeZh8DiU,1029
4
- yutipy/itunes.py,sha256=NhrllA3U0PUY3dNoMyiG1oZuyvdMQKYRBHzYSkt8ikM,5843
5
- yutipy/models.py,sha256=-UEi05on5XsKjFSqUNt1yZnLod_A9upqh2nIPWTtEEI,1415
6
- yutipy/musicyt.py,sha256=kvRYuhlNVuvy-WahJq895T3JAzmGTTiKB89r_uTMTAI,7155
7
- yutipy/spotify.py,sha256=YYTF1git7mRVFWWGM1TILIIyZMPPDbD_F_BLjfqU6cI,12906
8
- yutipy/__pycache__/__init__.cpython-312.pyc,sha256=q-CaghM107NTOH0ZfH9LKDDUZJuGA_teF1QyoO1wNb8,392
9
- yutipy/__pycache__/deezer.cpython-312.pyc,sha256=UfRxyBobsocMxkpF4HP06EpFsAz96ZHU4QlYn8EzQcY,10588
10
- yutipy/__pycache__/exceptions.cpython-312.pyc,sha256=Zc73Tr79emWJ9pmpAVoL12YnBBOWJulV1hxtSso4hy8,2096
11
- yutipy/__pycache__/itunes.cpython-312.pyc,sha256=59fJpxL2ZjQoyzq_1U6sesJEuNrfa62kvtPYeSwzZNg,7521
12
- yutipy/__pycache__/models.cpython-312.pyc,sha256=KJUa3-C67TMq1EAGuQlZYT99JS1AEDUi74KY8bEHkkg,1848
13
- yutipy/__pycache__/musicyt.cpython-312.pyc,sha256=y508WR8Cl8DtisJli3FwNDEC6CJZajiZdcWJLN7xXyY,9176
14
- yutipy/__pycache__/spotify.cpython-312.pyc,sha256=0Knor0AN6zG6ATYSGL0YPb6b7rrT2krnpBv-e2ynam8,15870
15
- yutipy/utils/__init__.py,sha256=7UFcFZ7fBtNXOTngjnRD3MeobT3x5UT2Gag94TXVgLk,169
16
- yutipy/utils/cheap_utils.py,sha256=LIuEHib_97NuLahXxdHJUD9v-ccXNUc3NrLYk8EQ52A,1652
17
- yutipy/utils/__pycache__/__init__.cpython-312.pyc,sha256=_ldEMv66ELlrY1M7fN_2r5XySvgAsqTE0U8ASGXx8bs,286
18
- yutipy/utils/__pycache__/cheap_utils.cpython-312.pyc,sha256=i6nRmXcRtCkTzKQ1mWyjN7Xz4NapqgBdXA33so8xgLU,2376
19
- yutipy-0.1.1.dist-info/LICENSE,sha256=Tymbhvk1F_6BDFE0X8BBiQp7cBCjbzKfRAITmvxJIyA,1092
20
- yutipy-0.1.1.dist-info/METADATA,sha256=nWGLHD2t-1GvvU9vUXcU5TtvLe05_w2G0U8pKhWw_Uc,5865
21
- yutipy-0.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
- yutipy-0.1.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
23
- yutipy-0.1.1.dist-info/RECORD,,