Browse Source

Update TMDB api

pull/2875/head
Ruud 11 years ago
parent
commit
a1c0b000a4
  1. 3
      libs/tmdb3/__init__.py
  2. 43
      libs/tmdb3/cache.py
  3. 30
      libs/tmdb3/cache_engine.py
  4. 55
      libs/tmdb3/cache_file.py
  5. 18
      libs/tmdb3/cache_null.py
  6. 24
      libs/tmdb3/locales.py
  7. 31
      libs/tmdb3/pager.py
  8. 66
      libs/tmdb3/request.py
  9. 663
      libs/tmdb3/tmdb_api.py
  10. 35
      libs/tmdb3/tmdb_auth.py
  11. 90
      libs/tmdb3/tmdb_exceptions.py
  12. 201
      libs/tmdb3/util.py

3
libs/tmdb3/__init__.py

@ -2,7 +2,8 @@
from tmdb_api import Configuration, searchMovie, searchMovieWithYear, \
searchPerson, searchStudio, searchList, searchCollection, \
Person, Movie, Collection, Genre, List, __version__
searchSeries, Person, Movie, Collection, Genre, List, \
Series, Studio, Network, Episode, Season, __version__
from request import set_key, set_cache
from locales import get_locale, set_locale
from tmdb_auth import get_session, set_session

43
libs/tmdb3/cache.py

@ -7,20 +7,27 @@
# Purpose: Caching framework to store TMDb API results
#-----------------------
import time
import os
from tmdb_exceptions import *
from cache_engine import Engines
import cache_null
import cache_file
class Cache( object ):
class Cache(object):
"""
This class implements a persistent cache, backed in a file specified in
the object creation. The file is protected for safe, concurrent access
by multiple instances using flock.
This cache uses JSON for speed and storage efficiency, so only simple
data types are supported.
Data is stored in a simple format {key:(expiretimestamp, data)}
This class implements a cache framework, allowing selecting of a
pluggable engine. The framework stores data in a key/value manner,
along with a lifetime, after which data will be expired and
pulled fresh next time it is requested from the cache.
This class defines a wrapper to be used with query functions. The
wrapper will automatically cache the inputs and outputs of the
wrapped function, pulling the output from local storage for
subsequent calls with those inputs.
"""
def __init__(self, engine=None, *args, **kwargs):
self._engine = None
@ -37,7 +44,7 @@ class Cache( object ):
self._age = max(self._age, obj.creation)
def _expire(self):
for k,v in self._data.items():
for k, v in self._data.items():
if v.expired:
del self._data[k]
@ -87,19 +94,22 @@ class Cache( object ):
self.__doc__ = func.__doc__
def __call__(self, *args, **kwargs):
if self.func is None: # decorator is waiting to be given a function
if self.func is None:
# decorator is waiting to be given a function
if len(kwargs) or (len(args) != 1):
raise TMDBCacheError('Cache.Cached decorator must be called '+\
'a single callable argument before it '+\
'be used.')
raise TMDBCacheError(
'Cache.Cached decorator must be called a single ' +
'callable argument before it be used.')
elif args[0] is None:
raise TMDBCacheError('Cache.Cached decorator called before '+\
'being given a function to wrap.')
raise TMDBCacheError(
'Cache.Cached decorator called before being given ' +
'a function to wrap.')
elif not callable(args[0]):
raise TMDBCacheError('Cache.Cached must be provided a '+\
'callable object.')
raise TMDBCacheError(
'Cache.Cached must be provided a callable object.')
return self.__class__(self.cache, self.callback, args[0])
elif self.inst.lifetime == 0:
# lifetime of zero means never cache
return self.func(*args, **kwargs)
else:
key = self.callback()
@ -118,4 +128,3 @@ class Cache( object ):
func = self.func.__get__(inst, owner)
callback = self.callback.__get__(inst, owner)
return self.__class__(self.cache, callback, func, inst)

30
libs/tmdb3/cache_engine.py

@ -10,35 +10,46 @@
import time
from weakref import ref
class Engines( object ):
class Engines(object):
"""
Static collector for engines to register against.
"""
def __init__(self):
self._engines = {}
def register(self, engine):
self._engines[engine.__name__] = engine
self._engines[engine.name] = engine
def __getitem__(self, key):
return self._engines[key]
def __contains__(self, key):
return self._engines.__contains__(key)
Engines = Engines()
class CacheEngineType( type ):
class CacheEngineType(type):
"""
Cache Engine Metaclass that registers new engines against the cache
for named selection and use.
"""
def __init__(mcs, name, bases, attrs):
super(CacheEngineType, mcs).__init__(name, bases, attrs)
def __init__(cls, name, bases, attrs):
super(CacheEngineType, cls).__init__(name, bases, attrs)
if name != 'CacheEngine':
# skip base class
Engines.register(mcs)
Engines.register(cls)
class CacheEngine( object ):
__metaclass__ = CacheEngineType
class CacheEngine(object):
__metaclass__ = CacheEngineType
name = 'unspecified'
def __init__(self, parent):
self.parent = ref(parent)
def configure(self):
raise RuntimeError
def get(self, date):
@ -48,7 +59,8 @@ class CacheEngine( object ):
def expire(self, key):
raise RuntimeError
class CacheObject( object ):
class CacheObject(object):
"""
Cache object class, containing one stored record.
"""
@ -64,7 +76,7 @@ class CacheObject( object ):
@property
def expired(self):
return (self.remaining == 0)
return self.remaining == 0
@property
def remaining(self):

55
libs/tmdb3/cache_file.py

@ -12,6 +12,7 @@
import struct
import errno
import json
import time
import os
import io
@ -54,11 +55,11 @@ def _donothing(*args, **kwargs):
try:
import fcntl
class Flock( object ):
class Flock(object):
"""
Context manager to flock file for the duration the object exists.
Referenced file will be automatically unflocked as the interpreter
exits the context.
Context manager to flock file for the duration the object
exists. Referenced file will be automatically unflocked as the
interpreter exits the context.
Supports an optional callback to process the error and optionally
suppress it.
"""
@ -69,8 +70,10 @@ try:
self.fileobj = fileobj
self.operation = operation
self.callback = callback
def __enter__(self):
fcntl.flock(self.fileobj, self.operation)
def __exit__(self, exc_type, exc_value, exc_tb):
suppress = False
if callable(self.callback):
@ -101,9 +104,11 @@ except ImportError:
self.fileobj = fileobj
self.operation = operation
self.callback = callback
def __enter__(self):
self.size = os.path.getsize(self.fileobj.name)
msvcrt.locking(self.fileobj.fileno(), self.operation, self.size)
def __exit__(self, exc_type, exc_value, exc_tb):
suppress = False
if callable(self.callback):
@ -118,7 +123,7 @@ except ImportError:
if filename.startswith('~'):
# check for home directory
return os.path.expanduser(filename)
elif (ord(filename[0]) in (range(65,91)+range(99,123))) \
elif (ord(filename[0]) in (range(65, 91) + range(99, 123))) \
and (filename[1:3] == ':\\'):
# check for absolute drive path (e.g. C:\...)
return filename
@ -126,12 +131,12 @@ except ImportError:
# check for absolute UNC path (e.g. \\server\...)
return filename
# return path with temp directory prepended
return os.path.expandvars(os.path.join('%TEMP%',filename))
return os.path.expandvars(os.path.join('%TEMP%', filename))
class FileCacheObject( CacheObject ):
_struct = struct.Struct('dII') # double and two ints
# timestamp, lifetime, position
class FileCacheObject(CacheObject):
_struct = struct.Struct('dII') # double and two ints
# timestamp, lifetime, position
@classmethod
def fromFile(cls, fd):
@ -150,7 +155,7 @@ class FileCacheObject( CacheObject ):
@property
def size(self):
if self._size is None:
self._buff.seek(0,2)
self._buff.seek(0, 2)
size = self._buff.tell()
if size == 0:
if (self._key is None) or (self._data is None):
@ -159,8 +164,10 @@ class FileCacheObject( CacheObject ):
self._size = self._buff.tell()
self._size = size
return self._size
@size.setter
def size(self, value): self._size = value
def size(self, value):
self._size = value
@property
def key(self):
@ -170,16 +177,20 @@ class FileCacheObject( CacheObject ):
except:
pass
return self._key
@key.setter
def key(self, value): self._key = value
def key(self, value):
self._key = value
@property
def data(self):
if self._data is None:
self._key, self._data = json.loads(self._buff.getvalue())
return self._data
@data.setter
def data(self, value): self._data = value
def data(self, value):
self._data = value
def load(self, fd):
fd.seek(self.position)
@ -199,7 +210,7 @@ class FileCacheObject( CacheObject ):
class FileEngine( CacheEngine ):
"""Simple file-backed engine."""
name = 'file'
_struct = struct.Struct('HH') # two shorts for version and count
_struct = struct.Struct('HH') # two shorts for version and count
_version = 2
def __init__(self, parent):
@ -219,7 +230,6 @@ class FileEngine( CacheEngine ):
if self.cachefile is None:
raise TMDBCacheError("No cache filename given.")
self.cachefile = parse_filename(self.cachefile)
try:
@ -246,7 +256,7 @@ class FileEngine( CacheEngine ):
else:
# let the unhandled error continue through
raise
elif e.errno == errno.EACCESS:
elif e.errno == errno.EACCES:
# file exists, but we do not have permission to access it
raise TMDBCacheReadError(self.cachefile)
else:
@ -257,7 +267,7 @@ class FileEngine( CacheEngine ):
self._init_cache()
self._open('r+b')
with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access
with Flock(self.cachefd, Flock.LOCK_SH):
# return any new objects in the cache
return self._read(date)
@ -265,7 +275,7 @@ class FileEngine( CacheEngine ):
self._init_cache()
self._open('r+b')
with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access
with Flock(self.cachefd, Flock.LOCK_EX):
newobjs = self._read(self.age)
newobjs.append(FileCacheObject(key, value, lifetime))
@ -283,7 +293,8 @@ class FileEngine( CacheEngine ):
# already opened in requested mode, nothing to do
self.cachefd.seek(0)
return
except: pass # catch issue of no cachefile yet opened
except:
pass # catch issue of no cachefile yet opened
self.cachefd = io.open(self.cachefile, mode)
def _read(self, date):
@ -310,7 +321,7 @@ class FileEngine( CacheEngine ):
return []
# get end of file
self.cachefd.seek(0,2)
self.cachefd.seek(0, 2)
position = self.cachefd.tell()
newobjs = []
emptycount = 0
@ -348,7 +359,7 @@ class FileEngine( CacheEngine ):
data = data[-1]
# determine write position of data in cache
self.cachefd.seek(0,2)
self.cachefd.seek(0, 2)
end = self.cachefd.tell()
data.position = end
@ -387,5 +398,3 @@ class FileEngine( CacheEngine ):
def expire(self, key):
pass

18
libs/tmdb3/cache_null.py

@ -9,11 +9,19 @@
from cache_engine import CacheEngine
class NullEngine( CacheEngine ):
class NullEngine(CacheEngine):
"""Non-caching engine for debugging."""
name = 'null'
def configure(self): pass
def get(self, date): return []
def put(self, key, value, lifetime): return []
def expire(self, key): pass
def configure(self):
pass
def get(self, date):
return []
def put(self, key, value, lifetime):
return []
def expire(self, key):
pass

24
libs/tmdb3/locales.py

@ -11,7 +11,8 @@ import locale
syslocale = None
class LocaleBase( object ):
class LocaleBase(object):
__slots__ = ['__immutable']
_stored = {}
fallthrough = False
@ -24,19 +25,21 @@ class LocaleBase( object ):
def __setattr__(self, key, value):
if getattr(self, '__immutable', False):
raise NotImplementedError(self.__class__.__name__ +
' does not support modification.')
' does not support modification.')
super(LocaleBase, self).__setattr__(key, value)
def __delattr__(self, key):
if getattr(self, '__immutable', False):
raise NotImplementedError(self.__class__.__name__ +
' does not support modification.')
' does not support modification.')
super(LocaleBase, self).__delattr__(key)
def __lt__(self, other):
return (id(self) != id(other)) and (str(self) > str(other))
def __gt__(self, other):
return (id(self) != id(other)) and (str(self) < str(other))
def __eq__(self, other):
return (id(self) == id(other)) or (str(self) == str(other))
@ -48,9 +51,10 @@ class LocaleBase( object ):
return cls._stored[key.lower()]
except:
raise TMDBLocaleError("'{0}' is not a known valid {1} code."\
.format(key, cls.__name__))
.format(key, cls.__name__))
class Language( LocaleBase ):
class Language(LocaleBase):
__slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname',
'nativename']
_stored = {}
@ -69,12 +73,13 @@ class Language( LocaleBase ):
def __repr__(self):
return u"<Language '{0.englishname}' ({0.ISO639_1})>".format(self)
class Country( LocaleBase ):
class Country(LocaleBase):
__slots__ = ['alpha2', 'name']
_stored = {}
def __init__(self, alpha2, name):
self.alpha2 = alpha2
self.alpha2 = alpha2
self.name = name
super(Country, self).__init__(alpha2)
@ -84,7 +89,8 @@ class Country( LocaleBase ):
def __repr__(self):
return u"<Country '{0.name}' ({0.alpha2})>".format(self)
class Locale( LocaleBase ):
class Locale(LocaleBase):
__slots__ = ['language', 'country', 'encoding']
def __init__(self, language, country, encoding):
@ -120,6 +126,7 @@ class Locale( LocaleBase ):
# just return unmodified and hope for the best
return dat
def set_locale(language=None, country=None, fallthrough=False):
global syslocale
LocaleBase.fallthrough = fallthrough
@ -142,6 +149,7 @@ def set_locale(language=None, country=None, fallthrough=False):
syslocale = Locale(language, country, sysenc)
def get_locale(language=-1, country=-1):
"""Output locale using provided attributes, or return system locale."""
global syslocale

31
libs/tmdb3/pager.py

@ -8,7 +8,8 @@
from collections import Sequence, Iterator
class PagedIterator( Iterator ):
class PagedIterator(Iterator):
def __init__(self, parent):
self._parent = parent
self._index = -1
@ -23,7 +24,8 @@ class PagedIterator( Iterator ):
raise StopIteration
return self._parent[self._index]
class UnpagedData( object ):
class UnpagedData(object):
def copy(self):
return self.__class__()
@ -33,10 +35,11 @@ class UnpagedData( object ):
def __rmul__(self, other):
return (self.copy() for a in range(other))
class PagedList( Sequence ):
class PagedList(Sequence):
"""
List-like object, with support for automatically grabbing additional
pages from a data source.
List-like object, with support for automatically grabbing
additional pages from a data source.
"""
_iter_class = None
@ -87,17 +90,19 @@ class PagedList( Sequence ):
pagestart += 1
def _getpage(self, page):
raise NotImplementedError("PagedList._getpage() must be provided "+\
raise NotImplementedError("PagedList._getpage() must be provided " +
"by subclass")
class PagedRequest( PagedList ):
class PagedRequest(PagedList):
"""
Derived PageList that provides a list-like object with automatic paging
intended for use with search requests.
Derived PageList that provides a list-like object with automatic
paging intended for use with search requests.
"""
def __init__(self, request, handler=None):
self._request = request
if handler: self._handler = handler
if handler:
self._handler = handler
super(PagedRequest, self).__init__(self._getpage(1), 20)
def _getpage(self, page):
@ -105,5 +110,7 @@ class PagedRequest( PagedList ):
res = req.readJSON()
self._len = res['total_results']
for item in res['results']:
yield self._handler(item)
if item is None:
yield None
else:
yield self._handler(item)

66
libs/tmdb3/request.py

@ -15,6 +15,7 @@ from cache import Cache
from urllib import urlencode
import urllib2
import json
import os
DEBUG = False
cache = Cache(filename='pytmdb3.cache')
@ -22,10 +23,11 @@ cache = Cache(filename='pytmdb3.cache')
#DEBUG = True
#cache = Cache(engine='null')
def set_key(key):
"""
Specify the API key to use retrieving data from themoviedb.org. This
key must be set before any calls will function.
Specify the API key to use retrieving data from themoviedb.org.
This key must be set before any calls will function.
"""
if len(key) != 32:
raise TMDBKeyInvalid("Specified API key must be 128-bit hex")
@ -35,42 +37,50 @@ def set_key(key):
raise TMDBKeyInvalid("Specified API key must be 128-bit hex")
Request._api_key = key
def set_cache(engine=None, *args, **kwargs):
"""Specify caching engine and properties."""
cache.configure(engine, *args, **kwargs)
class Request( urllib2.Request ):
class Request(urllib2.Request):
_api_key = None
_base_url = "http://api.themoviedb.org/3/"
@property
def api_key(self):
if self._api_key is None:
raise TMDBKeyMissing("API key must be specified before "+\
raise TMDBKeyMissing("API key must be specified before " +
"requests can be made")
return self._api_key
def __init__(self, url, **kwargs):
"""Return a request object, using specified API path and arguments."""
"""
Return a request object, using specified API path and
arguments.
"""
kwargs['api_key'] = self.api_key
self._url = url.lstrip('/')
self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items()
self._kwargs = dict([(kwa, kwv) for kwa, kwv in kwargs.items()
if kwv is not None])
locale = get_locale()
kwargs = {}
for k,v in self._kwargs.items():
for k, v in self._kwargs.items():
kwargs[k] = locale.encode(v)
url = '{0}{1}?{2}'.format(self._base_url, self._url, urlencode(kwargs))
url = '{0}{1}?{2}'\
.format(self._base_url, self._url, urlencode(kwargs))
urllib2.Request.__init__(self, url)
self.add_header('Accept', 'application/json')
self.lifetime = 3600 # 1hr
self.lifetime = 3600 # 1hr
def new(self, **kwargs):
"""Create a new instance of the request, with tweaked arguments."""
"""
Create a new instance of the request, with tweaked arguments.
"""
args = dict(self._kwargs)
for k,v in kwargs.items():
for k, v in kwargs.items():
if v is None:
if k in args:
del args[k]
@ -119,35 +129,35 @@ class Request( urllib2.Request ):
# no error from TMDB, just raise existing error
raise e
handle_status(data, url)
#if DEBUG:
# import pprint
# pprint.PrettyPrinter().pprint(data)
if DEBUG:
import pprint
pprint.PrettyPrinter().pprint(data)
return data
status_handlers = {
1: None,
2: TMDBRequestInvalid('Invalid service - This service does not exist.'),
3: TMDBRequestError('Authentication Failed - You do not have '+\
3: TMDBRequestError('Authentication Failed - You do not have ' +
'permissions to access this service.'),
4: TMDBRequestInvalid("Invalid format - This service doesn't exist "+\
4: TMDBRequestInvalid("Invalid format - This service doesn't exist " +
'in that format.'),
5: TMDBRequestInvalid('Invalid parameters - Your request parameters '+\
5: TMDBRequestInvalid('Invalid parameters - Your request parameters ' +
'are incorrect.'),
6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid '+\
6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid ' +
'or not found.'),
7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'),
8: TMDBRequestError('Duplicate entry - The data you tried to submit '+\
8: TMDBRequestError('Duplicate entry - The data you tried to submit ' +
'already exists.'),
9: TMDBOffline('This service is tempirarily offline. Try again later.'),
10: TMDBKeyRevoked('Suspended API key - Access to your account has been '+\
'suspended, contact TMDB.'),
11: TMDBError('Internal error - Something went wrong. Contact TMDb.'),
12: None,
13: None,
14: TMDBRequestError('Authentication Failed.'),
15: TMDBError('Failed'),
16: TMDBError('Device Denied'),
17: TMDBError('Session Denied')}
10: TMDBKeyRevoked('Suspended API key - Access to your account has been ' +
'suspended, contact TMDB.'),
11: TMDBError('Internal error - Something went wrong. Contact TMDb.'),
12: None,
13: None,
14: TMDBRequestError('Authentication Failed.'),
15: TMDBError('Failed'),
16: TMDBError('Device Denied'),
17: TMDBError('Session Denied')}
def handle_status(data, query):
status = status_handlers[data.get('status_code', 1)]

663
libs/tmdb3/tmdb_api.py

@ -13,8 +13,8 @@
# (http://creativecommons.org/licenses/GPL/2.0/)
#-----------------------
__title__ = "tmdb_api - Simple-to-use Python interface to TMDB's API v3 "+\
"(www.themoviedb.org)"
__title__ = ("tmdb_api - Simple-to-use Python interface to TMDB's API v3 " +
"(www.themoviedb.org)")
__author__ = "Raymond Wagner"
__purpose__ = """
This Python library is intended to provide a series of classes and methods
@ -22,7 +22,7 @@ for search and retrieval of text metadata and image URLs from TMDB.
Preliminary API specifications can be found at
http://help.themoviedb.org/kb/api/about-3"""
__version__="v0.6.17"
__version__ = "v0.7.0"
# 0.1.0 Initial development
# 0.2.0 Add caching mechanism for API queries
# 0.2.1 Temporary work around for broken search paging
@ -59,8 +59,9 @@ __version__="v0.6.17"
# 0.6.14 Add support for Lists
# 0.6.15 Add ability to search Collections
# 0.6.16 Make absent primary images return None (previously u'')
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove
# releasedate sorting from Collection Movies
# 0.7.0 Add support for television series data
from request import set_key, Request
from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr
@ -69,10 +70,14 @@ from locales import get_locale, set_locale
from tmdb_auth import get_session, set_session
from tmdb_exceptions import *
import json
import urllib
import urllib2
import datetime
DEBUG = False
def process_date(datestr):
try:
return datetime.date(*[int(x) for x in datestr.split('-')])
@ -82,34 +87,40 @@ def process_date(datestr):
import traceback
_,_,tb = sys.exc_info()
f,l,_,_ = traceback.extract_tb(tb)[-1]
warnings.warn_explicit(('"{0}" is not a supported date format. '
'Please fix upstream data at http://www.themoviedb.org.')\
.format(datestr), Warning, f, l)
warnings.warn_explicit(('"{0}" is not a supported date format. ' +
'Please fix upstream data at ' +
'http://www.themoviedb.org.'
).format(datestr), Warning, f, l)
return None
class Configuration( Element ):
class Configuration(Element):
images = Datapoint('images')
def _populate(self):
return Request('configuration')
Configuration = Configuration()
class Account( NameRepr, Element ):
class Account(NameRepr, Element):
def _populate(self):
return Request('account', session_id=self._session.sessionid)
id = Datapoint('id')
adult = Datapoint('include_adult')
country = Datapoint('iso_3166_1')
language = Datapoint('iso_639_1')
name = Datapoint('name')
username = Datapoint('username')
id = Datapoint('id')
adult = Datapoint('include_adult')
country = Datapoint('iso_3166_1')
language = Datapoint('iso_639_1')
name = Datapoint('name')
username = Datapoint('username')
@property
def locale(self):
return get_locale(self.language, self.country)
def searchMovie(query, locale=None, adult=False, year=None):
kwargs = {'query':query, 'include_adult':adult}
kwargs = {'query': query, 'include_adult': adult}
if year is not None:
try:
kwargs['year'] = year.year
@ -117,6 +128,7 @@ def searchMovie(query, locale=None, adult=False, year=None):
kwargs['year'] = year
return MovieSearchResult(Request('search/movie', **kwargs), locale=locale)
def searchMovieWithYear(query, locale=None, adult=False):
year = None
if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('):
@ -134,70 +146,95 @@ def searchMovieWithYear(query, locale=None, adult=False):
year = None
return searchMovie(query, locale, adult, year)
class MovieSearchResult( SearchRepr, PagedRequest ):
class MovieSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request, locale=None):
if locale is None:
locale = get_locale()
super(MovieSearchResult, self).__init__(
request.new(language=locale.language),
lambda x: Movie(raw=x, locale=locale))
request.new(language=locale.language),
lambda x: Movie(raw=x, locale=locale))
def searchSeries(query, first_air_date_year=None, search_type=None, locale=None):
return SeriesSearchResult(
Request('search/tv', query=query, first_air_date_year=first_air_date_year, search_type=search_type),
locale=locale)
class SeriesSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request, locale=None):
if locale is None:
locale = get_locale()
super(SeriesSearchResult, self).__init__(
request.new(language=locale.language),
lambda x: Series(raw=x, locale=locale))
def searchPerson(query, adult=False):
return PeopleSearchResult(Request('search/person', query=query,
include_adult=adult))
class PeopleSearchResult( SearchRepr, PagedRequest ):
class PeopleSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request):
super(PeopleSearchResult, self).__init__(request,
lambda x: Person(raw=x))
super(PeopleSearchResult, self).__init__(
request, lambda x: Person(raw=x))
def searchStudio(query):
return StudioSearchResult(Request('search/company', query=query))
class StudioSearchResult( SearchRepr, PagedRequest ):
class StudioSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request):
super(StudioSearchResult, self).__init__(request,
lambda x: Studio(raw=x))
super(StudioSearchResult, self).__init__(
request, lambda x: Studio(raw=x))
def searchList(query, adult=False):
ListSearchResult(Request('search/list', query=query, include_adult=adult))
class ListSearchResult( SearchRepr, PagedRequest ):
class ListSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request):
super(ListSearchResult, self).__init__(request,
lambda x: List(raw=x))
super(ListSearchResult, self).__init__(
request, lambda x: List(raw=x))
def searchCollection(query, locale=None):
return CollectionSearchResult(Request('search/collection', query=query),
locale=locale)
class CollectionSearchResult( SearchRepr, PagedRequest ):
class CollectionSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name=None
def __init__(self, request, locale=None):
if locale is None:
locale = get_locale()
super(CollectionSearchResult, self).__init__(
request.new(language=locale.language),
lambda x: Collection(raw=x, locale=locale))
class Image( Element ):
filename = Datapoint('file_path', initarg=1,
handler=lambda x: x.lstrip('/'))
aspectratio = Datapoint('aspect_ratio')
height = Datapoint('height')
width = Datapoint('width')
language = Datapoint('iso_639_1')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
request.new(language=locale.language),
lambda x: Collection(raw=x, locale=locale))
class Image(Element):
filename = Datapoint('file_path', initarg=1,
handler=lambda x: x.lstrip('/'))
aspectratio = Datapoint('aspect_ratio')
height = Datapoint('height')
width = Datapoint('width')
language = Datapoint('iso_639_1')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
def sizes(self):
return ['original']
@ -205,19 +242,28 @@ class Image( Element ):
def geturl(self, size='original'):
if size not in self.sizes():
raise TMDBImageSizeError
url = Configuration.images['base_url'].rstrip('/')
url = Configuration.images['secure_base_url'].rstrip('/')
return url+'/{0}/{1}'.format(size, self.filename)
# sort preferring locale's language, but keep remaining ordering consistent
def __lt__(self, other):
if not isinstance(other, Image):
return False
return (self.language == self._locale.language) \
and (self.language != other.language)
def __gt__(self, other):
if not isinstance(other, Image):
return True
return (self.language != other.language) \
and (other.language == self._locale.language)
# direct match for comparison
def __eq__(self, other):
if not isinstance(other, Image):
return False
return self.filename == other.filename
# special handling for boolean to see if exists
def __nonzero__(self):
if len(self.filename) == 0:
@ -228,20 +274,28 @@ class Image( Element ):
# BASE62 encoded filename, no need to worry about unicode
return u"<{0.__class__.__name__} '{0.filename}'>".format(self)
class Backdrop( Image ):
class Backdrop(Image):
def sizes(self):
return Configuration.images['backdrop_sizes']
class Poster( Image ):
class Poster(Image):
def sizes(self):
return Configuration.images['poster_sizes']
class Profile( Image ):
class Profile(Image):
def sizes(self):
return Configuration.images['profile_sizes']
class Logo( Image ):
class Logo(Image):
def sizes(self):
return Configuration.images['logo_sizes']
class AlternateTitle( Element ):
class AlternateTitle(Element):
country = Datapoint('iso_3166_1')
title = Datapoint('title')
@ -249,28 +303,31 @@ class AlternateTitle( Element ):
def __lt__(self, other):
return (self.country == self._locale.country) \
and (self.country != other.country)
def __gt__(self, other):
return (self.country != other.country) \
and (other.country == self._locale.country)
def __eq__(self, other):
return self.country == other.country
def __repr__(self):
return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\
.format(self).encode('utf-8')
class Person( Element ):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
biography = Datapoint('biography')
dayofbirth = Datapoint('birthday', default=None, handler=process_date)
dayofdeath = Datapoint('deathday', default=None, handler=process_date)
homepage = Datapoint('homepage')
birthplace = Datapoint('place_of_birth')
profile = Datapoint('profile_path', handler=Profile, \
raw=False, default=None)
adult = Datapoint('adult')
aliases = Datalist('also_known_as')
.format(self).encode('utf-8')
class Person(Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
biography = Datapoint('biography')
dayofbirth = Datapoint('birthday', default=None, handler=process_date)
dayofdeath = Datapoint('deathday', default=None, handler=process_date)
homepage = Datapoint('homepage')
birthplace = Datapoint('place_of_birth')
profile = Datapoint('profile_path', handler=Profile,
raw=False, default=None)
adult = Datapoint('adult')
aliases = Datalist('also_known_as')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>"\
@ -278,55 +335,63 @@ class Person( Element ):
def _populate(self):
return Request('person/{0}'.format(self.id))
def _populate_credits(self):
return Request('person/{0}/credits'.format(self.id), \
language=self._locale.language)
return Request('person/{0}/credits'.format(self.id),
language=self._locale.language)
def _populate_images(self):
return Request('person/{0}/images'.format(self.id))
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \
poller=_populate_credits)
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \
poller=_populate_credits)
profiles = Datalist('profiles', handler=Profile, poller=_populate_images)
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x),
poller=_populate_credits)
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x),
poller=_populate_credits)
profiles = Datalist('profiles', handler=Profile, poller=_populate_images)
class Cast( Person ):
character = Datapoint('character')
order = Datapoint('order')
class Cast(Person):
character = Datapoint('character')
order = Datapoint('order')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\
.format(self).encode('utf-8')
.format(self).encode('utf-8')
class Crew( Person ):
job = Datapoint('job')
department = Datapoint('department')
class Crew(Person):
job = Datapoint('job')
department = Datapoint('department')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\
.format(self).encode('utf-8')
.format(self).encode('utf-8')
class Keyword( Element ):
class Keyword(Element):
id = Datapoint('id')
name = Datapoint('name')
def __repr__(self):
return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8')
return u"<{0.__class__.__name__} {0.name}>"\
.format(self).encode('utf-8')
class Release( Element ):
certification = Datapoint('certification')
country = Datapoint('iso_3166_1')
releasedate = Datapoint('release_date', handler=process_date)
class Release(Element):
certification = Datapoint('certification')
country = Datapoint('iso_3166_1')
releasedate = Datapoint('release_date', handler=process_date)
def __repr__(self):
return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\
.format(self).encode('utf-8')
.format(self).encode('utf-8')
class Trailer( Element ):
name = Datapoint('name')
size = Datapoint('size')
source = Datapoint('source')
class YoutubeTrailer( Trailer ):
class Trailer(Element):
name = Datapoint('name')
size = Datapoint('size')
source = Datapoint('source')
class YoutubeTrailer(Trailer):
def geturl(self):
return "http://www.youtube.com/watch?v={0}".format(self.source)
@ -334,8 +399,9 @@ class YoutubeTrailer( Trailer ):
# modified BASE64 encoding, no need to worry about unicode
return u"<{0.__class__.__name__} '{0.name}'>".format(self)
class AppleTrailer( Element ):
name = Datapoint('name')
class AppleTrailer(Element):
name = Datapoint('name')
sources = Datadict('sources', handler=Trailer, attr='size')
def sizes(self):
@ -344,84 +410,91 @@ class AppleTrailer( Element ):
def geturl(self, size=None):
if size is None:
# sort assuming ###p format for now, take largest resolution
size = str(sorted([int(size[:-1]) for size in self.sources])[-1])+'p'
size = str(sorted(
[int(size[:-1]) for size in self.sources]
)[-1]) + 'p'
return self.sources[size].source
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>".format(self)
class Translation( Element ):
name = Datapoint('name')
language = Datapoint('iso_639_1')
englishname = Datapoint('english_name')
class Translation(Element):
name = Datapoint('name')
language = Datapoint('iso_639_1')
englishname = Datapoint('english_name')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\
.format(self).encode('utf-8')
.format(self).encode('utf-8')
class Genre( NameRepr, Element ):
id = Datapoint('id')
name = Datapoint('name')
class Genre(NameRepr, Element):
id = Datapoint('id')
name = Datapoint('name')
def _populate_movies(self):
return Request('genre/{0}/movies'.format(self.id), \
language=self._locale.language)
language=self._locale.language)
@property
def movies(self):
if 'movies' not in self._data:
search = MovieSearchResult(self._populate_movies(), \
locale=self._locale)
locale=self._locale)
search._name = "{0.name} Movies".format(self)
self._data['movies'] = search
return self._data['movies']
@classmethod
def getAll(cls, locale=None):
class GenreList( Element ):
class GenreList(Element):
genres = Datalist('genres', handler=Genre)
def _populate(self):
return Request('genre/list', language=self._locale.language)
return GenreList(locale=locale).genres
class Studio( NameRepr, Element ):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
description = Datapoint('description')
headquarters = Datapoint('headquarters')
logo = Datapoint('logo_path', handler=Logo, \
raw=False, default=None)
class Studio(NameRepr, Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
description = Datapoint('description')
headquarters = Datapoint('headquarters')
logo = Datapoint('logo_path', handler=Logo, raw=False, default=None)
# FIXME: manage not-yet-defined handlers in a way that will propogate
# locale information properly
parent = Datapoint('parent_company', \
handler=lambda x: Studio(raw=x))
parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x))
def _populate(self):
return Request('company/{0}'.format(self.id))
def _populate_movies(self):
return Request('company/{0}/movies'.format(self.id), \
language=self._locale.language)
return Request('company/{0}/movies'.format(self.id),
language=self._locale.language)
# FIXME: add a cleaner way of adding types with no additional processing
@property
def movies(self):
if 'movies' not in self._data:
search = MovieSearchResult(self._populate_movies(), \
locale=self._locale)
search = MovieSearchResult(self._populate_movies(),
locale=self._locale)
search._name = "{0.name} Movies".format(self)
self._data['movies'] = search
return self._data['movies']
class Country( NameRepr, Element ):
code = Datapoint('iso_3166_1')
name = Datapoint('name')
class Language( NameRepr, Element ):
code = Datapoint('iso_639_1')
name = Datapoint('name')
class Country(NameRepr, Element):
code = Datapoint('iso_3166_1')
name = Datapoint('name')
class Language(NameRepr, Element):
code = Datapoint('iso_639_1')
name = Datapoint('name')
class Movie( Element ):
class Movie(Element):
@classmethod
def latest(cls):
req = Request('latest/movie')
@ -459,7 +532,7 @@ class Movie( Element ):
account = Account(session=session)
res = MovieSearchResult(
Request('account/{0}/favorite_movies'.format(account.id),
session_id=session.sessionid))
session_id=session.sessionid))
res._name = "Favorites"
return res
@ -470,7 +543,7 @@ class Movie( Element ):
account = Account(session=session)
res = MovieSearchResult(
Request('account/{0}/rated_movies'.format(account.id),
session_id=session.sessionid))
session_id=session.sessionid))
res._name = "Movies You Rated"
return res
@ -481,7 +554,7 @@ class Movie( Element ):
account = Account(session=session)
res = MovieSearchResult(
Request('account/{0}/movie_watchlist'.format(account.id),
session_id=session.sessionid))
session_id=session.sessionid))
res._name = "Movies You're Watching"
return res
@ -500,104 +573,116 @@ class Movie( Element ):
movie._populate()
return movie
id = Datapoint('id', initarg=1)
title = Datapoint('title')
originaltitle = Datapoint('original_title')
tagline = Datapoint('tagline')
overview = Datapoint('overview')
runtime = Datapoint('runtime')
budget = Datapoint('budget')
revenue = Datapoint('revenue')
releasedate = Datapoint('release_date', handler=process_date)
homepage = Datapoint('homepage')
imdb = Datapoint('imdb_id')
backdrop = Datapoint('backdrop_path', handler=Backdrop, \
raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster, \
raw=False, default=None)
popularity = Datapoint('popularity')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
adult = Datapoint('adult')
collection = Datapoint('belongs_to_collection', handler=lambda x: \
id = Datapoint('id', initarg=1)
title = Datapoint('title')
originaltitle = Datapoint('original_title')
tagline = Datapoint('tagline')
overview = Datapoint('overview')
runtime = Datapoint('runtime')
budget = Datapoint('budget')
revenue = Datapoint('revenue')
releasedate = Datapoint('release_date', handler=process_date)
homepage = Datapoint('homepage')
imdb = Datapoint('imdb_id')
backdrop = Datapoint('backdrop_path', handler=Backdrop,
raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster,
raw=False, default=None)
popularity = Datapoint('popularity')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
adult = Datapoint('adult')
collection = Datapoint('belongs_to_collection', handler=lambda x: \
Collection(raw=x))
genres = Datalist('genres', handler=Genre)
studios = Datalist('production_companies', handler=Studio)
countries = Datalist('production_countries', handler=Country)
languages = Datalist('spoken_languages', handler=Language)
genres = Datalist('genres', handler=Genre)
studios = Datalist('production_companies', handler=Studio)
countries = Datalist('production_countries', handler=Country)
languages = Datalist('spoken_languages', handler=Language)
def _populate(self):
return Request('movie/{0}'.format(self.id), \
language=self._locale.language)
language=self._locale.language)
def _populate_titles(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['country'] = self._locale.country
return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs)
return Request('movie/{0}/alternative_titles'.format(self.id),
**kwargs)
def _populate_cast(self):
return Request('movie/{0}/casts'.format(self.id))
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('movie/{0}/images'.format(self.id), **kwargs)
def _populate_keywords(self):
return Request('movie/{0}/keywords'.format(self.id))
def _populate_releases(self):
return Request('movie/{0}/releases'.format(self.id))
def _populate_trailers(self):
return Request('movie/{0}/trailers'.format(self.id), \
return Request('movie/{0}/trailers'.format(self.id),
language=self._locale.language)
def _populate_translations(self):
return Request('movie/{0}/translations'.format(self.id))
alternate_titles = Datalist('titles', handler=AlternateTitle, \
poller=_populate_titles, sort=True)
cast = Datalist('cast', handler=Cast, \
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
backdrops = Datalist('backdrops', handler=Backdrop, \
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster, \
poller=_populate_images, sort=True)
keywords = Datalist('keywords', handler=Keyword, \
poller=_populate_keywords)
releases = Datadict('countries', handler=Release, \
poller=_populate_releases, attr='country')
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \
poller=_populate_trailers)
apple_trailers = Datalist('quicktime', handler=AppleTrailer, \
poller=_populate_trailers)
translations = Datalist('translations', handler=Translation, \
poller=_populate_translations)
poller=_populate_titles, sort=True)
# FIXME: this data point will need to be changed to 'credits' at some point
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
backdrops = Datalist('backdrops', handler=Backdrop,
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
keywords = Datalist('keywords', handler=Keyword,
poller=_populate_keywords)
releases = Datadict('countries', handler=Release,
poller=_populate_releases, attr='country')
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer,
poller=_populate_trailers)
apple_trailers = Datalist('quicktime', handler=AppleTrailer,
poller=_populate_trailers)
translations = Datalist('translations', handler=Translation,
poller=_populate_translations)
def setFavorite(self, value):
req = Request('account/{0}/favorite'.format(\
Account(session=self._session).id),
session_id=self._session.sessionid)
req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()})
req = Request('account/{0}/favorite'.format(
Account(session=self._session).id),
session_id=self._session.sessionid)
req.add_data({'movie_id': self.id,
'favorite': str(bool(value)).lower()})
req.lifetime = 0
req.readJSON()
def setRating(self, value):
if not (0 <= value <= 10):
raise TMDBError("Ratings must be between '0' and '10'.")
req = Request('movie/{0}/rating'.format(self.id), \
session_id=self._session.sessionid)
req = Request('movie/{0}/rating'.format(self.id),
session_id=self._session.sessionid)
req.lifetime = 0
req.add_data({'value':value})
req.readJSON()
def setWatchlist(self, value):
req = Request('account/{0}/movie_watchlist'.format(\
Account(session=self._session).id),
session_id=self._session.sessionid)
req = Request('account/{0}/movie_watchlist'.format(
Account(session=self._session).id),
session_id=self._session.sessionid)
req.lifetime = 0
req.add_data({'movie_id':self.id,
'movie_watchlist':str(bool(value)).lower()})
req.add_data({'movie_id': self.id,
'movie_watchlist': str(bool(value)).lower()})
req.readJSON()
def getSimilar(self):
@ -605,9 +690,9 @@ class Movie( Element ):
@property
def similar(self):
res = MovieSearchResult(Request('movie/{0}/similar_movies'\
.format(self.id)),
locale=self._locale)
res = MovieSearchResult(Request(
'movie/{0}/similar_movies'.format(self.id)),
locale=self._locale)
res._name = 'Similar to {0}'.format(self._printable_name())
return res
@ -629,61 +714,197 @@ class Movie( Element ):
return s
def __repr__(self):
return u"<{0} {1}>".format(self.__class__.__name__,\
return u"<{0} {1}>".format(self.__class__.__name__,
self._printable_name()).encode('utf-8')
class ReverseCast( Movie ):
character = Datapoint('character')
character = Datapoint('character')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.character}' on {1}>"\
.format(self, self._printable_name()).encode('utf-8')
return (u"<{0.__class__.__name__} '{0.character}' on {1}>"
.format(self, self._printable_name()).encode('utf-8'))
class ReverseCrew( Movie ):
department = Datapoint('department')
job = Datapoint('job')
department = Datapoint('department')
job = Datapoint('job')
def __repr__(self):
return u"<{0.__class__.__name__} '{0.job}' for {1}>"\
.format(self, self._printable_name()).encode('utf-8')
return (u"<{0.__class__.__name__} '{0.job}' for {1}>"
.format(self, self._printable_name()).encode('utf-8'))
class Collection( NameRepr, Element ):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
class Collection(NameRepr, Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
backdrop = Datapoint('backdrop_path', handler=Backdrop, \
raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster, \
raw=False, default=None)
members = Datalist('parts', handler=Movie)
raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
members = Datalist('parts', handler=Movie)
overview = Datapoint('overview')
def _populate(self):
return Request('collection/{0}'.format(self.id), \
language=self._locale.language)
return Request('collection/{0}'.format(self.id),
language=self._locale.language)
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('collection/{0}/images'.format(self.id), **kwargs)
backdrops = Datalist('backdrops', handler=Backdrop, \
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster, \
poller=_populate_images, sort=True)
backdrops = Datalist('backdrops', handler=Backdrop,
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
class List( NameRepr, Element ):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
author = Datapoint('created_by')
class List(NameRepr, Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
author = Datapoint('created_by')
description = Datapoint('description')
favorites = Datapoint('favorite_count')
language = Datapoint('iso_639_1')
count = Datapoint('item_count')
poster = Datapoint('poster_path', handler=Poster, \
raw=False, default=None)
members = Datalist('items', handler=Movie)
favorites = Datapoint('favorite_count')
language = Datapoint('iso_639_1')
count = Datapoint('item_count')
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
members = Datalist('items', handler=Movie)
def _populate(self):
return Request('list/{0}'.format(self.id))
class Network(NameRepr,Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
class Episode(NameRepr, Element):
episode_number = Datapoint('episode_number', initarg=3)
season_number = Datapoint('season_number', initarg=2)
series_id = Datapoint('series_id', initarg=1)
air_date = Datapoint('air_date', handler=process_date)
overview = Datapoint('overview')
name = Datapoint('name')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
id = Datapoint('id')
production_code = Datapoint('production_code')
still = Datapoint('still_path', handler=Backdrop, raw=False, default=None)
def _populate(self):
return Request('tv/{0}/season/{1}/episode/{2}'.format(self.series_id, self.season_number, self.episode_number),
language=self._locale.language)
def _populate_cast(self):
return Request('tv/{0}/season/{1}/episode/{2}/credits'.format(
self.series_id, self.season_number, self.episode_number),
language=self._locale.language)
def _populate_external_ids(self):
return Request('tv/{0}/season/{1}/episode/{2}/external_ids'.format(
self.series_id, self.season_number, self.episode_number))
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/season/{1}/episode/{2}/images'.format(
self.series_id, self.season_number, self.episode_number), **kwargs)
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
guest_stars = Datalist('guest_stars', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
stills = Datalist('stills', handler=Backdrop, poller=_populate_images, sort=True)
class Season(NameRepr, Element):
season_number = Datapoint('season_number', initarg=2)
series_id = Datapoint('series_id', initarg=1)
id = Datapoint('id')
air_date = Datapoint('air_date', handler=process_date)
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
overview = Datapoint('overview')
name = Datapoint('name')
episodes = Datadict('episodes', attr='episode_number', handler=Episode,
passthrough={'series_id': 'series_id', 'season_number': 'season_number'})
def _populate(self):
return Request('tv/{0}/season/{1}'.format(self.series_id, self.season_number),
language=self._locale.language)
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/season/{1}/images'.format(self.series_id, self.season_number), **kwargs)
def _populate_external_ids(self):
return Request('tv/{0}/season/{1}/external_ids'.format(self.series_id, self.season_number))
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
class Series(NameRepr, Element):
id = Datapoint('id', initarg=1)
backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None)
authors = Datalist('created_by', handler=Person)
episode_run_times = Datalist('episode_run_time')
first_air_date = Datapoint('first_air_date', handler=process_date)
last_air_date = Datapoint('last_air_date', handler=process_date)
genres = Datalist('genres', handler=Genre)
homepage = Datapoint('homepage')
in_production = Datapoint('in_production')
languages = Datalist('languages')
origin_countries = Datalist('origin_country')
name = Datapoint('name')
original_name = Datapoint('original_name')
number_of_episodes = Datapoint('number_of_episodes')
number_of_seasons = Datapoint('number_of_seasons')
overview = Datapoint('overview')
popularity = Datapoint('popularity')
status = Datapoint('status')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
networks = Datalist('networks', handler=Network)
seasons = Datadict('seasons', attr='season_number', handler=Season, passthrough={'id': 'series_id'})
def _populate(self):
return Request('tv/{0}'.format(self.id),
language=self._locale.language)
def _populate_cast(self):
return Request('tv/{0}/credits'.format(self.id))
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/images'.format(self.id), **kwargs)
def _populate_external_ids(self):
return Request('tv/{0}/external_ids'.format(self.id))
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
backdrops = Datalist('backdrops', handler=Backdrop,
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)

35
libs/tmdb3/tmdb_auth.py

@ -11,7 +11,7 @@
from datetime import datetime as _pydatetime, \
tzinfo as _pytzinfo
import re
class datetime( _pydatetime ):
class datetime(_pydatetime):
"""Customized datetime class with ISO format parsing."""
_reiso = re.compile('(?P<year>[0-9]{4})'
'-(?P<month>[0-9]{1,2})'
@ -27,21 +27,27 @@ class datetime( _pydatetime ):
'(?P<tzmin>[0-9]{2})?'
')?')
class _tzinfo( _pytzinfo):
class _tzinfo(_pytzinfo):
def __init__(self, direc='+', hr=0, min=0):
if direc == '-':
hr = -1*int(hr)
self._offset = timedelta(hours=int(hr), minutes=int(min))
def utcoffset(self, dt): return self._offset
def tzname(self, dt): return ''
def dst(self, dt): return timedelta(0)
def utcoffset(self, dt):
return self._offset
def tzname(self, dt):
return ''
def dst(self, dt):
return timedelta(0)
@classmethod
def fromIso(cls, isotime, sep='T'):
match = cls._reiso.match(isotime)
if match is None:
raise TypeError("time data '%s' does not match ISO 8601 format" \
% isotime)
raise TypeError("time data '%s' does not match ISO 8601 format"
% isotime)
dt = [int(a) for a in match.groups()[:5]]
if match.group('sec') is not None:
@ -52,9 +58,9 @@ class datetime( _pydatetime ):
if match.group('tz') == 'Z':
tz = cls._tzinfo()
elif match.group('tzmin'):
tz = cls._tzinfo(*match.group('tzdirec','tzhour','tzmin'))
tz = cls._tzinfo(*match.group('tzdirec', 'tzhour', 'tzmin'))
else:
tz = cls._tzinfo(*match.group('tzdirec','tzhour'))
tz = cls._tzinfo(*match.group('tzdirec', 'tzhour'))
dt.append(0)
dt.append(tz)
return cls(*dt)
@ -64,10 +70,12 @@ from tmdb_exceptions import *
syssession = None
def set_session(sessionid):
global syssession
syssession = Session(sessionid)
def get_session(sessionid=None):
global syssession
if sessionid:
@ -77,8 +85,8 @@ def get_session(sessionid=None):
else:
return Session.new()
class Session( object ):
class Session(object):
@classmethod
def new(cls):
return cls(None)
@ -91,9 +99,9 @@ class Session( object ):
if self._sessionid is None:
if self._authtoken is None:
raise TMDBError("No Auth Token to produce Session for")
# TODO: check authtokenexpiration against current time
req = Request('authentication/session/new', \
request_token=self._authtoken)
# TODO: check authtoken expiration against current time
req = Request('authentication/session/new',
request_token=self._authtoken)
req.lifetime = 0
dat = req.readJSON()
if not dat['success']:
@ -128,4 +136,3 @@ class Session( object ):
@property
def callbackurl(self):
return "http://www.themoviedb.org/authenticate/"+self._authtoken

90
libs/tmdb3/tmdb_exceptions.py

@ -6,23 +6,24 @@
# Author: Raymond Wagner
#-----------------------
class TMDBError( Exception ):
Error = 0
KeyError = 10
KeyMissing = 20
KeyInvalid = 30
KeyRevoked = 40
RequestError = 50
RequestInvalid = 51
PagingIssue = 60
CacheError = 70
CacheReadError = 71
CacheWriteError = 72
CacheDirectoryError = 73
ImageSizeError = 80
HTTPError = 90
Offline = 100
LocaleError = 110
class TMDBError(Exception):
Error = 0
KeyError = 10
KeyMissing = 20
KeyInvalid = 30
KeyRevoked = 40
RequestError = 50
RequestInvalid = 51
PagingIssue = 60
CacheError = 70
CacheReadError = 71
CacheWriteError = 72
CacheDirectoryError = 73
ImageSizeError = 80
HTTPError = 90
Offline = 100
LocaleError = 110
def __init__(self, msg=None, errno=0):
self.errno = errno
@ -30,60 +31,77 @@ class TMDBError( Exception ):
self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno)
self.args = (msg,)
class TMDBKeyError( TMDBError ):
class TMDBKeyError(TMDBError):
pass
class TMDBKeyMissing( TMDBKeyError ):
class TMDBKeyMissing(TMDBKeyError):
pass
class TMDBKeyInvalid( TMDBKeyError ):
class TMDBKeyInvalid(TMDBKeyError):
pass
class TMDBKeyRevoked( TMDBKeyInvalid ):
class TMDBKeyRevoked(TMDBKeyInvalid):
pass
class TMDBRequestError( TMDBError ):
class TMDBRequestError(TMDBError):
pass
class TMDBRequestInvalid( TMDBRequestError ):
class TMDBRequestInvalid(TMDBRequestError):
pass
class TMDBPagingIssue( TMDBRequestError ):
class TMDBPagingIssue(TMDBRequestError):
pass
class TMDBCacheError( TMDBRequestError ):
class TMDBCacheError(TMDBRequestError):
pass
class TMDBCacheReadError( TMDBCacheError ):
class TMDBCacheReadError(TMDBCacheError):
def __init__(self, filename):
super(TMDBCacheReadError, self).__init__(
"User does not have permission to access cache file: {0}.".format(filename))
"User does not have permission to access cache file: {0}."\
.format(filename))
self.filename = filename
class TMDBCacheWriteError( TMDBCacheError ):
class TMDBCacheWriteError(TMDBCacheError):
def __init__(self, filename):
super(TMDBCacheWriteError, self).__init__(
"User does not have permission to write cache file: {0}.".format(filename))
"User does not have permission to write cache file: {0}."\
.format(filename))
self.filename = filename
class TMDBCacheDirectoryError( TMDBCacheError ):
class TMDBCacheDirectoryError(TMDBCacheError):
def __init__(self, filename):
super(TMDBCacheDirectoryError, self).__init__(
"Directory containing cache file does not exist: {0}.".format(filename))
"Directory containing cache file does not exist: {0}."\
.format(filename))
self.filename = filename
class TMDBImageSizeError( TMDBError ):
class TMDBImageSizeError(TMDBError ):
pass
class TMDBHTTPError( TMDBError ):
class TMDBHTTPError(TMDBError):
def __init__(self, err):
self.httperrno = err.code
self.response = err.fp.read()
super(TMDBHTTPError, self).__init__(str(err))
class TMDBOffline( TMDBError ):
pass
class TMDBLocaleError( TMDBError ):
class TMDBOffline(TMDBError):
pass
class TMDBLocaleError(TMDBError):
pass

201
libs/tmdb3/util.py

@ -10,13 +10,15 @@ from copy import copy
from locales import get_locale
from tmdb_auth import get_session
class NameRepr( object ):
class NameRepr(object):
"""Mixin for __repr__ methods using 'name' attribute."""
def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>"\
.format(self).encode('utf-8')
.format(self).encode('utf-8')
class SearchRepr( object ):
class SearchRepr(object):
"""
Mixin for __repr__ methods for classes with '_name' and
'_request' attributes.
@ -25,10 +27,11 @@ class SearchRepr( object ):
name = self._name if self._name else self._request._kwargs['query']
return u"<Search Results: {0}>".format(name).encode('utf-8')
class Poller( object ):
class Poller(object):
"""
Wrapper for an optional callable to populate an Element derived class
with raw data, or data from a Request.
Wrapper for an optional callable to populate an Element derived
class with raw data, or data from a Request.
"""
def __init__(self, func, lookup, inst=None):
self.func = func
@ -60,7 +63,7 @@ class Poller( object ):
if not callable(self.func):
raise RuntimeError('Poller object called without a source function')
req = self.func()
if (('language' in req._kwargs) or ('country' in req._kwargs)) \
if ('language' in req._kwargs) or ('country' in req._kwargs) \
and self.inst._locale.fallthrough:
# request specifies a locale filter, and fallthrough is enabled
# run a first pass with specified filter
@ -79,7 +82,7 @@ class Poller( object ):
def apply(self, data, set_nones=True):
# apply data directly, bypassing callable function
unfilled = False
for k,v in self.lookup.items():
for k, v in self.lookup.items():
if (k in data) and \
((data[k] is not None) if callable(self.func) else True):
# argument received data, populate it
@ -100,32 +103,38 @@ class Poller( object ):
unfilled = True
return unfilled
class Data( object ):
class Data(object):
"""
Basic response definition class
This maps to a single key in a JSON dictionary received from the API
"""
def __init__(self, field, initarg=None, handler=None, poller=None,
raw=True, default=u'', lang=False):
raw=True, default=u'', lang=None, passthrough={}):
"""
This defines how the dictionary value is to be processed by the poller
field -- defines the dictionary key that filters what data this uses
initarg -- (optional) specifies that this field must be supplied
when creating a new instance of the Element class this
definition is mapped to. Takes an integer for the order
it should be used in the input arguments
handler -- (optional) callable used to process the received value
before being stored in the Element object.
poller -- (optional) callable to be used if data is requested and
this value has not yet been defined. the callable should
return a dictionary of data from a JSON query. many
definitions may share a single poller, which will be
and the data used to populate all referenced definitions
based off their defined field
raw -- (optional) if the specified handler is an Element class,
the data will be passed into it using the 'raw' keyword
attribute. setting this to false will force the data to
instead be passed in as the first argument
This defines how the dictionary value is to be processed by the
poller
field -- defines the dictionary key that filters what data
this uses
initarg -- (optional) specifies that this field must be
supplied when creating a new instance of the Element
class this definition is mapped to. Takes an integer
for the order it should be used in the input
arguments
handler -- (optional) callable used to process the received
value before being stored in the Element object.
poller -- (optional) callable to be used if data is requested
and this value has not yet been defined. the
callable should return a dictionary of data from a
JSON query. many definitions may share a single
poller, which will be and the data used to populate
all referenced definitions based off their defined
field
raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false
will force the data to instead be passed in as the
first argument
"""
self.field = field
self.initarg = initarg
@ -133,6 +142,7 @@ class Data( object ):
self.raw = raw
self.default = default
self.sethandler(handler)
self.passthrough = passthrough
def __get__(self, inst, owner):
if inst is None:
@ -151,6 +161,9 @@ class Data( object ):
if isinstance(value, Element):
value._locale = inst._locale
value._session = inst._session
for source, dest in self.passthrough:
setattr(value, dest, getattr(inst, source))
inst._data[self.field] = value
def sethandler(self, handler):
@ -162,37 +175,44 @@ class Data( object ):
else:
self.handler = lambda x: handler(x)
class Datapoint( Data ):
class Datapoint(Data):
pass
class Datalist( Data ):
class Datalist(Data):
"""
Response definition class for list data
This maps to a key in a JSON dictionary storing a list of data
"""
def __init__(self, field, handler=None, poller=None, sort=None, raw=True):
def __init__(self, field, handler=None, poller=None, sort=None, raw=True, passthrough={}):
"""
This defines how the dictionary value is to be processed by the poller
field -- defines the dictionary key that filters what data this uses
handler -- (optional) callable used to process the received value
before being stored in the Element object.
poller -- (optional) callable to be used if data is requested and
this value has not yet been defined. the callable should
return a dictionary of data from a JSON query. many
definitions may share a single poller, which will be
and the data used to populate all referenced definitions
based off their defined field
sort -- (optional) name of attribute in resultant data to be used
to sort the list after processing. this effectively
a handler be defined to process the data into something
that has attributes
raw -- (optional) if the specified handler is an Element class,
the data will be passed into it using the 'raw' keyword
attribute. setting this to false will force the data to
instead be passed in as the first argument
This defines how the dictionary value is to be processed by the
poller
field -- defines the dictionary key that filters what data
this uses
handler -- (optional) callable used to process the received
value before being stored in the Element object.
poller -- (optional) callable to be used if data is requested
and this value has not yet been defined. the
callable should return a dictionary of data from a
JSON query. many definitions may share a single
poller, which will be and the data used to populate
all referenced definitions based off their defined
field
sort -- (optional) name of attribute in resultant data to be
used to sort the list after processing. this
effectively requires a handler be defined to process
the data into something that has attributes
raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false will
force the data to instead be passed in as the first
argument
"""
super(Datalist, self).__init__(field, None, handler, poller, raw)
super(Datalist, self).__init__(field, None, handler, poller, raw, passthrough=passthrough)
self.sort = sort
def __set__(self, inst, value):
data = []
if value:
@ -201,6 +221,10 @@ class Datalist( Data ):
if isinstance(val, Element):
val._locale = inst._locale
val._session = inst._session
for source, dest in self.passthrough.items():
setattr(val, dest, getattr(inst, source))
data.append(val)
if self.sort:
if self.sort is True:
@ -209,45 +233,52 @@ class Datalist( Data ):
data.sort(key=lambda x: getattr(x, self.sort))
inst._data[self.field] = data
class Datadict( Data ):
class Datadict(Data):
"""
Response definition class for dictionary data
This maps to a key in a JSON dictionary storing a dictionary of data
"""
def __init__(self, field, handler=None, poller=None, raw=True,
key=None, attr=None):
key=None, attr=None, passthrough={}):
"""
This defines how the dictionary value is to be processed by the poller
field -- defines the dictionary key that filters what data this uses
handler -- (optional) callable used to process the received value
before being stored in the Element object.
poller -- (optional) callable to be used if data is requested and
this value has not yet been defined. the callable should
return a dictionary of data from a JSON query. many
definitions may share a single poller, which will be
and the data used to populate all referenced definitions
based off their defined field
key -- (optional) name of key in resultant data to be used as
the key in the stored dictionary. if this is not the
field name from the source data is used instead
attr -- (optional) name of attribute in resultant data to be used
This defines how the dictionary value is to be processed by the
poller
field -- defines the dictionary key that filters what data
this uses
handler -- (optional) callable used to process the received
value before being stored in the Element object.
poller -- (optional) callable to be used if data is requested
and this value has not yet been defined. the
callable should return a dictionary of data from a
JSON query. many definitions may share a single
poller, which will be and the data used to populate
all referenced definitions based off their defined
field
key -- (optional) name of key in resultant data to be used
as the key in the stored dictionary. if this is not
the field name from the source data is used instead
raw -- (optional) if the specified handler is an Element class,
the data will be passed into it using the 'raw' keyword
attribute. setting this to false will force the data to
instead be passed in as the first argument
attr -- (optional) name of attribute in resultant data to be
used as the key in the stored dictionary. if this is
not the field name from the source data is used
instead
raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false will
force the data to instead be passed in as the first
argument
"""
if key and attr:
raise TypeError("`key` and `attr` cannot both be defined")
super(Datadict, self).__init__(field, None, handler, poller, raw)
super(Datadict, self).__init__(field, None, handler, poller, raw, passthrough=passthrough)
if key:
self.getkey = lambda x: x[key]
elif attr:
self.getkey = lambda x: getattr(x, attr)
else:
raise TypeError("Datadict requires `key` or `attr` be defined "+\
raise TypeError("Datadict requires `key` or `attr` be defined " +
"for populating the dictionary")
def __set__(self, inst, value):
data = {}
if value:
@ -256,6 +287,10 @@ class Datadict( Data ):
if isinstance(val, Element):
val._locale = inst._locale
val._session = inst._session
for source, dest in self.passthrough.items():
setattr(val, dest, getattr(inst, source))
data[self.getkey(val)] = val
inst._data[self.field] = data
@ -286,7 +321,7 @@ class ElementType( type ):
# extract copies of each defined Poller function
# from parent classes
pollers[k] = attr.func
for k,attr in attrs.items():
for k, attr in attrs.items():
if isinstance(attr, Data):
data[k] = attr
if '_populate' in attrs:
@ -295,9 +330,9 @@ class ElementType( type ):
# process all defined Data attribues, testing for use as an initial
# argument, and building a list of what Pollers are used to populate
# which Data points
pollermap = dict([(k,[]) for k in pollers])
pollermap = dict([(k, []) for k in pollers])
initargs = []
for k,v in data.items():
for k, v in data.items():
v.name = k
if v.initarg:
initargs.append(v)
@ -313,7 +348,7 @@ class ElementType( type ):
# wrap each used poller function with a Poller class, and push into
# the new class attributes
for k,v in pollermap.items():
for k, v in pollermap.items():
if len(v) == 0:
continue
lookup = dict([(attr.field, attr.name) for attr in v])
@ -326,8 +361,8 @@ class ElementType( type ):
attrs[attr.name] = attr
# build sorted list of arguments used for intialization
attrs['_InitArgs'] = tuple([a.name for a in \
sorted(initargs, key=lambda x: x.initarg)])
attrs['_InitArgs'] = tuple(
[a.name for a in sorted(initargs, key=lambda x: x.initarg)])
return type.__new__(mcs, name, bases, attrs)
def __call__(cls, *args, **kwargs):
@ -346,21 +381,23 @@ class ElementType( type ):
if 'raw' in kwargs:
# if 'raw' keyword is supplied, create populate object manually
if len(args) != 0:
raise TypeError('__init__() takes exactly 2 arguments (1 given)')
raise TypeError(
'__init__() takes exactly 2 arguments (1 given)')
obj._populate.apply(kwargs['raw'], False)
else:
# if not, the number of input arguments must exactly match that
# defined by the Data definitions
if len(args) != len(cls._InitArgs):
raise TypeError('__init__() takes exactly {0} arguments ({1} given)'\
raise TypeError(
'__init__() takes exactly {0} arguments ({1} given)'\
.format(len(cls._InitArgs)+1, len(args)+1))
for a,v in zip(cls._InitArgs, args):
for a, v in zip(cls._InitArgs, args):
setattr(obj, a, v)
obj.__init__()
return obj
class Element( object ):
__metaclass__ = ElementType
_lang = 'en'

Loading…
Cancel
Save