flexget.plugins.api_tmdb
Covered: 186 lines
Missed: 93 lines
Skipped 61 lines
Percent: 66 %
  1
from datetime import datetime, timedelta
  2
import logging
  3
from urllib2 import URLError
  4
import os
  5
import posixpath
  6
from sqlalchemy import Table, Column, Integer, Float, String, Unicode, Boolean, DateTime, func
  7
from sqlalchemy.schema import ForeignKey
  8
from sqlalchemy.orm import relation
  9
from flexget import schema
 10
from flexget.utils.sqlalchemy_utils import table_add_column, table_schema
 11
from flexget.utils.titles import MovieParser
 12
from flexget.utils.tools import urlopener
 13
from flexget.utils.database import text_date_synonym, year_property, with_session
 14
from flexget.manager import Session
 15
from flexget.plugin import register_plugin, DependencyError
 17
try:
 18
    import simplejson as json
 19
except ImportError:
 20
    try:
 21
        import json
 22
    except ImportError:
 23
        raise DependencyError(issued_by='api_tmdb', missing='simplejson', message='api_tmdb requrires either '
 24
                'simplejson module or python > 2.5')
 26
log = logging.getLogger('api_tmdb')
 27
Base = schema.versioned_base('api_tmdb', 0)
 30
api_key = 'bdfc018dbdb7c243dc7cb1454ff74b95'
 31
lang = 'en'
 32
server = 'http://api.themoviedb.org'
 35
@schema.upgrade('api_tmdb')
 36
def upgrade(ver, session):
 37
    if ver is None:
 38
        log.info('Adding columns to tmdb cache table, marking current cache as expired.')
 39
        table_add_column('tmdb_movies', 'runtime', Integer, session)
 40
        table_add_column('tmdb_movies', 'tagline', Unicode, session)
 41
        table_add_column('tmdb_movies', 'budget', Integer, session)
 42
        table_add_column('tmdb_movies', 'revenue', Integer, session)
 43
        table_add_column('tmdb_movies', 'homepage', String, session)
 44
        table_add_column('tmdb_movies', 'trailer', String, session)
 46
        movie_table = table_schema('tmdb_movies', session)
 47
        session.execute(movie_table.update(values={'updated': datetime(1970, 1, 1)}))
 48
        ver = 0
 49
    return ver
 53
genres_table = Table('tmdb_movie_genres', Base.metadata,
 54
    Column('movie_id', Integer, ForeignKey('tmdb_movies.id')),
 55
    Column('genre_id', Integer, ForeignKey('tmdb_genres.id')))
 58
class TMDBContainer(object):
 59
    """Base class for TMDb objects"""
 61
    def __init__(self, init_dict=None):
 62
        if isinstance(init_dict, dict):
 63
            self.update_from_dict(init_dict)
 65
    def update_from_dict(self, update_dict):
 66
        """Populates any simple (string or number) attributes from a dict"""
 67
        for col in self.__table__.columns:
 68
            if isinstance(update_dict.get(col.name), (basestring, int, float)):
 69
                setattr(self, col.name, update_dict[col.name])
 72
class TMDBMovie(TMDBContainer, Base):
 74
    __tablename__ = 'tmdb_movies'
 76
    id = Column(Integer, primary_key=True, autoincrement=False, nullable=False)
 77
    updated = Column(DateTime, default=datetime.now, nullable=False)
 78
    popularity = Column(Integer)
 79
    translated = Column(Boolean)
 80
    adult = Column(Boolean)
 81
    language = Column(String)
 82
    original_name = Column(Unicode)
 83
    name = Column(Unicode)
 84
    alternative_name = Column(Unicode)
 85
    movie_type = Column(String)
 86
    imdb_id = Column(String)
 87
    url = Column(String)
 88
    votes = Column(Integer)
 89
    rating = Column(Float)
 90
    certification = Column(String)
 91
    overview = Column(Unicode)
 92
    runtime = Column(Integer)
 93
    tagline = Column(Unicode)
 94
    budget = Column(Integer)
 95
    revenue = Column(Integer)
 96
    homepage = Column(String)
 97
    trailer = Column(String)
 98
    _released = Column('released', DateTime)
 99
    released = text_date_synonym('_released')
100
    year = year_property('released')
101
    posters = relation('TMDBPoster', backref='movie', cascade='all, delete, delete-orphan')
102
    genres = relation('TMDBGenre', secondary=genres_table, backref='movies')
105
class TMDBGenre(TMDBContainer, Base):
107
    __tablename__ = 'tmdb_genres'
109
    id = Column(Integer, primary_key=True, autoincrement=False)
110
    name = Column(String, nullable=False)
113
class TMDBPoster(TMDBContainer, Base):
115
    __tablename__ = 'tmdb_posters'
117
    db_id = Column(Integer, primary_key=True)
118
    movie_id = Column(Integer, ForeignKey('tmdb_movies.id'))
119
    size = Column(String)
120
    url = Column(String)
121
    id = Column(String)
122
    type = Column(String)
123
    file = Column(Unicode)
125
    def get_file(self, only_cached=False):
126
        """Makes sure the poster is downloaded to the local cache (in userstatic folder) and
127
        returns the path split into a list of directory and file components"""
128
        from flexget.manager import manager
129
        base_dir = os.path.join(manager.config_base, 'userstatic')
130
        if self.file and os.path.isfile(os.path.join(base_dir, self.file)):
131
            return self.file.split(os.sep)
132
        elif only_cached:
133
            return
135
        log.debug('Downloading poster %s' % self.url)
136
        dirname = os.path.join('tmdb', 'posters', str(self.movie_id))
138
        fullpath = os.path.join(base_dir, dirname)
139
        if not os.path.isdir(fullpath):
140
            os.makedirs(fullpath)
141
        filename = os.path.join(dirname, posixpath.basename(self.url))
142
        thefile = file(os.path.join(base_dir, filename), 'wb')
143
        thefile.write(urlopener(self.url, log).read())
144
        self.file = filename
146
        if not Session.object_session(self):
147
            session = Session()
148
            poster = session.query(TMDBPoster).filter(TMDBPoster.db_id == self.db_id).first()
149
            if poster:
150
                poster.file = filename
151
                session.commit()
152
            session.close()
153
        return filename.split(os.sep)
156
class TMDBSearchResult(Base):
158
    __tablename__ = 'tmdb_search_results'
160
    id = Column(Integer, primary_key=True)
161
    search = Column(Unicode, nullable=False)
162
    movie_id = Column(Integer, ForeignKey('tmdb_movies.id'), nullable=True)
163
    movie = relation(TMDBMovie, backref='search_strings')
166
class ApiTmdb(object):
167
    """Does lookups to TMDb and provides movie information. Caches lookups."""
169
    @staticmethod
170
    @with_session
171
    def lookup(title=None, year=None, tmdb_id=None, imdb_id=None, smart_match=None, only_cached=False, session=None):
172
        """Do a lookup from tmdb for the movie matching the passed arguments.
174
        Any combination of criteria can be passed, the most specific criteria specified will be used.
176
        Returns:
177
            The Movie object populated with data from tmdb
179
        Raises:
180
            LookupError if a match cannot be found or there are other problems with the lookup
182
        Args:
183
            tmdb_id: tmdb_id of desired movie
184
            imdb_id: imdb_id of desired movie
185
            title: title of desired movie
186
            year: release year of desired movie
187
            smart_match: attempt to clean and parse title and year from a string
188
            only_cached: if this is specified, an online lookup will not occur if the movie is not in the cache
189
            session: optionally specify a session to use, if specified, returned Movie will be live in that session
190
        """
192
        if not (tmdb_id or imdb_id or title) and smart_match:
194
            title_parser = MovieParser()
195
            title_parser.parse(smart_match)
196
            title = title_parser.name
197
            year = title_parser.year
199
        if title:
200
            search_string = title.lower()
201
            if year:
202
                search_string = '%s %s' % (search_string, year)
203
        elif not (tmdb_id or imdb_id):
204
            raise LookupError('No criteria specified for tmdb lookup')
205
        log.debug('Looking up tmdb information for %r' % {'title': title, 'tmdb_id': tmdb_id, 'imdb_id': imdb_id})
207
        movie = None
209
        def id_str():
210
            return '<title=%s,tmdb_id=%s,imdb_id=%s>' % (title, tmdb_id, imdb_id)
211
        if tmdb_id:
212
            movie = session.query(TMDBMovie).filter(TMDBMovie.id == tmdb_id).first()
213
        if not movie and imdb_id:
214
            movie = session.query(TMDBMovie).filter(TMDBMovie.imdb_id == imdb_id).first()
215
        if not movie and title:
216
            movie_filter = session.query(TMDBMovie).filter(func.lower(TMDBMovie.name) == title.lower())
217
            if year:
218
                movie_filter = movie_filter.filter(TMDBMovie.year == year)
219
            movie = movie_filter.first()
220
            if not movie:
221
                found = session.query(TMDBSearchResult). \
222
                        filter(func.lower(TMDBSearchResult.search) == search_string).first()
223
                if found and found.movie:
224
                    movie = found.movie
225
        if movie:
227
            refresh_time = timedelta(days=2)
228
            if movie.released:
229
                if movie.released > datetime.now() - timedelta(days=7):
231
                    refresh_time = timedelta(days=1)
232
                else:
233
                    age_in_years = (datetime.now() - movie.released).days / 365
234
                    refresh_time += timedelta(days=age_in_years * 5)
235
            if movie.updated < datetime.now() - refresh_time and not only_cached:
236
                log.debug('Cache has expired for %s, attempting to refresh from TMDb.' % id_str())
237
                try:
238
                    ApiTmdb.get_movie_details(movie, session)
239
                except URLError:
240
                    log.error('Error refreshing movie details from TMDb, cached info being used.')
241
            else:
242
                log.debug('Movie %s information restored from cache.' % id_str())
243
        else:
244
            if only_cached:
245
                raise LookupError('Movie %s not found from cache' % id_str())
247
            log.debug('Movie %s not found in cache, looking up from tmdb.' % id_str())
248
            try:
249
                if imdb_id and not tmdb_id:
250
                    result = get_first_result('imdbLookup', imdb_id)
251
                    if result:
252
                        movie = session.query(TMDBMovie).filter(TMDBMovie.id == result['id']).first()
253
                        if movie:
255
                            ApiTmdb.get_movie_details(movie, session)
256
                        else:
257
                            tmdb_id = result['id']
258
                if tmdb_id:
259
                    movie = TMDBMovie()
260
                    movie.id = tmdb_id
261
                    ApiTmdb.get_movie_details(movie, session)
262
                    if movie.name:
263
                        session.merge(movie)
264
                    else:
265
                        movie = None
266
                elif title:
267
                    result = get_first_result('search', search_string)
268
                    if result:
269
                        movie = session.query(TMDBMovie).filter(TMDBMovie.id == result['id']).first()
270
                        if not movie:
271
                            movie = TMDBMovie(result)
272
                            ApiTmdb.get_movie_details(movie, session)
273
                            session.merge(movie)
274
                        if title.lower() != movie.name.lower():
275
                            session.merge(TMDBSearchResult(search=search_string, movie=movie))
276
            except URLError:
277
                raise LookupError('Error looking up movie from TMDb')
279
        if not movie:
280
            raise LookupError('No results found from tmdb for %s' % id_str())
281
        else:
283
            movie.genres
284
            movie.posters
285
            return movie
287
    @staticmethod
288
    def get_movie_details(movie, session):
289
        """Populate details for this :movie: from TMDb"""
291
        if not movie.id:
292
            raise LookupError('Cannot get tmdb details without tmdb id')
293
        result = get_first_result('getInfo', movie.id)
294
        if result:
295
            movie.update_from_dict(result)
296
            posters = result.get('posters')
297
            if posters:
300
                poster_urls = [p.url for p in movie.posters]
301
                for item in posters:
302
                    if item.get('image') and item['image']['url'] not in poster_urls:
303
                        movie.posters.append(TMDBPoster(item['image']))
304
            genres = result.get('genres')
305
            if genres:
306
                for genre in genres:
307
                    if not genre.get('id'):
308
                        continue
309
                    db_genre = session.query(TMDBGenre).filter(TMDBGenre.id == genre['id']).first()
310
                    if not db_genre:
311
                        db_genre = TMDBGenre(genre)
312
                    if db_genre not in movie.genres:
313
                        movie.genres.append(db_genre)
314
            movie.updated = datetime.now()
315
        else:
316
            raise LookupError('No results for tmdb_id %s' % movie.id)
319
def get_first_result(tmdb_function, value):
320
    if isinstance(value, basestring):
321
        value = value.replace(' ', '+').encode('utf-8')
322
    url = '%s/2.1/Movie.%s/%s/json/%s/%s' % (server, tmdb_function, lang, api_key, value)
323
    try:
324
        data = urlopener(url, log)
325
    except URLError, e:
326
        log.warning('Request failed %s' % url)
327
        return
328
    try:
329
        result = json.load(data)
330
    except ValueError:
331
        log.warning('TMDb returned invalid json.')
332
        return
334
    if isinstance(result, list) and len(result):
335
        result = result[0]
336
        if isinstance(result, dict) and result.get('id'):
337
            return result
339
register_plugin(ApiTmdb, 'api_tmdb')