@@ -1429,6 +1429,44 @@ def items(self):
14291429###############################################################################
14301430# API Interfaces ##############################################################
14311431
1432+ class LearningDelay ():
1433+ '''
1434+ Deviantart API is throttled dynamically over time
1435+ you can learn the ideal ammount to wait
1436+ '''
1437+
1438+ def __init__ (self , initial = 1 , error_margin = 0.01 ):
1439+ '''
1440+ Inital is the inital time to wait in seconds,
1441+ error_margin is added on top of the esitmate but not used in
1442+ the update. It accounts for fuzzyness in the choice of value
1443+
1444+ TODO: You can store an load this calue across gallery-dl execution.
1445+ '''
1446+ self ._min = 0
1447+ self ._current = initial
1448+ self ._prev = initial + error_margin
1449+ self ._error = error_margin
1450+ self ._convegered = False
1451+
1452+ def reject (self ):
1453+ '''The current estimate was too low, increase it'''
1454+ self ._min = self ._current
1455+ self ._current = self ._prev + self ._error
1456+
1457+ def update (self ):
1458+ '''Try a smaller estimate'''
1459+ if self ._convegered :
1460+ return
1461+ self ._current = 0.5 * (self ._current + self ._min )
1462+ self ._convegered = abs (self ._current - self ._prev ) < self ._error
1463+
1464+ @property
1465+ def value (self ):
1466+ '''Current guess pluss an engineering margin for rounding errors'''
1467+ return self ._current + self ._error
1468+
1469+
14321470class DeviantartOAuthAPI ():
14331471 """Interface for the DeviantArt OAuth API
14341472
@@ -1445,6 +1483,9 @@ def __init__(self, extractor):
14451483
14461484 self .delay = extractor .config ("wait-min" , 0 )
14471485 self .delay_min = max (2 , self .delay )
1486+ self .exp_backoff_mul = 2
1487+ self .learned_delay = LearningDelay (initial = self .delay )
1488+ self .is_throttled = False
14481489
14491490 self .mature = extractor .config ("mature" , "true" )
14501491 if not isinstance (self .mature , str ):
@@ -1746,7 +1787,10 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17461787
17471788 while True :
17481789 if self .delay :
1749- self .extractor .sleep (self .delay , "api" )
1790+ if self .is_throttled :
1791+ self .extractor .sleep (self .delay , "api" )
1792+ else :
1793+ self .extractor .sleep (self .learned_delay .value , "api" )
17501794
17511795 self .authenticate (None if public else self .refresh_token_key )
17521796 kwargs ["headers" ] = self .headers
@@ -1760,6 +1804,14 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17601804
17611805 status = response .status_code
17621806 if 200 <= status < 400 :
1807+ self .learned_delay .update ()
1808+ self .is_throttled = False
1809+
1810+ # Exp backoff shouldn't be but could be significant
1811+ # so nap back to a sane value for linear cooldown
1812+ if self .delay > 10 :
1813+ self .delay = 10
1814+
17631815 if self .delay > self .delay_min :
17641816 self .delay -= 1
17651817 return data
@@ -1775,8 +1827,15 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17751827 self .log .debug (response .text )
17761828 msg = f"API responded with { status } { response .reason } "
17771829 if status == 429 :
1778- if self .delay < 30 :
1779- self .delay += 1
1830+
1831+ # When throttled we use exp backoff
1832+ self .is_throttled = True
1833+
1834+ # Exp backoff is a multiplier so enure sane initial delay
1835+ if self .delay < 1 :
1836+ self .delay = 1
1837+ else :
1838+ self .delay *= self .exp_backoff_mul
17801839 self .log .warning ("%s. Using %ds delay." , msg , self .delay )
17811840
17821841 if self ._warn_429 and self .delay >= 3 :
0 commit comments