mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-05 17:43:37 +00:00
Merge branch 'master' into develop
This commit is contained in:
commit
8b384e22d7
11 changed files with 13 additions and 1104 deletions
|
@ -74,6 +74,11 @@
|
|||
* Update cachecontrol library 0.11.5 to 0.11.7 (3b3b776)
|
||||
|
||||
|
||||
### 0.12.23 (2017-07-18 16:55:00 UTC)
|
||||
|
||||
* Remove obsolete tvrage_api lib
|
||||
|
||||
|
||||
### 0.12.22 (2017-07-13 20:20:00 UTC)
|
||||
|
||||
* Fix "Server failed to return anything useful" when should be using cached .torrent file
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
Original Author:
|
||||
------------
|
||||
* Christian Kreutzer
|
||||
|
||||
Contributors
|
||||
------------
|
||||
* topdeck (http://bitbucket.org/topdeck)
|
||||
* samueltardieu (http://bitbucket.org/samueltardieu)
|
||||
* chevox (https://bitbucket.org/chexov)
|
|
@ -1,26 +0,0 @@
|
|||
Copyright (c) 2009, Christian Kreutzer
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions, and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions, and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author of this software nor the name of
|
||||
contributors to this software may be used to endorse or promote products
|
||||
derived from this software without specific prior written consent.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,29 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
from distutils.core import setup
|
||||
from tvrage import __version__, __author__, __license__
|
||||
|
||||
setup(name='python-tvrage',
|
||||
description='python client for the tvrage.com XML API',
|
||||
long_description = file(
|
||||
os.path.join(os.path.dirname(__file__),'README.rst')).read(),
|
||||
license=__license__,
|
||||
version=__version__,
|
||||
author=__author__,
|
||||
author_email='herr.kreutzer@gmail.com',
|
||||
# url='http://bitbucket.org/ckreutzer/python-tvrage/',
|
||||
url='https://github.com/ckreutzer/python-tvrage',
|
||||
packages=['tvrage'],
|
||||
install_requires = ["BeautifulSoup"],
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Programming Language :: Python',
|
||||
'Operating System :: OS Independent'
|
||||
]
|
||||
)
|
||||
|
|
@ -1,701 +0,0 @@
|
|||
# !/usr/bin/env python2
|
||||
# encoding:utf-8
|
||||
#author:dbr/Ben (ripped from tvdb:echel0n)
|
||||
#project:tvrage_api
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""
|
||||
Modified from http://github.com/dbr/tvrage_api
|
||||
Simple-to-use Python interface to The TVRage's API (tvrage.com)
|
||||
"""
|
||||
from functools import wraps
|
||||
import traceback
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import getpass
|
||||
import tempfile
|
||||
import warnings
|
||||
import logging
|
||||
import datetime as dt
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import xmltodict
|
||||
from sickbeard.network_timezones import standardize_network
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ElementTree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from lib.dateutil.parser import parse
|
||||
from lib.cachecontrol import CacheControl, caches
|
||||
|
||||
from tvrage_ui import BaseUI
|
||||
from tvrage_exceptions import (tvrage_error, tvrage_userabort, tvrage_shownotfound,
|
||||
tvrage_seasonnotfound, tvrage_episodenotfound, tvrage_attributenotfound)
|
||||
|
||||
|
||||
def log():
|
||||
return logging.getLogger("tvrage_api")
|
||||
|
||||
|
||||
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
|
||||
"""Retry calling the decorated function using an exponential backoff.
|
||||
|
||||
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
|
||||
original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
|
||||
|
||||
:param ExceptionToCheck: the exception to check. may be a tuple of
|
||||
exceptions to check
|
||||
:type ExceptionToCheck: Exception or tuple
|
||||
:param tries: number of times to try (not retry) before giving up
|
||||
:type tries: int
|
||||
:param delay: initial delay between retries in seconds
|
||||
:type delay: int
|
||||
:param backoff: backoff multiplier e.g. value of 2 will double the delay
|
||||
each retry
|
||||
:type backoff: int
|
||||
:param logger: logger to use. If None, print
|
||||
:type logger: logging.Logger instance
|
||||
"""
|
||||
|
||||
def deco_retry(f):
|
||||
|
||||
@wraps(f)
|
||||
def f_retry(*args, **kwargs):
|
||||
mtries, mdelay = tries, delay
|
||||
while mtries > 1:
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ExceptionToCheck, e:
|
||||
msg = "%s, Retrying in %d seconds..." % (str(e), mdelay)
|
||||
if logger:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
print msg
|
||||
time.sleep(mdelay)
|
||||
mtries -= 1
|
||||
mdelay *= backoff
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return f_retry # true decorator
|
||||
|
||||
return deco_retry
|
||||
|
||||
|
||||
class ShowContainer(dict):
|
||||
"""Simple dict that holds a series of Show instances
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._stack = []
|
||||
self._lastgc = time.time()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._stack.append(key)
|
||||
|
||||
#keep only the 100th latest results
|
||||
if time.time() - self._lastgc > 20:
|
||||
for o in self._stack[:-100]:
|
||||
del self[o]
|
||||
|
||||
self._stack = self._stack[-100:]
|
||||
|
||||
self._lastgc = time.time()
|
||||
|
||||
super(ShowContainer, self).__setitem__(key, value)
|
||||
|
||||
|
||||
class Show(dict):
|
||||
"""Holds a dict of seasons, and show data.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.data = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Show %s (containing %s seasons)>" % (
|
||||
self.data.get(u'seriesname', 'instance'),
|
||||
len(self)
|
||||
)
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key in self:
|
||||
# Key is an episode, return it
|
||||
return self[key]
|
||||
|
||||
if key in self.data:
|
||||
# Non-numeric request is for show-data
|
||||
return self.data[key]
|
||||
|
||||
raise AttributeError
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self:
|
||||
# Key is an episode, return it
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
if key in self.data:
|
||||
# Non-numeric request is for show-data
|
||||
return dict.__getitem__(self.data, key)
|
||||
|
||||
# Data wasn't found, raise appropriate error
|
||||
if isinstance(key, int) or key.isdigit():
|
||||
# Episode number x was not found
|
||||
raise tvrage_seasonnotfound("Could not find season %s" % (repr(key)))
|
||||
else:
|
||||
# If it's not numeric, it must be an attribute name, which
|
||||
# doesn't exist, so attribute error.
|
||||
raise tvrage_attributenotfound("Cannot find attribute %s" % (repr(key)))
|
||||
|
||||
def aired_on(self, date):
|
||||
ret = self.search(str(date), 'firstaired')
|
||||
if len(ret) == 0:
|
||||
raise tvrage_episodenotfound("Could not find any episodes that aired on %s" % date)
|
||||
return ret
|
||||
|
||||
def search(self, term=None, key=None):
|
||||
"""
|
||||
Search all episodes in show. Can search all data, or a specific key (for
|
||||
example, episodename)
|
||||
|
||||
Always returns an array (can be empty). First index contains the first
|
||||
match, and so on.
|
||||
|
||||
Each array index is an Episode() instance, so doing
|
||||
search_results[0]['episodename'] will retrieve the episode name of the
|
||||
first match.
|
||||
|
||||
Search terms are converted to lower case (unicode) strings.
|
||||
"""
|
||||
results = []
|
||||
for cur_season in self.values():
|
||||
searchresult = cur_season.search(term=term, key=key)
|
||||
if len(searchresult) != 0:
|
||||
results.extend(searchresult)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
class Season(dict):
|
||||
def __init__(self, show=None):
|
||||
"""The show attribute points to the parent show
|
||||
"""
|
||||
self.show = show
|
||||
|
||||
def __repr__(self):
|
||||
return "<Season instance (containing %s episodes)>" % (
|
||||
len(self.keys())
|
||||
)
|
||||
|
||||
def __getattr__(self, episode_number):
|
||||
if episode_number in self:
|
||||
return self[episode_number]
|
||||
raise AttributeError
|
||||
|
||||
def __getitem__(self, episode_number):
|
||||
if episode_number not in self:
|
||||
raise tvrage_episodenotfound("Could not find episode %s" % (repr(episode_number)))
|
||||
else:
|
||||
return dict.__getitem__(self, episode_number)
|
||||
|
||||
def search(self, term=None, key=None):
|
||||
"""Search all episodes in season, returns a list of matching Episode
|
||||
instances.
|
||||
"""
|
||||
results = []
|
||||
for ep in self.values():
|
||||
searchresult = ep.search(term=term, key=key)
|
||||
if searchresult is not None:
|
||||
results.append(
|
||||
searchresult
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
class Episode(dict):
|
||||
def __init__(self, season=None):
|
||||
"""The season attribute points to the parent season
|
||||
"""
|
||||
self.season = season
|
||||
|
||||
def __repr__(self):
|
||||
seasno = int(self.get(u'seasonnumber', 0))
|
||||
epno = int(self.get(u'episodenumber', 0))
|
||||
epname = self.get(u'episodename')
|
||||
if epname is not None:
|
||||
return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
|
||||
else:
|
||||
return "<Episode %02dx%02d>" % (seasno, epno)
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key in self:
|
||||
return self[key]
|
||||
raise AttributeError
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
raise tvrage_attributenotfound("Cannot find attribute %s" % (repr(key)))
|
||||
|
||||
def search(self, term=None, key=None):
|
||||
"""Search episode data for term, if it matches, return the Episode (self).
|
||||
The key parameter can be used to limit the search to a specific element,
|
||||
for example, episodename.
|
||||
|
||||
This primarily for use use by Show.search and Season.search.
|
||||
"""
|
||||
if term == None:
|
||||
raise TypeError("must supply string to search for (contents)")
|
||||
|
||||
term = unicode(term).lower()
|
||||
for cur_key, cur_value in self.items():
|
||||
cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
|
||||
if key is not None and cur_key != key:
|
||||
# Do not search this key
|
||||
continue
|
||||
if cur_value.find(unicode(term).lower()) > -1:
|
||||
return self
|
||||
|
||||
|
||||
class TVRage:
|
||||
"""Create easy-to-use interface to name of season/episode name"""
|
||||
|
||||
def __init__(self,
|
||||
interactive=False,
|
||||
select_first=False,
|
||||
debug=False,
|
||||
cache=True,
|
||||
banners=False,
|
||||
actors=False,
|
||||
custom_ui=None,
|
||||
language=None,
|
||||
search_all_languages=False,
|
||||
apikey=None,
|
||||
forceConnect=False,
|
||||
useZip=False,
|
||||
dvdorder=False,
|
||||
proxy=None):
|
||||
|
||||
"""
|
||||
cache (True/False/str/unicode/urllib2 opener):
|
||||
Retrieved XML are persisted to to disc. If true, stores in
|
||||
tvrage_api folder under your systems TEMP_DIR, if set to
|
||||
str/unicode instance it will use this as the cache
|
||||
location. If False, disables caching. Can also be passed
|
||||
an arbitrary Python object, which is used as a urllib2
|
||||
opener, which should be created by urllib2.build_opener
|
||||
|
||||
forceConnect (bool):
|
||||
If true it will always try to connect to tvrage.com even if we
|
||||
recently timed out. By default it will wait one minute before
|
||||
trying again, and any requests within that one minute window will
|
||||
return an exception immediately.
|
||||
"""
|
||||
|
||||
self.shows = ShowContainer() # Holds all Show classes
|
||||
self.corrections = {} # Holds show-name to show_id mapping
|
||||
|
||||
self.config = {}
|
||||
|
||||
if apikey is not None:
|
||||
self.config['apikey'] = apikey
|
||||
else:
|
||||
self.config['apikey'] = "Uhewg1Rr0o62fvZvUIZt" # tvdb_api's API key
|
||||
|
||||
self.config['debug_enabled'] = debug # show debugging messages
|
||||
|
||||
self.config['custom_ui'] = custom_ui
|
||||
|
||||
self.config['proxy'] = proxy
|
||||
|
||||
if cache is True:
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = self._getTempDir()
|
||||
elif cache is False:
|
||||
self.config['cache_enabled'] = False
|
||||
elif isinstance(cache, basestring):
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = cache
|
||||
else:
|
||||
raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache)))
|
||||
|
||||
if self.config['debug_enabled']:
|
||||
warnings.warn("The debug argument to tvrage_api.__init__ will be removed in the next version. "
|
||||
"To enable debug messages, use the following code before importing: "
|
||||
"import logging; logging.basicConfig(level=logging.DEBUG)")
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
# List of language from http://tvrage.com/api/0629B785CE550C8D/languages.xml
|
||||
# Hard-coded here as it is realtively static, and saves another HTTP request, as
|
||||
# recommended on http://tvrage.com/wiki/index.php/API:languages.xml
|
||||
self.config['valid_languages'] = [
|
||||
"da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr",
|
||||
"ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", "no"
|
||||
]
|
||||
|
||||
# tvrage.com should be based around numeric language codes,
|
||||
# but to link to a series like http://tvrage.com/?tab=series&id=79349&lid=16
|
||||
# requires the language ID, thus this mapping is required (mainly
|
||||
# for usage in tvrage_ui - internally tvrage_api will use the language abbreviations)
|
||||
self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27,
|
||||
'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9,
|
||||
'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11,
|
||||
'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30}
|
||||
|
||||
if language is None:
|
||||
self.config['language'] = 'en'
|
||||
else:
|
||||
if language not in self.config['valid_languages']:
|
||||
raise ValueError("Invalid language %s, options are: %s" % (
|
||||
language, self.config['valid_languages']
|
||||
))
|
||||
else:
|
||||
self.config['language'] = language
|
||||
|
||||
# The following url_ configs are based of the
|
||||
# http://tvrage.com/wiki/index.php/Programmers_API
|
||||
|
||||
self.config['base_url'] = "http://services.tvrage.com"
|
||||
|
||||
self.config['url_getSeries'] = u"%(base_url)s/feeds/full_search.php" % self.config
|
||||
self.config['params_getSeries'] = {"show": ""}
|
||||
|
||||
self.config['url_epInfo'] = u"%(base_url)s/myfeeds/episode_list.php" % self.config
|
||||
self.config['params_epInfo'] = {"key": self.config['apikey'], "sid": ""}
|
||||
|
||||
self.config['url_seriesInfo'] = u"%(base_url)s/myfeeds/showinfo.php" % self.config
|
||||
self.config['params_seriesInfo'] = {"key": self.config['apikey'], "sid": ""}
|
||||
|
||||
self.config['url_updtes_all'] = u"%(base_url)s/myfeeds/currentshows.php" % self.config
|
||||
|
||||
def _getTempDir(self):
|
||||
"""Returns the [system temp dir]/tvrage_api-u501 (or
|
||||
tvrage_api-myuser)
|
||||
"""
|
||||
if hasattr(os, 'getuid'):
|
||||
uid = "u%d" % (os.getuid())
|
||||
else:
|
||||
# For Windows
|
||||
try:
|
||||
uid = getpass.getuser()
|
||||
except ImportError:
|
||||
return os.path.join(tempfile.gettempdir(), "tvrage_api")
|
||||
|
||||
return os.path.join(tempfile.gettempdir(), "tvrage_api-%s" % (uid))
|
||||
|
||||
#@retry(tvrage_error)
|
||||
def _loadUrl(self, url, params=None):
|
||||
log().debug('Retrieving URL %s' % url)
|
||||
|
||||
session = requests.session()
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
session = CacheControl(session, cache=caches.FileCache(self.config['cache_location']))
|
||||
|
||||
if self.config['proxy']:
|
||||
log().debug('Using proxy for URL: %s' % url)
|
||||
session.proxies = {'http': self.config['proxy'], 'https': self.config['proxy']}
|
||||
|
||||
session.headers.update({'Accept-Encoding': 'gzip,deflate'})
|
||||
|
||||
try:
|
||||
resp = session.get(url.strip(), params=params)
|
||||
except requests.exceptions.HTTPError, e:
|
||||
raise tvrage_error('HTTP error %s while loading URL %s' % (e.errno, url))
|
||||
except requests.exceptions.ConnectionError, e:
|
||||
raise tvrage_error('Connection error %s while loading URL %s' % (e.message, url))
|
||||
except requests.exceptions.Timeout, e:
|
||||
raise tvrage_error('Connection timed out %s while loading URL %s' % (e.message, url))
|
||||
except Exception:
|
||||
raise tvrage_error('Unknown exception while loading URL %s: %s' % (url, traceback.format_exc()))
|
||||
|
||||
def remap_keys(path, key, value):
|
||||
name_map = {
|
||||
'showid': 'id',
|
||||
'showname': 'seriesname',
|
||||
'name': 'seriesname',
|
||||
'summary': 'overview',
|
||||
'started': 'firstaired',
|
||||
'genres': 'genre',
|
||||
'airtime': 'airs_time',
|
||||
'airday': 'airs_dayofweek',
|
||||
'image': 'fanart',
|
||||
'epnum': 'absolute_number',
|
||||
'title': 'episodename',
|
||||
'airdate': 'firstaired',
|
||||
'screencap': 'filename',
|
||||
'seasonnum': 'episodenumber'
|
||||
}
|
||||
|
||||
try:
|
||||
key = name_map[key.lower()]
|
||||
except (ValueError, TypeError, KeyError):
|
||||
key = key.lower()
|
||||
|
||||
# clean up value and do type changes
|
||||
if value:
|
||||
if isinstance(value, dict):
|
||||
if key == 'network':
|
||||
network = value['#text']
|
||||
country = value['@country']
|
||||
value = standardize_network(network, country)
|
||||
if key == 'genre':
|
||||
value = value['genre']
|
||||
if not value:
|
||||
value = []
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
value = filter(None, value)
|
||||
value = '|' + '|'.join(value) + '|'
|
||||
try:
|
||||
if key == 'firstaired' and value in '0000-00-00':
|
||||
new_value = str(dt.date.fromordinal(1))
|
||||
new_value = re.sub('([-]0{2})+', '', new_value)
|
||||
fix_date = parse(new_value, fuzzy=True).date()
|
||||
value = fix_date.strftime('%Y-%m-%d')
|
||||
elif key == 'firstaired':
|
||||
value = parse(value, fuzzy=True).date()
|
||||
value = value.strftime('%Y-%m-%d')
|
||||
|
||||
#if key == 'airs_time':
|
||||
# value = parse(value).time()
|
||||
# value = value.strftime('%I:%M %p')
|
||||
except:
|
||||
pass
|
||||
|
||||
return key, value
|
||||
|
||||
if resp.ok:
|
||||
try:
|
||||
return xmltodict.parse(resp.content.strip(), postprocessor=remap_keys)
|
||||
except:
|
||||
return dict([(u'data', None)])
|
||||
|
||||
def _getetsrc(self, url, params=None):
|
||||
"""Loads a URL using caching, returns an ElementTree of the source
|
||||
"""
|
||||
|
||||
try:
|
||||
src = self._loadUrl(url, params).values()[0]
|
||||
return src
|
||||
except:
|
||||
return []
|
||||
|
||||
def _setItem(self, sid, seas, ep, attrib, value):
|
||||
"""Creates a new episode, creating Show(), Season() and
|
||||
Episode()s as required. Called by _getShowData to populate show
|
||||
|
||||
Since the nice-to-use tvrage[1][24]['name] interface
|
||||
makes it impossible to do tvrage[1][24]['name] = "name"
|
||||
and still be capable of checking if an episode exists
|
||||
so we can raise tvrage_shownotfound, we have a slightly
|
||||
less pretty method of setting items.. but since the API
|
||||
is supposed to be read-only, this is the best way to
|
||||
do it!
|
||||
The problem is that calling tvrage[1][24]['episodename'] = "name"
|
||||
calls __getitem__ on tvrage[1], there is no way to check if
|
||||
tvrage.__dict__ should have a key "1" before we auto-create it
|
||||
"""
|
||||
if sid not in self.shows:
|
||||
self.shows[sid] = Show()
|
||||
if seas not in self.shows[sid]:
|
||||
self.shows[sid][seas] = Season(show=self.shows[sid])
|
||||
if ep not in self.shows[sid][seas]:
|
||||
self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas])
|
||||
self.shows[sid][seas][ep][attrib] = value
|
||||
|
||||
def _setShowData(self, sid, key, value):
|
||||
"""Sets self.shows[sid] to a new Show instance, or sets the data
|
||||
"""
|
||||
if sid not in self.shows:
|
||||
self.shows[sid] = Show()
|
||||
|
||||
if not isinstance(key, dict or list) and not isinstance(value, dict or list):
|
||||
self.shows[sid].data[key] = value
|
||||
|
||||
def _cleanData(self, data):
|
||||
"""Cleans up strings returned by tvrage.com
|
||||
|
||||
Issues corrected:
|
||||
- Replaces & with &
|
||||
- Trailing whitespace
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict or list):
|
||||
data = data.replace(u"&", u"&")
|
||||
data = data.strip()
|
||||
|
||||
return data
|
||||
|
||||
def search(self, series):
|
||||
"""This searches tvrage.com for the series name
|
||||
and returns the result list
|
||||
"""
|
||||
series = series.encode("utf-8")
|
||||
log().debug("Searching for show %s" % series)
|
||||
self.config['params_getSeries']['show'] = series
|
||||
|
||||
try:
|
||||
seriesFound = self._getetsrc(self.config['url_getSeries'], self.config['params_getSeries'])
|
||||
if seriesFound:
|
||||
return seriesFound.values()[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
return []
|
||||
|
||||
def _getSeries(self, series):
|
||||
"""This searches tvrage.com for the series name,
|
||||
If a custom_ui UI is configured, it uses this to select the correct
|
||||
series. If not, and interactive == True, ConsoleUI is used, if not
|
||||
BaseUI is used to select the first result.
|
||||
"""
|
||||
allSeries = self.search(series)
|
||||
if not isinstance(allSeries, list):
|
||||
allSeries = [allSeries]
|
||||
|
||||
if len(allSeries) == 0:
|
||||
log().debug('Series result returned zero')
|
||||
raise tvrage_shownotfound("Show-name search returned zero results (cannot find show on TVRAGE)")
|
||||
|
||||
if self.config['custom_ui'] is not None:
|
||||
log().debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
|
||||
CustomUI = self.config['custom_ui']
|
||||
ui = CustomUI(config=self.config)
|
||||
else:
|
||||
log().debug('Auto-selecting first search result using BaseUI')
|
||||
ui = BaseUI(config=self.config)
|
||||
|
||||
return ui.selectSeries(allSeries)
|
||||
|
||||
def _getShowData(self, sid, getEpInfo=False):
|
||||
"""Takes a series ID, gets the epInfo URL and parses the TVRAGE
|
||||
XML file into the shows dict in layout:
|
||||
shows[series_id][season_number][episode_number]
|
||||
"""
|
||||
|
||||
# Parse show information
|
||||
log().debug('Getting all series data for %s' % (sid))
|
||||
self.config['params_seriesInfo']['sid'] = sid
|
||||
seriesInfoEt = self._getetsrc(
|
||||
self.config['url_seriesInfo'],
|
||||
self.config['params_seriesInfo']
|
||||
)
|
||||
|
||||
# check and make sure we have data to process and that it contains a series name
|
||||
if not len(seriesInfoEt) or (isinstance(seriesInfoEt, dict) and 'seriesname' not in seriesInfoEt):
|
||||
return False
|
||||
|
||||
for k, v in seriesInfoEt.items():
|
||||
if v is not None:
|
||||
v = self._cleanData(v)
|
||||
|
||||
self._setShowData(sid, k, v)
|
||||
|
||||
# series search ends here
|
||||
if getEpInfo:
|
||||
# Parse episode data
|
||||
log().debug('Getting all episodes of %s' % (sid))
|
||||
|
||||
self.config['params_epInfo']['sid'] = sid
|
||||
epsEt = self._getetsrc(self.config['url_epInfo'], self.config['params_epInfo'])
|
||||
if 'episodelist' not in epsEt or 'season' not in epsEt['episodelist']:
|
||||
return False
|
||||
|
||||
seasons = epsEt['episodelist']['season']
|
||||
if not isinstance(seasons, list):
|
||||
seasons = [seasons]
|
||||
|
||||
for season in seasons:
|
||||
seas_no = int(season['@no'])
|
||||
episodes = season['episode']
|
||||
if not isinstance(episodes, list):
|
||||
episodes = [episodes]
|
||||
|
||||
for episode in episodes:
|
||||
ep_no = int(episode['episodenumber'])
|
||||
self._setItem(sid, seas_no, ep_no, 'seasonnumber', seas_no)
|
||||
|
||||
for k, v in episode.items():
|
||||
try:
|
||||
k = k.lower()
|
||||
if v is not None:
|
||||
if k == 'link':
|
||||
v = v.rsplit('/', 1)[1]
|
||||
k = 'id'
|
||||
v = self._cleanData(v)
|
||||
|
||||
self._setItem(sid, seas_no, ep_no, k, v)
|
||||
except:
|
||||
continue
|
||||
return True
|
||||
|
||||
def _nameToSid(self, name):
|
||||
"""Takes show name, returns the correct series ID (if the show has
|
||||
already been grabbed), or grabs all episodes and returns
|
||||
the correct SID.
|
||||
"""
|
||||
if name in self.corrections:
|
||||
log().debug('Correcting %s to %s' % (name, self.corrections[name]))
|
||||
return self.corrections[name]
|
||||
else:
|
||||
log().debug('Getting show %s' % (name))
|
||||
selected_series = self._getSeries(name)
|
||||
if isinstance(selected_series, dict):
|
||||
selected_series = [selected_series]
|
||||
sids = list(int(x['id']) for x in selected_series if self._getShowData(int(x['id'])))
|
||||
self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series))
|
||||
return sids
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Handles tvrage_instance['seriesname'] calls.
|
||||
The dict index should be the show id
|
||||
"""
|
||||
arg = None
|
||||
if isinstance(key, tuple) and 2 == len(key):
|
||||
key, arg = key
|
||||
if not isinstance(arg, bool):
|
||||
arg = None
|
||||
|
||||
if isinstance(key, (int, long)):
|
||||
# Item is integer, treat as show id
|
||||
if key not in self.shows:
|
||||
self._getShowData(key, (True, arg)[arg is not None])
|
||||
return None if key not in self.shows else self.shows[key]
|
||||
|
||||
key = key.lower()
|
||||
self.config['searchterm'] = key
|
||||
selected_series = self._getSeries(key)
|
||||
if isinstance(selected_series, dict):
|
||||
selected_series = [selected_series]
|
||||
[[self._setShowData(show['id'], k, v) for k, v in show.items()] for show in selected_series]
|
||||
return selected_series
|
||||
#test = self._getSeries(key)
|
||||
#sids = self._nameToSid(key)
|
||||
#return list(self.shows[sid] for sid in sids)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.shows)
|
||||
|
||||
|
||||
def main():
|
||||
"""Simple example of using tvrage_api - it just
|
||||
grabs an episode name interactively.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
tvrage_instance = TVRage(cache=False)
|
||||
print tvrage_instance['Lost']['seriesname']
|
||||
print tvrage_instance['Lost'][1][4]['episodename']
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,247 +0,0 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben (ripped from tvdb:echel0n)
|
||||
#project:tvrage_api
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""
|
||||
urllib2 caching handler
|
||||
Modified from http://code.activestate.com/recipes/491261/
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
import errno
|
||||
import httplib
|
||||
import urllib2
|
||||
import StringIO
|
||||
from hashlib import md5
|
||||
from threading import RLock
|
||||
|
||||
cache_lock = RLock()
|
||||
|
||||
def locked_function(origfunc):
|
||||
"""Decorator to execute function under lock"""
|
||||
def wrapped(*args, **kwargs):
|
||||
cache_lock.acquire()
|
||||
try:
|
||||
return origfunc(*args, **kwargs)
|
||||
finally:
|
||||
cache_lock.release()
|
||||
return wrapped
|
||||
|
||||
def calculate_cache_path(cache_location, url):
|
||||
"""Checks if [cache_location]/[hash_of_url].headers and .body exist
|
||||
"""
|
||||
thumb = md5(url).hexdigest()
|
||||
header = os.path.join(cache_location, thumb + ".headers")
|
||||
body = os.path.join(cache_location, thumb + ".body")
|
||||
return header, body
|
||||
|
||||
def check_cache_time(path, max_age):
|
||||
"""Checks if a file has been created/modified in the [last max_age] seconds.
|
||||
False means the file is too old (or doesn't exist), True means it is
|
||||
up-to-date and valid"""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
cache_modified_time = os.stat(path).st_mtime
|
||||
time_now = time.time()
|
||||
if cache_modified_time < time_now - max_age:
|
||||
# Cache is old
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@locked_function
|
||||
def exists_in_cache(cache_location, url, max_age):
|
||||
"""Returns if header AND body cache file exist (and are up-to-date)"""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
if os.path.exists(hpath) and os.path.exists(bpath):
|
||||
return(
|
||||
check_cache_time(hpath, max_age)
|
||||
and check_cache_time(bpath, max_age)
|
||||
)
|
||||
else:
|
||||
# File does not exist
|
||||
return False
|
||||
|
||||
@locked_function
|
||||
def store_in_cache(cache_location, url, response):
|
||||
"""Tries to store response in cache."""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
try:
|
||||
outf = open(hpath, "wb")
|
||||
headers = str(response.info())
|
||||
outf.write(headers)
|
||||
outf.close()
|
||||
|
||||
outf = open(bpath, "wb")
|
||||
outf.write(response.read())
|
||||
outf.close()
|
||||
except IOError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@locked_function
|
||||
def delete_from_cache(cache_location, url):
|
||||
"""Deletes a response in cache."""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
try:
|
||||
if os.path.exists(hpath):
|
||||
os.remove(hpath)
|
||||
if os.path.exists(bpath):
|
||||
os.remove(bpath)
|
||||
except IOError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class CacheHandler(urllib2.BaseHandler):
|
||||
"""Stores responses in a persistant on-disk cache.
|
||||
|
||||
If a subsequent GET request is made for the same URL, the stored
|
||||
response is returned, saving time, resources and bandwidth
|
||||
"""
|
||||
@locked_function
|
||||
def __init__(self, cache_location, max_age = 21600):
|
||||
"""The location of the cache directory"""
|
||||
self.max_age = max_age
|
||||
self.cache_location = cache_location
|
||||
if not os.path.exists(self.cache_location):
|
||||
try:
|
||||
os.mkdir(self.cache_location)
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
|
||||
# File exists, and it's a directory,
|
||||
# another process beat us to creating this dir, that's OK.
|
||||
pass
|
||||
else:
|
||||
# Our target dir is already a file, or different error,
|
||||
# relay the error!
|
||||
raise
|
||||
|
||||
def default_open(self, request):
|
||||
"""Handles GET requests, if the response is cached it returns it
|
||||
"""
|
||||
if request.get_method() != "GET":
|
||||
return None # let the next handler try to handle the request
|
||||
|
||||
if exists_in_cache(
|
||||
self.cache_location, request.get_full_url(), self.max_age
|
||||
):
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = True
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def http_response(self, request, response):
|
||||
"""Gets a HTTP response, if it was a GET request and the status code
|
||||
starts with 2 (200 OK etc) it caches it and returns a CachedResponse
|
||||
"""
|
||||
if (request.get_method() == "GET"
|
||||
and str(response.code).startswith("2")
|
||||
):
|
||||
if 'x-local-cache' not in response.info():
|
||||
# Response is not cached
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
response
|
||||
)
|
||||
else:
|
||||
set_cache_header = True
|
||||
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = set_cache_header
|
||||
)
|
||||
else:
|
||||
return response
|
||||
|
||||
class CachedResponse(StringIO.StringIO):
|
||||
"""An urllib2.response-like object for cached responses.
|
||||
|
||||
To determine if a response is cached or coming directly from
|
||||
the network, check the x-local-cache header rather than the object type.
|
||||
"""
|
||||
|
||||
@locked_function
|
||||
def __init__(self, cache_location, url, set_cache_header=True):
|
||||
self.cache_location = cache_location
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
|
||||
StringIO.StringIO.__init__(self, file(bpath, "rb").read())
|
||||
|
||||
self.url = url
|
||||
self.code = 200
|
||||
self.msg = "OK"
|
||||
headerbuf = file(hpath, "rb").read()
|
||||
if set_cache_header:
|
||||
headerbuf += "x-local-cache: %s\r\n" % (bpath)
|
||||
self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
|
||||
|
||||
def info(self):
|
||||
"""Returns headers
|
||||
"""
|
||||
return self.headers
|
||||
|
||||
def geturl(self):
|
||||
"""Returns original URL
|
||||
"""
|
||||
return self.url
|
||||
|
||||
@locked_function
|
||||
def recache(self):
|
||||
new_request = urllib2.urlopen(self.url)
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
new_request.url,
|
||||
new_request
|
||||
)
|
||||
CachedResponse.__init__(self, self.cache_location, self.url, True)
|
||||
|
||||
@locked_function
|
||||
def delete_cache(self):
|
||||
delete_from_cache(
|
||||
self.cache_location,
|
||||
self.url
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
"""Quick test/example of CacheHandler"""
|
||||
opener = urllib2.build_opener(CacheHandler("/tmp/"))
|
||||
response = opener.open("http://google.com")
|
||||
print response.headers
|
||||
print "Response:", response.read()
|
||||
|
||||
response.recache()
|
||||
print response.headers
|
||||
print "After recache:", response.read()
|
||||
|
||||
# Test usage in threads
|
||||
from threading import Thread
|
||||
class CacheThreadTest(Thread):
|
||||
lastdata = None
|
||||
def run(self):
|
||||
req = opener.open("http://google.com")
|
||||
newdata = req.read()
|
||||
if self.lastdata is None:
|
||||
self.lastdata = newdata
|
||||
assert self.lastdata == newdata, "Data was not consistent, uhoh"
|
||||
req.recache()
|
||||
threads = [CacheThreadTest() for x in range(50)]
|
||||
print "Starting threads"
|
||||
[t.start() for t in threads]
|
||||
print "..done"
|
||||
print "Joining threads"
|
||||
[t.join() for t in threads]
|
||||
print "..done"
|
||||
main()
|
|
@ -1,48 +0,0 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben (ripped from tvdb:echel0n)
|
||||
#project:tvrage_api
|
||||
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""Custom exceptions used or raised by tvrage_api"""
|
||||
|
||||
__all__ = ["tvrage_error", "tvrage_userabort", "tvrage_shownotfound",
|
||||
"tvrage_seasonnotfound", "tvrage_episodenotfound", "tvrage_attributenotfound"]
|
||||
|
||||
class tvrage_exception(Exception):
|
||||
"""Any exception generated by tvrage_api
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_error(tvrage_exception):
|
||||
"""An error with tvrage.com (Cannot connect, for example)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_userabort(tvrage_exception):
|
||||
"""User aborted the interactive selection (via
|
||||
the q command, ^c etc)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_shownotfound(tvrage_exception):
|
||||
"""Show cannot be found on tvrage.com (non-existant show)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_seasonnotfound(tvrage_exception):
|
||||
"""Season cannot be found on tvrage.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_episodenotfound(tvrage_exception):
|
||||
"""Episode cannot be found on tvrage.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvrage_attributenotfound(tvrage_exception):
|
||||
"""Raised if an episode does not have the requested
|
||||
attribute (such as a episode name)
|
||||
"""
|
||||
pass
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben (ripped from tvdb:echel0n)
|
||||
#project:tvrage_api
|
||||
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""Contains included user interface for TVRage show selection"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
def log():
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log = None):
|
||||
self.config = config
|
||||
if log is not None:
|
||||
warnings.warn("the UI's log parameter is deprecated, instead use\n"
|
||||
"use import logging; logging.getLogger('ui').info('blah')\n"
|
||||
"The self.log attribute will be removed in the next version")
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
def selectSeries(self, allSeries):
|
||||
return allSeries[0]
|
|
@ -1,5 +1,4 @@
|
|||
from lib.tvdb_api.tvdb_api import Tvdb
|
||||
from lib.tvrage_api.tvrage_api import TVRage
|
||||
|
||||
INDEXER_TVDB = 1
|
||||
INDEXER_TVRAGE = 2
|
||||
|
@ -33,7 +32,7 @@ indexerConfig = {
|
|||
main_url='http://tvrage.com/',
|
||||
id=INDEXER_TVRAGE,
|
||||
name='TVRage',
|
||||
module=TVRage,
|
||||
module=None,
|
||||
api_params=dict(apikey='Uhewg1Rr0o62fvZvUIZt', language='en'),
|
||||
active=False,
|
||||
dupekey='tvr',
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
|
||||
"""Custom exceptions used or raised by indexer_api"""
|
||||
|
||||
from lib.tvrage_api.tvrage_exceptions import \
|
||||
tvrage_exception, tvrage_attributenotfound, tvrage_episodenotfound, tvrage_error, \
|
||||
tvrage_seasonnotfound, tvrage_shownotfound, tvrage_userabort
|
||||
|
||||
from lib.tvdb_api.tvdb_exceptions import \
|
||||
tvdb_exception, tvdb_attributenotfound, tvdb_episodenotfound, tvdb_error, \
|
||||
tvdb_seasonnotfound, tvdb_shownotfound, tvdb_userabort
|
||||
|
@ -19,14 +15,11 @@ indexerExcepts = ["indexer_exception", "indexer_error", "indexer_userabort", "in
|
|||
tvdbExcepts = ["tvdb_exception", "tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
|
||||
"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
|
||||
|
||||
tvrageExcepts = ["tvdb_exception", "tvrage_error", "tvrage_userabort", "tvrage_shownotfound",
|
||||
"tvrage_seasonnotfound", "tvrage_episodenotfound", "tvrage_attributenotfound"]
|
||||
|
||||
# link API exceptions to our exception handler
|
||||
indexer_exception = tvdb_exception, tvrage_exception
|
||||
indexer_error = tvdb_error, tvrage_error
|
||||
indexer_userabort = tvdb_userabort, tvrage_userabort
|
||||
indexer_attributenotfound = tvdb_attributenotfound, tvrage_attributenotfound
|
||||
indexer_episodenotfound = tvdb_episodenotfound, tvrage_episodenotfound
|
||||
indexer_seasonnotfound = tvdb_seasonnotfound, tvrage_seasonnotfound
|
||||
indexer_shownotfound = tvdb_shownotfound, tvrage_shownotfound
|
||||
indexer_exception = tvdb_exception
|
||||
indexer_error = tvdb_error
|
||||
indexer_userabort = tvdb_userabort
|
||||
indexer_attributenotfound = tvdb_attributenotfound
|
||||
indexer_episodenotfound = tvdb_episodenotfound
|
||||
indexer_seasonnotfound = tvdb_seasonnotfound
|
||||
indexer_shownotfound = tvdb_shownotfound
|
Loading…
Reference in a new issue