1
from datetime import datetime, timedelta
3
from urllib2 import URLError
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
18
import simplejson as json
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)
29
# This is a FlexGet API key
30
api_key = 'bdfc018dbdb7c243dc7cb1454ff74b95'
32
server = 'http://api.themoviedb.org'
35
@schema.upgrade('api_tmdb')
36
def upgrade(ver, session):
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)
45
# Mark all cached movies as expired, so new fields get populated next lookup
46
movie_table = table_schema('tmdb_movies', session)
47
session.execute(movie_table.update(values={'updated': datetime(1970, 1, 1)}))
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)
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)
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)
134
# If we don't already have a local copy, download one.
135
log.debug('Downloading poster %s' % self.url)
136
dirname = os.path.join('tmdb', 'posters', str(self.movie_id))
137
# Create folders if they don't exist
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())
145
# If we are detached from a session, update the db
146
if not Session.object_session(self):
148
poster = session.query(TMDBPoster).filter(TMDBPoster.db_id == self.db_id).first()
150
poster.file = filename
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."""
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.
177
The Movie object populated with data from tmdb
180
LookupError if a match cannot be found or there are other problems with the lookup
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
192
if not (tmdb_id or imdb_id or title) and smart_match:
193
# If smart_match was specified, and we don't have more specific criteria, parse it into a title and year
194
title_parser = MovieParser()
195
title_parser.parse(smart_match)
196
title = title_parser.name
197
year = title_parser.year
200
search_string = title.lower()
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})
210
return '<title=%s,tmdb_id=%s,imdb_id=%s>' % (title, tmdb_id, imdb_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())
218
movie_filter = movie_filter.filter(TMDBMovie.year == year)
219
movie = movie_filter.first()
221
found = session.query(TMDBSearchResult). \
222
filter(func.lower(TMDBSearchResult.search) == search_string).first()
223
if found and found.movie:
226
# Movie found in cache, check if cache has expired.
227
refresh_time = timedelta(days=2)
229
if movie.released > datetime.now() - timedelta(days=7):
230
# Movie is less than a week old, expire after 1 day
231
refresh_time = timedelta(days=1)
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())
238
ApiTmdb.get_movie_details(movie, session)
240
log.error('Error refreshing movie details from TMDb, cached info being used.')
242
log.debug('Movie %s information restored from cache.' % id_str())
245
raise LookupError('Movie %s not found from cache' % id_str())
246
# There was no movie found in the cache, do a lookup from tmdb
247
log.debug('Movie %s not found in cache, looking up from tmdb.' % id_str())
249
if imdb_id and not tmdb_id:
250
result = get_first_result('imdbLookup', imdb_id)
252
movie = session.query(TMDBMovie).filter(TMDBMovie.id == result['id']).first()
254
# Movie was in database, but did not have the imdb_id stored, force an update
255
ApiTmdb.get_movie_details(movie, session)
257
tmdb_id = result['id']
261
ApiTmdb.get_movie_details(movie, session)
267
result = get_first_result('search', search_string)
269
movie = session.query(TMDBMovie).filter(TMDBMovie.id == result['id']).first()
271
movie = TMDBMovie(result)
272
ApiTmdb.get_movie_details(movie, session)
274
if title.lower() != movie.name.lower():
275
session.merge(TMDBSearchResult(search=search_string, movie=movie))
277
raise LookupError('Error looking up movie from TMDb')
280
raise LookupError('No results found from tmdb for %s' % id_str())
282
# Access attributes to force the relationships to eager load before we detach from session
288
def get_movie_details(movie, session):
289
"""Populate details for this :movie: from TMDb"""
292
raise LookupError('Cannot get tmdb details without tmdb id')
293
result = get_first_result('getInfo', movie.id)
295
movie.update_from_dict(result)
296
posters = result.get('posters')
298
# Add any posters we don't already have
299
# TODO: There are quite a few posters per movie, do we need to cache them all?
300
poster_urls = [p.url for p in movie.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')
307
if not genre.get('id'):
309
db_genre = session.query(TMDBGenre).filter(TMDBGenre.id == genre['id']).first()
311
db_genre = TMDBGenre(genre)
312
if db_genre not in movie.genres:
313
movie.genres.append(db_genre)
314
movie.updated = datetime.now()
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)
324
data = urlopener(url, log)
326
log.warning('Request failed %s' % url)
329
result = json.load(data)
331
log.warning('TMDb returned invalid json.')
333
# Make sure there is a valid result to return
334
if isinstance(result, list) and len(result):
336
if isinstance(result, dict) and result.get('id'):
339
register_plugin(ApiTmdb, 'api_tmdb')