yutipy 2.2.6__py3-none-any.whl → 2.2.7__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/spotify.py CHANGED
@@ -1,19 +1,15 @@
1
1
  __all__ = ["Spotify", "SpotifyException", "SpotifyAuthException"]
2
2
 
3
- import base64
4
3
  import os
5
- import secrets
6
4
  import webbrowser
7
5
  from pprint import pprint
8
- from time import time
9
6
  from typing import Optional, Union
10
- from urllib.parse import urlencode
11
7
 
12
8
  import requests
13
9
  from dotenv import load_dotenv
14
10
 
11
+ from yutipy.base_clients import BaseAuthClient, BaseClient
15
12
  from yutipy.exceptions import (
16
- AuthenticationException,
17
13
  InvalidValueException,
18
14
  SpotifyAuthException,
19
15
  SpotifyException,
@@ -34,7 +30,7 @@ SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
34
30
  SPOTIFY_REDIRECT_URI = os.getenv("SPOTIFY_REDIRECT_URI")
35
31
 
36
32
 
37
- class Spotify:
33
+ class Spotify(BaseClient):
38
34
  """
39
35
  A class to interact with the Spotify API. It uses "Client Credentials" grant type (or flow).
40
36
 
@@ -55,9 +51,8 @@ class Spotify:
55
51
  client_secret : str, optional
56
52
  The Client secret for the Spotify API. Defaults to ``SPOTIFY_CLIENT_SECRET`` from environment variable or the ``.env`` file.
57
53
  defer_load : bool, optional
58
- Whether to defer loading the access token during initialization. Default is ``False``.
54
+ Whether to defer loading the access token during initialization, by default ``False``
59
55
  """
60
-
61
56
  self.client_id = client_id or SPOTIFY_CLIENT_ID
62
57
  self.client_secret = client_secret or SPOTIFY_CLIENT_SECRET
63
58
 
@@ -71,224 +66,15 @@ class Spotify:
71
66
  "Client Secret was not found. Set it in environment variable or directly pass it when creating object."
72
67
  )
73
68
 
74
- self.defer_load = defer_load
75
-
76
- self._is_session_closed = False
77
- self._normalize_non_english = True
78
-
79
- self.__api_url = "https://api.spotify.com/v1"
80
- self.__access_token = None
81
- self.__token_expires_in = None
82
- self.__token_requested_at = None
83
- self.__session = requests.Session()
84
- self.__translation_session = requests.Session()
85
-
86
- if not defer_load:
87
- # Attempt to load access token during initialization if not deferred
88
- token_info = None
89
- try:
90
- token_info = self.load_access_token()
91
- except NotImplementedError:
92
- logger.warning(
93
- "`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
94
- )
95
- finally:
96
- if not token_info:
97
- token_info = self.__get_access_token()
98
- self.__access_token = token_info.get("access_token")
99
- self.__token_expires_in = token_info.get("expires_in")
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
- )
108
- else:
109
- logger.warning(
110
- "`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
111
- )
112
-
113
- def __enter__(self):
114
- """Enters the runtime context related to this object."""
115
- return self
116
-
117
- def __exit__(self, exc_type, exc_value, exc_traceback):
118
- """Exits the runtime context related to this object."""
119
- self.close_session()
120
-
121
- def close_session(self) -> None:
122
- """Closes the current session(s)."""
123
- if not self.is_session_closed:
124
- self.__session.close()
125
- self.__translation_session.close()
126
- self._is_session_closed = True
127
-
128
- @property
129
- def is_session_closed(self) -> bool:
130
- """Checks if the session is closed."""
131
- return self._is_session_closed
132
-
133
- def load_token_after_init(self):
134
- """
135
- Explicitly load the access token after initialization.
136
- This is useful when ``defer_load`` is set to ``True`` during initialization.
137
- """
138
- token_info = None
139
- try:
140
- token_info = self.load_access_token()
141
- except NotImplementedError:
142
- logger.warning(
143
- "`load_access_token` is not implemented. Falling back to in-memory storage and requesting new access token."
144
- )
145
- finally:
146
- if not token_info:
147
- token_info = self.__get_access_token()
148
- self.__access_token = token_info.get("access_token")
149
- self.__token_expires_in = token_info.get("expires_in")
150
- self.__token_requested_at = token_info.get("requested_at")
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
-
159
- def __authorization_header(self) -> dict:
160
- """
161
- Generates the authorization header for Spotify API requests.
162
-
163
- Returns
164
- -------
165
- dict
166
- A dictionary containing the Bearer token for authentication.
167
- """
168
- return {"Authorization": f"Bearer {self.__access_token}"}
169
-
170
- def __get_access_token(self) -> dict:
171
- """
172
- Gets the Spotify API access token information.
173
-
174
- Returns
175
- -------
176
- dict
177
- The Spotify API access token, with additional information such as expires in, etc.
178
- """
179
- auth_string = f"{self.client_id}:{self.client_secret}"
180
- auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
181
-
182
- url = "https://accounts.spotify.com/api/token"
183
- headers = {
184
- "Authorization": f"Basic {auth_base64}",
185
- "Content-Type": "application/x-www-form-urlencoded",
186
- }
187
- data = {"grant_type": "client_credentials"}
188
-
189
- try:
190
- logger.info(
191
- "Authenticating with Spotify API using Client Credentials grant type."
192
- )
193
- response = self.__session.post(
194
- url=url, headers=headers, data=data, timeout=30
195
- )
196
- logger.debug(f"Authentication response status code: {response.status_code}")
197
- response.raise_for_status()
198
- except requests.RequestException as e:
199
- raise requests.RequestException(
200
- f"Network error during Spotify authentication: {e}"
201
- )
202
-
203
- if response.status_code == 200:
204
- response_json = response.json()
205
- response_json["requested_at"] = time()
206
- return response_json
207
- else:
208
- raise AuthenticationException(
209
- f"Invalid response received: {response.json()}"
210
- )
211
-
212
- def __refresh_access_token(self):
213
- """Refreshes the token if it has expired."""
214
- if not self.__access_token:
215
- raise SpotifyAuthException("No access token was found.")
216
-
217
- try:
218
- if time() - self.__token_requested_at >= self.__token_expires_in:
219
- token_info = self.__get_access_token()
220
-
221
- try:
222
- self.save_access_token(token_info)
223
- except NotImplementedError as e:
224
- logger.warning(e)
225
-
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
- )
242
-
243
- def save_access_token(self, token_info: dict) -> None:
244
- """
245
- Saves the access token and related information.
246
-
247
- This method must be overridden in a subclass to persist the access token and other
248
- related information (e.g., expiration time). If not implemented,
249
- the access token will not be saved, and it will be requested each time the
250
- application restarts.
251
-
252
- Parameters
253
- ----------
254
- token_info : dict
255
- A dictionary containing the access token and related information, such as
256
- refresh token, expiration time, etc.
257
-
258
- Raises
259
- ------
260
- NotImplementedError
261
- If the method is not overridden in a subclass.
262
- """
263
- raise NotImplementedError(
264
- "The `save_access_token` method must be overridden in a subclass to save the access token and related information. "
265
- "If not implemented, access token information will not be persisted, and users will need to re-authenticate after application restarts."
69
+ super().__init__(
70
+ service_name="Spotify",
71
+ access_token_url="https://accounts.spotify.com/api/token",
72
+ client_id=self.client_id,
73
+ client_secret=self.client_secret,
74
+ defer_load=defer_load,
266
75
  )
267
76
 
268
- def load_access_token(self) -> Union[dict, None]:
269
- """
270
- Loads the access token and related information.
271
-
272
- This method must be overridden in a subclass to retrieve the access token and other
273
- related information (e.g., expiration time) from persistent storage.
274
- If not implemented, the access token will not be loaded, and it will be requested
275
- each time the application restarts.
276
-
277
- Returns
278
- -------
279
- dict | None
280
- A dictionary containing the access token and related information, such as
281
- refresh token, expiration time, etc., or None if no token is found.
282
-
283
- Raises
284
- ------
285
- NotImplementedError
286
- If the method is not overridden in a subclass.
287
- """
288
- raise NotImplementedError(
289
- "The `load_access_token` method must be overridden in a subclass to load access token and related information. "
290
- "If not implemented, access token information will not be loaded, and users will need to re-authenticate after application restarts."
291
- )
77
+ self.__api_url = "https://api.spotify.com/v1"
292
78
 
293
79
  def search(
294
80
  self,
@@ -334,7 +120,7 @@ class Spotify:
334
120
  if music_info:
335
121
  return music_info
336
122
 
337
- self.__refresh_access_token()
123
+ self._refresh_access_token()
338
124
 
339
125
  query_url = f"{self.__api_url}/search{query}"
340
126
 
@@ -344,17 +130,14 @@ class Spotify:
344
130
  logger.debug(f"Query URL: {query_url}")
345
131
 
346
132
  try:
347
- response = self.__session.get(
348
- query_url, headers=self.__authorization_header(), timeout=30
133
+ response = self._session.get(
134
+ query_url, headers=self._authorization_header(), timeout=30
349
135
  )
350
136
  response.raise_for_status()
351
137
  except requests.RequestException as e:
352
- logger.warning(f"Network error during Spotify search: {e}")
138
+ logger.warning(f"Failed to search for music: {response.json()}")
353
139
  return None
354
140
 
355
- if response.status_code != 200:
356
- raise SpotifyException(f"Failed to search for music: {response.json()}")
357
-
358
141
  artist_ids = artist_ids if artist_ids else self._get_artists_ids(artist)
359
142
  music_info = self._find_music_info(
360
143
  artist, song, response.json(), artist_ids
@@ -400,8 +183,7 @@ class Spotify:
400
183
  )
401
184
 
402
185
  self._normalize_non_english = normalize_non_english
403
-
404
- self.__refresh_access_token()
186
+ self._refresh_access_token()
405
187
 
406
188
  if isrc:
407
189
  query = f"?q={artist} {song} isrc:{isrc}&type=track&limit={limit}"
@@ -412,18 +194,15 @@ class Spotify:
412
194
 
413
195
  query_url = f"{self.__api_url}/search{query}"
414
196
  try:
415
- response = self.__session.get(
416
- query_url, headers=self.__authorization_header(), timeout=30
197
+ response = self._session.get(
198
+ query_url, headers=self._authorization_header(), timeout=30
417
199
  )
418
200
  response.raise_for_status()
419
201
  except requests.RequestException as e:
420
- logger.warning(f"Network error during Spotify search (advanced): {e}")
421
- return None
422
-
423
- if response.status_code != 200:
424
- raise SpotifyException(
202
+ raise logger.warning(
425
203
  f"Failed to search music with ISRC/UPC: {response.json()}"
426
204
  )
205
+ return None
427
206
 
428
207
  artist_ids = self._get_artists_ids(artist)
429
208
  return self._find_music_info(artist, song, response.json(), artist_ids)
@@ -446,8 +225,8 @@ class Spotify:
446
225
  for name in separate_artists(artist):
447
226
  query_url = f"{self.__api_url}/search?q={name}&type=artist&limit=5"
448
227
  try:
449
- response = self.__session.get(
450
- query_url, headers=self.__authorization_header(), timeout=30
228
+ response = self._session.get(
229
+ query_url, headers=self._authorization_header(), timeout=30
451
230
  )
452
231
  response.raise_for_status()
453
232
  except requests.RequestException as e:
@@ -531,7 +310,7 @@ class Spotify:
531
310
  track["name"],
532
311
  song,
533
312
  use_translation=self._normalize_non_english,
534
- translation_session=self.__translation_session,
313
+ translation_session=self._translation_session,
535
314
  ):
536
315
  return None
537
316
 
@@ -543,7 +322,7 @@ class Spotify:
543
322
  x["name"],
544
323
  artist,
545
324
  use_translation=self._normalize_non_english,
546
- translation_session=self.__translation_session,
325
+ translation_session=self._translation_session,
547
326
  )
548
327
  or x["id"] in artist_ids
549
328
  ]
@@ -594,7 +373,7 @@ class Spotify:
594
373
  album["name"],
595
374
  song,
596
375
  use_translation=self._normalize_non_english,
597
- translation_session=self.__translation_session,
376
+ translation_session=self._translation_session,
598
377
  ):
599
378
  return None
600
379
 
@@ -606,7 +385,7 @@ class Spotify:
606
385
  x["name"],
607
386
  artist,
608
387
  use_translation=self._normalize_non_english,
609
- translation_session=self.__translation_session,
388
+ translation_session=self._translation_session,
610
389
  )
611
390
  or x["id"] in artist_ids
612
391
  ]
@@ -637,7 +416,7 @@ class Spotify:
637
416
  return None
638
417
 
639
418
 
640
- class SpotifyAuth:
419
+ class SpotifyAuth(BaseAuthClient):
641
420
  """
642
421
  A class to interact with the Spotify API. It uses "Authorization Code" grant type (or flow).
643
422
 
@@ -673,6 +452,7 @@ class SpotifyAuth:
673
452
  self.client_id = client_id or os.getenv("SPOTIFY_CLIENT_ID")
674
453
  self.client_secret = client_secret or os.getenv("SPOTIFY_CLIENT_SECRET")
675
454
  self.redirect_uri = redirect_uri or os.getenv("SPOTIFY_REDIRECT_URI")
455
+ self.scopes = scopes
676
456
 
677
457
  if not self.client_id:
678
458
  raise SpotifyAuthException(
@@ -689,366 +469,25 @@ class SpotifyAuth:
689
469
  "No redirect URI was provided! Set it in environment variable or directly pass it when creating object."
690
470
  )
691
471
 
692
- self.scope = scopes
693
- self.defer_load = defer_load
694
-
695
- self._is_session_closed = False
696
-
697
- self.__api_url = "https://api.spotify.com/v1/me"
698
- self.__access_token = None
699
- self.__refresh_token = None
700
- self.__token_expires_in = None
701
- self.__token_requested_at = None
702
- self.__session = requests.Session()
703
-
704
472
  if not scopes:
705
473
  logger.warning(
706
474
  "No scopes were provided. Authorization will only grant access to publicly available information."
707
475
  )
708
- self.scope = None
709
- else:
710
- self.scope = " ".join(scopes)
711
-
712
- if not defer_load:
713
- # Attempt to load access token during initialization if not deferred
714
- try:
715
- token_info = self.load_access_token()
716
- if token_info:
717
- self.__access_token = token_info.get("access_token")
718
- self.__refresh_token = token_info.get("refresh_token")
719
- self.__token_expires_in = token_info.get("expires_in")
720
- self.__token_requested_at = token_info.get("requested_at")
721
- else:
722
- logger.warning(
723
- "No access token found during initialization. You must authenticate to obtain a new token."
724
- )
725
- except NotImplementedError:
726
- logger.warning(
727
- "`load_access_token` is not implemented. Falling back to in-memory storage."
728
- )
729
- else:
730
- logger.warning(
731
- "`defer_load` is set to `True`. Make sure to call `load_token_after_init()`."
732
- )
733
-
734
- def __enter__(self):
735
- """Enters the runtime context related to this object."""
736
- return self
737
-
738
- def __exit__(self, exc_type, exc_value, exc_traceback):
739
- """Exits the runtime context related to this object."""
740
- self.close_session()
741
-
742
- def close_session(self) -> None:
743
- """Closes the current session(s)."""
744
- if not self.is_session_closed:
745
- self.__session.close()
746
- self._is_session_closed = True
747
-
748
- @property
749
- def is_session_closed(self) -> bool:
750
- """Checks if the session is closed."""
751
- return self._is_session_closed
752
-
753
- def load_token_after_init(self):
754
- """
755
- Explicitly load the access token after initialization.
756
- This is useful when ``defer_load`` is set to ``True`` during initialization.
757
- """
758
- try:
759
- token_info = self.load_access_token()
760
- if token_info:
761
- self.__access_token = token_info.get("access_token")
762
- self.__refresh_token = token_info.get("refresh_token")
763
- self.__token_expires_in = token_info.get("expires_in")
764
- self.__token_requested_at = token_info.get("requested_at")
765
- else:
766
- logger.warning(
767
- "No access token found. You must authenticate to obtain a new token."
768
- )
769
- except NotImplementedError:
770
- logger.warning(
771
- "`load_access_token` is not implemented. Falling back to in-memory storage."
772
- )
773
-
774
- def __authorization_header(self) -> dict:
775
- """
776
- Generates the authorization header for Spotify API requests.
777
-
778
- Returns
779
- -------
780
- dict
781
- A dictionary containing the Bearer token for authentication.
782
- """
783
- return {"Authorization": f"Bearer {self.__access_token}"}
784
-
785
- def __get_access_token(
786
- self,
787
- authorization_code: str = None,
788
- refresh_token: str = None,
789
- ) -> dict:
790
- """
791
- Gets the Spotify API access token information.
792
-
793
- If ``authorization_code`` provided, it will try to get a new access token from Spotify.
794
- Otherwise, if `refresh_token` is provided, it will refresh the access token using it
795
- and return new access token information.
796
-
797
- Returns
798
- -------
799
- dict
800
- The Spotify API access token, with additional information such as expires in, refresh token, etc.
801
- """
802
- auth_string = f"{self.client_id}:{self.client_secret}"
803
- auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
804
-
805
- url = "https://accounts.spotify.com/api/token"
806
- headers = {
807
- "Authorization": f"Basic {auth_base64}",
808
- "Content-Type": "application/x-www-form-urlencoded",
809
- }
810
-
811
- if authorization_code:
812
- data = {
813
- "grant_type": "authorization_code",
814
- "code": authorization_code,
815
- "redirect_uri": self.redirect_uri,
816
- }
817
-
818
- if refresh_token:
819
- data = {
820
- "grant_type": "refresh_token",
821
- "refresh_token": self.__refresh_token,
822
- }
823
-
824
- try:
825
- logger.info(
826
- "Authenticating with Spotify API using Authorization Code grant type."
827
- )
828
- response = self.__session.post(
829
- url=url, headers=headers, data=data, timeout=30
830
- )
831
- logger.debug(f"Authentication response status code: {response.status_code}")
832
- response.raise_for_status()
833
- except requests.RequestException as e:
834
- raise requests.RequestException(
835
- f"Network error during Spotify authentication: {e}"
836
- )
837
-
838
- if response.status_code == 200:
839
- response_json = response.json()
840
- response_json["requested_at"] = time()
841
- return response_json
842
476
  else:
843
- raise AuthenticationException(
844
- f"Invalid response received: {response.json()}"
845
- )
846
-
847
- def __refresh_access_token(self):
848
- """Refreshes the token if it has expired."""
849
- if not self.__access_token:
850
- raise SpotifyAuthException("No access token was found.")
851
-
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)
855
-
856
- try:
857
- self.save_access_token(token_info)
858
- except NotImplementedError as e:
859
- logger.warning(e)
860
-
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")
865
-
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
- )
876
-
877
- @staticmethod
878
- def generate_state() -> str:
879
- """
880
- Generates a random state string for use in OAuth 2.0 authorization.
881
-
882
- This method creates a cryptographically secure, URL-safe string that can be used
883
- to prevent cross-site request forgery (CSRF) attacks during the authorization process.
884
-
885
- Returns
886
- -------
887
- str
888
- A random URL-safe string to be used as the state parameter in OAuth 2.0.
889
- """
890
- return secrets.token_urlsafe(16)
891
-
892
- def get_authorization_url(self, state: str = None, show_dialog: bool = False):
893
- """
894
- Constructs the Spotify authorization URL for user authentication.
895
-
896
- This method generates a URL that can be used to redirect users to Spotify's
897
- authorization page for user authentication.
898
-
899
- Parameters
900
- ----------
901
- state : str, optional
902
- A random string to maintain state between the request and callback.
903
- If not provided, no state parameter is included.
904
-
905
- You may use :meth:`SpotifyAuth.generate_state` method to generate one.
906
- show_dialog : bool, optional
907
- Whether or not to force the user to approve the app again if they’ve already done so.
908
- If ``False`` (default), a user who has already approved the application may be automatically
909
- redirected to the URI specified by redirect_uri. If ``True``, the user will not be automatically
910
- redirected and will have to approve the app again.
911
-
912
- Returns
913
- -------
914
- str
915
- The full authorization URL to redirect users for Spotify authentication.
916
- """
917
- auth_endpoint = "https://accounts.spotify.com/authorize"
918
- payload = {
919
- "response_type": "code",
920
- "client_id": self.client_id,
921
- "redirect_uri": self.redirect_uri,
922
- "show_dialog": show_dialog,
923
- }
924
-
925
- if self.scope:
926
- payload["scope"] = self.scope
927
-
928
- if state:
929
- payload["state"] = state
930
-
931
- return f"{auth_endpoint}?{urlencode(payload)}"
932
-
933
- def save_access_token(self, token_info: dict) -> None:
934
- """
935
- Saves the access token and related information.
936
-
937
- This method must be overridden in a subclass to persist the access token and other
938
- related information (e.g., refresh token, expiration time). If not implemented,
939
- the access token will not be saved, and users will need to re-authenticate after
940
- application restarts.
941
-
942
- Parameters
943
- ----------
944
- token_info : dict
945
- A dictionary containing the access token and related information, such as
946
- refresh token, expiration time, etc.
947
-
948
- Raises
949
- ------
950
- NotImplementedError
951
- If the method is not overridden in a subclass.
952
- """
953
- raise NotImplementedError(
954
- "The `save_access_token` method must be overridden in a subclass to save the access token and related information. "
955
- "If not implemented, access token information will not be persisted, and users will need to re-authenticate after application restarts."
477
+ self.scopes = " ".join(scopes)
478
+
479
+ super().__init__(
480
+ service_name="Spotify",
481
+ access_token_url="https://accounts.spotify.com/api/token",
482
+ user_auth_url="https://accounts.spotify.com/authorize",
483
+ client_id=self.client_id,
484
+ client_secret=self.client_secret,
485
+ redirect_uri=self.redirect_uri,
486
+ scopes=self.scopes,
487
+ defer_load=defer_load,
956
488
  )
957
489
 
958
- def load_access_token(self) -> Union[dict, None]:
959
- """
960
- Loads the access token and related information.
961
-
962
- This method must be overridden in a subclass to retrieve the access token and other
963
- related information (e.g., refresh token, expiration time) from persistent storage.
964
- If not implemented, the access token will not be loaded, and users will need to
965
- re-authenticate after application restarts.
966
-
967
- Returns
968
- -------
969
- dict | None
970
- A dictionary containing the access token and related information, such as
971
- refresh token, expiration time, etc., or None if no token is found.
972
-
973
- Raises
974
- ------
975
- NotImplementedError
976
- If the method is not overridden in a subclass.
977
- """
978
- raise NotImplementedError(
979
- "The `load_access_token` method must be overridden in a subclass to load access token and related information. "
980
- "If not implemented, access token information will not be loaded, and users will need to re-authenticate after application restarts."
981
- )
982
-
983
- def callback_handler(self, code, state, expected_state):
984
- """
985
- Handles the callback phase of the OAuth 2.0 authorization process.
986
-
987
- This method processes the authorization code and state returned by Spotify after the user
988
- has granted permission. It validates the state to prevent CSRF attacks, exchanges the
989
- authorization code for an access token, and saves the token for future use.
990
-
991
- Parameters
992
- ----------
993
- code : str
994
- The authorization code returned by Spotify after user authorization.
995
- state : str
996
- The state parameter returned by Spotify to ensure the request's integrity.
997
- expected_state : str
998
- The original state parameter sent during the authorization request, used to validate the response.
999
-
1000
- Raises
1001
- ------
1002
- SpotifyAuthException
1003
- If the returned state does not match the expected state.
1004
-
1005
- Notes
1006
- -----
1007
- - This method can be used in a web application (e.g., Flask) in the `/callback` route to handle
1008
- successful authorization.
1009
- - Ensure that the ``save_access_token`` and ``load_access_token`` methods are implemented in a subclass
1010
- if token persistence is required.
1011
-
1012
- Example
1013
- -------
1014
- In a Flask application, you can use this method in the ``/callback`` route:
1015
-
1016
- .. code-block:: python
1017
-
1018
- @app.route('/callback')
1019
- def callback():
1020
- code = request.args.get('code')
1021
- state = request.args.get('state')
1022
- expected_state = session['state'] # Retrieve the state stored during authorization URL generation
1023
-
1024
- try:
1025
- spotify_auth.callback_handler(code, state, expected_state)
1026
- return "Authorization successful!"
1027
- except SpotifyAuthException as e:
1028
- return f"Authorization failed: {e}", 400
1029
- """
1030
- if state != expected_state:
1031
- raise SpotifyAuthException("state does not match!")
1032
-
1033
- token_info = None
1034
-
1035
- try:
1036
- token_info = self.load_access_token()
1037
- except NotImplementedError as e:
1038
- logger.warning(e)
1039
-
1040
- if not token_info:
1041
- token_info = self.__get_access_token(authorization_code=code)
1042
-
1043
- self.__access_token = token_info.get("access_token")
1044
- self.__refresh_token = token_info.get("refresh_token")
1045
- self.__token_expires_in = token_info.get("expires_in")
1046
- self.__token_requested_at = token_info.get("requested_at")
1047
-
1048
- try:
1049
- self.save_access_token(token_info)
1050
- except NotImplementedError as e:
1051
- logger.warning(e)
490
+ self.__api_url = "https://api.spotify.com/v1/me"
1052
491
 
1053
492
  def get_user_profile(self) -> Optional[dict]:
1054
493
  """
@@ -1063,19 +502,11 @@ class SpotifyAuth:
1063
502
  dict
1064
503
  A dictionary containing the user's display name and profile images.
1065
504
  """
1066
- try:
1067
- self.__refresh_access_token()
1068
- except SpotifyAuthException:
1069
- logger.warning(
1070
- "No access token was found. You may authenticate the user again."
1071
- )
1072
- return None
1073
-
1074
505
  query_url = self.__api_url
1075
- header = self.__authorization_header()
506
+ header = self._authorization_header()
1076
507
 
1077
508
  try:
1078
- response = self.__session.get(query_url, headers=header, timeout=30)
509
+ response = self._session.get(query_url, headers=header, timeout=30)
1079
510
  response.raise_for_status()
1080
511
  except requests.RequestException as e:
1081
512
  logger.warning(f"Failed to fetch user profile: {e}")
@@ -1089,7 +520,7 @@ class SpotifyAuth:
1089
520
  return {
1090
521
  "display_name": result.get("display_name"),
1091
522
  "images": result.get("images", []),
1092
- "url": result.get("external_urls", {}).get("spotify")
523
+ "url": result.get("external_urls", {}).get("spotify"),
1093
524
  }
1094
525
 
1095
526
  def get_currently_playing(self) -> Optional[UserPlaying]:
@@ -1113,19 +544,11 @@ class SpotifyAuth:
1113
544
  - If the API response does not contain the expected data, the method will return `None`.
1114
545
 
1115
546
  """
1116
- try:
1117
- self.__refresh_access_token()
1118
- except SpotifyAuthException:
1119
- logger.warning(
1120
- "No access token was found. You may authenticate the user again."
1121
- )
1122
- return None
1123
-
1124
547
  query_url = f"{self.__api_url}/player/currently-playing"
1125
- header = self.__authorization_header()
548
+ header = self._authorization_header()
1126
549
 
1127
550
  try:
1128
- response = self.__session.get(query_url, headers=header, timeout=30)
551
+ response = self._session.get(query_url, headers=header, timeout=30)
1129
552
  response.raise_for_status()
1130
553
  except requests.RequestException as e:
1131
554
  logger.warning(f"Error while getting Spotify user activity: {e}")
@@ -1134,14 +557,6 @@ class SpotifyAuth:
1134
557
  if response.status_code == 204:
1135
558
  logger.info("Requested user is currently not listening to any music.")
1136
559
  return None
1137
- if response.status_code != 200:
1138
- try:
1139
- logger.warning(f"Unexpected response: {response.json()}")
1140
- except requests.exceptions.JSONDecodeError:
1141
- logger.warning(
1142
- f"Response Code: {response.status_code}, Reason: {response.reason}"
1143
- )
1144
- return None
1145
560
 
1146
561
  response_json = response.json()
1147
562
  result = response_json.get("item")
@@ -1194,7 +609,7 @@ if __name__ == "__main__":
1194
609
  choice = input("\nEnter your choice (1 or 2): ")
1195
610
 
1196
611
  if choice == "1":
1197
- spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
612
+ spotify = Spotify()
1198
613
 
1199
614
  try:
1200
615
  artist_name = input("Artist Name: ")
@@ -1208,12 +623,7 @@ if __name__ == "__main__":
1208
623
  redirect_uri = input("Enter Redirect URI: ")
1209
624
  scopes = ["user-read-email", "user-read-private"]
1210
625
 
1211
- spotify_auth = SpotifyAuth(
1212
- client_id=SPOTIFY_CLIENT_ID,
1213
- client_secret=SPOTIFY_CLIENT_SECRET,
1214
- redirect_uri=redirect_uri,
1215
- scopes=scopes,
1216
- )
626
+ spotify_auth = SpotifyAuth(scopes=scopes)
1217
627
 
1218
628
  try:
1219
629
  state = spotify_auth.generate_state()