diff --git a/.gitignore b/.gitignore index effc861..81b4a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -# GVIM stuff # -############## +# Editor stuff # +################ .*.swp +.idea # Python stuff # ################ diff --git a/README.md b/README.md index d5e6e3a..6aae066 100644 --- a/README.md +++ b/README.md @@ -225,17 +225,20 @@ List of Available Data | string | tagline | | | string | overview | | | integer | runtime | | +| string | status | | | integer | budget | | | integer | revenue | | | datetime | releasedate | | +| string | originallanguage | | | string | homepage | | -| string | IMDB reference id | 'ttXXXXXXX' | +| string | imdb | imdb reference: 'ttXXXXXXX' | | Backdrop | backdrop | | | Poster | poster | | | float | popularity | | | float | userrating | | | integer | votes | | | boolean | adult | | +| boolean | video | | | Collection | collection | | | list(Genre) | genres | | | list(Studio) | studios | | @@ -246,12 +249,14 @@ List of Available Data | list(Crew) | crew | | | list(Backdrop) | backdrops | | | list(Poster) | posters | | +| list(Video) | videos | | | list(Keyword) | keywords | | -| dict(Release) | releases | indexed by country | +| dict(ReleaseDate) | release_dates | indexed by country | | list(Translation) | translations | | | list(Movie) | similar | | | list(List) | lists | | | list(Movie) | getSimilar() | | +| list(Movie) | recommendations | | | None | setFavorite(bool) | mark favorite status for current user | | None | setRating(int) | rate movie by current user | | None | setWatchlist(bool) | mark watchlist status for current user | @@ -268,6 +273,7 @@ List of Available Data | list(Movie) | favorites() | current user's favorite movies | | list(Movie) | ratedmovies() | movies rated by current user | | list(Movie) | watchlist() | movies marked to watch by current user | +| list(Movie) | discover() | discover movies by different types of data | #### Series: | type | name | @@ -374,6 +380,9 @@ List of Available Data | string | homepage | | Profile | profile | | boolean | adult | +| integer | gender | +| string | imdb | +| float | popularity | | list(string) | aliases | | list(ReverseCast) | roles | | list(ReverseCrew) | crew | @@ -517,3 +526,64 @@ Logo (derived from `Image`) | dict(Trailer) | sources | indexed by size | | list(string) | sizes() | | | string | geturl(size=None) | | + + +Changelog +--------- +- 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 +- 0.7.0 Add support for television series data +- 0.7.1 Add rate limiter to cache engine +- 0.7.2 Add similar and keywords to TV Series, + Fix unicode issues with search result object names, + Temporary fix for youtube videos with malformed URLs. +- 0.7.3 Added a few more missing Person properties: + (gender, imdb, popularity), + Added Video element, + Added Movie class method discover, + Added missing Movie properties and methods: + (status, originallanguage, video, videos, recommendations), + Updated API statuses (from https://github.com/pawel-zet), + Added Series methods (from https://github.com/alanjds): + (latest, discover, ontheair, airingtoday, mostpopular, toprated), + PEP8 fixes and some typos, + Updated readme. +- 0.7.4 Added experimental sizes to profiles, + Fixed process_date to handle input dates like "1987-04-03T00:00:00.000Z", + Replaced Movie.releases with Movie.release_dates, following the API. diff --git a/setup.py b/setup.py index bd0d5cd..884e5c0 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='tmdb3', - version='0.7.2', + version='0.7.4', description='TheMovieDB.org APIv3 interface', long_description=long_description, author='Raymond Wagner', diff --git a/tmdb3/cache.py b/tmdb3/cache.py index 9e9c9e3..1ca335a 100644 --- a/tmdb3/cache.py +++ b/tmdb3/cache.py @@ -18,6 +18,7 @@ DEBUG = False + class Cache(object): """ This class implements a cache framework, allowing selecting of a @@ -81,10 +82,13 @@ def get(self, key): # wait to ensure proper rate limiting if len(self._rate_limiter) == 30: w = 10 - (time.time() - self._rate_limiter.pop(0)) - if (w > 0): + if w > 0: if DEBUG: print "rate limiting - waiting {0} seconds".format(w) - time.sleep(w) + try: + time.sleep(w) + except IOError: + pass return None def cached(self, callback): @@ -94,7 +98,7 @@ def cached(self, callback): """ return self.Cached(self, callback) - class Cached( object ): + class Cached(object): def __init__(self, cache, callback, func=None, inst=None): self.cache = cache self.callback = callback diff --git a/tmdb3/cache_file.py b/tmdb3/cache_file.py index 4e96581..a6ecb19 100644 --- a/tmdb3/cache_file.py +++ b/tmdb3/cache_file.py @@ -55,6 +55,7 @@ def _donothing(*args, **kwargs): try: import fcntl + class Flock(object): """ Context manager to flock file for the duration the object @@ -96,7 +97,8 @@ def parse_filename(filename): except ImportError: import msvcrt - class Flock( object ): + + class Flock(object): LOCK_EX = msvcrt.LK_LOCK LOCK_SH = msvcrt.LK_LOCK @@ -207,7 +209,7 @@ def dumpdata(self, fd): fd.write(self._buff.getvalue()) -class FileEngine( CacheEngine ): +class FileEngine(CacheEngine): """Simple file-backed engine.""" name = 'file' _struct = struct.Struct('HH') # two shorts for version and count diff --git a/tmdb3/request.py b/tmdb3/request.py index 2d51dcd..1559c55 100644 --- a/tmdb3/request.py +++ b/tmdb3/request.py @@ -68,8 +68,7 @@ def __init__(self, url, **kwargs): kwargs = {} 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') @@ -135,33 +134,49 @@ def readJSON(self): return data status_handlers = { - 1: None, - 2: TMDBRequestInvalid('Invalid service - This service does not exist.'), - 3: TMDBRequestError('Authentication Failed - You do not have ' + - 'permissions to access this service.'), - 4: TMDBRequestInvalid("Invalid format - This service doesn't exist " + - 'in that format.'), - 5: TMDBRequestInvalid('Invalid parameters - Your request parameters ' + - 'are incorrect.'), - 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 ' + - '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')} + 1: None, # Success + 2: TMDBRequestInvalid, # Invalid service + 3: TMDBRequestError, # Authentication failed + 4: TMDBRequestInvalid, # Invalid format + 5: TMDBRequestInvalid, # Invalid parameters + 6: TMDBRequestInvalid, # Invalid id + 7: TMDBKeyInvalid, # Invalid API key + 8: TMDBRequestError, # Duplicate entry + 9: TMDBOffline, # Service offline + 10: TMDBKeyRevoked, # Suspended API key + 11: TMDBError, # Internal error + 12: None, # Item update success + 13: None, # Item delete success + 14: TMDBRequestError, # Authentication failed + 15: TMDBError, # Failed + 16: TMDBError, # Device denied + 17: TMDBError, # Session denied + 18: TMDBRequestError, # Validation denied + 19: TMDBRequestInvalid, # Invalid accept header + 20: TMDBRequestInvalid, # Invalid date range + 21: TMDBRequestError, # Entry not found + 22: TMDBPagingIssue, # Invalid page + 23: TMDBRequestInvalid, # Invalud date + 24: TMDBError, # Request time out + 25: TMDBRequestError, # Request limit reached + 26: TMDBRequestInvalid, # Missing usernam and password + 27: TMDBRequestError, # Too many append + 28: TMDBRequestInvalid, # Invalud timezone + 29: TMDBRequestInvalid, # Action confirmation required + 30: TMDBRequestError, # Invalid username or password + 31: TMDBRequestError, # Accound disabled + 32: TMDBRequestError, # Email not verified, + 33: TMDBKeyInvalid, # Invalud request token + 34: TMDBRequestError # Resource could not be found +} + def handle_status(data, query): - status = status_handlers[data.get('status_code', 1)] - if status is not None: - status.tmdberrno = data['status_code'] + status_code = data.get('status_code', 1) + exception_class = status_handlers[status_code] + if exception_class is not None: + status_message = data.get('status_message', None) + status = exception_class(status_message) + status.tmdberrno = status_code status.query = query raise status diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index ed1fc96..d57fba8 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,50 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__ = "v0.7.2" -# 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 -# 0.7.0 Add support for television series data -# 0.7.1 Add rate limiter to cache engine -# 0.7.2 Add similar and keywords to TV Series -# Fix unicode issues with search result object names -# Temporary fix for youtube videos with malformed URLs +__version__ = "v0.7.4" from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr @@ -84,7 +41,7 @@ def process_date(datestr): try: - return datetime.date(*[int(x) for x in datestr.split('-')]) + return datetime.date(*[int(x) for x in datestr[:10].split('-')]) except (TypeError, ValueError): import sys import warnings @@ -154,6 +111,7 @@ def searchMovieWithYear(query, locale=None, adult=False): 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() @@ -161,6 +119,7 @@ def __init__(self, request, locale=None): 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), @@ -170,6 +129,7 @@ def searchSeries(query, first_air_date_year=None, search_type=None, locale=None) 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() @@ -177,6 +137,7 @@ def __init__(self, request, locale=None): 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)) @@ -185,6 +146,7 @@ def searchPerson(query, adult=False): 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)) @@ -197,6 +159,7 @@ def searchStudio(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)) @@ -209,6 +172,7 @@ def searchList(query, adult=False): 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)) @@ -222,6 +186,7 @@ def searchCollection(query, locale=None): 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() @@ -291,7 +256,8 @@ def sizes(self): class Profile(Image): def sizes(self): - return Configuration.images['profile_sizes'] + experimental = ['w132_and_h132_bestv2', 'w264_and_h264_bestv2'] + return experimental + Configuration.images['profile_sizes'] class Logo(Image): @@ -300,8 +266,8 @@ def sizes(self): class AlternateTitle(Element): - country = Datapoint('iso_3166_1') - title = Datapoint('title') + country = Datapoint('iso_3166_1') + title = Datapoint('title') # sort preferring locale's country, but keep remaining ordering consistent def __lt__(self, other): @@ -332,6 +298,9 @@ class Person(Element): raw=False, default=None) adult = Datapoint('adult') aliases = Datalist('also_known_as') + gender = Datapoint('gender') + imdb = Datapoint('imdb_id') + popularity = Datapoint('popularity') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>"\ @@ -343,6 +312,7 @@ def _populate(self): 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)) @@ -381,12 +351,54 @@ def __repr__(self): class Release(Element): + + TYPES = ( + (1, 'Premiere'), + (2, 'Theatrical (limited)'), + (3, 'Theatrical'), + (4, 'Digital'), + (5, 'Physical'), + (6, 'TV'), + ) + certification = Datapoint('certification') + language = Datapoint('iso_639_1') + # note = Datapoint('note') + date = Datapoint('release_date', handler=process_date) + type = Datapoint('type') + + def __repr__(self): + return u"<{0.__class__.__name__} '{0.date} ({0.language})'>".format(self) + + def get_type(self): + return self.TYPES[self.type][1] + + +class ReleaseDate(Element): country = Datapoint('iso_3166_1') - releasedate = Datapoint('release_date', handler=process_date) + releases = Datalist('release_dates', handler=Release) + def __repr__(self): - return u"<{0.__class__.__name__} '{0.country}', {0.releasedate}>"\ - .format(self).encode('utf-8') + return u"<{0.__class__.__name__} '{0.country}'>".format(self) + + +class Video(Element): + id = Datapoint('id') + name = Datapoint('name') + country = Datapoint('iso_3166_1') + language = Datapoint('iso_639_1') + size = Datapoint('size') + key = Datapoint('key') + site = Datapoint('site') + type = Datapoint('type') + + def geturl(self): + if self.site == 'YouTube': + self.key = self.key.encode('ascii', errors='ignore') + return "http://www.youtube.com/watch?v={0}".format(self.key) + + def __repr__(self): + return u"<{0.__class__.__name__} '{0.name}'>".format(self) class Trailer(Element): @@ -439,13 +451,13 @@ class Genre(NameRepr, Element): name = Datapoint('name') def _populate_movies(self): - return Request('genre/{0}/movies'.format(self.id), \ + 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(), \ + search = MovieSearchResult(self._populate_movies(), locale=self._locale) search._name = u"{0.name} Movies".format(self) self._data['movies'] = search @@ -506,9 +518,15 @@ def latest(cls): req.lifetime = 600 return cls(raw=req.readJSON()) + @classmethod + def discover(cls, locale=None, **kwargs): + res = MovieSearchResult(Request('discover/movie', **kwargs), locale=locale) + res._name = 'Discover' + return res + @classmethod def nowplaying(cls, locale=None): - res = MovieSearchResult(Request('movie/now-playing'), locale=locale) + res = MovieSearchResult(Request('movie/now_playing'), locale=locale) res._name = 'Now Playing' return res @@ -589,6 +607,8 @@ def fromIMDB(cls, imdbid, locale=None): releasedate = Datapoint('release_date', handler=process_date) homepage = Datapoint('homepage') imdb = Datapoint('imdb_id') + originallanguage = Datapoint('original_language') + status = Datapoint('status') backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None) @@ -600,7 +620,8 @@ def fromIMDB(cls, imdbid, locale=None): votes = Datapoint('vote_count') adult = Datapoint('adult') - collection = Datapoint('belongs_to_collection', handler=lambda x: \ + video = Datapoint('video') + collection = Datapoint('belongs_to_collection', handler=lambda x: Collection(raw=x)) genres = Datalist('genres', handler=Genre) studios = Datalist('production_companies', handler=Studio) @@ -608,7 +629,7 @@ def fromIMDB(cls, imdbid, locale=None): languages = Datalist('spoken_languages', handler=Language) def _populate(self): - return Request('movie/{0}'.format(self.id), \ + return Request('movie/{0}'.format(self.id), language=self._locale.language) def _populate_titles(self): @@ -618,8 +639,12 @@ def _populate_titles(self): return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) - def _populate_cast(self): - return Request('movie/{0}/casts'.format(self.id)) + # TODO: implement changes + # def _populate_changes(self): + # return Request('movie/{movie_id}/changes').format(movie_id=self.id) + + def _populate_credits(self): + return Request('movie/{0}/credits'.format(self.id)) def _populate_images(self): kwargs = {} @@ -630,36 +655,40 @@ def _populate_images(self): 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_release_dates(self): + return Request('movie/{0}/release_dates'.format(self.id)) def _populate_trailers(self): return Request('movie/{0}/trailers'.format(self.id), - language=self._locale.language) + language=self._locale.language) + + def _populate_videos(self): + return Request('movie/{0}/videos'.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, \ + alternate_titles = Datalist('titles', handler=AlternateTitle, 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') + poller=_populate_credits, sort='order') + crew = Datalist('crew', handler=Crew, poller=_populate_credits) - 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') + release_dates = Datadict('results', handler=ReleaseDate, + poller=_populate_release_dates, attr='country') youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, poller=_populate_trailers) apple_trailers = Datalist('quicktime', handler=AppleTrailer, poller=_populate_trailers) + videos = Datalist('results', handler=Video, poller=_populate_videos) translations = Datalist('translations', handler=Translation, poller=_populate_translations) @@ -678,7 +707,7 @@ def setRating(self, value): req = Request('movie/{0}/rating'.format(self.id), session_id=self._session.sessionid) req.lifetime = 0 - req.add_data({'value':value}) + req.add_data({'value': value}) req.readJSON() def setWatchlist(self, value): @@ -696,9 +725,15 @@ def getSimilar(self): @property def similar(self): res = MovieSearchResult(Request( - 'movie/{0}/similar_movies'.format(self.id)), - locale=self._locale) - res._name = u'Similar to {0}'.format(self._printable_name()) + 'movie/{0}/similar'.format(self.id)), locale=self._locale) + res._name = 'Similar to {0}'.format(self._printable_name()) + return res + + @property + def recommendations(self): + res = MovieSearchResult(Request( + 'movie/{0}/recommendations'.format(self.id)), locale=self._locale) + res._name = 'Recommendations for {0}'.format(self._printable_name()) return res @property @@ -723,7 +758,7 @@ def __repr__(self): self._printable_name()).encode('utf-8') -class ReverseCast( Movie ): +class ReverseCast(Movie): character = Datapoint('character') def __repr__(self): @@ -731,7 +766,7 @@ def __repr__(self): .format(self, self._printable_name()).encode('utf-8')) -class ReverseCrew( Movie ): +class ReverseCrew(Movie): department = Datapoint('department') job = Datapoint('job') @@ -764,6 +799,7 @@ def _populate_images(self): posters = Datalist('posters', handler=Poster, poller=_populate_images, sort=True) + class List(NameRepr, Element): id = Datapoint('id', initarg=1) name = Datapoint('name') @@ -778,10 +814,12 @@ class List(NameRepr, Element): def _populate(self): return Request('list/{0}'.format(self.id)) -class Network(NameRepr,Element): + +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) @@ -827,6 +865,7 @@ def _populate_images(self): 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) @@ -859,7 +898,45 @@ def _populate_external_ids(self): tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids) tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids) + class Series(NameRepr, Element): + + @classmethod + def latest(cls): + req = Request('tv/latest') + req.lifetime = 600 + return cls(raw=req.readJSON()) + + @classmethod + def discover(cls, locale=None, **kwargs): + res = SeriesSearchResult(Request('discover/tv', **kwargs), locale=locale) + res._name = 'Discover' + return res + + @classmethod + def ontheair(cls, locale=None): + res = SeriesSearchResult(Request('tv/on_the_air'), locale=locale) + res._name = 'On The Air' + return res + + @classmethod + def airingtoday(cls, locale=None): + res = SeriesSearchResult(Request('tv/airing_today'), locale=locale) + res._name = 'Airing Today' + return res + + @classmethod + def mostpopular(cls, locale=None): + res = SeriesSearchResult(Request('tv/popular'), locale=locale) + res._name = 'Popular' + return res + + @classmethod + def toprated(cls, locale=None): + res = SeriesSearchResult(Request('tv/top_rated'), locale=locale) + res._name = 'Top Rated' + return res + id = Datapoint('id', initarg=1) backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None) authors = Datalist('created_by', handler=Person) diff --git a/tmdb3/util.py b/tmdb3/util.py index 703eac1..d33f5f6 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -259,7 +259,7 @@ def __init__(self, field, handler=None, poller=None, raw=True, the field name from the source data is used instead 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 + None, 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 @@ -293,7 +293,8 @@ def __set__(self, inst, value): data[self.getkey(val)] = val inst._data[self.field] = data -class ElementType( type ): + +class ElementType(type): """ MetaClass used to pre-process Element-derived classes and set up the Data definitions @@ -305,7 +306,7 @@ def __new__(mcs, name, bases, attrs): # a copy into this class's attributes # run in reverse order so higher priority values overwrite lower ones data = {} - pollers = {'_populate':None} + pollers = {'_populate': None} for base in reversed(bases): if isinstance(base, mcs): @@ -326,7 +327,7 @@ def __new__(mcs, name, bases, attrs): if '_populate' in attrs: pollers['_populate'] = attrs['_populate'] - # process all defined Data attribues, testing for use as an initial + # process all defined Data attributes, 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]) @@ -359,7 +360,7 @@ def __new__(mcs, name, bases, attrs): attr.poller = poller attrs[attr.name] = attr - # build sorted list of arguments used for intialization + # build sorted list of arguments used for initialization attrs['_InitArgs'] = tuple( [a.name for a in sorted(initargs, key=lambda x: x.initarg)]) return type.__new__(mcs, name, bases, attrs) @@ -397,6 +398,6 @@ def __call__(cls, *args, **kwargs): return obj -class Element( object ): +class Element(object): __metaclass__ = ElementType _lang = 'en'