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 +13 -0
- yutipy/__pycache__/__init__.cpython-312.pyc +0 -0
- yutipy/__pycache__/deezer.cpython-312.pyc +0 -0
- yutipy/__pycache__/exceptions.cpython-312.pyc +0 -0
- yutipy/__pycache__/itunes.cpython-312.pyc +0 -0
- yutipy/__pycache__/models.cpython-312.pyc +0 -0
- yutipy/__pycache__/musicyt.cpython-312.pyc +0 -0
- yutipy/__pycache__/spotify.cpython-312.pyc +0 -0
- yutipy/deezer.py +277 -0
- yutipy/exceptions.py +52 -0
- yutipy/itunes.py +189 -0
- yutipy/models.py +55 -0
- yutipy/musicyt.py +247 -0
- yutipy/spotify.py +413 -0
- yutipy/utils/__init__.py +7 -0
- yutipy/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- yutipy/utils/__pycache__/cheap_utils.cpython-312.pyc +0 -0
- yutipy/utils/cheap_utils.py +50 -0
- yutipy-0.1.0.dist-info/LICENSE +21 -0
- yutipy-0.1.0.dist-info/METADATA +119 -0
- yutipy-0.1.0.dist-info/RECORD +23 -0
- yutipy-0.1.0.dist-info/WHEEL +5 -0
- yutipy-0.1.0.dist-info/top_level.txt +1 -0
yutipy/musicyt.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pprint import pprint
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ytmusicapi import YTMusic, exceptions
|
|
6
|
+
|
|
7
|
+
from yutipy.exceptions import (
|
|
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 MusicYT:
|
|
17
|
+
"""A class to interact with the YouTube Music API."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""Initializes the YouTube Music class and sets up the session."""
|
|
21
|
+
self.ytmusic = YTMusic()
|
|
22
|
+
|
|
23
|
+
def search(self, artist: str, song: str) -> Optional[MusicInfo]:
|
|
24
|
+
"""
|
|
25
|
+
Searches for a song by artist and title.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
artist : str
|
|
30
|
+
The name of the artist.
|
|
31
|
+
song : str
|
|
32
|
+
The title of the song.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Optional[MusicInfo]
|
|
37
|
+
The music information if found, otherwise None.
|
|
38
|
+
"""
|
|
39
|
+
if not is_valid_string(artist) or not is_valid_string(song):
|
|
40
|
+
raise InvalidValueException(
|
|
41
|
+
"Artist and song names must be valid strings and can't be empty."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
query = f"{artist} - {song}"
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
results = self.ytmusic.search(query=query)
|
|
48
|
+
except exceptions.YTMusicServerError as e:
|
|
49
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
50
|
+
|
|
51
|
+
for result in results:
|
|
52
|
+
if self._is_relevant_result(artist, song, result):
|
|
53
|
+
return self._process_result(result)
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def _is_relevant_result(self, artist: str, song: str, result: dict) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Determine if a search result is relevant.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
artist : str
|
|
64
|
+
The name of the artist.
|
|
65
|
+
song : str
|
|
66
|
+
The title of the song.
|
|
67
|
+
result : dict
|
|
68
|
+
The search result from the API.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
bool
|
|
73
|
+
Whether the result is relevant.
|
|
74
|
+
"""
|
|
75
|
+
if self._skip_categories(result):
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
return any(
|
|
79
|
+
are_strings_similar(result.get("title"), song)
|
|
80
|
+
and are_strings_similar(_artist.get("name"), artist)
|
|
81
|
+
for _artist in result.get("artists", [])
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _skip_categories(self, result: dict) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Skip certain categories in search results.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
result : dict
|
|
91
|
+
The search result from the API.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
bool
|
|
96
|
+
Return `True` if the result should be skipped, else `False`.
|
|
97
|
+
"""
|
|
98
|
+
categories_skip = [
|
|
99
|
+
"artists",
|
|
100
|
+
"community playlists",
|
|
101
|
+
"featured playlists",
|
|
102
|
+
"podcasts",
|
|
103
|
+
"profiles",
|
|
104
|
+
"uploads",
|
|
105
|
+
"episode",
|
|
106
|
+
"episodes",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
result.get("category", "").lower() in categories_skip
|
|
111
|
+
or result.get("resultType", "").lower() in categories_skip
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _process_result(self, result: dict) -> MusicInfo:
|
|
115
|
+
"""
|
|
116
|
+
Process the search result and return relevant information as `MusicInfo`.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
result : dict
|
|
121
|
+
The search result from the API.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
MusicInfo
|
|
126
|
+
The extracted music information.
|
|
127
|
+
"""
|
|
128
|
+
if result["resultType"] in ["song", "video"]:
|
|
129
|
+
return self._get_song(result)
|
|
130
|
+
else:
|
|
131
|
+
return self._get_album(result)
|
|
132
|
+
|
|
133
|
+
def _get_song(self, result: dict) -> MusicInfo:
|
|
134
|
+
"""
|
|
135
|
+
Return song info as a `MusicInfo` object.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
result : dict
|
|
140
|
+
The search result from the API.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
MusicInfo
|
|
145
|
+
The extracted music information.
|
|
146
|
+
"""
|
|
147
|
+
title = result["title"]
|
|
148
|
+
artist_names = ", ".join([artist["name"] for artist in result["artists"]])
|
|
149
|
+
video_id = result["videoId"]
|
|
150
|
+
song_url = f"https://music.youtube.com/watch?v={video_id}"
|
|
151
|
+
lyrics_id = self.ytmusic.get_watch_playlist(video_id)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
song_data = self.ytmusic.get_song(video_id)
|
|
155
|
+
release_date = (
|
|
156
|
+
song_data.get("microformat", {})
|
|
157
|
+
.get("microformatDataRenderer", {})
|
|
158
|
+
.get("uploadDate", "")
|
|
159
|
+
.split("T")[0]
|
|
160
|
+
)
|
|
161
|
+
except (exceptions.YTMusicServerError, exceptions.YTMusicError) as e:
|
|
162
|
+
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
lyrics = self.ytmusic.get_lyrics(lyrics_id.get("lyrics"))
|
|
166
|
+
except exceptions.YTMusicUserError:
|
|
167
|
+
lyrics = {}
|
|
168
|
+
|
|
169
|
+
album_art = result.get("thumbnails", [{}])[-1].get("url", None)
|
|
170
|
+
|
|
171
|
+
return MusicInfo(
|
|
172
|
+
album_art=album_art,
|
|
173
|
+
album_title=None,
|
|
174
|
+
album_type="single",
|
|
175
|
+
artists=artist_names,
|
|
176
|
+
genre=None,
|
|
177
|
+
id=video_id,
|
|
178
|
+
isrc=None,
|
|
179
|
+
lyrics=lyrics.get("lyrics"),
|
|
180
|
+
release_date=release_date,
|
|
181
|
+
tempo=None,
|
|
182
|
+
title=title,
|
|
183
|
+
type="song",
|
|
184
|
+
upc=None,
|
|
185
|
+
url=song_url,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _get_album(self, result: dict) -> MusicInfo:
|
|
189
|
+
"""
|
|
190
|
+
Return album info as a `MusicInfo` object.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
result : dict
|
|
195
|
+
The search result from the API.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
MusicInfo
|
|
200
|
+
The extracted music information.
|
|
201
|
+
"""
|
|
202
|
+
title = result["title"]
|
|
203
|
+
artist_names = ", ".join([artist["name"] for artist in result["artists"]])
|
|
204
|
+
browse_id = result["browseId"]
|
|
205
|
+
album_url = f"https://music.youtube.com/browse/{browse_id}"
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
album_data = self.ytmusic.get_album(browse_id)
|
|
209
|
+
release_date = album_data["year"]
|
|
210
|
+
except (exceptions.YTMusicServerError, exceptions.YTMusicError) as e:
|
|
211
|
+
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
lyrics_id = self.ytmusic.get_watch_playlist(browse_id)
|
|
215
|
+
lyrics = self.ytmusic.get_lyrics(lyrics_id.get("lyrics"))
|
|
216
|
+
except (exceptions.YTMusicServerError, exceptions.YTMusicUserError):
|
|
217
|
+
lyrics = {}
|
|
218
|
+
|
|
219
|
+
album_art = result.get("thumbnails", [{}])[-1].get(
|
|
220
|
+
"url", album_data.get("thumbnails", [{}])[-1].get("url", None)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return MusicInfo(
|
|
224
|
+
album_art=album_art,
|
|
225
|
+
album_title=title,
|
|
226
|
+
album_type="Album",
|
|
227
|
+
artists=artist_names,
|
|
228
|
+
genre=None,
|
|
229
|
+
id=browse_id,
|
|
230
|
+
isrc=None,
|
|
231
|
+
lyrics=lyrics.get("lyrics"),
|
|
232
|
+
release_date=release_date,
|
|
233
|
+
tempo=None,
|
|
234
|
+
title=title,
|
|
235
|
+
type="album",
|
|
236
|
+
upc=None,
|
|
237
|
+
url=album_url,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
music_yt = MusicYT()
|
|
243
|
+
|
|
244
|
+
artist_name = input("Artist Name: ")
|
|
245
|
+
song_name = input("Song Name: ")
|
|
246
|
+
|
|
247
|
+
pprint(music_yt.search(artist_name, song_name))
|
yutipy/spotify.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from pprint import pprint
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from yutipy.exceptions import (
|
|
11
|
+
AuthenticationException,
|
|
12
|
+
InvalidResponseException,
|
|
13
|
+
InvalidValueException,
|
|
14
|
+
NetworkException,
|
|
15
|
+
SpotifyException,
|
|
16
|
+
)
|
|
17
|
+
from yutipy.models import MusicInfo
|
|
18
|
+
from yutipy.utils.cheap_utils import (
|
|
19
|
+
are_strings_similar,
|
|
20
|
+
is_valid_string,
|
|
21
|
+
separate_artists,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
|
|
26
|
+
CLIENT_ID = os.getenv("CLIENT_ID")
|
|
27
|
+
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Spotipy:
|
|
31
|
+
"""
|
|
32
|
+
A class to interact with the Spotify API.
|
|
33
|
+
|
|
34
|
+
This class reads the CLIENT_ID and CLIENT_SECRET from the .env file by default.
|
|
35
|
+
Alternatively, users can manually provide these values when creating an object.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, client_id: str = CLIENT_ID, client_secret: str = CLIENT_SECRET
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Initializes the Spotipy class and sets up the session.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
client_id : str, optional
|
|
47
|
+
The client ID for the Spotify API. Defaults to CLIENT_ID from .env file.
|
|
48
|
+
client_secret : str, optional
|
|
49
|
+
The client secret for the Spotify API. Defaults to CLIENT_SECRET from .env file.
|
|
50
|
+
"""
|
|
51
|
+
if not client_id or not client_secret:
|
|
52
|
+
raise SpotifyException(
|
|
53
|
+
"Failed to read CLIENT_ID and CLIENT_SECRET from environment variables. Client ID and Client Secret must be provided."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.client_id = client_id
|
|
57
|
+
self.client_secret = client_secret
|
|
58
|
+
self._session = requests.Session()
|
|
59
|
+
self.api_url = "https://api.spotify.com/v1"
|
|
60
|
+
self.__header = self.__authenticate()
|
|
61
|
+
self.__start_time = time.time()
|
|
62
|
+
self._is_session_closed = False
|
|
63
|
+
|
|
64
|
+
def __enter__(self):
|
|
65
|
+
"""Enters the runtime context related to this object."""
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
69
|
+
"""Exits the runtime context related to this object."""
|
|
70
|
+
self._close_session()
|
|
71
|
+
|
|
72
|
+
def _close_session(self) -> None:
|
|
73
|
+
"""Closes the current session."""
|
|
74
|
+
if not self.is_session_closed:
|
|
75
|
+
self._session.close()
|
|
76
|
+
self._is_session_closed = True
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_session_closed(self) -> bool:
|
|
80
|
+
"""Checks if the session is closed."""
|
|
81
|
+
return self._is_session_closed
|
|
82
|
+
|
|
83
|
+
def __authenticate(self) -> dict:
|
|
84
|
+
"""
|
|
85
|
+
Authenticates with the Spotify API and returns the authorization header.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
dict
|
|
90
|
+
The authorization header.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
token = self.__get_spotify_token()
|
|
94
|
+
return {"Authorization": f"Bearer {token}"}
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise AuthenticationException(
|
|
97
|
+
"Failed to authenticate with Spotify API"
|
|
98
|
+
) from e
|
|
99
|
+
|
|
100
|
+
def __get_spotify_token(self) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Gets the Spotify API token.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
str
|
|
107
|
+
The Spotify API token.
|
|
108
|
+
"""
|
|
109
|
+
auth_string = f"{self.client_id}:{self.client_secret}"
|
|
110
|
+
auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
111
|
+
|
|
112
|
+
url = "https://accounts.spotify.com/api/token"
|
|
113
|
+
headers = {
|
|
114
|
+
"Authorization": f"Basic {auth_base64}",
|
|
115
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
116
|
+
}
|
|
117
|
+
data = {"grant_type": "client_credentials"}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
response = self._session.post(
|
|
121
|
+
url=url, headers=headers, data=data, timeout=30
|
|
122
|
+
)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
except requests.RequestException as e:
|
|
125
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
return response.json().get("access_token")
|
|
129
|
+
except (KeyError, ValueError) as e:
|
|
130
|
+
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
131
|
+
|
|
132
|
+
def __refresh_token_if_expired(self):
|
|
133
|
+
"""Refreshes the token if it has expired."""
|
|
134
|
+
if time.time() - self.__start_time >= 3600:
|
|
135
|
+
self.__header = self.__authenticate()
|
|
136
|
+
|
|
137
|
+
def search(self, artist: str, song: str) -> 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
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
Optional[MusicInfo]
|
|
151
|
+
The music information if found, otherwise None.
|
|
152
|
+
"""
|
|
153
|
+
if not is_valid_string(artist) or not is_valid_string(song):
|
|
154
|
+
raise InvalidValueException(
|
|
155
|
+
"Artist and song names must be valid strings and can't be empty."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self.__refresh_token_if_expired()
|
|
159
|
+
|
|
160
|
+
query = f"?q=artist:{artist} track:{song}&type=track&limit=10"
|
|
161
|
+
query_url = f"{self.api_url}/search{query}"
|
|
162
|
+
|
|
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}")
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
result = response.json()["tracks"]["items"]
|
|
171
|
+
except (KeyError, ValueError) as e:
|
|
172
|
+
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
173
|
+
|
|
174
|
+
return self._parse_results(artist, song, result)
|
|
175
|
+
|
|
176
|
+
def search_advanced(
|
|
177
|
+
self, artist: str, song: str, isrc: str = None, upc: str = None
|
|
178
|
+
) -> Optional[MusicInfo]:
|
|
179
|
+
"""
|
|
180
|
+
Searches for a song by artist, title, ISRC, or UPC.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
artist : str
|
|
185
|
+
The name of the artist.
|
|
186
|
+
song : str
|
|
187
|
+
The title of the song.
|
|
188
|
+
isrc : str, optional
|
|
189
|
+
The ISRC of the track.
|
|
190
|
+
upc : str, optional
|
|
191
|
+
The UPC of the album.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
Optional[MusicInfo]
|
|
196
|
+
The music information if found, otherwise None.
|
|
197
|
+
"""
|
|
198
|
+
if not is_valid_string(artist) or not is_valid_string(song):
|
|
199
|
+
raise InvalidValueException(
|
|
200
|
+
"Artist and song names must be valid strings and can't be empty."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.__refresh_token_if_expired()
|
|
204
|
+
|
|
205
|
+
if isrc:
|
|
206
|
+
query = f"?q={artist} {song} isrc:{isrc}&type=track&limit=1"
|
|
207
|
+
elif upc:
|
|
208
|
+
query = f"?q={artist} {song} upc:{upc}&type=album&limit=1"
|
|
209
|
+
else:
|
|
210
|
+
raise InvalidValueException("ISRC or UPC must be provided.")
|
|
211
|
+
|
|
212
|
+
query_url = f"{self.api_url}/search{query}"
|
|
213
|
+
try:
|
|
214
|
+
response = self._session.get(query_url, headers=self.__header, timeout=30)
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
except requests.RequestException as e:
|
|
217
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
218
|
+
|
|
219
|
+
if response.status_code != 200:
|
|
220
|
+
raise SpotifyException(
|
|
221
|
+
f"Failed to search music with ISRC/UPC: {response.json()}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
artist_ids = self._get_artists_ids(artist)
|
|
225
|
+
return self._find_music_info(artist, song, response.json(), artist_ids)
|
|
226
|
+
|
|
227
|
+
def _get_artists_ids(self, artist: str) -> list | None:
|
|
228
|
+
"""
|
|
229
|
+
Retrieves the IDs of the artists.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
artist : str
|
|
234
|
+
The name of the artist.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
list | None
|
|
239
|
+
A list of artist IDs or None if not found.
|
|
240
|
+
"""
|
|
241
|
+
artist_ids = []
|
|
242
|
+
for name in separate_artists(artist):
|
|
243
|
+
query_url = f"{self.api_url}/search?q={name}&type=artist&limit=5"
|
|
244
|
+
try:
|
|
245
|
+
response = self._session.get(
|
|
246
|
+
query_url, headers=self.__header, timeout=30
|
|
247
|
+
)
|
|
248
|
+
response.raise_for_status()
|
|
249
|
+
except requests.RequestException as e:
|
|
250
|
+
raise NetworkException(f"Network error occurred: {e}")
|
|
251
|
+
|
|
252
|
+
if response.status_code != 200:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
artist_ids.extend(
|
|
256
|
+
artist["id"] for artist in response.json()["artists"]["items"]
|
|
257
|
+
)
|
|
258
|
+
return artist_ids
|
|
259
|
+
|
|
260
|
+
def _find_music_info(
|
|
261
|
+
self, artist: str, song: str, response_json: dict, artist_ids: list
|
|
262
|
+
) -> Optional[MusicInfo]:
|
|
263
|
+
"""
|
|
264
|
+
Finds the music information from the search results.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
artist : str
|
|
269
|
+
The name of the artist.
|
|
270
|
+
song : str
|
|
271
|
+
The title of the song.
|
|
272
|
+
response_json : dict
|
|
273
|
+
The JSON response from the API.
|
|
274
|
+
artist_ids : list
|
|
275
|
+
A list of artist IDs.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
Optional[MusicInfo]
|
|
280
|
+
The music information if found, otherwise None.
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
for track in response_json["tracks"]["items"]:
|
|
284
|
+
music_info = self._find_tracks(song, artist, track, artist_ids)
|
|
285
|
+
if music_info:
|
|
286
|
+
return music_info
|
|
287
|
+
except KeyError:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
for album in response_json["albums"]["items"]:
|
|
292
|
+
music_info = self._find_album(song, artist, album, artist_ids)
|
|
293
|
+
if music_info:
|
|
294
|
+
return music_info
|
|
295
|
+
except KeyError:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
def _find_tracks(
|
|
301
|
+
self, song: str, artist: str, track: dict, artist_ids: list
|
|
302
|
+
) -> Optional[MusicInfo]:
|
|
303
|
+
"""
|
|
304
|
+
Finds the track information from the search results.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
song : str
|
|
309
|
+
The title of the song.
|
|
310
|
+
artist : str
|
|
311
|
+
The name of the artist.
|
|
312
|
+
track : dict
|
|
313
|
+
A single track from the search results.
|
|
314
|
+
artist_ids : list
|
|
315
|
+
A list of artist IDs.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
Optional[MusicInfo]
|
|
320
|
+
The music information if found, otherwise None.
|
|
321
|
+
"""
|
|
322
|
+
if not are_strings_similar(track["name"], song):
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
artists_name = [x["name"] for x in track["artists"]]
|
|
326
|
+
matching_artists = [
|
|
327
|
+
x["name"]
|
|
328
|
+
for x in track["artists"]
|
|
329
|
+
if are_strings_similar(x["name"], artist) or x["id"] in artist_ids
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
if matching_artists:
|
|
333
|
+
return MusicInfo(
|
|
334
|
+
album_art=track["album"]["images"][0]["url"],
|
|
335
|
+
album_title=track["album"]["name"],
|
|
336
|
+
album_type=track["album"]["album_type"],
|
|
337
|
+
artists=", ".join(artists_name),
|
|
338
|
+
genre=None,
|
|
339
|
+
id=track["id"],
|
|
340
|
+
isrc=track.get("external_ids").get("isrc"),
|
|
341
|
+
lyrics=None,
|
|
342
|
+
release_date=track["album"]["release_date"],
|
|
343
|
+
tempo=None,
|
|
344
|
+
title=track["name"],
|
|
345
|
+
type="track",
|
|
346
|
+
upc=None,
|
|
347
|
+
url=track["external_urls"]["spotify"],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def _find_album(
|
|
353
|
+
self, song: str, artist: str, album: dict, artist_ids: list
|
|
354
|
+
) -> Optional[MusicInfo]:
|
|
355
|
+
"""
|
|
356
|
+
Finds the album information from the search results.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
song : str
|
|
361
|
+
The title of the song.
|
|
362
|
+
artist : str
|
|
363
|
+
The name of the artist.
|
|
364
|
+
album : dict
|
|
365
|
+
A single album from the search results.
|
|
366
|
+
artist_ids : list
|
|
367
|
+
A list of artist IDs.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
Optional[MusicInfo]
|
|
372
|
+
The music information if found, otherwise None.
|
|
373
|
+
"""
|
|
374
|
+
if not are_strings_similar(album["name"], song):
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
artists_name = [x["name"] for x in album["artists"]]
|
|
378
|
+
matching_artists = [
|
|
379
|
+
x["name"]
|
|
380
|
+
for x in album["artists"]
|
|
381
|
+
if are_strings_similar(x["name"], artist) or x["id"] in artist_ids
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
if matching_artists:
|
|
385
|
+
return MusicInfo(
|
|
386
|
+
album_art=album["images"][0]["url"],
|
|
387
|
+
album_title=album["name"],
|
|
388
|
+
album_type=album["album_type"],
|
|
389
|
+
artists=", ".join(artists_name),
|
|
390
|
+
genre=None,
|
|
391
|
+
id=album["id"],
|
|
392
|
+
isrc=None,
|
|
393
|
+
lyrics=None,
|
|
394
|
+
release_date=album["release_date"],
|
|
395
|
+
tempo=None,
|
|
396
|
+
title=album["name"],
|
|
397
|
+
type="album",
|
|
398
|
+
upc=None,
|
|
399
|
+
url=album["external_urls"]["spotify"],
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
spotipy = Spotipy(CLIENT_ID, CLIENT_SECRET)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
artist_name = input("Artist Name: ")
|
|
410
|
+
song_name = input("Song Name: ")
|
|
411
|
+
pprint(spotipy.search(artist_name, song_name))
|
|
412
|
+
finally:
|
|
413
|
+
spotipy._close_session()
|
yutipy/utils/__init__.py
ADDED
|
Binary file
|
|
Binary file
|