yutipy 2.2.3__py3-none-any.whl → 2.2.5__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/deezer.py +14 -14
- yutipy/exceptions.py +0 -5
- yutipy/itunes.py +4 -5
- yutipy/kkbox.py +4 -6
- yutipy/lastfm.py +62 -17
- yutipy/logger.py +1 -1
- yutipy/models.py +3 -0
- yutipy/musicyt.py +14 -6
- yutipy/spotify.py +73 -40
- yutipy/yutipy_music.py +22 -7
- {yutipy-2.2.3.dist-info → yutipy-2.2.5.dist-info}/METADATA +1 -1
- yutipy-2.2.5.dist-info/RECORD +22 -0
- {yutipy-2.2.3.dist-info → yutipy-2.2.5.dist-info}/WHEEL +1 -1
- yutipy-2.2.3.dist-info/RECORD +0 -22
- {yutipy-2.2.3.dist-info → yutipy-2.2.5.dist-info}/entry_points.txt +0 -0
- {yutipy-2.2.3.dist-info → yutipy-2.2.5.dist-info}/licenses/LICENSE +0 -0
- {yutipy-2.2.3.dist-info → yutipy-2.2.5.dist-info}/top_level.txt +0 -0
yutipy/deezer.py
CHANGED
|
@@ -9,11 +9,10 @@ from yutipy.exceptions import (
|
|
|
9
9
|
DeezerException,
|
|
10
10
|
InvalidResponseException,
|
|
11
11
|
InvalidValueException,
|
|
12
|
-
NetworkException,
|
|
13
12
|
)
|
|
13
|
+
from yutipy.logger import logger
|
|
14
14
|
from yutipy.models import MusicInfo
|
|
15
15
|
from yutipy.utils.helpers import are_strings_similar, is_valid_string
|
|
16
|
-
from yutipy.logger import logger
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class Deezer:
|
|
@@ -97,17 +96,17 @@ class Deezer:
|
|
|
97
96
|
logger.debug(f"Response status code: {response.status_code}")
|
|
98
97
|
response.raise_for_status()
|
|
99
98
|
except requests.RequestException as e:
|
|
100
|
-
logger.
|
|
101
|
-
|
|
99
|
+
logger.warning(f"Network error while fetching music info: {e}")
|
|
100
|
+
return None
|
|
102
101
|
except Exception as e:
|
|
103
|
-
logger.
|
|
102
|
+
logger.warning(f"Unexpected error while searching Deezer: {e}")
|
|
104
103
|
raise DeezerException(f"An error occurred while searching Deezer: {e}")
|
|
105
104
|
|
|
106
105
|
try:
|
|
107
106
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
108
107
|
result = response.json()["data"]
|
|
109
108
|
except (IndexError, KeyError, ValueError) as e:
|
|
110
|
-
logger.
|
|
109
|
+
logger.warning(f"Invalid response structure from Deezer: {e}")
|
|
111
110
|
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
112
111
|
|
|
113
112
|
music_info = self._parse_results(artist, song, result)
|
|
@@ -164,17 +163,17 @@ class Deezer:
|
|
|
164
163
|
logger.debug(f"Response status code: {response.status_code}")
|
|
165
164
|
response.raise_for_status()
|
|
166
165
|
except requests.RequestException as e:
|
|
167
|
-
logger.
|
|
168
|
-
|
|
166
|
+
logger.warning(f"Error fetching track info: {e}")
|
|
167
|
+
return None
|
|
169
168
|
except Exception as e:
|
|
170
|
-
logger.
|
|
169
|
+
logger.warning(f"Error fetching track info: {e}")
|
|
171
170
|
raise DeezerException(f"An error occurred while fetching track info: {e}")
|
|
172
171
|
|
|
173
172
|
try:
|
|
174
173
|
logger.debug(f"Response JSON: {response.json()}")
|
|
175
174
|
result = response.json()
|
|
176
175
|
except ValueError as e:
|
|
177
|
-
logger.
|
|
176
|
+
logger.warning(f"Invalid response received from Deezer: {e}")
|
|
178
177
|
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
179
178
|
|
|
180
179
|
return {
|
|
@@ -205,17 +204,17 @@ class Deezer:
|
|
|
205
204
|
logger.info(f"Response status code: {response.status_code}")
|
|
206
205
|
response.raise_for_status()
|
|
207
206
|
except requests.RequestException as e:
|
|
208
|
-
logger.
|
|
209
|
-
|
|
207
|
+
logger.warning(f"Error fetching album info: {e}")
|
|
208
|
+
return None
|
|
210
209
|
except Exception as e:
|
|
211
|
-
logger.
|
|
210
|
+
logger.warning(f"Error fetching album info: {e}")
|
|
212
211
|
raise DeezerException(f"An error occurred while fetching album info: {e}")
|
|
213
212
|
|
|
214
213
|
try:
|
|
215
214
|
logger.debug(f"Response JSON: {response.json()}")
|
|
216
215
|
result = response.json()
|
|
217
216
|
except ValueError as e:
|
|
218
|
-
logger.
|
|
217
|
+
logger.warning(f"Invalid response received from Deezer: {e}")
|
|
219
218
|
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
220
219
|
|
|
221
220
|
return {
|
|
@@ -323,6 +322,7 @@ class Deezer:
|
|
|
323
322
|
|
|
324
323
|
if __name__ == "__main__":
|
|
325
324
|
import logging
|
|
325
|
+
|
|
326
326
|
from yutipy.logger import enable_logging
|
|
327
327
|
|
|
328
328
|
enable_logging(level=logging.DEBUG)
|
yutipy/exceptions.py
CHANGED
|
@@ -2,7 +2,6 @@ __all__ = [
|
|
|
2
2
|
"AuthenticationException",
|
|
3
3
|
"InvalidResponseException",
|
|
4
4
|
"InvalidValueException",
|
|
5
|
-
"NetworkException",
|
|
6
5
|
"YutipyException",
|
|
7
6
|
]
|
|
8
7
|
|
|
@@ -25,10 +24,6 @@ class InvalidValueException(YutipyException):
|
|
|
25
24
|
"""Exception raised for invalid values."""
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
class NetworkException(YutipyException):
|
|
29
|
-
"""Exception raised for network-related errors."""
|
|
30
|
-
|
|
31
|
-
|
|
32
27
|
# Service Exceptions
|
|
33
28
|
class DeezerException(YutipyException):
|
|
34
29
|
"""Exception raised for errors related to the Deezer API."""
|
yutipy/itunes.py
CHANGED
|
@@ -9,8 +9,7 @@ import requests
|
|
|
9
9
|
from yutipy.exceptions import (
|
|
10
10
|
InvalidResponseException,
|
|
11
11
|
InvalidValueException,
|
|
12
|
-
ItunesException
|
|
13
|
-
NetworkException,
|
|
12
|
+
ItunesException
|
|
14
13
|
)
|
|
15
14
|
from yutipy.models import MusicInfo
|
|
16
15
|
from yutipy.utils.helpers import (
|
|
@@ -100,8 +99,8 @@ class Itunes:
|
|
|
100
99
|
logger.debug(f"Response status code: {response.status_code}")
|
|
101
100
|
response.raise_for_status()
|
|
102
101
|
except requests.RequestException as e:
|
|
103
|
-
logger.
|
|
104
|
-
|
|
102
|
+
logger.warning(f"Network error while searching iTunes: {e}")
|
|
103
|
+
return None
|
|
105
104
|
except Exception as e:
|
|
106
105
|
logger.exception(f"Unexpected error while searching iTunes: {e}")
|
|
107
106
|
raise ItunesException(f"An error occurred while searching iTunes: {e}")
|
|
@@ -110,7 +109,7 @@ class Itunes:
|
|
|
110
109
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
111
110
|
result = response.json()["results"]
|
|
112
111
|
except (IndexError, KeyError, ValueError) as e:
|
|
113
|
-
logger.
|
|
112
|
+
logger.warning(f"Invalid response structure from iTunes: {e}")
|
|
114
113
|
raise InvalidResponseException(f"Invalid response received: {e}")
|
|
115
114
|
|
|
116
115
|
music_info = self._parse_result(artist, song, result)
|
yutipy/kkbox.py
CHANGED
|
@@ -12,10 +12,8 @@ from dotenv import load_dotenv
|
|
|
12
12
|
|
|
13
13
|
from yutipy.exceptions import (
|
|
14
14
|
AuthenticationException,
|
|
15
|
-
InvalidResponseException,
|
|
16
15
|
InvalidValueException,
|
|
17
16
|
KKBoxException,
|
|
18
|
-
NetworkException,
|
|
19
17
|
)
|
|
20
18
|
from yutipy.logger import logger
|
|
21
19
|
from yutipy.models import MusicInfo
|
|
@@ -187,15 +185,15 @@ class KKBox:
|
|
|
187
185
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
188
186
|
response.raise_for_status()
|
|
189
187
|
except requests.RequestException as e:
|
|
190
|
-
logger.
|
|
191
|
-
|
|
188
|
+
logger.warning(f"Network error during KKBOX authentication: {e}")
|
|
189
|
+
return None
|
|
192
190
|
|
|
193
191
|
if response.status_code == 200:
|
|
194
192
|
response_json = response.json()
|
|
195
193
|
response_json["requested_at"] = time()
|
|
196
194
|
return response_json
|
|
197
195
|
else:
|
|
198
|
-
raise
|
|
196
|
+
raise AuthenticationException(
|
|
199
197
|
f"Invalid response received: {response.json()}"
|
|
200
198
|
)
|
|
201
199
|
|
|
@@ -319,7 +317,7 @@ class KKBox:
|
|
|
319
317
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
320
318
|
response.raise_for_status()
|
|
321
319
|
except requests.RequestException as e:
|
|
322
|
-
|
|
320
|
+
return None
|
|
323
321
|
|
|
324
322
|
if response.status_code != 200:
|
|
325
323
|
raise KKBoxException(f"Failed to search for music: {response.json()}")
|
yutipy/lastfm.py
CHANGED
|
@@ -73,6 +73,46 @@ class LastFm:
|
|
|
73
73
|
"""Checks if the session is closed."""
|
|
74
74
|
return self._is_session_closed
|
|
75
75
|
|
|
76
|
+
def get_user_profile(self, username: str):
|
|
77
|
+
"""
|
|
78
|
+
Fetches the user profile information for the provided username.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
dict
|
|
83
|
+
A dictionary containing the user's profile information or error is username does not exist.
|
|
84
|
+
"""
|
|
85
|
+
query = (
|
|
86
|
+
f"?method=user.getinfo&user={username}&api_key={self.api_key}&format=json"
|
|
87
|
+
)
|
|
88
|
+
query_url = self.__api_url + query
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
response = self.__session.get(query_url, timeout=30)
|
|
92
|
+
except requests.RequestException as e:
|
|
93
|
+
logger.warning(f"Failed to fetch user profile: {e}")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
response_json = response.json()
|
|
97
|
+
result = response_json.get("user")
|
|
98
|
+
error = response_json.get("message")
|
|
99
|
+
if result:
|
|
100
|
+
images = [
|
|
101
|
+
{"size": image.get("size"), "url": image.get("#text")}
|
|
102
|
+
for image in result.get("image", [])
|
|
103
|
+
]
|
|
104
|
+
return {
|
|
105
|
+
"name": result.get("realname"),
|
|
106
|
+
"username": result.get("name"),
|
|
107
|
+
"type": result.get("type"),
|
|
108
|
+
"url": result.get("url"),
|
|
109
|
+
"images": images,
|
|
110
|
+
}
|
|
111
|
+
elif error:
|
|
112
|
+
return {"error": error}
|
|
113
|
+
else:
|
|
114
|
+
return None
|
|
115
|
+
|
|
76
116
|
def get_currently_playing(self, username: str) -> Optional[UserPlaying]:
|
|
77
117
|
"""
|
|
78
118
|
Fetches information about the currently playing or most recent track for a user.
|
|
@@ -95,33 +135,38 @@ class LastFm:
|
|
|
95
135
|
response = self.__session.get(query_url, timeout=30)
|
|
96
136
|
response.raise_for_status()
|
|
97
137
|
except requests.RequestException as e:
|
|
98
|
-
logger.
|
|
138
|
+
logger.warning(f"Failed to fetch user profile: {e}")
|
|
99
139
|
return None
|
|
100
140
|
|
|
101
141
|
response_json = response.json()
|
|
102
142
|
result = response_json.get("recenttracks", {}).get("track", [])[0]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
if result:
|
|
144
|
+
album_art = [
|
|
145
|
+
img.get("#text")
|
|
146
|
+
for img in result.get("image", [])
|
|
147
|
+
if img.get("size") == "extralarge"
|
|
148
|
+
]
|
|
149
|
+
return UserPlaying(
|
|
150
|
+
album_art="".join(album_art),
|
|
151
|
+
album_title=result.get("album", {}).get("#text"),
|
|
152
|
+
artists=", ".join(
|
|
153
|
+
separate_artists(result.get("artist", {}).get("#text"))
|
|
154
|
+
),
|
|
155
|
+
id=result.get("mbid"),
|
|
156
|
+
timestamp=result.get("date", {}).get("uts"),
|
|
157
|
+
title=result.get("name"),
|
|
158
|
+
url=result.get("url"),
|
|
159
|
+
is_playing=result.get("@attr", {}).get("nowplaying", False),
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
117
162
|
|
|
118
163
|
|
|
119
164
|
if __name__ == "__main__":
|
|
120
165
|
with LastFm() as lastfm:
|
|
121
166
|
username = input("Enter Lasfm Username: ").strip()
|
|
122
|
-
result = lastfm.
|
|
167
|
+
result = lastfm.get_user_profile(username=username)
|
|
123
168
|
|
|
124
169
|
if result:
|
|
125
|
-
pprint(
|
|
170
|
+
pprint(result)
|
|
126
171
|
else:
|
|
127
172
|
print("No result was found. Make sure the username is correct!")
|
yutipy/logger.py
CHANGED
yutipy/models.py
CHANGED
|
@@ -74,8 +74,11 @@ class UserPlaying(MusicInfo):
|
|
|
74
74
|
|
|
75
75
|
Attributes
|
|
76
76
|
----------
|
|
77
|
+
timetamp : Optional[int]
|
|
78
|
+
Unix Timestamp (in seconds) when playback was started.
|
|
77
79
|
is_playing : Optional[bool]
|
|
78
80
|
Whether the music is currently playing or paused.
|
|
79
81
|
"""
|
|
80
82
|
|
|
83
|
+
timestamp: Optional[int] = None
|
|
81
84
|
is_playing: Optional[bool] = None
|
yutipy/musicyt.py
CHANGED
|
@@ -11,9 +11,9 @@ from yutipy.exceptions import (
|
|
|
11
11
|
InvalidValueException,
|
|
12
12
|
MusicYTException,
|
|
13
13
|
)
|
|
14
|
+
from yutipy.logger import logger
|
|
14
15
|
from yutipy.models import MusicInfo
|
|
15
16
|
from yutipy.utils.helpers import are_strings_similar, is_valid_string
|
|
16
|
-
from yutipy.logger import logger
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class MusicYT:
|
|
@@ -87,8 +87,8 @@ class MusicYT:
|
|
|
87
87
|
try:
|
|
88
88
|
results = self.ytmusic.search(query=query, limit=limit)
|
|
89
89
|
except exceptions.YTMusicServerError as e:
|
|
90
|
-
logger.
|
|
91
|
-
|
|
90
|
+
logger.warning(f"Something went wrong while searching YTMusic: {e}")
|
|
91
|
+
return None
|
|
92
92
|
|
|
93
93
|
for result in results:
|
|
94
94
|
if self._is_relevant_result(artist, song, result):
|
|
@@ -181,9 +181,15 @@ class MusicYT:
|
|
|
181
181
|
The extracted music information.
|
|
182
182
|
"""
|
|
183
183
|
if result["resultType"] in ["song", "video"]:
|
|
184
|
-
|
|
184
|
+
try:
|
|
185
|
+
return self._get_song(result)
|
|
186
|
+
except InvalidResponseException:
|
|
187
|
+
return None
|
|
185
188
|
else:
|
|
186
|
-
|
|
189
|
+
try:
|
|
190
|
+
return self._get_album(result)
|
|
191
|
+
except InvalidResponseException:
|
|
192
|
+
return None
|
|
187
193
|
|
|
188
194
|
def _get_song(self, result: dict) -> MusicInfo:
|
|
189
195
|
"""
|
|
@@ -200,7 +206,9 @@ class MusicYT:
|
|
|
200
206
|
The extracted music information.
|
|
201
207
|
"""
|
|
202
208
|
title = result.get("title")
|
|
203
|
-
artist_names = ", ".join(
|
|
209
|
+
artist_names = ", ".join(
|
|
210
|
+
[artist.get("name") for artist in result.get("artists", [])]
|
|
211
|
+
)
|
|
204
212
|
video_id = result.get("videoId")
|
|
205
213
|
song_url = f"https://music.youtube.com/watch?v={video_id}"
|
|
206
214
|
lyrics_id = self.ytmusic.get_watch_playlist(video_id)
|
yutipy/spotify.py
CHANGED
|
@@ -14,9 +14,7 @@ from dotenv import load_dotenv
|
|
|
14
14
|
|
|
15
15
|
from yutipy.exceptions import (
|
|
16
16
|
AuthenticationException,
|
|
17
|
-
InvalidResponseException,
|
|
18
17
|
InvalidValueException,
|
|
19
|
-
NetworkException,
|
|
20
18
|
SpotifyAuthException,
|
|
21
19
|
SpotifyException,
|
|
22
20
|
)
|
|
@@ -198,8 +196,9 @@ class Spotify:
|
|
|
198
196
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
199
197
|
response.raise_for_status()
|
|
200
198
|
except requests.RequestException as e:
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
raise requests.RequestException(
|
|
200
|
+
f"Network error during Spotify authentication: {e}"
|
|
201
|
+
)
|
|
203
202
|
|
|
204
203
|
if response.status_code == 200:
|
|
205
204
|
response_json = response.json()
|
|
@@ -212,19 +211,34 @@ class Spotify:
|
|
|
212
211
|
|
|
213
212
|
def __refresh_access_token(self):
|
|
214
213
|
"""Refreshes the token if it has expired."""
|
|
215
|
-
if
|
|
216
|
-
|
|
214
|
+
if not self.__access_token:
|
|
215
|
+
raise SpotifyAuthException("No access token was found.")
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
logger.warning(e)
|
|
217
|
+
try:
|
|
218
|
+
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
219
|
+
token_info = self.__get_access_token()
|
|
222
220
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
try:
|
|
222
|
+
self.save_access_token(token_info)
|
|
223
|
+
except NotImplementedError as e:
|
|
224
|
+
logger.warning(e)
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
self.__access_token = token_info.get("access_token")
|
|
227
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
228
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
229
|
+
|
|
230
|
+
logger.info("The access token is still valid, no need to refresh.")
|
|
231
|
+
except (AuthenticationException, requests.RequestException) as e:
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"Failed to refresh the access toke due to following error: {e}"
|
|
234
|
+
)
|
|
235
|
+
except TypeError:
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
|
|
238
|
+
)
|
|
239
|
+
logger.info(
|
|
240
|
+
"Something went wrong while trying to refresh the access token. Set logging level to `DEBUG` to see the issue."
|
|
241
|
+
)
|
|
228
242
|
|
|
229
243
|
def save_access_token(self, token_info: dict) -> None:
|
|
230
244
|
"""
|
|
@@ -335,7 +349,8 @@ class Spotify:
|
|
|
335
349
|
)
|
|
336
350
|
response.raise_for_status()
|
|
337
351
|
except requests.RequestException as e:
|
|
338
|
-
|
|
352
|
+
logger.warning(f"Network error during Spotify search: {e}")
|
|
353
|
+
return None
|
|
339
354
|
|
|
340
355
|
if response.status_code != 200:
|
|
341
356
|
raise SpotifyException(f"Failed to search for music: {response.json()}")
|
|
@@ -402,7 +417,8 @@ class Spotify:
|
|
|
402
417
|
)
|
|
403
418
|
response.raise_for_status()
|
|
404
419
|
except requests.RequestException as e:
|
|
405
|
-
|
|
420
|
+
logger.warning(f"Network error during Spotify search (advanced): {e}")
|
|
421
|
+
return None
|
|
406
422
|
|
|
407
423
|
if response.status_code != 200:
|
|
408
424
|
raise SpotifyException(
|
|
@@ -435,7 +451,8 @@ class Spotify:
|
|
|
435
451
|
)
|
|
436
452
|
response.raise_for_status()
|
|
437
453
|
except requests.RequestException as e:
|
|
438
|
-
|
|
454
|
+
logger.warning(f"Network error during Spotify get artist ids: {e}")
|
|
455
|
+
return None
|
|
439
456
|
|
|
440
457
|
if response.status_code != 200:
|
|
441
458
|
return None
|
|
@@ -814,15 +831,16 @@ class SpotifyAuth:
|
|
|
814
831
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
815
832
|
response.raise_for_status()
|
|
816
833
|
except requests.RequestException as e:
|
|
817
|
-
|
|
818
|
-
|
|
834
|
+
raise requests.RequestException(
|
|
835
|
+
f"Network error during Spotify authentication: {e}"
|
|
836
|
+
)
|
|
819
837
|
|
|
820
838
|
if response.status_code == 200:
|
|
821
839
|
response_json = response.json()
|
|
822
840
|
response_json["requested_at"] = time()
|
|
823
841
|
return response_json
|
|
824
842
|
else:
|
|
825
|
-
raise
|
|
843
|
+
raise AuthenticationException(
|
|
826
844
|
f"Invalid response received: {response.json()}"
|
|
827
845
|
)
|
|
828
846
|
|
|
@@ -831,20 +849,30 @@ class SpotifyAuth:
|
|
|
831
849
|
if not self.__access_token:
|
|
832
850
|
raise SpotifyAuthException("No access token was found.")
|
|
833
851
|
|
|
834
|
-
|
|
835
|
-
|
|
852
|
+
try:
|
|
853
|
+
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
854
|
+
token_info = self.__get_access_token(refresh_token=self.__refresh_token)
|
|
836
855
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
856
|
+
try:
|
|
857
|
+
self.save_access_token(token_info)
|
|
858
|
+
except NotImplementedError as e:
|
|
859
|
+
logger.warning(e)
|
|
841
860
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
861
|
+
self.__access_token = token_info.get("access_token")
|
|
862
|
+
self.__refresh_token = token_info.get("refresh_token")
|
|
863
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
864
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
846
865
|
|
|
847
|
-
|
|
866
|
+
logger.info("The access token is still valid, no need to refresh.")
|
|
867
|
+
except (AuthenticationException, requests.RequestException) as e:
|
|
868
|
+
logger.warning(f"Failed to refresh the access toke due to following error: {e}")
|
|
869
|
+
except TypeError:
|
|
870
|
+
logger.debug(
|
|
871
|
+
f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
|
|
872
|
+
)
|
|
873
|
+
logger.warning(
|
|
874
|
+
"Something went wrong while trying to refresh the access token. Set logging level to `DEBUG` to see the issue."
|
|
875
|
+
)
|
|
848
876
|
|
|
849
877
|
@staticmethod
|
|
850
878
|
def generate_state() -> str:
|
|
@@ -1022,7 +1050,7 @@ class SpotifyAuth:
|
|
|
1022
1050
|
except NotImplementedError as e:
|
|
1023
1051
|
logger.warning(e)
|
|
1024
1052
|
|
|
1025
|
-
def get_user_profile(self):
|
|
1053
|
+
def get_user_profile(self) -> Optional[dict]:
|
|
1026
1054
|
"""
|
|
1027
1055
|
Fetches the user's display name and profile images.
|
|
1028
1056
|
|
|
@@ -1050,17 +1078,18 @@ class SpotifyAuth:
|
|
|
1050
1078
|
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
1051
1079
|
response.raise_for_status()
|
|
1052
1080
|
except requests.RequestException as e:
|
|
1053
|
-
logger.
|
|
1081
|
+
logger.warning(f"Failed to fetch user profile: {e}")
|
|
1054
1082
|
return None
|
|
1055
1083
|
|
|
1056
1084
|
if response.status_code != 200:
|
|
1057
|
-
logger.
|
|
1085
|
+
logger.warning(f"Unexpected response: {response.json()}")
|
|
1058
1086
|
return None
|
|
1059
1087
|
|
|
1060
|
-
|
|
1088
|
+
result = response.json()
|
|
1061
1089
|
return {
|
|
1062
|
-
"display_name":
|
|
1063
|
-
"images":
|
|
1090
|
+
"display_name": result.get("display_name"),
|
|
1091
|
+
"images": result.get("images", []),
|
|
1092
|
+
"url": result.get("external_urls", {}).get("spotify")
|
|
1064
1093
|
}
|
|
1065
1094
|
|
|
1066
1095
|
def get_currently_playing(self) -> Optional[UserPlaying]:
|
|
@@ -1099,16 +1128,17 @@ class SpotifyAuth:
|
|
|
1099
1128
|
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
1100
1129
|
response.raise_for_status()
|
|
1101
1130
|
except requests.RequestException as e:
|
|
1102
|
-
|
|
1131
|
+
logger.warning(f"Error while getting Spotify user activity: {e}")
|
|
1132
|
+
return None
|
|
1103
1133
|
|
|
1104
1134
|
if response.status_code == 204:
|
|
1105
1135
|
logger.info("Requested user is currently not listening to any music.")
|
|
1106
1136
|
return None
|
|
1107
1137
|
if response.status_code != 200:
|
|
1108
1138
|
try:
|
|
1109
|
-
logger.
|
|
1139
|
+
logger.warning(f"Unexpected response: {response.json()}")
|
|
1110
1140
|
except requests.exceptions.JSONDecodeError:
|
|
1111
|
-
logger.
|
|
1141
|
+
logger.warning(
|
|
1112
1142
|
f"Response Code: {response.status_code}, Reason: {response.reason}"
|
|
1113
1143
|
)
|
|
1114
1144
|
return None
|
|
@@ -1122,6 +1152,8 @@ class SpotifyAuth:
|
|
|
1122
1152
|
guess,
|
|
1123
1153
|
use_translation=False,
|
|
1124
1154
|
)
|
|
1155
|
+
# Spotify returns timestamp in milliseconds, so convert milliseconds to seconds:
|
|
1156
|
+
timestamp = response_json.get("timestamp") / 1000.0
|
|
1125
1157
|
return UserPlaying(
|
|
1126
1158
|
album_art=result.get("album", {}).get("images", [])[0].get("url"),
|
|
1127
1159
|
album_title=result.get("album", {}).get("name"),
|
|
@@ -1138,6 +1170,7 @@ class SpotifyAuth:
|
|
|
1138
1170
|
lyrics=None,
|
|
1139
1171
|
release_date=result.get("album", {}).get("release_date"),
|
|
1140
1172
|
tempo=None,
|
|
1173
|
+
timestamp=timestamp,
|
|
1141
1174
|
title=result.get("name"),
|
|
1142
1175
|
type=result.get("type"),
|
|
1143
1176
|
upc=result.get("external_ids", {}).get("upc"),
|
yutipy/yutipy_music.py
CHANGED
|
@@ -22,8 +22,23 @@ class YutipyMusic:
|
|
|
22
22
|
Instead of calling each service separately, you can use this class to get the information from all services at once.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
26
|
-
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
custom_kkbox_class = KKBox,
|
|
28
|
+
custom_spotify_class = Spotify,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the YutipyMusic class.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
custom_kkbox_class : Optional[type], optional
|
|
36
|
+
A custom class inherited from ``KKBox`` to override the default KKBox implementation.
|
|
37
|
+
This class should implement ``load_access_token()`` and ``save_access_token()`` methods. Default is ``KKBox``.
|
|
38
|
+
custom_spotify_class : Optional[type], optional
|
|
39
|
+
A custom class inherited from ``Spotify`` to override the default Spotify implementation.
|
|
40
|
+
This class should implement ``load_access_token()`` and ``save_access_token()`` methods. Default is ``Spotify``.
|
|
41
|
+
"""
|
|
27
42
|
self.music_info = MusicInfos()
|
|
28
43
|
self.normalize_non_english = True
|
|
29
44
|
self.album_art_priority = ["deezer", "ytmusic", "itunes"]
|
|
@@ -34,8 +49,8 @@ class YutipyMusic:
|
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
try:
|
|
37
|
-
self.services["kkbox"] =
|
|
38
|
-
except
|
|
52
|
+
self.services["kkbox"] = custom_kkbox_class()
|
|
53
|
+
except KKBoxException as e:
|
|
39
54
|
logger.warning(
|
|
40
55
|
f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
|
|
41
56
|
)
|
|
@@ -44,8 +59,8 @@ class YutipyMusic:
|
|
|
44
59
|
self.album_art_priority.insert(idx, "kkbox")
|
|
45
60
|
|
|
46
61
|
try:
|
|
47
|
-
self.services["spotify"] =
|
|
48
|
-
except
|
|
62
|
+
self.services["spotify"] = custom_spotify_class()
|
|
63
|
+
except SpotifyException as e:
|
|
49
64
|
logger.warning(
|
|
50
65
|
f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
|
|
51
66
|
)
|
|
@@ -114,7 +129,7 @@ class YutipyMusic:
|
|
|
114
129
|
result = future.result()
|
|
115
130
|
self._combine_results(result, service_name)
|
|
116
131
|
except Exception as e:
|
|
117
|
-
logger.
|
|
132
|
+
logger.warning(
|
|
118
133
|
f"Error occurred while searching with {service_name}: {e}"
|
|
119
134
|
)
|
|
120
135
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
yutipy/__init__.py,sha256=Zrw3cr_6khXp1IgQdZxGcUM9A64GYgPs-6rlqSukW5Q,294
|
|
2
|
+
yutipy/deezer.py,sha256=xI7ZDoPHES64_uRN4aYA-kj56V4tFU8xYkZYPXidl_I,11266
|
|
3
|
+
yutipy/exceptions.py,sha256=zz0XyyZr5xRcmRyw3hdTGaVRcwRn_RSYZdmwmuO0sEM,1379
|
|
4
|
+
yutipy/itunes.py,sha256=3YBLoWPhIx6ODE2G-msRppCHo0DNDGZADj-XvL_f5H0,7867
|
|
5
|
+
yutipy/kkbox.py,sha256=ZOIW8rQuiYEWmsXW3Dw9Q5mw71Jh_tMRAULvEgAuDew,19310
|
|
6
|
+
yutipy/lastfm.py,sha256=qv834EdJH5We90JME06dvUQBc0JLDphJOnbbzMfEQAQ,5797
|
|
7
|
+
yutipy/logger.py,sha256=GyLBlfQZ6pLNJ5MbyQSvcD_PkxmFdX41DPq5aeG1z68,1316
|
|
8
|
+
yutipy/models.py,sha256=45M-bNHusaAan_Ta_E9DyvsWujsT-ivbJqIfy2-i3R8,2343
|
|
9
|
+
yutipy/musicyt.py,sha256=WKR4J4ru1uqNa7ORSZKlet1FxXQlHzlXTg9CIUqnsoc,9462
|
|
10
|
+
yutipy/spotify.py,sha256=aKOntF5uBh_X8A779sDlRUAqihJid5NCSmEGOxTIQF0,45708
|
|
11
|
+
yutipy/yutipy_music.py,sha256=MNNh2WT-7GTAykAabLF6p4-0uXiIIbuogswmb-_QqtQ,7272
|
|
12
|
+
yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
yutipy/cli/config.py,sha256=e5RIq6RxVxxzx30nKVMa06gwyQ258s7U0WA1xvJuR_0,4543
|
|
14
|
+
yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
|
|
15
|
+
yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
|
|
16
|
+
yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
|
|
17
|
+
yutipy-2.2.5.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
18
|
+
yutipy-2.2.5.dist-info/METADATA,sha256=fTeNY_lf4jWyeVv1uhY74Ak1G58R3SOr-uFJGh0hGeI,6522
|
|
19
|
+
yutipy-2.2.5.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
|
20
|
+
yutipy-2.2.5.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
|
|
21
|
+
yutipy-2.2.5.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
22
|
+
yutipy-2.2.5.dist-info/RECORD,,
|
yutipy-2.2.3.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
yutipy/__init__.py,sha256=Zrw3cr_6khXp1IgQdZxGcUM9A64GYgPs-6rlqSukW5Q,294
|
|
2
|
-
yutipy/deezer.py,sha256=PTTTfeORh1HZ_ta7_Uu4YARouSknUnAxO9AQJPFm4v0,11402
|
|
3
|
-
yutipy/exceptions.py,sha256=oMuhNfDJ2AFsM_fJn6sayxMqIJRY_ihHRmL0U2IK6qQ,1501
|
|
4
|
-
yutipy/itunes.py,sha256=fV7KLsXWvfM_97KwVwn_KfnWM7j0cVGE7RytvnDGlZM,7929
|
|
5
|
-
yutipy/kkbox.py,sha256=7kgq0ZliiA16CeJa49faSUS8_SdqQPp6M-cKdnMFlaM,19447
|
|
6
|
-
yutipy/lastfm.py,sha256=0adVGigS8Kqnu52k-ry5eqHR6koktgKBhCNI1riUMfk,4302
|
|
7
|
-
yutipy/logger.py,sha256=4jXB-Qt2r75kpuAdxSGIwGrcpUyugxs9ROmBru9NtTw,1314
|
|
8
|
-
yutipy/models.py,sha256=_92e54uXXCw53oWZiNLBBai6C0InOZMJL7r8GJ5smbM,2215
|
|
9
|
-
yutipy/musicyt.py,sha256=6Vz8bI8hDNFoDKRh6GK90dGMRbn_d5d6eGPsaYogb_Y,9315
|
|
10
|
-
yutipy/spotify.py,sha256=pf_vaQXMxExMjIGuId4eTHaSO112hAqf-6YgWLSa5uQ,44108
|
|
11
|
-
yutipy/yutipy_music.py,sha256=cHJ95HxGILweVrnEacj8tTlU0NPxMpuDVMpngdX0mZQ,6558
|
|
12
|
-
yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
yutipy/cli/config.py,sha256=e5RIq6RxVxxzx30nKVMa06gwyQ258s7U0WA1xvJuR_0,4543
|
|
14
|
-
yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
|
|
15
|
-
yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
|
|
16
|
-
yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
|
|
17
|
-
yutipy-2.2.3.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
18
|
-
yutipy-2.2.3.dist-info/METADATA,sha256=GU0nDx1yzsjArFuBvjNw9LdFVmktbOFyFCjUFMklD-o,6522
|
|
19
|
-
yutipy-2.2.3.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
20
|
-
yutipy-2.2.3.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
|
|
21
|
-
yutipy-2.2.3.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
22
|
-
yutipy-2.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|