yutipy 0.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/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .deezer import Deezer
2
+ from .itunes import Itunes
3
+ from .models import MusicInfo
4
+ from .musicyt import MusicYT
5
+ from .spotify import Spotipy
6
+
7
+ __all__ = [
8
+ "Deezer",
9
+ "Itunes",
10
+ "MusicInfo",
11
+ "MusicYT",
12
+ "Spotipy",
13
+ ]
yutipy/deezer.py ADDED
@@ -0,0 +1,277 @@
1
+ from pprint import pprint
2
+ from typing import Dict, List, Optional
3
+
4
+ import requests
5
+
6
+ from yutipy.exceptions import (
7
+ DeezerException,
8
+ InvalidResponseException,
9
+ InvalidValueException,
10
+ NetworkException,
11
+ )
12
+ from yutipy.models import MusicInfo
13
+ from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
14
+
15
+
16
+ class Deezer:
17
+ """A class to interact with the Deezer API."""
18
+
19
+ def __init__(self) -> None:
20
+ """Initializes the Deezer class and sets up the session."""
21
+ self._session = requests.Session()
22
+ self.api_url = "https://api.deezer.com"
23
+ self._is_session_closed = False
24
+
25
+ def __enter__(self) -> "Deezer":
26
+ """Enters the runtime context related to this object."""
27
+ return self
28
+
29
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
30
+ """Exits the runtime context related to this object."""
31
+ self._close_session()
32
+
33
+ def _close_session(self) -> None:
34
+ """Closes the current session."""
35
+ if not self.is_session_closed:
36
+ self._session.close()
37
+ self._is_session_closed = True
38
+
39
+ @property
40
+ def is_session_closed(self) -> bool:
41
+ """Checks if the session is closed."""
42
+ return self._is_session_closed
43
+
44
+ def search(self, artist: str, song: str) -> Optional[MusicInfo]:
45
+ """
46
+ Searches for a song by artist and title.
47
+
48
+ Parameters
49
+ ----------
50
+ artist : str
51
+ The name of the artist.
52
+ song : str
53
+ The title of the song.
54
+
55
+ Returns
56
+ -------
57
+ Optional[MusicInfo]
58
+ The music information if found, otherwise None.
59
+ """
60
+ if not is_valid_string(artist) or not is_valid_string(song):
61
+ raise InvalidValueException(
62
+ "Artist and song names must be valid strings and can't be empty."
63
+ )
64
+
65
+ search_types = ["track", "album"]
66
+
67
+ for search_type in search_types:
68
+ endpoint = f"{self.api_url}/search/{search_type}"
69
+ query = f'?q=artist:"{artist}" {search_type}:"{song}"&limit=10'
70
+ query_url = endpoint + query
71
+
72
+ try:
73
+ response = self._session.get(query_url, timeout=30)
74
+ response.raise_for_status()
75
+ except requests.RequestException as e:
76
+ raise NetworkException(f"Network error occurred: {e}")
77
+ except Exception as e:
78
+ raise DeezerException(f"An error occurred while searching Deezer: {e}")
79
+
80
+ try:
81
+ result = response.json()["data"]
82
+ except (IndexError, KeyError, ValueError) as e:
83
+ raise InvalidResponseException(f"Invalid response received: {e}")
84
+
85
+ music_info = self._parse_results(artist, song, result)
86
+ if music_info:
87
+ return music_info
88
+
89
+ return None
90
+
91
+ def _get_upc_isrc(self, music_id: int, music_type: str) -> Optional[Dict]:
92
+ """
93
+ Retrieves UPC and ISRC information for a given music ID and type.
94
+
95
+ Parameters
96
+ ----------
97
+ music_id : int
98
+ The ID of the music.
99
+ music_type : str
100
+ The type of the music (track or album).
101
+
102
+ Returns
103
+ -------
104
+ Optional[Dict]
105
+ A dictionary containing UPC and ISRC information.
106
+ """
107
+ match music_type:
108
+ case "track":
109
+ return self._get_track_info(music_id)
110
+ case "album":
111
+ return self._get_album_info(music_id)
112
+ case _:
113
+ raise DeezerException(f"Invalid music type: {music_type}")
114
+
115
+ def _get_track_info(self, music_id: int) -> Optional[Dict]:
116
+ """
117
+ Retrieves track information for a given track ID.
118
+
119
+ Parameters
120
+ ----------
121
+ music_id : int
122
+ The ID of the track.
123
+
124
+ Returns
125
+ -------
126
+ Optional[Dict]
127
+ A dictionary containing track information.
128
+ """
129
+ query_url = f"{self.api_url}/track/{music_id}"
130
+ try:
131
+ response = self._session.get(query_url, timeout=30)
132
+ response.raise_for_status()
133
+ except requests.RequestException as e:
134
+ raise NetworkException(f"Network error occurred: {e}")
135
+ except Exception as e:
136
+ raise DeezerException(f"An error occurred while fetching track info: {e}")
137
+
138
+ try:
139
+ result = response.json()
140
+ except ValueError as e:
141
+ raise InvalidResponseException(f"Invalid response received: {e}")
142
+
143
+ return {
144
+ "isrc": result.get("isrc"),
145
+ "release_date": result.get("release_date"),
146
+ "tempo": result.get("bpm"),
147
+ }
148
+
149
+ def _get_album_info(self, music_id: int) -> Optional[Dict]:
150
+ """
151
+ Retrieves album information for a given album ID.
152
+
153
+ Parameters
154
+ ----------
155
+ music_id : int
156
+ The ID of the album.
157
+
158
+ Returns
159
+ -------
160
+ Optional[Dict]
161
+ A dictionary containing album information.
162
+ """
163
+ query_url = f"{self.api_url}/album/{music_id}"
164
+ try:
165
+ response = self._session.get(query_url, timeout=30)
166
+ response.raise_for_status()
167
+ except requests.RequestException as e:
168
+ raise NetworkException(f"Network error occurred: {e}")
169
+ except Exception as e:
170
+ raise DeezerException(f"An error occurred while fetching album info: {e}")
171
+
172
+ try:
173
+ result = response.json()
174
+ except ValueError as e:
175
+ raise InvalidResponseException(f"Invalid response received: {e}")
176
+
177
+ return {
178
+ "genre": (
179
+ result["genres"]["data"][0]["name"]
180
+ if result["genres"]["data"]
181
+ else None
182
+ ),
183
+ "release_date": result.get("release_date"),
184
+ "upc": result.get("upc"),
185
+ }
186
+
187
+ def _parse_results(
188
+ self, artist: str, song: str, results: List[Dict]
189
+ ) -> Optional[MusicInfo]:
190
+ """
191
+ Parses the search results to find a matching song.
192
+
193
+ Parameters
194
+ ----------
195
+ artist : str
196
+ The name of the artist.
197
+ song : str
198
+ The title of the song.
199
+ results : List[Dict]
200
+ The search results from the API.
201
+
202
+ Returns
203
+ -------
204
+ Optional[MusicInfo]
205
+ The music information if a match is found, otherwise None.
206
+ """
207
+ for result in results:
208
+ if not (
209
+ are_strings_similar(result["title"], song)
210
+ and are_strings_similar(result["artist"]["name"], artist)
211
+ ):
212
+ continue
213
+
214
+ return self._extract_music_info(result)
215
+
216
+ return None
217
+
218
+ def _extract_music_info(self, result: Dict) -> MusicInfo:
219
+ """
220
+ Extracts music information from a search result.
221
+
222
+ Parameters
223
+ ----------
224
+ result : Dict
225
+ A single search result from the API.
226
+
227
+ Returns
228
+ -------
229
+ MusicInfo
230
+ The extracted music information.
231
+ """
232
+ music_type = result["type"]
233
+ music_info = MusicInfo(
234
+ album_art=(
235
+ result["album"]["cover_xl"]
236
+ if music_type == "track"
237
+ else result["cover_xl"]
238
+ ),
239
+ album_title=(
240
+ result["album"]["title"] if music_type == "track" else result["title"]
241
+ ),
242
+ album_type=result.get("record_type", music_type),
243
+ artists=result["artist"]["name"],
244
+ genre=None,
245
+ id=result["id"],
246
+ isrc=None,
247
+ lyrics=None,
248
+ release_date=None,
249
+ tempo=None,
250
+ title=result["title"],
251
+ type=music_type,
252
+ upc=None,
253
+ url=result["link"],
254
+ )
255
+
256
+ if music_type == "track":
257
+ track_info = self._get_upc_isrc(result["id"], music_type)
258
+ music_info.isrc = track_info.get("isrc")
259
+ music_info.release_date = track_info.get("release_date")
260
+ music_info.tempo = track_info.get("tempo")
261
+ else:
262
+ album_info = self._get_upc_isrc(result["id"], music_type)
263
+ music_info.upc = album_info.get("upc")
264
+ music_info.release_date = album_info.get("release_date")
265
+ music_info.genre = album_info.get("genre")
266
+
267
+ return music_info
268
+
269
+
270
+ if __name__ == "__main__":
271
+ deezer = Deezer()
272
+ try:
273
+ artist_name = input("Artist Name: ")
274
+ song_name = input("Song Name: ")
275
+ pprint(deezer.search(artist_name, song_name))
276
+ finally:
277
+ deezer._close_session()
yutipy/exceptions.py ADDED
@@ -0,0 +1,52 @@
1
+ class YutipyException(Exception):
2
+ """Base class for exceptions in the Yutipy package."""
3
+
4
+ pass
5
+
6
+
7
+ class InvalidValueException(YutipyException):
8
+ """Exception raised for invalid values."""
9
+
10
+ pass
11
+
12
+
13
+ class DeezerException(YutipyException):
14
+ """Exception raised for errors related to the Deezer API."""
15
+
16
+ pass
17
+
18
+
19
+ class ItunesException(YutipyException):
20
+ """Exception raised for errors related to the iTunes API."""
21
+
22
+ pass
23
+
24
+
25
+ class SpotifyException(YutipyException):
26
+ """Exception raised for errors related to the Spotify API."""
27
+
28
+ pass
29
+
30
+
31
+ class MusicYTException(YutipyException):
32
+ """Exception raised for errors related to the YouTube Music API."""
33
+
34
+ pass
35
+
36
+
37
+ class AuthenticationException(YutipyException):
38
+ """Exception raised for authentication errors."""
39
+
40
+ pass
41
+
42
+
43
+ class NetworkException(YutipyException):
44
+ """Exception raised for network-related errors."""
45
+
46
+ pass
47
+
48
+
49
+ class InvalidResponseException(YutipyException):
50
+ """Exception raised for invalid responses from APIs."""
51
+
52
+ pass
yutipy/itunes.py ADDED
@@ -0,0 +1,189 @@
1
+ from datetime import datetime
2
+ from pprint import pprint
3
+ from typing import Optional, Dict
4
+
5
+ import requests
6
+
7
+ from yutipy.exceptions import (
8
+ InvalidResponseException,
9
+ InvalidValueException,
10
+ NetworkException,
11
+ ItunesException,
12
+ )
13
+ from yutipy.models import MusicInfo
14
+ from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
15
+
16
+
17
+ class Itunes:
18
+ """A class to interact with the iTunes API."""
19
+
20
+ def __init__(self) -> None:
21
+ """Initializes the iTunes class and sets up the session."""
22
+ self._session = requests.Session()
23
+ self.api_url = "https://itunes.apple.com"
24
+ self._is_session_closed = False
25
+
26
+ def __enter__(self) -> "Itunes":
27
+ """Enters the runtime context related to this object."""
28
+ return self
29
+
30
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
31
+ """Exits the runtime context related to this object."""
32
+ self._close_session()
33
+
34
+ def _close_session(self) -> None:
35
+ """Closes the current session."""
36
+ if not self.is_session_closed:
37
+ self._session.close()
38
+ self._is_session_closed = True
39
+
40
+ @property
41
+ def is_session_closed(self) -> bool:
42
+ """Checks if the session is closed."""
43
+ return self._is_session_closed
44
+
45
+ def search(self, artist: str, song: str) -> Optional[MusicInfo]:
46
+ """
47
+ Searches for a song by artist and title.
48
+
49
+ Parameters
50
+ ----------
51
+ artist : str
52
+ The name of the artist.
53
+ song : str
54
+ The title of the song.
55
+
56
+ Returns
57
+ -------
58
+ Optional[MusicInfo]
59
+ The music information if found, otherwise None.
60
+ """
61
+ if not is_valid_string(artist) or not is_valid_string(song):
62
+ raise InvalidValueException(
63
+ "Artist and song names must be valid strings and can't be empty."
64
+ )
65
+
66
+ entities = ["song", "album"]
67
+ for entity in entities:
68
+ endpoint = f"{self.api_url}/search"
69
+ query = f"?term={artist} - {song}&media=music&entity={entity}&limit=10"
70
+ query_url = endpoint + query
71
+
72
+ try:
73
+ response = self._session.get(query_url, timeout=30)
74
+ response.raise_for_status()
75
+ except requests.RequestException as e:
76
+ raise NetworkException(f"Network error occurred: {e}")
77
+ except Exception as e:
78
+ raise ItunesException(f"An error occurred while searching iTunes: {e}")
79
+
80
+ try:
81
+ result = response.json()["results"]
82
+ except (IndexError, KeyError, ValueError) as e:
83
+ raise InvalidResponseException(f"Invalid response received: {e}")
84
+
85
+ music_info = self._parse_result(artist, song, result)
86
+ if music_info:
87
+ return music_info
88
+
89
+ return None
90
+
91
+ def _parse_result(
92
+ self, artist: str, song: str, results: list[dict]
93
+ ) -> Optional[MusicInfo]:
94
+ """
95
+ Parses the search results to find a matching song.
96
+
97
+ Parameters
98
+ ----------
99
+ artist : str
100
+ The name of the artist.
101
+ song : str
102
+ The title of the song.
103
+ results : list
104
+ The search results from the API.
105
+
106
+ Returns
107
+ -------
108
+ Optional[MusicInfo]
109
+ The music information if a match is found, otherwise None.
110
+ """
111
+ for result in results:
112
+ if not (
113
+ are_strings_similar(
114
+ result.get("trackName", result["collectionName"]), song
115
+ )
116
+ and are_strings_similar(result["artistName"], artist)
117
+ ):
118
+ continue
119
+
120
+ album_title, album_type = self._extract_album_info(result)
121
+ release_date = self._format_release_date(result["releaseDate"])
122
+
123
+ return MusicInfo(
124
+ album_art=result["artworkUrl100"],
125
+ album_title=album_title,
126
+ album_type=album_type.lower(),
127
+ artists=result["artistName"],
128
+ genre=result["primaryGenreName"],
129
+ id=result.get("trackId", result["collectionId"]),
130
+ isrc=None,
131
+ lyrics=None,
132
+ release_date=release_date,
133
+ tempo=None,
134
+ title=result.get("trackName", album_title),
135
+ type=result["wrapperType"],
136
+ upc=None,
137
+ url=result.get("trackViewUrl", result["collectionViewUrl"]),
138
+ )
139
+
140
+ return None
141
+
142
+ def _extract_album_info(self, result: dict) -> tuple:
143
+ """
144
+ Extracts album information from a search result.
145
+
146
+ Parameters
147
+ ----------
148
+ result : dict
149
+ A single search result from the API.
150
+
151
+ Returns
152
+ -------
153
+ tuple
154
+ The extracted album title and type.
155
+ """
156
+ try:
157
+ album_title, album_type = result["collectionName"].split("-")
158
+ return album_title.strip(), album_type.strip()
159
+ except ValueError:
160
+ return result["collectionName"], result["wrapperType"]
161
+
162
+ def _format_release_date(self, release_date: str) -> str:
163
+ """
164
+ Formats the release date to a standard format.
165
+
166
+ Parameters
167
+ ----------
168
+ release_date : str
169
+ The release date from the API.
170
+
171
+ Returns
172
+ -------
173
+ str
174
+ The formatted release date.
175
+ """
176
+ return datetime.strptime(release_date, "%Y-%m-%dT%H:%M:%SZ").strftime(
177
+ "%Y-%m-%d"
178
+ )
179
+
180
+
181
+ if __name__ == "__main__":
182
+ itunes = Itunes()
183
+
184
+ try:
185
+ artist_name = input("Artist Name: ")
186
+ song_name = input("Song Name: ")
187
+ pprint(itunes.search(artist_name, song_name))
188
+ finally:
189
+ itunes._close_session()
yutipy/models.py ADDED
@@ -0,0 +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