yutipy 2.2.2__tar.gz → 2.2.4__tar.gz
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-2.2.2 → yutipy-2.2.4}/PKG-INFO +1 -1
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/api_reference.rst +1 -1
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_spotify.py +1 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/deezer.py +14 -14
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/exceptions.py +0 -5
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/itunes.py +4 -5
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/kkbox.py +18 -6
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/lastfm.py +2 -1
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/logger.py +2 -2
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/models.py +3 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/musicyt.py +14 -6
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/spotify.py +48 -25
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/yutipy_music.py +22 -7
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/PKG-INFO +1 -1
- {yutipy-2.2.2 → yutipy-2.2.4}/.gitattributes +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/FUNDING.yml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/dependabot.yml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/workflows/pytest-unit-testing.yml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.github/workflows/release.yml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.gitignore +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/.readthedocs.yaml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/LICENSE +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/MANIFEST.in +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/README.md +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/Makefile +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/_static/yutipy_header.png +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/_static/yutipy_logo.png +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/available_platforms.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/cli.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/conf.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/faq.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/index.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/installation.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/make.bat +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/requirements.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/docs/usage_examples.rst +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/pyproject.toml +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/requirements-dev.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/requirements.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/setup.cfg +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/__init__.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_deezer.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_itunes.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_kkbox.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_lastfm.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_models.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_musicyt.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/tests/test_utils.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/__init__.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/cli/__init__.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/cli/config.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/cli/search.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/utils/__init__.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy/utils/helpers.py +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/SOURCES.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/dependency_links.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/entry_points.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/requires.txt +0 -0
- {yutipy-2.2.2 → yutipy-2.2.4}/yutipy.egg-info/top_level.txt +0 -0
|
@@ -97,7 +97,7 @@ UserPlaying
|
|
|
97
97
|
.. autoclass:: yutipy.models.UserPlaying
|
|
98
98
|
:members:
|
|
99
99
|
:noindex:
|
|
100
|
-
:exclude-members: album_art, album_art_source, album_title, album_type, artists, genre, id, isrc, lyrics, release_date, tempo, title, type, upc, url, is_playing
|
|
100
|
+
:exclude-members: album_art, album_art_source, album_title, album_type, artists, genre, id, isrc, lyrics, release_date, tempo, title, type, upc, url, timestamp, is_playing
|
|
101
101
|
|
|
102
102
|
Exceptions
|
|
103
103
|
=============
|
|
@@ -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)
|
|
@@ -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."""
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -91,6 +89,13 @@ class KKBox:
|
|
|
91
89
|
self.__access_token = token_info.get("access_token")
|
|
92
90
|
self.__token_expires_in = token_info.get("expires_in")
|
|
93
91
|
self.__token_requested_at = token_info.get("requested_at")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
self.save_access_token(token_info)
|
|
95
|
+
except NotImplementedError:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
98
|
+
)
|
|
94
99
|
else:
|
|
95
100
|
logger.warning(
|
|
96
101
|
"`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
|
|
@@ -135,6 +140,13 @@ class KKBox:
|
|
|
135
140
|
self.__token_expires_in = token_info.get("expires_in")
|
|
136
141
|
self.__token_requested_at = token_info.get("requested_at")
|
|
137
142
|
|
|
143
|
+
try:
|
|
144
|
+
self.save_access_token(token_info)
|
|
145
|
+
except NotImplementedError:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
148
|
+
)
|
|
149
|
+
|
|
138
150
|
def __authorization_header(self) -> dict:
|
|
139
151
|
"""
|
|
140
152
|
Generates the authorization header for Spotify API requests.
|
|
@@ -173,15 +185,15 @@ class KKBox:
|
|
|
173
185
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
174
186
|
response.raise_for_status()
|
|
175
187
|
except requests.RequestException as e:
|
|
176
|
-
logger.
|
|
177
|
-
|
|
188
|
+
logger.warning(f"Network error during KKBOX authentication: {e}")
|
|
189
|
+
return None
|
|
178
190
|
|
|
179
191
|
if response.status_code == 200:
|
|
180
192
|
response_json = response.json()
|
|
181
193
|
response_json["requested_at"] = time()
|
|
182
194
|
return response_json
|
|
183
195
|
else:
|
|
184
|
-
raise
|
|
196
|
+
raise AuthenticationException(
|
|
185
197
|
f"Invalid response received: {response.json()}"
|
|
186
198
|
)
|
|
187
199
|
|
|
@@ -305,7 +317,7 @@ class KKBox:
|
|
|
305
317
|
logger.debug(f"Parsing response JSON: {response.json()}")
|
|
306
318
|
response.raise_for_status()
|
|
307
319
|
except requests.RequestException as e:
|
|
308
|
-
|
|
320
|
+
return None
|
|
309
321
|
|
|
310
322
|
if response.status_code != 200:
|
|
311
323
|
raise KKBoxException(f"Failed to search for music: {response.json()}")
|
|
@@ -95,7 +95,7 @@ class LastFm:
|
|
|
95
95
|
response = self.__session.get(query_url, timeout=30)
|
|
96
96
|
response.raise_for_status()
|
|
97
97
|
except requests.RequestException as e:
|
|
98
|
-
logger.
|
|
98
|
+
logger.warning(f"Failed to fetch user profile: {e}")
|
|
99
99
|
return None
|
|
100
100
|
|
|
101
101
|
response_json = response.json()
|
|
@@ -110,6 +110,7 @@ class LastFm:
|
|
|
110
110
|
album_title=result.get("album", {}).get("#text"),
|
|
111
111
|
artists=", ".join(separate_artists(result.get("artist", {}).get("#text"))),
|
|
112
112
|
id=result.get("mbid"),
|
|
113
|
+
timestamp=result.get("date", {}).get("uts"),
|
|
113
114
|
title=result.get("name"),
|
|
114
115
|
url=result.get("url"),
|
|
115
116
|
is_playing=result.get("@attr", {}).get("nowplaying", False),
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
# Create a logger for the library
|
|
4
4
|
logger = logging.getLogger("yutipy")
|
|
5
|
-
logger.setLevel(logging.
|
|
5
|
+
logger.setLevel(logging.CRITICAL)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def enable_logging(level=logging.INFO, handler=None):
|
|
@@ -37,6 +37,6 @@ def enable_logging(level=logging.INFO, handler=None):
|
|
|
37
37
|
|
|
38
38
|
def disable_logging():
|
|
39
39
|
"""Disable logging for the library."""
|
|
40
|
-
logger.setLevel(logging.
|
|
40
|
+
logger.setLevel(logging.NOTSET)
|
|
41
41
|
for handler in logger.handlers[:]: # Remove all handlers
|
|
42
42
|
logger.removeHandler(handler)
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
)
|
|
@@ -100,6 +98,13 @@ class Spotify:
|
|
|
100
98
|
self.__access_token = token_info.get("access_token")
|
|
101
99
|
self.__token_expires_in = token_info.get("expires_in")
|
|
102
100
|
self.__token_requested_at = token_info.get("requested_at")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
self.save_access_token(token_info)
|
|
104
|
+
except NotImplementedError:
|
|
105
|
+
logger.warning(
|
|
106
|
+
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
107
|
+
)
|
|
103
108
|
else:
|
|
104
109
|
logger.warning(
|
|
105
110
|
"`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
|
|
@@ -144,6 +149,13 @@ class Spotify:
|
|
|
144
149
|
self.__token_expires_in = token_info.get("expires_in")
|
|
145
150
|
self.__token_requested_at = token_info.get("requested_at")
|
|
146
151
|
|
|
152
|
+
try:
|
|
153
|
+
self.save_access_token(token_info)
|
|
154
|
+
except NotImplementedError:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"`save_access_token` is not implemented, falling back to in-memory storage. Access token will not be saved."
|
|
157
|
+
)
|
|
158
|
+
|
|
147
159
|
def __authorization_header(self) -> dict:
|
|
148
160
|
"""
|
|
149
161
|
Generates the authorization header for Spotify API requests.
|
|
@@ -184,8 +196,8 @@ class Spotify:
|
|
|
184
196
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
185
197
|
response.raise_for_status()
|
|
186
198
|
except requests.RequestException as e:
|
|
187
|
-
logger.
|
|
188
|
-
|
|
199
|
+
logger.warning(f"Network error during Spotify authentication: {e}")
|
|
200
|
+
return None
|
|
189
201
|
|
|
190
202
|
if response.status_code == 200:
|
|
191
203
|
response_json = response.json()
|
|
@@ -198,19 +210,27 @@ class Spotify:
|
|
|
198
210
|
|
|
199
211
|
def __refresh_access_token(self):
|
|
200
212
|
"""Refreshes the token if it has expired."""
|
|
201
|
-
|
|
202
|
-
|
|
213
|
+
try:
|
|
214
|
+
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
215
|
+
token_info = self.__get_access_token()
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
217
|
+
try:
|
|
218
|
+
self.save_access_token(token_info)
|
|
219
|
+
except NotImplementedError as e:
|
|
220
|
+
logger.warning(e)
|
|
208
221
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
self.__access_token = token_info.get("access_token")
|
|
223
|
+
self.__token_expires_in = token_info.get("expires_in")
|
|
224
|
+
self.__token_requested_at = token_info.get("requested_at")
|
|
212
225
|
|
|
213
|
-
|
|
226
|
+
logger.info("The access token is still valid, no need to refresh.")
|
|
227
|
+
except TypeError:
|
|
228
|
+
logger.debug(
|
|
229
|
+
f"token requested at: {self.__token_requested_at} | token expires in: {self.__token_expires_in}"
|
|
230
|
+
)
|
|
231
|
+
logger.info(
|
|
232
|
+
"Something went wrong while trying to refresh the token. Set logging level to `DEBUG` to see the issue."
|
|
233
|
+
)
|
|
214
234
|
|
|
215
235
|
def save_access_token(self, token_info: dict) -> None:
|
|
216
236
|
"""
|
|
@@ -321,7 +341,7 @@ class Spotify:
|
|
|
321
341
|
)
|
|
322
342
|
response.raise_for_status()
|
|
323
343
|
except requests.RequestException as e:
|
|
324
|
-
|
|
344
|
+
return None
|
|
325
345
|
|
|
326
346
|
if response.status_code != 200:
|
|
327
347
|
raise SpotifyException(f"Failed to search for music: {response.json()}")
|
|
@@ -388,7 +408,7 @@ class Spotify:
|
|
|
388
408
|
)
|
|
389
409
|
response.raise_for_status()
|
|
390
410
|
except requests.RequestException as e:
|
|
391
|
-
|
|
411
|
+
return None
|
|
392
412
|
|
|
393
413
|
if response.status_code != 200:
|
|
394
414
|
raise SpotifyException(
|
|
@@ -421,7 +441,7 @@ class Spotify:
|
|
|
421
441
|
)
|
|
422
442
|
response.raise_for_status()
|
|
423
443
|
except requests.RequestException as e:
|
|
424
|
-
|
|
444
|
+
return None
|
|
425
445
|
|
|
426
446
|
if response.status_code != 200:
|
|
427
447
|
return None
|
|
@@ -800,15 +820,15 @@ class SpotifyAuth:
|
|
|
800
820
|
logger.debug(f"Authentication response status code: {response.status_code}")
|
|
801
821
|
response.raise_for_status()
|
|
802
822
|
except requests.RequestException as e:
|
|
803
|
-
logger.
|
|
804
|
-
|
|
823
|
+
logger.warning(f"Network error during Spotify authentication: {e}")
|
|
824
|
+
return None
|
|
805
825
|
|
|
806
826
|
if response.status_code == 200:
|
|
807
827
|
response_json = response.json()
|
|
808
828
|
response_json["requested_at"] = time()
|
|
809
829
|
return response_json
|
|
810
830
|
else:
|
|
811
|
-
raise
|
|
831
|
+
raise AuthenticationException(
|
|
812
832
|
f"Invalid response received: {response.json()}"
|
|
813
833
|
)
|
|
814
834
|
|
|
@@ -1036,11 +1056,11 @@ class SpotifyAuth:
|
|
|
1036
1056
|
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
1037
1057
|
response.raise_for_status()
|
|
1038
1058
|
except requests.RequestException as e:
|
|
1039
|
-
logger.
|
|
1059
|
+
logger.warning(f"Failed to fetch user profile: {e}")
|
|
1040
1060
|
return None
|
|
1041
1061
|
|
|
1042
1062
|
if response.status_code != 200:
|
|
1043
|
-
logger.
|
|
1063
|
+
logger.warning(f"Unexpected response: {response.json()}")
|
|
1044
1064
|
return None
|
|
1045
1065
|
|
|
1046
1066
|
response_json = response.json()
|
|
@@ -1085,16 +1105,16 @@ class SpotifyAuth:
|
|
|
1085
1105
|
response = self.__session.get(query_url, headers=header, timeout=30)
|
|
1086
1106
|
response.raise_for_status()
|
|
1087
1107
|
except requests.RequestException as e:
|
|
1088
|
-
|
|
1108
|
+
return None
|
|
1089
1109
|
|
|
1090
1110
|
if response.status_code == 204:
|
|
1091
1111
|
logger.info("Requested user is currently not listening to any music.")
|
|
1092
1112
|
return None
|
|
1093
1113
|
if response.status_code != 200:
|
|
1094
1114
|
try:
|
|
1095
|
-
logger.
|
|
1115
|
+
logger.warning(f"Unexpected response: {response.json()}")
|
|
1096
1116
|
except requests.exceptions.JSONDecodeError:
|
|
1097
|
-
logger.
|
|
1117
|
+
logger.warning(
|
|
1098
1118
|
f"Response Code: {response.status_code}, Reason: {response.reason}"
|
|
1099
1119
|
)
|
|
1100
1120
|
return None
|
|
@@ -1108,6 +1128,8 @@ class SpotifyAuth:
|
|
|
1108
1128
|
guess,
|
|
1109
1129
|
use_translation=False,
|
|
1110
1130
|
)
|
|
1131
|
+
# Spotify returns timestamp in milliseconds, so convert milliseconds to seconds:
|
|
1132
|
+
timestamp = response_json.get("timestamp") / 1000.0
|
|
1111
1133
|
return UserPlaying(
|
|
1112
1134
|
album_art=result.get("album", {}).get("images", [])[0].get("url"),
|
|
1113
1135
|
album_title=result.get("album", {}).get("name"),
|
|
@@ -1124,6 +1146,7 @@ class SpotifyAuth:
|
|
|
1124
1146
|
lyrics=None,
|
|
1125
1147
|
release_date=result.get("album", {}).get("release_date"),
|
|
1126
1148
|
tempo=None,
|
|
1149
|
+
timestamp=timestamp,
|
|
1127
1150
|
title=result.get("name"),
|
|
1128
1151
|
type=result.get("type"),
|
|
1129
1152
|
upc=result.get("external_ids", {}).get("upc"),
|
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|