6
6
import re
7
7
import time
8
8
import urllib .parse
9
- from contextlib import contextmanager
10
9
from datetime import datetime
11
10
12
11
import requests
@@ -74,9 +73,10 @@ def get(self, path, cache=None, *args, **kwargs):
74
73
75
74
_trace (f"Get '{ path } '" )
76
75
76
+ ignore_expiry = kwargs .pop ("ignore_expiry" , False )
77
77
if cache is not None and path in cache :
78
78
cached_result = cache .get (path )
79
- if not cached_result .expired :
79
+ if cached_result .still_valid ( ignore_expiry ) :
80
80
return cached_result
81
81
kwargs .setdefault ("headers" , {}).update (cached_result .etag_headers )
82
82
@@ -260,6 +260,7 @@ def _parse_retry_after(self, response):
260
260
261
261
class WebResponse (dict ):
262
262
def __init__ (self , url , data , expires = 0.0 , etag = None , status_code = 400 ):
263
+ self ._from_cache = False
263
264
self .url = url
264
265
self ._expires = expires
265
266
self ._etag = etag
@@ -315,13 +316,24 @@ def _parse_etag(response):
315
316
if etag and len (etag .groups ()) == 2 :
316
317
return etag .groups ()[1 ]
317
318
318
- @property
319
- def expired (self ):
320
- status_str = {True : "expired" , False : "fresh" }
321
- result = self ._expires < time .time ()
322
- _trace (f"Cached data { status_str [result ]} for { self } " )
319
+ def still_valid (self , ignore_expiry = False ):
320
+ if ignore_expiry :
321
+ result = True
322
+ status = "forced"
323
+ elif self ._expires >= time .time ():
324
+ result = True
325
+ status = "fresh"
326
+ else :
327
+ result = False
328
+ status = "expired"
329
+ self ._from_cache = result
330
+ _trace ("Cached data %s for %s" , status , self )
323
331
return result
324
332
333
+ @property
334
+ def status_unchanged (self ):
335
+ return self ._from_cache or 304 == self ._status_code
336
+
325
337
@property
326
338
def status_ok (self ):
327
339
return self ._status_code >= 200 and self ._status_code < 400
@@ -334,6 +346,7 @@ def etag_headers(self):
334
346
return {}
335
347
336
348
def updated (self , response ):
349
+ self ._from_cache = False
337
350
if self ._etag is None :
338
351
return False
339
352
elif self .url != response .url :
@@ -349,6 +362,7 @@ def updated(self, response):
349
362
_trace (f"ETag match for { self } { response } " )
350
363
self ._expires = response ._expires
351
364
self ._etag = response ._etag
365
+ self ._status_code = response ._status_code
352
366
return True
353
367
354
368
def __str__ (self ):
@@ -358,6 +372,10 @@ def __str__(self):
358
372
f"[ETag: { self ._etag } ]"
359
373
)
360
374
375
+ def increase_expiry (self , delta_seconds ):
376
+ if self .status_ok and not self ._from_cache :
377
+ self ._expires += delta_seconds
378
+
361
379
362
380
class SpotifyOAuthClient (OAuthClient ):
363
381
@@ -368,6 +386,7 @@ class SpotifyOAuthClient(OAuthClient):
368
386
PLAYLIST_FIELDS = (
369
387
f"name,owner.id,type,uri,snapshot_id,tracks({ TRACK_FIELDS } ),"
370
388
)
389
+ DEFAULT_EXTRA_EXPIRY = 10
371
390
372
391
def __init__ (self , client_id , client_secret , proxy_config ):
373
392
super (SpotifyOAuthClient , self ).__init__ (
@@ -379,11 +398,17 @@ def __init__(self, client_id, client_secret, proxy_config):
379
398
)
380
399
self .user_id = None
381
400
self ._cache = {}
401
+ self ._extra_expiry = self .DEFAULT_EXTRA_EXPIRY
402
+
403
+ def get_one (self , path , * args , ** kwargs ):
404
+ logger .debug (f'Fetching page "{ path } "' )
405
+ result = self .get (path , cache = self ._cache , * args , ** kwargs )
406
+ result .increase_expiry (self ._extra_expiry )
407
+ return result
382
408
383
409
def get_all (self , path , * args , ** kwargs ):
384
410
while path is not None :
385
- logger .debug (f'Fetching page "{ path } "' )
386
- result = self .get (path , * args , ** kwargs )
411
+ result = self .get_one (path , * args , ** kwargs )
387
412
path = result .get ("next" )
388
413
yield result
389
414
@@ -396,11 +421,13 @@ def login(self):
396
421
logger .info (f"Logged into Spotify Web API as { self .user_id } " )
397
422
return True
398
423
424
+ @property
425
+ def logged_in (self ):
426
+ return self .user_id is not None
427
+
399
428
def get_user_playlists (self ):
400
429
with utils .time_logger ("get_user_playlists" ):
401
- pages = self .get_all (
402
- "me/playlists" , cache = self ._cache , params = {"limit" : 50 }
403
- )
430
+ pages = self .get_all ("me/playlists" , params = {"limit" : 50 })
404
431
for page in pages :
405
432
for playlist in page .get ("items" , []):
406
433
yield playlist
@@ -416,17 +443,16 @@ def get_playlist(self, uri):
416
443
logger .error (exc )
417
444
return {}
418
445
419
- playlist = self .get (
446
+ playlist = self .get_one (
420
447
f"playlists/{ parsed .id } " ,
421
- cache = self ._cache ,
422
448
params = {"fields" : self .PLAYLIST_FIELDS , "market" : "from_token" },
423
449
)
424
450
425
451
tracks_path = playlist .get ("tracks" , {}).get ("next" )
426
452
track_pages = self .get_all (
427
453
tracks_path ,
428
- cache = self ._cache ,
429
454
params = {"fields" : self .TRACK_FIELDS , "market" : "from_token" },
455
+ ignore_expiry = playlist .status_unchanged ,
430
456
)
431
457
432
458
more_tracks = []
@@ -440,14 +466,8 @@ def get_playlist(self, uri):
440
466
441
467
return playlist
442
468
443
- @contextmanager
444
- def refresh_playlists (self , extra_expiry = None ):
469
+ def clear_cache (self , extra_expiry = None ):
445
470
self ._cache .clear ()
446
- old_extra_expiry = self ._extra_expiry
447
- if extra_expiry is not None :
448
- self ._extra_expiry = extra_expiry
449
- yield
450
- self ._extra_expiry = old_extra_expiry
451
471
452
472
453
473
WebLink = collections .namedtuple ("WebLink" , ["uri" , "type" , "id" , "owner" ])
0 commit comments