#!/usr/bin/env python # -*- coding: utf-8 -*- #----------------------- # Name: tmdb_api.py Simple-to-use Python interface to TMDB's API v3 # Python Library # Author: Raymond Wagner # Purpose: This Python library is intended to provide a series of classes # and methods 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 # License: Creative Commons GNU GPL v2 # (http://creativecommons.org/licenses/GPL/2.0/) #----------------------- __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 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" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging # 0.3.0 Rework backend machinery for managing OO interface to results # 0.3.1 Add collection support # 0.3.2 Remove MythTV key from results.py # 0.3.3 Add functional language support # 0.3.4 Re-enable search paging # 0.3.5 Add methods for grabbing current, popular, and top rated movies # 0.3.6 Rework paging mechanism # 0.3.7 Generalize caching mechanism, and allow controllability # 0.4.0 Add full locale support (language and country) and optional fall through # 0.4.1 Add custom classmethod for dealing with IMDB movie IDs # 0.4.2 Improve cache file selection for Windows systems # 0.4.3 Add a few missed Person properties # 0.4.4 Add support for additional Studio information # 0.4.5 Add locale fallthrough for images and alternate titles # 0.4.6 Add slice support for search results # 0.5.0 Rework cache framework and improve file cache performance # 0.6.0 Add user authentication support # 0.6.1 Add adult filtering for people searches # 0.6.2 Add similar movie search for Movie objects # 0.6.3 Add Studio search # 0.6.4 Add Genre list and associated Movie search # 0.6.5 Prevent data from being blanked out by subsequent queries # 0.6.6 Turn date processing errors into mutable warnings # 0.6.7 Add support for searching by year # 0.6.8 Add support for collection images # 0.6.9 Correct Movie image language filtering # 0.6.10 Add upcoming movie classmethod # 0.6.11 Fix URL for top rated Movie query # 0.6.12 Add support for Movie watchlist query and editing # 0.6.13 Fix URL for rating Movies # 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 # releasedate sorting from Collection Movies from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr from pager import PagedRequest from locales import get_locale, set_locale from tmdb_auth import get_session, set_session from tmdb_exceptions import * import datetime DEBUG = False def process_date(datestr): try: return datetime.date(*[int(x) for x in datestr.split('-')]) except (TypeError, ValueError): import sys import warnings 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) return None class Configuration( Element ): images = Datapoint('images') def _populate(self): return Request('configuration') Configuration = Configuration() 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') @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} if year is not None: try: kwargs['year'] = year.year except AttributeError: 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] == '('): # simple syntax check, no need for regular expression try: year = int(query[-5:-1]) except ValueError: pass else: if 1885 < year < 2050: # strip out year from search query = query[:-7] else: # sanity check on resolved year failed, pass through year = None return searchMovie(query, locale, adult, year) 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)) def searchPerson(query, adult=False): return PeopleSearchResult(Request('search/person', query=query, include_adult=adult)) 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)) def searchStudio(query): return StudioSearchResult(Request('search/company', query=query)) 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)) def searchList(query, adult=False): ListSearchResult(Request('search/list', query=query, include_adult=adult)) 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)) def searchCollection(query, locale=None): return CollectionSearchResult(Request('search/collection', query=query), locale=locale) 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') def sizes(self): return ['original'] def geturl(self, size='original'): if size not in self.sizes(): raise TMDBImageSizeError url = Configuration.images['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): return (self.language == self._locale.language) \ and (self.language != other.language) def __gt__(self, other): return (self.language != other.language) \ and (other.language == self._locale.language) # direct match for comparison def __eq__(self, other): return self.filename == other.filename # special handling for boolean to see if exists def __nonzero__(self): if len(self.filename) == 0: return False return True def __repr__(self): # BASE62 encoded filename, no need to worry about unicode return u"<{0.__class__.__name__} '{0.filename}'>".format(self) class Backdrop( Image ): def sizes(self): return Configuration.images['backdrop_sizes'] class Poster( Image ): def sizes(self): return Configuration.images['poster_sizes'] class Profile( Image ): def sizes(self): return Configuration.images['profile_sizes'] class Logo( Image ): def sizes(self): return Configuration.images['logo_sizes'] class AlternateTitle( Element ): country = Datapoint('iso_3166_1') title = Datapoint('title') # sort preferring locale's country, but keep remaining ordering consistent 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') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>"\ .format(self).encode('utf-8') 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) 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) 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') 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') class Keyword( Element ): id = Datapoint('id') name = Datapoint('name') def __repr__(self): 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) def __repr__(self): return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\ .format(self).encode('utf-8') 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) def __repr__(self): # modified BASE64 encoding, no need to worry about unicode return u"<{0.__class__.__name__} '{0.name}'>".format(self) class AppleTrailer( Element ): name = Datapoint('name') sources = Datadict('sources', handler=Trailer, attr='size') def sizes(self): return self.sources.keys() 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' 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') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\ .format(self).encode('utf-8') 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) @property def movies(self): if 'movies' not in self._data: search = MovieSearchResult(self._populate_movies(), \ 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 ): 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) # 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)) 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) # 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._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 Movie( Element ): @classmethod def latest(cls): req = Request('latest/movie') req.lifetime = 600 return cls(raw=req.readJSON()) @classmethod def nowplaying(cls, locale=None): res = MovieSearchResult(Request('movie/now-playing'), locale=locale) res._name = 'Now Playing' return res @classmethod def mostpopular(cls, locale=None): res = MovieSearchResult(Request('movie/popular'), locale=locale) res._name = 'Popular' return res @classmethod def toprated(cls, locale=None): res = MovieSearchResult(Request('movie/top_rated'), locale=locale) res._name = 'Top Rated' return res @classmethod def upcoming(cls, locale=None): res = MovieSearchResult(Request('movie/upcoming'), locale=locale) res._name = 'Upcoming' return res @classmethod def favorites(cls, session=None): if session is None: session = get_session() account = Account(session=session) res = MovieSearchResult( Request('account/{0}/favorite_movies'.format(account.id), session_id=session.sessionid)) res._name = "Favorites" return res @classmethod def ratedmovies(cls, session=None): if session is None: session = get_session() account = Account(session=session) res = MovieSearchResult( Request('account/{0}/rated_movies'.format(account.id), session_id=session.sessionid)) res._name = "Movies You Rated" return res @classmethod def watchlist(cls, session=None): if session is None: session = get_session() account = Account(session=session) res = MovieSearchResult( Request('account/{0}/movie_watchlist'.format(account.id), session_id=session.sessionid)) res._name = "Movies You're Watching" return res @classmethod def fromIMDB(cls, imdbid, locale=None): try: # assume string if not imdbid.startswith('tt'): imdbid = "tt{0:0>7}".format(imdbid) except AttributeError: # assume integer imdbid = "tt{0:0>7}".format(imdbid) if locale is None: locale = get_locale() movie = cls(imdbid, locale=locale) 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: \ 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) def _populate(self): return Request('movie/{0}'.format(self.id), \ 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) 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), \ 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) 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.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.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.lifetime = 0 req.add_data({'movie_id':self.id, 'movie_watchlist':str(bool(value)).lower()}) req.readJSON() def getSimilar(self): return self.similar @property def similar(self): res = MovieSearchResult(Request('movie/{0}/similar_movies'\ .format(self.id)), locale=self._locale) res._name = 'Similar to {0}'.format(self._printable_name()) return res @property def lists(self): res = ListSearchResult(Request('movie/{0}/lists'.format(self.id))) res._name = "Lists containing {0}".format(self._printable_name()) return res def _printable_name(self): if self.title is not None: s = u"'{0}'".format(self.title) elif self.originaltitle is not None: s = u"'{0}'".format(self.originaltitle) else: s = u"'No Title'" if self.releasedate: s = u"{0} ({1})".format(s, self.releasedate.year) return s def __repr__(self): return u"<{0} {1}>".format(self.__class__.__name__,\ self._printable_name()).encode('utf-8') class ReverseCast( Movie ): character = Datapoint('character') def __repr__(self): 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') def __repr__(self): 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') 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) overview = Datapoint('overview') def _populate(self): 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) 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) def _populate(self): return Request('list/{0}'.format(self.id))