mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-05 17:43:37 +00:00
Change restore fetching metadata episode thumbs.
Add get_episode_data in tvdb_api v2 Usage: lINDEXER_API_PARMS = sickbeard.indexerApi(show.indexer).api_params.copy() t = sickbeard.indexerApi(show.indexer).indexer(**lINDEXER_API_PARMS) ep_data = t.get_episode_data(episodeid)
This commit is contained in:
parent
cceb010e0e
commit
68fa29e77a
12 changed files with 1527 additions and 12 deletions
|
@ -1,4 +1,10 @@
|
|||
### 0.13.2 (2017-12-08 19:00:00 UTC)
|
||||
### 0.13.3 (2017-12-10 20:30:00 UTC)
|
||||
|
||||
* Fix metadata Season Posters and Banners
|
||||
* Change restore fetching metadata episode thumbs
|
||||
|
||||
|
||||
### 0.13.2 (2017-12-08 19:00:00 UTC)
|
||||
|
||||
* Fix tools menu on Chrome mobile browser
|
||||
|
||||
|
|
|
@ -517,6 +517,7 @@ class Tvdb:
|
|||
self.config['url_epInfo'] = '%(base_url)sseries/%%s/episodes?page=%%s' % self.config
|
||||
|
||||
self.config['url_seriesInfo'] = '%(base_url)sseries/%%s' % self.config
|
||||
self.config['url_episodeInfo'] = '%(base_url)sepisodes/%%s' % self.config
|
||||
self.config['url_actorsInfo'] = '%(base_url)sseries/%%s/actors' % self.config
|
||||
|
||||
self.config['url_seriesBanner'] = '%(base_url)sseries/%%s/images/query?keyType=%%s' % self.config
|
||||
|
@ -795,6 +796,28 @@ class Tvdb:
|
|||
|
||||
self._set_show_data(sid, '_actors', cur_actors)
|
||||
|
||||
def get_episode_data(self, epid):
|
||||
# Parse episode information
|
||||
data = None
|
||||
log().debug('Getting all episode data for %s' % epid)
|
||||
url = self.config['url_episodeInfo'] % epid
|
||||
episode_data = self._getetsrc(url, language=self.config['language'])
|
||||
|
||||
if isinstance(episode_data, dict) and 'data' in episode_data:
|
||||
data = episode_data['data']
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.iteritems():
|
||||
k = k.lower()
|
||||
|
||||
if None is not v:
|
||||
if 'filename' == k and v:
|
||||
v = self.config['url_artworkPrefix'] % v
|
||||
else:
|
||||
v = self._clean_data(v)
|
||||
data[k] = v
|
||||
|
||||
return data
|
||||
|
||||
def _get_show_data(self, sid, language, get_ep_info=False):
|
||||
"""Takes a series ID, gets the epInfo URL and parses the TVDB
|
||||
XML file into the shows dict in layout:
|
||||
|
|
26
lib/tvdb_api_v1/UNLICENSE
Normal file
26
lib/tvdb_api_v1/UNLICENSE
Normal file
|
@ -0,0 +1,26 @@
|
|||
Copyright 2011-2012 Ben Dickson (dbr)
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
1
lib/tvdb_api_v1/__init__.py
Normal file
1
lib/tvdb_api_v1/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
950
lib/tvdb_api_v1/tvdb_api.py
Normal file
950
lib/tvdb_api_v1/tvdb_api.py
Normal file
|
@ -0,0 +1,950 @@
|
|||
# !/usr/bin/env python2
|
||||
# encoding:utf-8
|
||||
# author:dbr/Ben
|
||||
# project:tvdb_api
|
||||
# repository:http://github.com/dbr/tvdb_api
|
||||
# license:unlicense (http://unlicense.org/)
|
||||
|
||||
import traceback
|
||||
from functools import wraps
|
||||
|
||||
__author__ = 'dbr/Ben'
|
||||
__version__ = '1.9'
|
||||
|
||||
import os
|
||||
import time
|
||||
import getpass
|
||||
import StringIO
|
||||
import tempfile
|
||||
import warnings
|
||||
import logging
|
||||
import zipfile
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
try:
|
||||
import gzip
|
||||
except ImportError:
|
||||
gzip = None
|
||||
|
||||
from lib.dateutil.parser import parse
|
||||
from lib.cachecontrol import CacheControl, caches
|
||||
|
||||
from lib.etreetodict import ConvertXmlToDict
|
||||
from tvdb_ui import BaseUI, ConsoleUI
|
||||
from tvdb_exceptions import (tvdb_error_v1, tvdb_shownotfound_v1,
|
||||
tvdb_seasonnotfound_v1, tvdb_episodenotfound_v1, tvdb_attributenotfound_v1)
|
||||
|
||||
from sickbeard import logger
|
||||
|
||||
|
||||
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logr=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 logr: logger to use. If None, print
|
||||
:type logr: 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 = 'TVDB_API :: %s, Retrying in %d seconds...' % (str(e), mdelay)
|
||||
if logr:
|
||||
logger.log(msg, logger.WARNING)
|
||||
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 __set_item__(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).__set_item__(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 %r (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 tvdb_seasonnotfound_v1('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 tvdb_attributenotfound_v1('Cannot find attribute %s' % (repr(key)))
|
||||
|
||||
def airedOn(self, date):
|
||||
ret = self.search(str(date), 'firstaired')
|
||||
if 0 == len(ret):
|
||||
raise tvdb_episodenotfound_v1('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.
|
||||
|
||||
# Examples
|
||||
|
||||
These examples assume t is an instance of Tvdb():
|
||||
|
||||
>> t = Tvdb()
|
||||
>>
|
||||
|
||||
To search for all episodes of Scrubs with a bit of data
|
||||
containing "my first day":
|
||||
|
||||
>> t['Scrubs'].search("my first day")
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>
|
||||
|
||||
Search for "My Name Is Earl" episode named "Faked His Own Death":
|
||||
|
||||
>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
|
||||
[<Episode 01x04 - Faked His Own Death>]
|
||||
>>
|
||||
|
||||
To search Scrubs for all episodes with "mentor" in the episode name:
|
||||
|
||||
>> t['scrubs'].search('mentor', key = 'episodename')
|
||||
[<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
|
||||
>>
|
||||
|
||||
# Using search results
|
||||
|
||||
>> results = t['Scrubs'].search("my first")
|
||||
>> print results[0]['episodename']
|
||||
My First Day
|
||||
>> for x in results: print x['episodename']
|
||||
My First Day
|
||||
My First Step
|
||||
My First Kill
|
||||
>>
|
||||
"""
|
||||
results = []
|
||||
for cur_season in self.values():
|
||||
searchresult = cur_season.search(term=term, key=key)
|
||||
if 0 != len(searchresult):
|
||||
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 tvdb_episodenotfound_v1('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.
|
||||
|
||||
>> t = Tvdb()
|
||||
>> t['scrubs'][1].search('first day')
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>
|
||||
|
||||
See Show.search documentation for further information on search
|
||||
"""
|
||||
results = []
|
||||
for ep in self.values():
|
||||
searchresult = ep.search(term=term, key=key)
|
||||
if None is not searchresult:
|
||||
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, epno = int(self.get(u'seasonnumber', 0)), int(self.get(u'episodenumber', 0))
|
||||
epname = self.get(u'episodename')
|
||||
if None is not epname:
|
||||
return '<Episode %02dx%02d - %r>' % (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 tvdb_attributenotfound_v1('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. See
|
||||
Show.search for further information on search
|
||||
|
||||
Simple example:
|
||||
|
||||
>> e = Episode()
|
||||
>> e['episodename'] = "An Example"
|
||||
>> e.search("examp")
|
||||
<Episode 00x00 - An Example>
|
||||
>>
|
||||
|
||||
Limiting by key:
|
||||
|
||||
>> e.search("examp", key = "episodename")
|
||||
<Episode 00x00 - An Example>
|
||||
>>
|
||||
"""
|
||||
if None is term:
|
||||
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 None is not key and cur_key != key:
|
||||
# Do not search this key
|
||||
continue
|
||||
if cur_value.find(unicode(term).lower()) > -1:
|
||||
return self
|
||||
|
||||
|
||||
class Actors(list):
|
||||
"""Holds all Actor instances for a show
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Actor(dict):
|
||||
"""Represents a single actor. Should contain..
|
||||
|
||||
id,
|
||||
image,
|
||||
name,
|
||||
role,
|
||||
sortorder
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return '<Actor "%r">' % self.get('name')
|
||||
|
||||
|
||||
class TvdbV1:
|
||||
"""Create easy-to-use interface to name of season/episode name
|
||||
>> t = Tvdb()
|
||||
>> t['Scrubs'][1][24]['episodename']
|
||||
u'My Last Day'
|
||||
"""
|
||||
|
||||
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):
|
||||
|
||||
"""interactive (True/False):
|
||||
When True, uses built-in console UI is used to select the correct show.
|
||||
When False, the first search result is used.
|
||||
|
||||
select_first (True/False):
|
||||
Automatically selects the first series search result (rather
|
||||
than showing the user a list of more than one series).
|
||||
Is overridden by interactive = False, or specifying a custom_ui
|
||||
|
||||
debug (True/False) DEPRECATED:
|
||||
Replaced with proper use of logging module. To show debug messages:
|
||||
|
||||
>> import logging
|
||||
>> logging.basicConfig(level = logging.DEBUG)
|
||||
|
||||
cache (True/False/str/unicode/urllib2 opener):
|
||||
Retrieved XML are persisted to to disc. If true, stores in
|
||||
tvdb_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
|
||||
|
||||
banners (True/False):
|
||||
Retrieves the banners for a show. These are accessed
|
||||
via the _banners key of a Show(), for example:
|
||||
|
||||
>> Tvdb(banners=True)['scrubs']['_banners'].keys()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
|
||||
actors (True/False):
|
||||
Retrieves a list of the actors for a show. These are accessed
|
||||
via the _actors key of a Show(), for example:
|
||||
|
||||
>> t = Tvdb(actors=True)
|
||||
>> t['scrubs']['_actors'][0]['name']
|
||||
u'Zach Braff'
|
||||
|
||||
custom_ui (tvdb_ui.BaseUI subclass):
|
||||
A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
|
||||
|
||||
language (2 character language abbreviation):
|
||||
The language of the returned data. Is also the language search
|
||||
uses. Default is "en" (English). For full list, run..
|
||||
|
||||
>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS
|
||||
['da', 'fi', 'nl', ...]
|
||||
|
||||
search_all_languages (True/False):
|
||||
By default, Tvdb will only search in the language specified using
|
||||
the language option. When this is True, it will search for the
|
||||
show in and language
|
||||
|
||||
apikey (str/unicode):
|
||||
Override the default thetvdb.com API key. By default it will use
|
||||
tvdb_api's own key (fine for small scripts), but you can use your
|
||||
own key if desired - this is recommended if you are embedding
|
||||
tvdb_api in a larger application)
|
||||
See http://thetvdb.com/?tab=apiregister to get your own key
|
||||
|
||||
forceConnect (bool):
|
||||
If true it will always try to connect to theTVDB.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.
|
||||
|
||||
useZip (bool):
|
||||
Download the zip archive where possibale, instead of the xml.
|
||||
This is only used when all episodes are pulled.
|
||||
And only the main language xml is used, the actor and banner xml are lost.
|
||||
"""
|
||||
|
||||
self.shows = ShowContainer() # Holds all Show classes
|
||||
self.corrections = {} # Holds show-name to show_id mapping
|
||||
|
||||
self.config = {}
|
||||
|
||||
if None is not apikey:
|
||||
self.config['apikey'] = apikey
|
||||
else:
|
||||
self.config['apikey'] = '0629B785CE550C8D' # tvdb_api's API key
|
||||
|
||||
self.config['debug_enabled'] = debug # show debugging messages
|
||||
|
||||
self.config['custom_ui'] = custom_ui
|
||||
|
||||
self.config['interactive'] = interactive # prompt for correct series?
|
||||
|
||||
self.config['select_first'] = select_first
|
||||
|
||||
self.config['search_all_languages'] = search_all_languages
|
||||
|
||||
self.config['useZip'] = useZip
|
||||
|
||||
self.config['dvdorder'] = dvdorder
|
||||
|
||||
self.config['proxy'] = proxy
|
||||
|
||||
if cache is True:
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = self._get_temp_dir()
|
||||
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)))
|
||||
|
||||
self.config['banners_enabled'] = banners
|
||||
self.config['actors_enabled'] = actors
|
||||
|
||||
if self.config['debug_enabled']:
|
||||
warnings.warn('The debug argument to tvdb_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://thetvdb.com/api/0629B785CE550C8D/languages.xml
|
||||
# Hard-coded here as it is realtively static, and saves another HTTP request, as
|
||||
# recommended on http://thetvdb.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'
|
||||
]
|
||||
|
||||
# thetvdb.com should be based around numeric language codes,
|
||||
# but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16
|
||||
# requires the language ID, thus this mapping is required (mainly
|
||||
# for usage in tvdb_ui - internally tvdb_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 None is language:
|
||||
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://thetvdb.com/wiki/index.php/Programmers_API
|
||||
self.config['base_url'] = 'http://thetvdb.com'
|
||||
|
||||
if self.config['search_all_languages']:
|
||||
self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config
|
||||
self.config['params_get_series'] = {'seriesname': '', 'language': 'all'}
|
||||
else:
|
||||
self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config
|
||||
self.config['params_get_series'] = {'seriesname': '', 'language': self.config['language']}
|
||||
|
||||
self.config['url_epInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml' % self.config
|
||||
self.config['url_epInfo_zip'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip' % self.config
|
||||
|
||||
self.config['url_seriesInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml' % self.config
|
||||
self.config['url_actorsInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/actors.xml' % self.config
|
||||
|
||||
self.config['url_seriesBanner'] = u'%(base_url)s/api/%(apikey)s/series/%%s/banners.xml' % self.config
|
||||
self.config['url_artworkPrefix'] = u'%(base_url)s/banners/%%s' % self.config
|
||||
|
||||
def log(self, msg, log_level=logger.DEBUG):
|
||||
logger.log('TVDB_API :: %s' % (msg.replace(self.config['apikey'], '<apikey>')), log_level=log_level)
|
||||
|
||||
@staticmethod
|
||||
def _get_temp_dir():
|
||||
"""Returns the [system temp dir]/tvdb_api-u501 (or
|
||||
tvdb_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(), 'tvdb_api')
|
||||
|
||||
return os.path.join(tempfile.gettempdir(), 'tvdb_api-%s' % uid)
|
||||
|
||||
@retry(tvdb_error_v1)
|
||||
def _load_url(self, url, params=None, language=None):
|
||||
self.log('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']:
|
||||
self.log('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 tvdb_error_v1('HTTP error %s while loading URL %s' % (e.errno, url))
|
||||
except requests.exceptions.ConnectionError, e:
|
||||
raise tvdb_error_v1('Connection error %s while loading URL %s' % (e.message, url))
|
||||
except requests.exceptions.Timeout, e:
|
||||
raise tvdb_error_v1('Connection timed out %s while loading URL %s' % (e.message, url))
|
||||
except Exception:
|
||||
raise tvdb_error_v1('Unknown exception while loading URL %s: %s' % (url, traceback.format_exc()))
|
||||
|
||||
def process_data(data):
|
||||
te = ConvertXmlToDict(data)
|
||||
if isinstance(te, dict) and 'Data' in te and isinstance(te['Data'], dict) \
|
||||
and 'Series' in te['Data'] and isinstance(te['Data']['Series'], dict) \
|
||||
and 'FirstAired' in te['Data']['Series']:
|
||||
try:
|
||||
value = parse(te['Data']['Series']['FirstAired'], fuzzy=True).strftime('%Y-%m-%d')
|
||||
except (StandardError, Exception):
|
||||
value = None
|
||||
te['Data']['Series']['firstaired'] = value
|
||||
return te
|
||||
|
||||
if resp.ok:
|
||||
if 'application/zip' in resp.headers.get('Content-Type', ''):
|
||||
try:
|
||||
# TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20]
|
||||
self.log('We received a zip file unpacking now ...')
|
||||
zipdata = StringIO.StringIO()
|
||||
zipdata.write(resp.content)
|
||||
myzipfile = zipfile.ZipFile(zipdata)
|
||||
return process_data(myzipfile.read('%s.xml' % language))
|
||||
except zipfile.BadZipfile:
|
||||
raise tvdb_error_v1('Bad zip file received from thetvdb.com, could not read it')
|
||||
else:
|
||||
try:
|
||||
return process_data(resp.content.strip())
|
||||
except (StandardError, Exception):
|
||||
return dict([(u'data', None)])
|
||||
|
||||
def _getetsrc(self, url, params=None, language=None):
|
||||
"""Loads a URL using caching, returns an ElementTree of the source
|
||||
"""
|
||||
try:
|
||||
src = self._load_url(url, params=params, language=language).values()[0]
|
||||
return src
|
||||
except (StandardError, Exception):
|
||||
return []
|
||||
|
||||
def _set_item(self, sid, seas, ep, attrib, value):
|
||||
"""Creates a new episode, creating Show(), Season() and
|
||||
Episode()s as required. Called by _get_show_data to populate show
|
||||
|
||||
Since the nice-to-use tvdb[1][24]['name] interface
|
||||
makes it impossible to do tvdb[1][24]['name] = "name"
|
||||
and still be capable of checking if an episode exists
|
||||
so we can raise tvdb_shownotfound_v1, 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 tvdb[1][24]['episodename'] = "name"
|
||||
calls __getitem__ on tvdb[1], there is no way to check if
|
||||
tvdb.__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 _set_show_data(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()
|
||||
self.shows[sid].data[key] = value
|
||||
|
||||
@staticmethod
|
||||
def _clean_data(data):
|
||||
"""Cleans up strings returned by TheTVDB.com
|
||||
|
||||
Issues corrected:
|
||||
- Replaces & with &
|
||||
- Trailing whitespace
|
||||
"""
|
||||
return data if not isinstance(data, basestring) else data.strip().replace(u'&', u'&')
|
||||
|
||||
def _get_url_artwork(self, image):
|
||||
return image and (self.config['url_artworkPrefix'] % image) or image
|
||||
|
||||
def search(self, series):
|
||||
"""This searches TheTVDB.com for the series name
|
||||
and returns the result list
|
||||
"""
|
||||
series = series.encode('utf-8')
|
||||
self.log('Searching for show %s' % series)
|
||||
self.config['params_get_series']['seriesname'] = series
|
||||
|
||||
try:
|
||||
series_found = self._getetsrc(self.config['url_get_series'], self.config['params_get_series'])
|
||||
if series_found:
|
||||
if not isinstance(series_found['Series'], list):
|
||||
series_found['Series'] = [series_found['Series']]
|
||||
series_found['Series'] = [{k.lower(): v for k, v in s.iteritems()} for s in series_found['Series']]
|
||||
return series_found.values()[0]
|
||||
except (StandardError, Exception):
|
||||
pass
|
||||
|
||||
return []
|
||||
|
||||
def _get_series(self, series):
|
||||
"""This searches TheTVDB.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.
|
||||
"""
|
||||
all_series = self.search(series)
|
||||
if not isinstance(all_series, list):
|
||||
all_series = [all_series]
|
||||
|
||||
if 0 == len(all_series):
|
||||
self.log('Series result returned zero')
|
||||
raise tvdb_shownotfound_v1('Show-name search returned zero results (cannot find show on TVDB)')
|
||||
|
||||
if None is not self.config['custom_ui']:
|
||||
self.log('Using custom UI %s' % (repr(self.config['custom_ui'])))
|
||||
custom_ui = self.config['custom_ui']
|
||||
ui = custom_ui(config=self.config)
|
||||
else:
|
||||
if not self.config['interactive']:
|
||||
self.log('Auto-selecting first search result using BaseUI')
|
||||
ui = BaseUI(config=self.config)
|
||||
else:
|
||||
self.log('Interactively selecting show using ConsoleUI')
|
||||
ui = ConsoleUI(config=self.config)
|
||||
|
||||
return ui.selectSeries(all_series)
|
||||
|
||||
def _parse_banners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
|
||||
|
||||
Banners are retrieved using t['show name]['_banners'], for example:
|
||||
|
||||
>> t = Tvdb(banners = True)
|
||||
>> t['scrubs']['_banners'].keys()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
u'http://thetvdb.com/banners/posters/76156-2.jpg'
|
||||
>>
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
|
||||
This interface will be improved in future versions.
|
||||
"""
|
||||
self.log('Getting season banners for %s' % sid)
|
||||
banners_et = self._getetsrc(self.config['url_seriesBanner'] % sid)
|
||||
banners = {}
|
||||
|
||||
try:
|
||||
for cur_banner in banners_et['banner']:
|
||||
bid = cur_banner['id']
|
||||
btype = cur_banner['bannertype']
|
||||
btype2 = cur_banner['bannertype2']
|
||||
if None is btype or None is btype2:
|
||||
continue
|
||||
if btype not in banners:
|
||||
banners[btype] = {}
|
||||
if btype2 not in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if bid not in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
for k, v in cur_banner.items():
|
||||
if None is k or None is v:
|
||||
continue
|
||||
|
||||
k, v = k.lower(), v.lower()
|
||||
banners[btype][btype2][bid][k] = v
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith('path'):
|
||||
new_key = '_%s' % k
|
||||
self.log('Transforming %s to %s' % (k, new_key))
|
||||
new_url = self._get_url_artwork(v)
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
except (StandardError, Exception):
|
||||
pass
|
||||
|
||||
self._set_show_data(sid, '_banners', banners)
|
||||
|
||||
def _parse_actors(self, sid):
|
||||
"""Parsers actors XML, from
|
||||
http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
|
||||
|
||||
Actors are retrieved using t['show name]['_actors'], for example:
|
||||
|
||||
>> t = Tvdb(actors = True)
|
||||
>> actors = t['scrubs']['_actors']
|
||||
>> type(actors)
|
||||
<class 'tvdb_api.Actors'>
|
||||
>> type(actors[0])
|
||||
<class 'tvdb_api.Actor'>
|
||||
>> actors[0]
|
||||
<Actor "Zach Braff">
|
||||
>> sorted(actors[0].keys())
|
||||
['id', 'image', 'name', 'role', 'sortorder']
|
||||
>> actors[0]['name']
|
||||
u'Zach Braff'
|
||||
>> actors[0]['image']
|
||||
u'http://thetvdb.com/banners/actors/43640.jpg'
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
"""
|
||||
self.log('Getting actors for %s' % sid)
|
||||
actors_et = self._getetsrc(self.config['url_actorsInfo'] % sid)
|
||||
|
||||
cur_actors = Actors()
|
||||
try:
|
||||
for curActorItem in actors_et['actor']:
|
||||
cur_actor = Actor()
|
||||
for k, v in curActorItem.items():
|
||||
k = k.lower()
|
||||
if None is not v:
|
||||
if 'image' == k:
|
||||
v = self._get_url_artwork(v)
|
||||
else:
|
||||
v = self._clean_data(v)
|
||||
cur_actor[k] = v
|
||||
cur_actors.append(cur_actor)
|
||||
except (StandardError, Exception):
|
||||
pass
|
||||
|
||||
self._set_show_data(sid, '_actors', cur_actors)
|
||||
|
||||
def _get_show_data(self, sid, language, get_ep_info=False):
|
||||
"""Takes a series ID, gets the epInfo URL and parses the TVDB
|
||||
XML file into the shows dict in layout:
|
||||
shows[series_id][season_number][episode_number]
|
||||
"""
|
||||
|
||||
if None is self.config['language']:
|
||||
self.log('Config language is none, using show language')
|
||||
if None is language:
|
||||
raise tvdb_error_v1('config[\'language\'] was None, this should not happen')
|
||||
get_show_in_language = language
|
||||
else:
|
||||
self.log('Configured language %s override show language of %s' % (self.config['language'], language))
|
||||
get_show_in_language = self.config['language']
|
||||
|
||||
# Parse show information
|
||||
self.log('Getting all series data for %s' % sid)
|
||||
url = (self.config['url_seriesInfo'] % (sid, language), self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language))[get_ep_info]
|
||||
show_data = self._getetsrc(url, language=get_show_in_language)
|
||||
|
||||
# check and make sure we have data to process and that it contains a series name
|
||||
if not len(show_data) or (isinstance(show_data, dict) and 'SeriesName' not in show_data['Series']):
|
||||
return False
|
||||
|
||||
for k, v in show_data['Series'].iteritems():
|
||||
if None is not v:
|
||||
if k in ['banner', 'fanart', 'poster']:
|
||||
v = self._get_url_artwork(v)
|
||||
else:
|
||||
v = self._clean_data(v)
|
||||
|
||||
self._set_show_data(sid, k.lower(), v)
|
||||
|
||||
if get_ep_info:
|
||||
# Parse banners
|
||||
if self.config['banners_enabled']:
|
||||
self._parse_banners(sid)
|
||||
|
||||
# Parse actors
|
||||
if self.config['actors_enabled']:
|
||||
self._parse_actors(sid)
|
||||
|
||||
# Parse episode data
|
||||
self.log('Getting all episodes of %s' % sid)
|
||||
|
||||
if 'Episode' not in show_data:
|
||||
return False
|
||||
|
||||
episodes = show_data['Episode']
|
||||
if not isinstance(episodes, list):
|
||||
episodes = [episodes]
|
||||
|
||||
dvd_order = {'dvd': [], 'network': []}
|
||||
for cur_ep in episodes:
|
||||
if self.config['dvdorder']:
|
||||
use_dvd = cur_ep['DVD_season'] not in (None, '') and cur_ep['DVD_episodenumber'] not in (None, '')
|
||||
else:
|
||||
use_dvd = False
|
||||
|
||||
if use_dvd:
|
||||
elem_seasnum, elem_epno = cur_ep['DVD_season'], cur_ep['DVD_episodenumber']
|
||||
else:
|
||||
elem_seasnum, elem_epno = cur_ep['SeasonNumber'], cur_ep['EpisodeNumber']
|
||||
|
||||
if None is elem_seasnum or None is elem_epno:
|
||||
self.log('An episode has incomplete season/episode number (season: %r, episode: %r)' % (
|
||||
elem_seasnum, elem_epno), logger.WARNING)
|
||||
continue # Skip to next episode
|
||||
|
||||
# float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data
|
||||
seas_no = int(float(elem_seasnum))
|
||||
ep_no = int(float(elem_epno))
|
||||
|
||||
if self.config['dvdorder']:
|
||||
dvd_order[('network', 'dvd')[use_dvd]] += ['S%02dE%02d' % (seas_no, ep_no)]
|
||||
|
||||
for k, v in cur_ep.items():
|
||||
k = k.lower()
|
||||
|
||||
if None is not v:
|
||||
if 'filename' == k:
|
||||
v = self._get_url_artwork(v)
|
||||
else:
|
||||
v = self._clean_data(v)
|
||||
|
||||
self._set_item(sid, seas_no, ep_no, k, v)
|
||||
|
||||
if self.config['dvdorder']:
|
||||
num_dvd, num_network = [len(dvd_order[x]) for x in 'dvd', 'network']
|
||||
num_all = num_dvd + num_network
|
||||
if num_all:
|
||||
self.log('Of %s episodes, %s use the DVD order, and %s use the network aired order' % (
|
||||
num_all, num_dvd, num_network))
|
||||
for ep_numbers in [', '.join(dvd_order['dvd'][i:i + 5]) for i in xrange(0, num_dvd, 5)]:
|
||||
self.log('Using DVD order: %s' % ep_numbers)
|
||||
|
||||
return True
|
||||
|
||||
def _name_to_sid(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:
|
||||
self.log('Correcting %s to %s' % (name, self.corrections[name]))
|
||||
return self.corrections[name]
|
||||
else:
|
||||
self.log('Getting show %s' % name)
|
||||
selected_series = self._get_series(name)
|
||||
if isinstance(selected_series, dict):
|
||||
selected_series = [selected_series]
|
||||
sids = list(int(x['id']) for x in selected_series if
|
||||
self._get_show_data(int(x['id']), self.config['language']))
|
||||
self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series))
|
||||
return sids
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Handles tvdb_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._get_show_data(key, self.config['language'], (True, arg)[arg is not None])
|
||||
return None if key not in self.shows else self.shows[key]
|
||||
|
||||
key = str(key).lower()
|
||||
self.config['searchterm'] = key
|
||||
selected_series = self._get_series(key)
|
||||
if isinstance(selected_series, dict):
|
||||
selected_series = [selected_series]
|
||||
[[self._set_show_data(show['id'], k, v) for k, v in show.items()] for show in selected_series]
|
||||
return selected_series
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.shows)
|
||||
|
||||
|
||||
def main():
|
||||
"""Simple example of using tvdb_api - it just
|
||||
grabs an episode name interactively.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
tvdb_instance = Tvdb(interactive=True, cache=False)
|
||||
print tvdb_instance['Lost']['seriesname']
|
||||
print tvdb_instance['Lost'][1][4]['episodename']
|
||||
|
||||
|
||||
if '__main__' == __name__:
|
||||
main()
|
251
lib/tvdb_api_v1/tvdb_cache.py
Normal file
251
lib/tvdb_api_v1/tvdb_cache.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""
|
||||
urllib2 caching handler
|
||||
Modified from http://code.activestate.com/recipes/491261/
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.9"
|
||||
|
||||
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()
|
52
lib/tvdb_api_v1/tvdb_exceptions.py
Normal file
52
lib/tvdb_api_v1/tvdb_exceptions.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""Custom exceptions used or raised by tvdb_api
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.9"
|
||||
|
||||
__all__ = ["tvdb_error_v1", "tvdb_userabort_v1", "tvdb_shownotfound_v1",
|
||||
"tvdb_seasonnotfound_v1", "tvdb_episodenotfound_v1", "tvdb_attributenotfound_v1"]
|
||||
|
||||
class tvdb_exception_v1(Exception):
|
||||
"""Any exception generated by tvdb_api
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_error_v1(tvdb_exception_v1):
|
||||
"""An error with thetvdb.com (Cannot connect, for example)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_userabort_v1(tvdb_exception_v1):
|
||||
"""User aborted the interactive selection (via
|
||||
the q command, ^c etc)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_shownotfound_v1(tvdb_exception_v1):
|
||||
"""Show cannot be found on thetvdb.com (non-existant show)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_seasonnotfound_v1(tvdb_exception_v1):
|
||||
"""Season cannot be found on thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_episodenotfound_v1(tvdb_exception_v1):
|
||||
"""Episode cannot be found on thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_attributenotfound_v1(tvdb_exception_v1):
|
||||
"""Raised if an episode does not have the requested
|
||||
attribute (such as a episode name)
|
||||
"""
|
||||
pass
|
153
lib/tvdb_api_v1/tvdb_ui.py
Normal file
153
lib/tvdb_api_v1/tvdb_ui.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python2
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:unlicense (http://unlicense.org/)
|
||||
|
||||
"""Contains included user interfaces for Tvdb show selection.
|
||||
|
||||
A UI is a callback. A class, it's __init__ function takes two arguments:
|
||||
|
||||
- config, which is the Tvdb config dict, setup in tvdb_api.py
|
||||
- log, which is Tvdb's logger instance (which uses the logging module). You can
|
||||
call log.info() log.warning() etc
|
||||
|
||||
It must have a method "selectSeries", this is passed a list of dicts, each dict
|
||||
contains the the keys "name" (human readable show name), and "sid" (the shows
|
||||
ID as on thetvdb.com). For example:
|
||||
|
||||
[{'name': u'Lost', 'sid': u'73739'},
|
||||
{'name': u'Lost Universe', 'sid': u'73181'}]
|
||||
|
||||
The "selectSeries" method must return the appropriate dict, or it can raise
|
||||
tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
|
||||
cannot be found).
|
||||
|
||||
A simple example callback, which returns a random series:
|
||||
|
||||
>>> import random
|
||||
>>> from tvdb_ui import BaseUI
|
||||
>>> class RandomUI(BaseUI):
|
||||
... def selectSeries(self, allSeries):
|
||||
... import random
|
||||
... return random.choice(allSeries)
|
||||
|
||||
Then to use it..
|
||||
|
||||
>>> from tvdb_api import Tvdb
|
||||
>>> t = Tvdb(custom_ui = RandomUI)
|
||||
>>> random_matching_series = t['Lost']
|
||||
>>> type(random_matching_series)
|
||||
<class 'tvdb_api.Show'>
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.9"
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from tvdb_exceptions import tvdb_userabort_v1
|
||||
|
||||
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]
|
||||
|
||||
|
||||
class ConsoleUI(BaseUI):
|
||||
"""Interactively allows the user to select a show from a console based UI
|
||||
"""
|
||||
|
||||
def _displaySeries(self, allSeries, limit = 6):
|
||||
"""Helper function, lists series with corresponding ID
|
||||
"""
|
||||
if limit is not None:
|
||||
toshow = allSeries[:limit]
|
||||
else:
|
||||
toshow = allSeries
|
||||
|
||||
print "TVDB Search Results:"
|
||||
for i, cshow in enumerate(toshow):
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesname']))
|
||||
if i == 0:
|
||||
extra = " (default)"
|
||||
else:
|
||||
extra = ""
|
||||
|
||||
print "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % (
|
||||
i_show,
|
||||
cshow['seriesname'].encode("UTF-8", "ignore"),
|
||||
cshow['language'].encode("UTF-8", "ignore"),
|
||||
str(cshow['id']),
|
||||
cshow['lid'],
|
||||
extra
|
||||
)
|
||||
|
||||
def selectSeries(self, allSeries):
|
||||
self._displaySeries(allSeries)
|
||||
|
||||
if len(allSeries) == 1:
|
||||
# Single result, return it!
|
||||
print "Automatically selecting only result"
|
||||
return allSeries[0]
|
||||
|
||||
if self.config['select_first'] is True:
|
||||
print "Automatically returning first search result"
|
||||
return allSeries[0]
|
||||
|
||||
while True: # return breaks this loop
|
||||
try:
|
||||
print "Enter choice (first number, return for default, 'all', ? for help):"
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise tvdb_userabort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise tvdb_userabort("User aborted (EOF received)")
|
||||
|
||||
log().debug('Got choice of: %s' % (ans))
|
||||
try:
|
||||
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
|
||||
except ValueError: # Input was not number
|
||||
if len(ans.strip()) == 0:
|
||||
# Default option
|
||||
log().debug('Default option, returning first series')
|
||||
return allSeries[0]
|
||||
if ans == "q":
|
||||
log().debug('Got quit command (q)')
|
||||
raise tvdb_userabort("User aborted ('q' quit command)")
|
||||
elif ans == "?":
|
||||
print "## Help"
|
||||
print "# Enter the number that corresponds to the correct show."
|
||||
print "# a - display all results"
|
||||
print "# all - display all results"
|
||||
print "# ? - this help"
|
||||
print "# q - abort tvnamer"
|
||||
print "# Press return with no input to select first result"
|
||||
elif ans.lower() in ["a", "all"]:
|
||||
self._displaySeries(allSeries, limit = None)
|
||||
else:
|
||||
log().debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
log().debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
return allSeries[selected_id]
|
||||
except IndexError:
|
||||
log().debug('Invalid show number entered!')
|
||||
print "Invalid number (%s) selected!"
|
||||
self._displaySeries(allSeries)
|
||||
|
|
@ -83,7 +83,9 @@ class indexerApi(object):
|
|||
|
||||
def indexer(self, *args, **kwargs):
|
||||
if self.indexerID:
|
||||
if indexerConfig[self.indexerID]['active']:
|
||||
if indexerConfig[self.indexerID]['active'] or ('no_dummy' in kwargs and True is kwargs['no_dummy']):
|
||||
if 'no_dummy' in kwargs:
|
||||
kwargs.pop('no_dummy')
|
||||
return indexerConfig[self.indexerID]['module'](*args, **kwargs)
|
||||
else:
|
||||
return DummyIndexer(*args, **kwargs)
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
from lib.tvdb_api.tvdb_api import Tvdb
|
||||
from lib.tvdb_api_v1.tvdb_api import TvdbV1
|
||||
from lib.libtrakt.indexerapiinterface import TraktIndexer
|
||||
|
||||
INDEXER_TVDB = 1
|
||||
INDEXER_TVRAGE = 2
|
||||
INDEXER_TVMAZE = 3
|
||||
|
||||
# old tvdb api - version 1
|
||||
INDEXER_TVDB_V1 = 10001
|
||||
|
||||
# mapped only indexer
|
||||
INDEXER_IMDB = 100
|
||||
INDEXER_TRAKT = 101
|
||||
|
@ -29,6 +33,17 @@ indexerConfig = {
|
|||
mapped_only=False,
|
||||
icon='thetvdb16.png',
|
||||
),
|
||||
INDEXER_TVDB_V1: dict(
|
||||
main_url='https://thetvdb.com/',
|
||||
id=INDEXER_TVDB_V1,
|
||||
name='TheTVDBV1',
|
||||
module=TvdbV1,
|
||||
api_params=dict(apikey='F9C450E78D99172E', language='en'),
|
||||
active=False,
|
||||
dupekey='',
|
||||
mapped_only=True,
|
||||
icon='thetvdb16.png',
|
||||
),
|
||||
INDEXER_TVRAGE: dict(
|
||||
main_url='http://tvrage.com/',
|
||||
id=INDEXER_TVRAGE,
|
||||
|
@ -97,6 +112,15 @@ indexerConfig[info_src].update(dict(
|
|||
xem_origin='tvdb',
|
||||
))
|
||||
|
||||
info_src = INDEXER_TVDB_V1
|
||||
indexerConfig[info_src].update(dict(
|
||||
base_url=(indexerConfig[info_src]['main_url'] +
|
||||
'api/%(apikey)s/series/' % indexerConfig[info_src]['api_params']),
|
||||
show_url='%s?tab=series&id=' % indexerConfig[info_src]['main_url'],
|
||||
finder=(indexerConfig[info_src]['main_url'] +
|
||||
'index.php?fieldlocation=2&language=7&order=translation&searching=Search&tab=advancedsearch&seriesname=%s'),
|
||||
))
|
||||
|
||||
info_src = INDEXER_TVRAGE
|
||||
indexerConfig[info_src].update(dict(
|
||||
base_url=(indexerConfig[info_src]['main_url'] +
|
||||
|
|
|
@ -9,6 +9,10 @@ from lib.tvdb_api.tvdb_exceptions import \
|
|||
tvdb_exception, tvdb_attributenotfound, tvdb_episodenotfound, tvdb_error, \
|
||||
tvdb_seasonnotfound, tvdb_shownotfound, tvdb_userabort, tvdb_tokenexpired
|
||||
|
||||
from lib.tvdb_api_v1.tvdb_exceptions import \
|
||||
tvdb_exception_v1, tvdb_attributenotfound_v1, tvdb_episodenotfound_v1, tvdb_error_v1, \
|
||||
tvdb_seasonnotfound_v1, tvdb_shownotfound_v1, tvdb_userabort_v1
|
||||
|
||||
indexerExcepts = [
|
||||
'indexer_exception', 'indexer_error', 'indexer_userabort',
|
||||
'indexer_shownotfound', 'indexer_seasonnotfound', 'indexer_episodenotfound',
|
||||
|
@ -19,12 +23,16 @@ tvdbExcepts = [
|
|||
'tvdb_seasonnotfound', 'tvdb_episodenotfound', 'tvdb_attributenotfound',
|
||||
'tvdb_tokenexpired']
|
||||
|
||||
tvdbV1Excepts = [
|
||||
'tvdb_exception_v1', 'tvdb_error_v1', 'tvdb_userabort_v1', 'tvdb_shownotfound_v1',
|
||||
'tvdb_seasonnotfound_v1', 'tvdb_episodenotfound_v1', 'tvdb_attributenotfound_v1']
|
||||
|
||||
# link API exceptions to our exception handler
|
||||
indexer_exception = tvdb_exception
|
||||
indexer_error = tvdb_error
|
||||
indexer_exception = tvdb_exception, tvdb_exception_v1
|
||||
indexer_error = tvdb_error, tvdb_error_v1
|
||||
indexer_authenticationerror = tvdb_tokenexpired
|
||||
indexer_userabort = tvdb_userabort
|
||||
indexer_attributenotfound = tvdb_attributenotfound
|
||||
indexer_episodenotfound = tvdb_episodenotfound
|
||||
indexer_seasonnotfound = tvdb_seasonnotfound
|
||||
indexer_shownotfound = tvdb_shownotfound
|
||||
indexer_userabort = tvdb_userabort, tvdb_userabort_v1
|
||||
indexer_attributenotfound = tvdb_attributenotfound, tvdb_attributenotfound_v1
|
||||
indexer_episodenotfound = tvdb_episodenotfound, tvdb_episodenotfound_v1
|
||||
indexer_seasonnotfound = tvdb_seasonnotfound, tvdb_seasonnotfound_v1
|
||||
indexer_shownotfound = tvdb_shownotfound, tvdb_shownotfound_v1
|
|
@ -39,6 +39,7 @@ from sickbeard import encodingKludge as ek
|
|||
from sickbeard.exceptions import ex
|
||||
from sickbeard.show_name_helpers import allPossibleShowNames
|
||||
from sickbeard.indexers import indexer_config
|
||||
from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVDB_V1
|
||||
|
||||
from six import iteritems
|
||||
|
||||
|
@ -397,12 +398,30 @@ class GenericMetadata():
|
|||
|
||||
# try all included episodes in case some have thumbs and others don't
|
||||
for cur_ep in all_eps:
|
||||
myEp = helpers.validateShow(cur_ep.show, cur_ep.season, cur_ep.episode)
|
||||
if INDEXER_TVDB == cur_ep.show.indexer:
|
||||
indexer_lang = cur_ep.show.lang
|
||||
|
||||
try:
|
||||
lINDEXER_API_PARMS = sickbeard.indexerApi(INDEXER_TVDB_V1).api_params.copy()
|
||||
lINDEXER_API_PARMS['dvdorder'] = 0 != cur_ep.show.dvdorder
|
||||
lINDEXER_API_PARMS['no_dummy'] = True
|
||||
|
||||
if indexer_lang and not indexer_lang == 'en':
|
||||
lINDEXER_API_PARMS['language'] = indexer_lang
|
||||
|
||||
t = sickbeard.indexerApi(INDEXER_TVDB_V1).indexer(**lINDEXER_API_PARMS)
|
||||
|
||||
myEp = t[cur_ep.show.indexerid][cur_ep.season][cur_ep.episode]
|
||||
except (sickbeard.indexer_episodenotfound, sickbeard.indexer_seasonnotfound, TypeError):
|
||||
myEp = None
|
||||
else:
|
||||
myEp = helpers.validateShow(cur_ep.show, cur_ep.season, cur_ep.episode)
|
||||
|
||||
if not myEp:
|
||||
continue
|
||||
|
||||
thumb_url = getattr(myEp, 'filename', None)
|
||||
if thumb_url is not None:
|
||||
thumb_url = getattr(myEp, 'filename', None) or (isinstance(myEp, dict) and myEp.get('filename', None))
|
||||
if thumb_url not in (None, False, ''):
|
||||
return thumb_url
|
||||
|
||||
return None
|
||||
|
|
Loading…
Reference in a new issue