Add Imgur rate limiting

This commit is contained in:
JonnyWong16 2018-03-25 13:47:49 -07:00
parent dec5931fd4
commit 80df2b0fad
4 changed files with 105 additions and 6 deletions

View file

@ -977,7 +977,7 @@
<p class="help-block"> <p class="help-block">
Enter your Imgur API client ID in order to upload posters. Enter your Imgur API client ID in order to upload posters.
You can register a new application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>. You can register a new application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.
</p>> </p>
</div> </div>
</div> </div>
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}"> <div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
@ -2543,7 +2543,7 @@ $(document).ready(function() {
}); });
function newsletterUploadEnabled() { function newsletterUploadEnabled() {
if ($('#notify_upload_posters').is(':checked') || $('#newsletter_self_hosted').is(':checked')) { if ($('#notify_upload_posters').val() === '1' || $('#newsletter_self_hosted').is(':checked')) {
$('#newsletter_upload_warning').hide(); $('#newsletter_upload_warning').hide();
} else { } else {
$('#newsletter_upload_warning').show(); $('#newsletter_upload_warning').show();

59
lib/ratelimit/__init__.py Normal file
View file

@ -0,0 +1,59 @@
from math import floor
import time
import sys
import threading
import functools
def clamp(value):
'''
Clamp integer between 1 and max
There must be at least 1 method invocation
made over the time period. Make sure the
value passed is at least 1 and is not a
fraction of an invocation.
:param float value: The number of method invocations.
:return: Clamped number of invocations.
:rtype: int
'''
return max(1, min(sys.maxsize, floor(value)))
class RateLimitDecorator:
def __init__(self, period=1, every=1.0):
self.frequency = abs(every) / float(clamp(period))
self.last_called = 0.0
self.lock = threading.RLock()
def __call__(self, func):
'''
Extend the behaviour of the following
function, forwarding method invocations
if the time window hes elapsed.
:param function func: The function to decorate.
:return: Decorated function.
:rtype: function
'''
@functools.wraps(func)
def wrapper(*args, **kwargs):
'''Decorator wrapper function'''
with self.lock:
elapsed = time.time() - self.last_called
left_to_wait = self.frequency - elapsed
if left_to_wait > 0:
time.sleep(left_to_wait)
self.last_called = time.time()
return func(*args, **kwargs)
return wrapper
rate_limited = RateLimitDecorator
__all__ = [
'rate_limited'
]

9
lib/ratelimit/version.py Normal file
View file

@ -0,0 +1,9 @@
class Version(object):
'''Version of the package'''
def __setattr__(self, *args):
raise TypeError('cannot modify immutable instance')
__delattr__ = __setattr__
def __init__(self, num):
super(Version, self).__setattr__('number', num)

View file

@ -28,6 +28,7 @@ import math
import maxminddb import maxminddb
from operator import itemgetter from operator import itemgetter
import os import os
from ratelimit import rate_limited
import re import re
import socket import socket
import sys import sys
@ -76,6 +77,7 @@ def addtoapi(*dargs, **dkwargs):
return rd return rd
def multikeysort(items, columns): def multikeysort(items, columns):
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns] comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
@ -161,6 +163,7 @@ def convert_milliseconds(ms):
return minutes return minutes
def convert_milliseconds_to_minutes(ms): def convert_milliseconds_to_minutes(ms):
if str(ms).isdigit(): if str(ms).isdigit():
@ -171,6 +174,7 @@ def convert_milliseconds_to_minutes(ms):
return 0 return 0
def convert_seconds(s): def convert_seconds(s):
gmtime = time.gmtime(s) gmtime = time.gmtime(s)
@ -181,6 +185,7 @@ def convert_seconds(s):
return minutes return minutes
def convert_seconds_to_minutes(s): def convert_seconds_to_minutes(s):
if str(s).isdigit(): if str(s).isdigit():
@ -201,6 +206,7 @@ def now():
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def human_duration(s, sig='dhms'): def human_duration(s, sig='dhms'):
hd = '' hd = ''
@ -233,6 +239,7 @@ def human_duration(s, sig='dhms'):
return hd return hd
def get_age(date): def get_age(date):
try: try:
@ -385,6 +392,7 @@ def split_string(mystring, splitvar=','):
mylist.append(each_word.strip()) mylist.append(each_word.strip())
return mylist return mylist
def create_https_certificates(ssl_cert, ssl_key): def create_https_certificates(ssl_cert, ssl_key):
""" """
Create a self-signed HTTPS certificate and store in it in Create a self-signed HTTPS certificate and store in it in
@ -424,12 +432,14 @@ def cast_to_int(s):
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def cast_to_float(s): def cast_to_float(s):
try: try:
return float(s) return float(s)
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def convert_xml_to_json(xml): def convert_xml_to_json(xml):
o = xmltodict.parse(xml) o = xmltodict.parse(xml)
return json.dumps(o) return json.dumps(o)
@ -452,12 +462,14 @@ def get_percent(value1, value2):
return math.trunc(percent) return math.trunc(percent)
def hex_to_int(hex): def hex_to_int(hex):
try: try:
return int(hex, 16) return int(hex, 16)
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def parse_xml(unparsed=None): def parse_xml(unparsed=None):
if unparsed: if unparsed:
try: try:
@ -473,10 +485,11 @@ def parse_xml(unparsed=None):
logger.warn("XML parse request made but no data received.") logger.warn("XML parse request made but no data received.")
return [] return []
def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
""" """
Validate xml keys to make sure they exist and return their attribute value, return blank value is none found Validate xml keys to make sure they exist and return their attribute value, return blank value is none found
""" """
def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
if xml_key.getAttribute(attribute): if xml_key.getAttribute(attribute):
if return_bool: if return_bool:
return True return True
@ -488,6 +501,7 @@ def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
else: else:
return default_return return default_return
def process_json_kwargs(json_kwargs): def process_json_kwargs(json_kwargs):
params = {} params = {}
if json_kwargs: if json_kwargs:
@ -495,18 +509,21 @@ def process_json_kwargs(json_kwargs):
return params return params
def sanitize(string): def sanitize(string):
if string: if string:
return unicode(string).replace('<','&lt;').replace('>','&gt;') return unicode(string).replace('<','&lt;').replace('>','&gt;')
else: else:
return '' return ''
def is_public_ip(host): def is_public_ip(host):
ip = is_valid_ip(get_ip(host)) ip = is_valid_ip(get_ip(host))
if ip and ip.iptype() == 'PUBLIC': if ip and ip.iptype() == 'PUBLIC':
return True return True
return False return False
def get_ip(host): def get_ip(host):
ip_address = '' ip_address = ''
if is_valid_ip(host): if is_valid_ip(host):
@ -519,6 +536,7 @@ def get_ip(host):
logger.error(u"IP Checker :: Bad IP or hostname provided.") logger.error(u"IP Checker :: Bad IP or hostname provided.")
return ip_address return ip_address
def is_valid_ip(address): def is_valid_ip(address):
try: try:
return IP(address) return IP(address)
@ -527,6 +545,7 @@ def is_valid_ip(address):
except ValueError: except ValueError:
return False return False
def install_geoip_db(): def install_geoip_db():
maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/' maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/'
geolite2_gz = 'GeoLite2-City.mmdb.gz' geolite2_gz = 'GeoLite2-City.mmdb.gz'
@ -587,6 +606,7 @@ def install_geoip_db():
return True return True
def uninstall_geoip_db(): def uninstall_geoip_db():
logger.debug(u"Tautulli Helpers :: Uninstalling the GeoLite2 database...") logger.debug(u"Tautulli Helpers :: Uninstalling the GeoLite2 database...")
try: try:
@ -600,6 +620,7 @@ def uninstall_geoip_db():
logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.") logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
return True return True
def geoip_lookup(ip_address): def geoip_lookup(ip_address):
if not plexpy.CONFIG.GEOIP_DB: if not plexpy.CONFIG.GEOIP_DB:
return 'GeoLite2 database not installed. Please install from the ' \ return 'GeoLite2 database not installed. Please install from the ' \
@ -638,6 +659,7 @@ def geoip_lookup(ip_address):
return geo_info return geo_info
def whois_lookup(ip_address): def whois_lookup(ip_address):
nets = [] nets = []
@ -674,6 +696,7 @@ def whois_lookup(ip_address):
return whois_info return whois_info
# Taken from SickRage # Taken from SickRage
def anon_url(*url): def anon_url(*url):
""" """
@ -682,6 +705,7 @@ def anon_url(*url):
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url)) return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
@rate_limited(450, 3600)
def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''): def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
""" Uploads an image to Imgur """ """ Uploads an image to Imgur """
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
@ -769,6 +793,7 @@ def cache_image(url, image=None):
return imagefile, imagetype return imagefile, imagetype
def build_datatables_json(kwargs, dt_columns, default_sort_col=None): def build_datatables_json(kwargs, dt_columns, default_sort_col=None):
""" Builds datatables json data """ Builds datatables json data
@ -793,6 +818,7 @@ def build_datatables_json(kwargs, dt_columns, default_sort_col=None):
} }
return json.dumps(json_data) return json.dumps(json_data)
def humanFileSize(bytes, si=False): def humanFileSize(bytes, si=False):
if str(bytes).isdigit(): if str(bytes).isdigit():
bytes = int(bytes) bytes = int(bytes)
@ -816,6 +842,7 @@ def humanFileSize(bytes, si=False):
return "{0:.1f} {1}".format(bytes, units[u]) return "{0:.1f} {1}".format(bytes, units[u])
def parse_condition_logic_string(s, num_cond=0): def parse_condition_logic_string(s, num_cond=0):
""" Parse a logic string into a nested list """ Parse a logic string into a nested list
Based on http://stackoverflow.com/a/23185606 Based on http://stackoverflow.com/a/23185606
@ -900,6 +927,7 @@ def parse_condition_logic_string(s, num_cond=0):
return stack.pop() return stack.pop()
def nested_list_to_string(l): def nested_list_to_string(l):
for i, x in enumerate(l): for i, x in enumerate(l):
if isinstance(x, list): if isinstance(x, list):
@ -907,6 +935,7 @@ def nested_list_to_string(l):
s = '(' + ' '.join(l) + ')' s = '(' + ' '.join(l) + ')'
return s return s
def eval_logic_groups_to_bool(logic_groups, eval_conds): def eval_logic_groups_to_bool(logic_groups, eval_conds):
first_cond = logic_groups[0] first_cond = logic_groups[0]
@ -928,6 +957,7 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
return result return result
def get_plexpy_url(hostname=None): def get_plexpy_url(hostname=None):
if plexpy.CONFIG.ENABLE_HTTPS: if plexpy.CONFIG.ENABLE_HTTPS:
scheme = 'https' scheme = 'https'
@ -961,6 +991,7 @@ def get_plexpy_url(hostname=None):
return scheme + '://' + hostname + port + root return scheme + '://' + hostname + port + root
def momentjs_to_arrow(format, duration=False): def momentjs_to_arrow(format, duration=False):
invalid_formats = ['Mo', 'DDDo', 'do'] invalid_formats = ['Mo', 'DDDo', 'do']
if duration: if duration: