472 changes: 472 additions & 0 deletions mythtv/bindings/python/tmdb3/tmdb3/lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,472 @@
# -*- coding: UTF-8 -*-
# ----------------------
# Name: tmdb3.py
# Python Script
# Author: Raymond Wagner
# Purpose: This python script is intended to translate lookups between the
# TheMovieDB.org V3 API and MythTV's internal metadata format.
# http://www.mythtv.org/wiki/MythVideo_Grabber_Script_Format
# http://help.themoviedb.org/kb/api/about-3
# Code was originally in metadata/Movie/tmdb3.py.
# Moved here so it can be called for movies or TV
#-----------------------
__title__ = "TheMovieDB.org V3"
__author__ = "Raymond Wagner, Roland Ernst"
__version__ = "0.3.9"
# 0.1.0 Initial version
# 0.2.0 Add language support, move cache to home directory
# 0.3.0 Enable version detection to allow use in MythTV
# 0.3.1 Add --test parameter for proper compatibility with mythmetadatalookup
# 0.3.2 Add --area parameter to allow country selection for release date and
# parental ratings
# 0.3.3 Use translated title if available
# 0.3.4 Add support for finding by IMDB under -D (simulate previous version)
# 0.3.5 Add debugging mode
# 0.3.6 Add handling for TMDB site and library returning null results in
# search. This should only need to be a temporary fix, and should be
# resolved upstream.
# 0.3.7 Add handling for TMDB site returning insufficient results from a
# query
# 0.3.7.a : Added compatibiliy to python3, tested with python 3.6 and 2.7
# 0.3.8 Sort posters by system language or 'en', if not found for given language
# 0.3.9 Support TV lookup

# ~ from optparse import OptionParser
import sys
import signal

def print_etree(etostr):
"""lxml.etree.tostring is a bytes object in python3, and a str in python2.
"""
if sys.version_info[0] == 2:
sys.stdout.write(etostr)
else:
sys.stdout.write(etostr.decode())

def timeouthandler(signal, frame):
raise RuntimeError("Timed out")

def buildSingle(inetref, opts):
from MythTV.tmdb3.tmdb_exceptions import TMDBRequestInvalid
from MythTV.tmdb3 import Movie, get_locale
from MythTV import VideoMetadata
from lxml import etree

import locale as py_locale
import re

if re.match('^0[0-9]{6}$', inetref):
movie = Movie.fromIMDB(inetref)
else:
movie = Movie(inetref)

tree = etree.XML(u'<metadata></metadata>')
mapping = [['runtime', 'runtime'], ['title', 'originaltitle'],
['releasedate', 'releasedate'], ['tagline', 'tagline'],
['description', 'overview'], ['homepage', 'homepage'],
['userrating', 'userrating'], ['popularity', 'popularity'],
['budget', 'budget'], ['revenue', 'revenue']]
m = VideoMetadata()
for i,j in mapping:
try:
if getattr(movie, j):
setattr(m, i, getattr(movie, j))
except TMDBRequestInvalid:
return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

if movie.title:
m.title = movie.title

releases = list(movie.releases.items())

# get the release date for the wanted country
# TODO if that is not part of the reply use the primary release date (Primary=true)
# if that is not part of the reply use whatever release date is first in list
# if there is not a single release date in the reply, then leave it empty
if len(releases) > 0:
if opts.country:
# resort releases with selected country at top to ensure it
# is selected by the metadata libraries
r = list(zip(*releases))
if opts.country in r[0]:
index = r[0].index(opts.country)
releases.insert(0, releases.pop(index))

m.releasedate = releases[0][1].releasedate

m.inetref = str(movie.id)
if movie.collection:
m.collectionref = str(movie.collection.id)
if m.releasedate:
m.year = m.releasedate.year
for country, release in releases:
if release.certification:
m.certifications[country] = release.certification
for genre in movie.genres:
m.categories.append(genre.name)
for studio in movie.studios:
m.studios.append(studio.name)
for country in movie.countries:
m.countries.append(country.name)
for cast in movie.cast:
d = {'name':cast.name, 'character':cast.character, 'department':'Actors',
'job':'Actor', 'url':'http://www.themoviedb.org/people/{0}'.format(cast.id)}
if cast.profile: d['thumb'] = cast.profile.geturl()
m.people.append(d)
for crew in movie.crew:
d = {'name':crew.name, 'job':crew.job, 'department':crew.department,
'url':'http://www.themoviedb.org/people/{0}'.format(crew.id)}
if crew.profile: d['thumb'] = crew.profile.geturl()
m.people.append(d)
for backdrop in movie.backdrops:
m.images.append({'type':'fanart', 'url':backdrop.geturl(),
'thumb':backdrop.geturl(backdrop.sizes()[0]),
'height':str(backdrop.height),
'width':str(backdrop.width)})

# tmdb already sorts the posters by language
# if no poster of given language was found,
# try to sort by system language and then by language "en"
system_language = py_locale.getdefaultlocale()[0].split("_")[0]
locale_language = get_locale().language
if opts.debug:
print("system_language : ", system_language)
print("locale_language : ", locale_language)

loc_posters = movie.posters
if loc_posters[0].language != locale_language \
and locale_language != system_language:
if opts.debug:
print("1: No poster found for language '%s', trying to sort posters by '%s' :"
%(locale_language, system_language))
loc_posters = sorted(movie.posters,
key = lambda x: x.language==system_language, reverse = True)

if loc_posters[0].language != system_language \
and loc_posters[0].language != locale_language:
if opts.debug:
print("2: No poster found for language '%s', trying to sort posters by '%s' :"
%(system_language, "en"))
loc_posters = sorted(movie.posters,
key = lambda x: x.language=="en", reverse = True)

for poster in loc_posters:
if opts.debug:
print("Poster : ", poster.language, " | ", poster.userrating,
"\t | ", poster.geturl())
m.images.append({'type':'coverart', 'url':poster.geturl(),
'thumb':poster.geturl(poster.sizes()[0]),
'height':str(poster.height),
'width':str(poster.width)})

tree.append(m.toXML())
return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

def buildMovieList(query, opts):
# TEMPORARY FIX:
# replace all dashes from queries to work around search behavior
# as negative to all text that comes afterwards
query = query.replace('-',' ')

from MythTV.tmdb3 import searchMovie
from MythTV import VideoMetadata
from lxml import etree
results = iter(searchMovie(query))
tree = etree.XML(u'<metadata></metadata>')
mapping = [['runtime', 'runtime'], ['title', 'originaltitle'],
['releasedate', 'releasedate'], ['tagline', 'tagline'],
['description', 'overview'], ['homepage', 'homepage'],
['userrating', 'userrating'], ['popularity', 'popularity']]

count = 0
while True:
try:
res = next(results)
except StopIteration:
# end of results
break
except IndexError:
# unexpected end of results
# we still want to return whatever we have so far
break

if res is None:
# faulty data, skip it and continue
continue

m = VideoMetadata()
for i,j in mapping:
if getattr(res, j):
setattr(m, i, getattr(res, j))
m.inetref = str(res.id)

if res.title:
m.title = res.title

#TODO:
# should releasedate and year be pulled from the country-specific data
# or should it be left to the default information to cut down on
# traffic from searches
if res.releasedate:
m.year = res.releasedate.year
if res.backdrop:
b = res.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if res.poster:
p = res.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())
count += 1
if count >= 60:
# page limiter, dont want to overload the server
break

return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

def buildTVList(query, opts):
from MythTV.tmdb3 import searchSeries
from MythTV import VideoMetadata
from lxml import etree
from datetime import date

resultsx = searchSeries(query)
results = iter(resultsx) # searchSeries(query))
mapping = [['language', 'original_language'],
['title', 'name'], ['inetref','id'],
['collectionref','id'], ['description','overview'],
['releasedate','first_air_date']]
tree = etree.XML(u'<metadata></metadata>')

count = 0
while True:
try:
res = next(results)
except StopIteration:
# end of results
break
except IndexError:
# unexpected end of results
# we still want to return whatever we have so far
break

if res is None:
# faulty data, skip it and continue
continue

m = VideoMetadata()
for i,j in mapping:
if getattr(res, j):
setattr(m, i, getattr(res, j))

# These need to be strings not ints
m.inetref = str(res.id)
m.collectionref = str(res.id)

if res.backdrop:
b = res.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if res.poster:
p = res.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())
count += 1
if count >= 60:
# page limiter, dont want to overload the server
break

return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

# for buildEpisode, args are one of the following
# - title subtitle (-N command line)
# - inetref subtitle (-N command line)
# - inetref season episode (-D command line)
def buildEpisode(args, opts):

query = args[0]

from MythTV.tmdb3 import Series, Season, Episode
from MythTV import VideoMetadata
from lxml import etree
from MythTV.tmdb3 import searchSeries

if query.isnumeric():
inetref = query
else:
results = searchSeries(query)
series = results[0]
inetref = str(series.id)

series = Series(inetref)
season_number = series.number_of_seasons
episode_number = None
subtitle = None
if len(args) == 2:
subtitle = args[1]
elif len(args) == 3:
season_number = int(args[1])
episode_number = int(args[2])

episode = None
# process seasons backwards because it is more likely
# that you have a recent one than an old one
while season_number > 0:
season = Season(inetref,str(season_number))
if episode_number:
episode = season.episodes[episode_number]
break
for ep_num, ep in season.episodes.items():
if ep.name == subtitle:
episode = ep
episode_number = int(ep_num)
break
if episode:
break
season_number = season_number - 1

if not episode_number and not episode:
sys.stdout.write('ERROR: Episode not found: ' + str(args))
return 9

# reload episode with full details
episode = Episode(inetref,season_number,episode_number)

tree = etree.XML(u'<metadata></metadata>')
mapping = [['subtitle','name'],
['description', 'overview'], ['season', 'season_number'],
['episode', 'episode_number'], ['releasedate', 'air_date']]
m = VideoMetadata()
m.title = series.name
for i,j in mapping:
if getattr(episode, j):
setattr(m, i, getattr(episode, j))

# These need to be strings not ints
m.inetref = inetref
m.collectionref = inetref

for cast in episode.cast:
d = {'name':cast.name, 'character':cast.character, 'department':'Actors',
'job':'Actor', 'url':'http://www.themoviedb.org/people/{0}'.format(cast.id)}
if cast.profile: d['thumb'] = cast.profile.geturl()
m.people.append(d)
for crew in episode.crew:
d = {'name':crew.name, 'job':crew.job, 'department':crew.department,
'url':'http://www.themoviedb.org/people/{0}'.format(crew.id)}
if crew.profile: d['thumb'] = crew.profile.geturl()
m.people.append(d)
for guest in episode.guest_stars:
d = {'name':guest.name, 'job':"Guest Star",
'url':'http://www.themoviedb.org/people/{0}'.format(guest.id)}
if guest.profile: d['thumb'] = guest.profile.geturl()
m.people.append(d)
if episode.still:
b = episode.still
m.images.append({'type':'screenshot', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if season.poster:
p = season.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
m.language = series.original_language
if series.backdrop:
b = series.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
for genre in series.genres:
m.categories.append(genre.name)

tree.append(m.toXML())

return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

def buildCollection(inetref, opts):
from MythTV.tmdb3.tmdb_exceptions import TMDBRequestInvalid
from MythTV.tmdb3 import Collection
from MythTV import VideoMetadata
from lxml import etree
collection = Collection(inetref)
tree = etree.XML(u'<metadata></metadata>')
m = VideoMetadata()
m.collectionref = str(collection.id)
try:
m.title = collection.name
except TMDBRequestInvalid:
print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit()
if collection.backdrop:
b = collection.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if collection.poster:
p = collection.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())
return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

# For television only - get series info
# Same results as for television search but only 1 result
def buildTVSeries(inetref, opts):

from MythTV.tmdb3 import Series
from MythTV import VideoMetadata
from lxml import etree

series = Series(inetref)

mapping = [['language', 'original_language'],
['title', 'name'], ['inetref','id'],
['collectionref','id'], ['description','overview'],
['releasedate','first_air_date']]
tree = etree.XML(u'<metadata></metadata>')
m = VideoMetadata()
for i,j in mapping:
if getattr(series, j):
setattr(m, i, getattr(series, j))

# These need to be strings not ints
m.inetref = str(series.id)
m.collectionref = str(series.id)

for genre in series.genres:
m.categories.append(genre.name)

if series.backdrop:
b = series.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if series.poster:
p = series.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())

return etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True)

def buildVersion(showType, command):
from lxml import etree
version = etree.XML(u'<grabber></grabber>')
etree.SubElement(version, "name").text = __title__ + ' ' + showType
etree.SubElement(version, "author").text = __author__
etree.SubElement(version, "thumbnail").text = 'tmdb.png'
etree.SubElement(version, "command").text = command
etree.SubElement(version, "type").text = showType
etree.SubElement(version, "description").text = \
'Search and metadata downloads for themoviedb.org'
etree.SubElement(version, "version").text = __version__
etree.SubElement(version, "accepts").text = 'tmdb.py'
etree.SubElement(version, "accepts").text = 'tmdb.pl'
return etree.tostring(version, encoding='UTF-8', pretty_print=True,
xml_declaration=True)
42 changes: 27 additions & 15 deletions mythtv/bindings/python/tmdb3/tmdb3/tmdb_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
Preliminary API specifications can be found at
http://help.themoviedb.org/kb/api/about-3"""

__version__ = "v0.7.0"
__version__ = "v0.7.1"
# 0.1.0 Initial development
# 0.2.0 Add caching mechanism for API queries
# 0.2.1 Temporary work around for broken search paging
Expand Down Expand Up @@ -62,6 +62,7 @@
# releasedate sorting from Collection Movies
# 0.7.0 Add support for television series data
# 0.7.0.a Added compatibility to python3, tested with python 3.6 and 2.7
# 0.7.1 Changes to support TV series lookup.

from . import IS_PY2

Expand Down Expand Up @@ -171,7 +172,7 @@ def __init__(self, request, locale=None):
locale = get_locale()
super(SeriesSearchResult, self).__init__(
request.new(language=locale.language),
lambda x: Series(raw=x, locale=locale))
lambda x: Series_base(raw=x, locale=locale))

def searchPerson(query, adult=False):
return PeopleSearchResult(Request('search/person', query=query,
Expand Down Expand Up @@ -779,10 +780,12 @@ 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)
# This class excludes the _populate members because they are harmful
# when doing a Season as they cause it to take forever.
class Episode_base(NameRepr, Element):
series_id = Datapoint('series_id', initarg=1)
season_number = Datapoint('season_number', initarg=2)
episode_number = Datapoint('episode_number', initarg=3)
air_date = Datapoint('air_date', handler=process_date)
overview = Datapoint('overview')
name = Datapoint('name')
Expand All @@ -791,16 +794,22 @@ class Episode(NameRepr, Element):
id = Datapoint('id')
production_code = Datapoint('production_code')
still = Datapoint('still_path', handler=Backdrop, raw=False, default=None)
guest_stars = Datalist('guest_stars', handler=Cast,
sort='order')

class Episode(Episode_base):
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)
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)

# Items not currently used by tmdb3tv
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))
Expand All @@ -812,11 +821,6 @@ def _populate_images(self):
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)
Expand All @@ -825,20 +829,21 @@ def _populate_images(self):
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)
season_number = Datapoint('season_number', initarg=2)
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,
episodes = Datadict('episodes', attr='episode_number', handler=Episode_base,
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)

# Items not currently used by tmdb3tv
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
Expand All @@ -856,9 +861,11 @@ 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):
# This class excludes the _populate members because they are harmful.
# when doing a search as they cause the search to take forever.
class Series_base(NameRepr, Element):
id = Datapoint('id', initarg=1)
backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None)
original_language = Datapoint('original_language')
authors = Datalist('created_by', handler=Person)
episode_run_times = Datalist('episode_run_time')
first_air_date = Datapoint('first_air_date', handler=process_date)
Expand All @@ -873,6 +880,8 @@ class Series(NameRepr, Element):
number_of_episodes = Datapoint('number_of_episodes')
number_of_seasons = Datapoint('number_of_seasons')
overview = Datapoint('overview')
backdrop = Datapoint('backdrop_path', handler=Backdrop,
raw=False, default=None)
popularity = Datapoint('popularity')
status = Datapoint('status')
userrating = Datapoint('vote_average')
Expand All @@ -881,10 +890,13 @@ class Series(NameRepr, Element):
networks = Datalist('networks', handler=Network)
seasons = Datadict('seasons', attr='season_number', handler=Season, passthrough={'id': 'series_id'})


class Series(Series_base):
def _populate(self):
return Request('tv/{0}'.format(self.id),
language=self._locale.language)

# Items not currently used by tmdb3tv
def _populate_cast(self):
return Request('tv/{0}/credits'.format(self.id))

Expand Down
2 changes: 1 addition & 1 deletion mythtv/bindings/python/tmdb3/tmdb3/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def __get__(self, inst, owner):
if inst is None:
return self
if self.field not in inst._data:
if self.poller is None:
if self.poller is None or not callable(self.poller.func):
return None
self.poller.__get__(inst, owner)()
return inst._data[self.field]
Expand Down
347 changes: 59 additions & 288 deletions mythtv/programs/scripts/metadata/Movie/tmdb3.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,277 +3,26 @@
# ----------------------
# Name: tmdb3.py
# Python Script
# Author: Raymond Wagner
# Purpose: This python script is intended to translate lookups between the
# TheMovieDB.org V3 API and MythTV's internal metadata format.
# http://www.mythtv.org/wiki/MythVideo_Grabber_Script_Format
# http://help.themoviedb.org/kb/api/about-3
#-----------------------
__title__ = "TheMovieDB.org V3"
__author__ = "Raymond Wagner, Roland Ernst"
__version__ = "0.3.8"
# 0.1.0 Initial version
# 0.2.0 Add language support, move cache to home directory
# 0.3.0 Enable version detection to allow use in MythTV
# 0.3.1 Add --test parameter for proper compatibility with mythmetadatalookup
# 0.3.2 Add --area parameter to allow country selection for release date and
# parental ratings
# 0.3.3 Use translated title if available
# 0.3.4 Add support for finding by IMDB under -D (simulate previous version)
# 0.3.5 Add debugging mode
# 0.3.6 Add handling for TMDB site and library returning null results in
# search. This should only need to be a temporary fix, and should be
# resolved upstream.
# 0.3.7 Add handling for TMDB site returning insufficient results from a
# query
# 0.3.7.a : Added compatibiliy to python3, tested with python 3.6 and 2.7
# 0.3.8 Sort posters by system language or 'en', if not found for given language
# Author: Peter Bennett
# Purpose:
# Frontend for the tmdb3 lookup.py script that now supports both Movies and
# TV shows. This frontend supports Movies
# Command example:
# See help (-h) options
#
# Code that was originally here is now in tmdb3/lookup.py
#
# License:Creative Commons GNU GPL v2
# (http://creativecommons.org/licenses/GPL/2.0/)
# -------------------------------------
#
__title__ = "TheMovieDB.org for TV V3"
__author__ = "Peter Bennett"

from optparse import OptionParser
import sys
import signal

def print_etree(etostr):
"""lxml.etree.tostring is a bytes object in python3, and a str in python2.
"""
if sys.version_info[0] == 2:
sys.stdout.write(etostr)
else:
sys.stdout.write(etostr.decode())

def timeouthandler(signal, frame):
raise RuntimeError("Timed out")

def buildSingle(inetref, opts):
from MythTV.tmdb3.tmdb_exceptions import TMDBRequestInvalid
from MythTV.tmdb3 import Movie, get_locale
from MythTV import VideoMetadata
from lxml import etree

import locale as py_locale
import re

if re.match('^0[0-9]{6}$', inetref):
movie = Movie.fromIMDB(inetref)
else:
movie = Movie(inetref)

tree = etree.XML(u'<metadata></metadata>')
mapping = [['runtime', 'runtime'], ['title', 'originaltitle'],
['releasedate', 'releasedate'], ['tagline', 'tagline'],
['description', 'overview'], ['homepage', 'homepage'],
['userrating', 'userrating'], ['popularity', 'popularity'],
['budget', 'budget'], ['revenue', 'revenue']]
m = VideoMetadata()
for i,j in mapping:
try:
if getattr(movie, j):
setattr(m, i, getattr(movie, j))
except TMDBRequestInvalid:
print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit()

if movie.title:
m.title = movie.title

releases = list(movie.releases.items())

# get the release date for the wanted country
# TODO if that is not part of the reply use the primary release date (Primary=true)
# if that is not part of the reply use whatever release date is first in list
# if there is not a single release date in the reply, then leave it empty
if len(releases) > 0:
if opts.country:
# resort releases with selected country at top to ensure it
# is selected by the metadata libraries
r = list(zip(*releases))
if opts.country in r[0]:
index = r[0].index(opts.country)
releases.insert(0, releases.pop(index))

m.releasedate = releases[0][1].releasedate

m.inetref = str(movie.id)
if movie.collection:
m.collectionref = str(movie.collection.id)
if m.releasedate:
m.year = m.releasedate.year
for country, release in releases:
if release.certification:
m.certifications[country] = release.certification
for genre in movie.genres:
m.categories.append(genre.name)
for studio in movie.studios:
m.studios.append(studio.name)
for country in movie.countries:
m.countries.append(country.name)
for cast in movie.cast:
d = {'name':cast.name, 'character':cast.character, 'department':'Actors',
'job':'Actor', 'url':'http://www.themoviedb.org/people/{0}'.format(cast.id)}
if cast.profile: d['thumb'] = cast.profile.geturl()
m.people.append(d)
for crew in movie.crew:
d = {'name':crew.name, 'job':crew.job, 'department':crew.department,
'url':'http://www.themoviedb.org/people/{0}'.format(crew.id)}
if crew.profile: d['thumb'] = crew.profile.geturl()
m.people.append(d)
for backdrop in movie.backdrops:
m.images.append({'type':'fanart', 'url':backdrop.geturl(),
'thumb':backdrop.geturl(backdrop.sizes()[0]),
'height':str(backdrop.height),
'width':str(backdrop.width)})

# tmdb already sorts the posters by language
# if no poster of given language was found,
# try to sort by system language and then by language "en"
system_language = py_locale.getdefaultlocale()[0].split("_")[0]
locale_language = get_locale().language
if opts.debug:
print("system_language : ", system_language)
print("locale_language : ", locale_language)

loc_posters = movie.posters
if loc_posters[0].language != locale_language \
and locale_language != system_language:
if opts.debug:
print("1: No poster found for language '%s', trying to sort posters by '%s' :"
%(locale_language, system_language))
loc_posters = sorted(movie.posters,
key = lambda x: x.language==system_language, reverse = True)

if loc_posters[0].language != system_language \
and loc_posters[0].language != locale_language:
if opts.debug:
print("2: No poster found for language '%s', trying to sort posters by '%s' :"
%(system_language, "en"))
loc_posters = sorted(movie.posters,
key = lambda x: x.language=="en", reverse = True)

for poster in loc_posters:
if opts.debug:
print("Poster : ", poster.language, " | ", poster.userrating,
"\t | ", poster.geturl())
m.images.append({'type':'coverart', 'url':poster.geturl(),
'thumb':poster.geturl(poster.sizes()[0]),
'height':str(poster.height),
'width':str(poster.width)})

tree.append(m.toXML())
print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit()

def buildList(query, opts):
# TEMPORARY FIX:
# replace all dashes from queries to work around search behavior
# as negative to all text that comes afterwards
query = query.replace('-',' ')

from MythTV.tmdb3 import searchMovie
from MythTV import VideoMetadata
from lxml import etree
results = iter(searchMovie(query))
tree = etree.XML(u'<metadata></metadata>')
mapping = [['runtime', 'runtime'], ['title', 'originaltitle'],
['releasedate', 'releasedate'], ['tagline', 'tagline'],
['description', 'overview'], ['homepage', 'homepage'],
['userrating', 'userrating'], ['popularity', 'popularity']]

count = 0
while True:
try:
res = next(results)
except StopIteration:
# end of results
break
except IndexError:
# unexpected end of results
# we still want to return whatever we have so far
break

if res is None:
# faulty data, skip it and continue
continue

m = VideoMetadata()
for i,j in mapping:
if getattr(res, j):
setattr(m, i, getattr(res, j))
m.inetref = str(res.id)

if res.title:
m.title = res.title

#TODO:
# should releasedate and year be pulled from the country-specific data
# or should it be left to the default information to cut down on
# traffic from searches
if res.releasedate:
m.year = res.releasedate.year
if res.backdrop:
b = res.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if res.poster:
p = res.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())
count += 1
if count >= 60:
# page limiter, dont want to overload the server
break

print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit(0)

def buildCollection(inetref, opts):
from MythTV.tmdb3.tmdb_exceptions import TMDBRequestInvalid
from MythTV.tmdb3 import Collection
from MythTV import VideoMetadata
from lxml import etree
collection = Collection(inetref)
tree = etree.XML(u'<metadata></metadata>')
m = VideoMetadata()
m.collectionref = str(collection.id)
try:
m.title = collection.name
except TMDBRequestInvalid:
print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit()
if collection.backdrop:
b = collection.backdrop
m.images.append({'type':'fanart', 'url':b.geturl(),
'thumb':b.geturl(b.sizes()[0])})
if collection.poster:
p = collection.poster
m.images.append({'type':'coverart', 'url':p.geturl(),
'thumb':p.geturl(p.sizes()[0])})
tree.append(m.toXML())
print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit()

def buildVersion():
from lxml import etree
version = etree.XML(u'<grabber></grabber>')
etree.SubElement(version, "name").text = __title__
etree.SubElement(version, "author").text = __author__
etree.SubElement(version, "thumbnail").text = 'tmdb.png'
etree.SubElement(version, "command").text = 'tmdb3.py'
etree.SubElement(version, "type").text = 'movie'
etree.SubElement(version, "description").text = \
'Search and metadata downloads for themoviedb.org'
etree.SubElement(version, "version").text = __version__
etree.SubElement(version, "accepts").text = 'tmdb.py'
etree.SubElement(version, "accepts").text = 'tmdb.pl'
print_etree(etree.tostring(version, encoding='UTF-8', pretty_print=True,
xml_declaration=True))
sys.exit(0)

def performSelfTest():
err = 0
try:
Expand All @@ -297,19 +46,22 @@ def performSelfTest():

if not err:
print ("Everything appears in order.")
sys.exit(err)
return err

def main(showType, command):

def main():
parser = OptionParser()

parser.add_option('-v', "--version", action="store_true", default=False,
dest="version", help="Display version and author")
parser.add_option('-t', "--test", action="store_true", default=False,
dest="test", help="Perform self-test for dependencies.")
parser.add_option('-M', "--movielist", action="store_true", default=False,
dest="movielist", help="Get Movies matching search.")
parser.add_option('-D', "--moviedata", action="store_true", default=False,
dest="moviedata", help="Get Movie data.")
parser.add_option('-M', "--movielist", "--list", action="store_true", default=False,
dest="movielist",
help="Get Movies. Needs search key.")
parser.add_option('-D', "--moviedata", "--data", action="store_true", default=False,
dest="moviedata", help="Get Movie data. " \
"Needs inetref. ")
parser.add_option('-C', "--collection", action="store_true", default=False,
dest="collectiondata", help="Get Collection data.")
parser.add_option('-l', "--language", metavar="LANGUAGE", default=u'en',
Expand All @@ -322,14 +74,12 @@ def main():

opts, args = parser.parse_args()

from MythTV.tmdb3.lookup import timeouthandler
signal.signal(signal.SIGALRM, timeouthandler)
signal.alarm(180)

if opts.version:
buildVersion()

if opts.test:
performSelfTest()
return performSelfTest()

from MythTV.tmdb3 import set_key, set_cache, set_locale
set_key('c27cb71cff5bd76e1a7a009380562c62')
Expand All @@ -345,7 +95,7 @@ def main():
confdir = os.environ.get('HOME', '')
if (not confdir) or (confdir == '/'):
print ("Unable to find MythTV directory for metadata cache.")
sys.exit(1)
return 1
confdir = os.path.join(confdir, '.mythtv')
cachedir = os.path.join(confdir, 'cache')
if not os.path.exists(cachedir):
Expand All @@ -358,22 +108,43 @@ def main():
if opts.country:
set_locale(country=opts.country, fallthrough=True)

if (len(args) != 1) or (args[0] == ''):
sys.stdout.write('ERROR: tmdb3.py requires exactly one non-empty argument')
sys.exit(1)
if (not opts.version):
if (len(args) < 1) or (args[0] == ''):
sys.stdout.write('ERROR: tmdb3.py requires at least one non-empty argument.\n')
return 1

from MythTV.tmdb3.lookup import buildVersion, buildMovieList, \
buildSingle, buildCollection, print_etree
try:
if opts.movielist:
buildList(args[0], opts)
xml = None
if opts.version:
xml = buildVersion(showType, command)

elif opts.movielist:
xml = buildMovieList(args[0], opts)

if opts.moviedata:
buildSingle(args[0], opts)
elif opts.moviedata:
xml = buildSingle(args[0], opts)

elif opts.collectiondata:
xml = buildCollection(args[0], opts)

# if a number is returned, it is an error code return
if isinstance(xml,int):
return xml

if xml:
print_etree(xml)
else:
return 1

if opts.collectiondata:
buildCollection(args[0], opts)
except RuntimeError as exc:
sys.stdout.write('ERROR: ' + str(exc) + ' exception')
sys.exit(1)
import traceback
traceback.print_exc()
return 1

return 0

if __name__ == '__main__':
main()
sys.exit(main("movie",'tmdb3.py'))
159 changes: 159 additions & 0 deletions mythtv/programs/scripts/metadata/Television/tmdb3tv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# ----------------------
# Name: tmdb3tv.py
# Python Script
# Author: Peter Bennett
# Purpose:
# Frontend for the tmdb3 lookup.py script that now supports both Movies and
# TV shows. This frontend supports TV shows.
# Command example:
# See help (-h) options
#
# License:Creative Commons GNU GPL v2
# (http://creativecommons.org/licenses/GPL/2.0/)
# -------------------------------------
#
__title__ = "TheMovieDB.org for TV V3"
__author__ = "Peter Bennett"

from optparse import OptionParser
import sys
import signal

def performSelfTest():
err = 0
try:
import MythTV
except:
err = 1
print ("Failed to import MythTV bindings. Check your `configure` output "
"to make sure installation was not disabled due to external "
"dependencies")
try:
import MythTV.tmdb3
except:
err = 1
print ("Failed to import PyTMDB3 library. This should have been included "
"with the python MythTV bindings.")
try:
import lxml
except:
err = 1
print ("Failed to import python lxml library.")

if not err:
print ("Everything appears in order.")
return err

def main(showType, command):

parser = OptionParser()

parser.add_option('-v', "--version", action="store_true", default=False,
dest="version", help="Display version and author")
parser.add_option('-t', "--test", action="store_true", default=False,
dest="test", help="Perform self-test for dependencies.")
parser.add_option('-M', "--movielist", "--list", action="store_true", default=False,
dest="movielist",
help="Get Television Series List. Needs search key.")
parser.add_option('-D', "--moviedata", "--data", action="store_true", default=False,
dest="moviedata", help="Get TV Episode data. " \
"Needs inetref, season and episode. ")
parser.add_option('-C', "--collection", action="store_true", default=False,
dest="collectiondata",
help='Get a television Series (collection) "series" level information')
parser.add_option("-N", "--numbers", action="store_true", default=False, dest="numbers",
help="Get television Season and Episode numbers. " \
"Needs title and subtitle or inetref and subtitle.")
parser.add_option('-l', "--language", metavar="LANGUAGE", default=u'en',
dest="language", help="Specify language for filtering.")
parser.add_option('-a', "--area", metavar="COUNTRY", default=None,
dest="country", help="Specify country for custom data.")
parser.add_option('--debug', action="store_true", default=False,
dest="debug", help=("Disable caching and enable raw "
"data output."))

opts, args = parser.parse_args()

from MythTV.tmdb3.lookup import timeouthandler
signal.signal(signal.SIGALRM, timeouthandler)
signal.alarm(180)

if opts.test:
return performSelfTest()

from MythTV.tmdb3 import set_key, set_cache, set_locale
set_key('c27cb71cff5bd76e1a7a009380562c62')

if opts.debug:
import MythTV.tmdb3
MythTV.tmdb3.request.DEBUG = True
set_cache(engine='null')
else:
import os
confdir = os.environ.get('MYTHCONFDIR', '')
if (not confdir) or (confdir == '/'):
confdir = os.environ.get('HOME', '')
if (not confdir) or (confdir == '/'):
print ("Unable to find MythTV directory for metadata cache.")
return 1
confdir = os.path.join(confdir, '.mythtv')
cachedir = os.path.join(confdir, 'cache')
if not os.path.exists(cachedir):
os.makedirs(cachedir)
cachepath = os.path.join(cachedir, 'pytmdb3.cache')
set_cache(engine='file', filename=cachepath)

if opts.language:
set_locale(language=opts.language, fallthrough=True)
if opts.country:
set_locale(country=opts.country, fallthrough=True)

if (not opts.version):
if (len(args) < 1) or (args[0] == ''):
sys.stdout.write('ERROR: tmdb3.py requires at least one non-empty argument.\n')
return 1

if opts.moviedata and len(args) < 3:
sys.stdout.write('ERROR: tmdb3.py -D requires three non-empty arguments.\n')
return 1

from MythTV.tmdb3.lookup import buildVersion, buildTVList, \
buildEpisode, buildTVSeries, print_etree
try:
xml = None
if opts.version:
xml = buildVersion(showType, command)

elif opts.movielist:
xml = buildTVList(args[0], opts)

elif opts.numbers:
xml = buildEpisode(args[0:2], opts)

elif opts.moviedata:
xml = buildEpisode(args[0:3], opts)

elif opts.collectiondata:
xml = buildTVSeries(args[0], opts)

# if a number is returned, it is an error code return
if isinstance(xml,int):
return xml

if xml:
print_etree(xml)
else:
return 1

except RuntimeError as exc:
sys.stdout.write('ERROR: ' + str(exc) + ' exception')
import traceback
traceback.print_exc()
return 1

return 0

if __name__ == '__main__':
sys.exit(main("television",'tmdb3tv.py'))