# The Movie Database
# Multi-language support added by Aqntbghd
# 3.0 API update by ToMM

# apiary.io debugging URL
# BASE_URL = 'http://private-ad99a-themoviedb.apiary.io/3'

TMDB_BASE_URL = 'http://127.0.0.1:32400/services/tmdb?uri=%s'
TMDB_CONFIG = '/configuration'

# Movies
TMDB_MOVIE_SEARCH = '/search/movie?query=%s&year=%s&language=%s&include_adult=%s'
TMDB_MOVIE = '/movie/%s?append_to_response=releases,credits&language=%s'
TMDB_MOVIE_IMAGES = '/movie/%s/images'

# TV
TMDB_TV_SEARCH = '/search/tv?query=%s&year=%s&language=%s&include_adult=%s'
TMDB_TV = '/tv/%s?append_to_response=credits&language=%s'
TMDB_TV_SEASON = '/tv/%s/season/%s?language=%s'
TMDB_TV_EPISODE = '/tv/%s/season/%s/episode/%s?&append_to_response=credits,images&language=%s'
TMDB_TV_IMAGES = '/tv/%s/images'
TMDB_TV_EXTERNAL_IDS = '/tv/%s/external_ids'
TMDB_TV_TVDB = '/tv/find/%s?external_source=tvdb_id'

ARTWORK_ITEM_LIMIT = 15
POSTER_SCORE_RATIO = .3 # How much weight to give ratings vs. vote counts when picking best posters. 0 means use only ratings.
BACKDROP_SCORE_RATIO = .3
STILLS_SCORE_RATIO = .3
RE_IMDB_ID = Regex('^tt\d{7}$')

# TMDB does not seem to have an official set of supported languages.  Users can register and 'translate'
# any movie to any ISO 639-1 language.  The following is a realistic list from a popular title.
# This agent falls back to English metadata and sorts out foreign artwork to to ensure the best
# user experience when less common languages are chosen.
LANGUAGES = [
             Locale.Language.English, Locale.Language.Czech, Locale.Language.Danish, Locale.Language.German,
             Locale.Language.Greek, Locale.Language.Spanish, Locale.Language.Finnish, Locale.Language.French,
             Locale.Language.Hebrew, Locale.Language.Croatian, Locale.Language.Hungarian, Locale.Language.Italian,
             Locale.Language.Latvian, Locale.Language.Dutch, Locale.Language.Norwegian, Locale.Language.Polish,
             Locale.Language.Portuguese, Locale.Language.Russian, Locale.Language.Slovak, Locale.Language.Swedish,
             Locale.Language.Thai, Locale.Language.Turkish, Locale.Language.Vietnamese, Locale.Language.Chinese,
             Locale.Language.Korean
            ]

####################################################################################################
def Start():

  pass

####################################################################################################
@expose
def GetImdbId(tmdb_id, lang='en'):

  tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE % (tmdb_id, lang))

  if isinstance(tmdb_dict, dict) and 'imdb_id' in tmdb_dict and tmdb_dict['imdb_id'] and RE_IMDB_ID.search(tmdb_dict['imdb_id']):
    return tmdb_dict['imdb_id']

  return None

####################################################################################################
@expose
def GetTvdbId(tmdb_id):

  tmdb_dict = GetTMDBJSON(url=TMDB_TV_EXTERNAL_IDS % tmdb_id)

  if isinstance(tmdb_dict, dict) and 'tvdb_id' in tmdb_dict and tmdb_dict['tvdb_id'] and tmdb_dict['tvdb_id']:
    return str(tmdb_dict['tvdb_id'])

  return None

####################################################################################################
@expose
def GetTvRageId(tmdb_id):

  tmdb_dict = GetTMDBJSON(url=TMDB_TV_EXTERNAL_IDS % tmdb_id)

  if isinstance(tmdb_dict, dict) and 'tvrage_id' in tmdb_dict and tmdb_dict['tvrage_id'] and tmdb_dict['tvrage_id']:
    return str(tmdb_dict['tvrage_id'])

  return None

####################################################################################################
@expose
def GetTMDbSearchResults(id, name, year, lang, manual, get_imdb_id):

  media = FakeMediaObj(id, name, year)
  results = []
  PerformTMDbMovieSearch(results, media, lang, manual, get_imdb_id)

  return results if len(results) > 0 else None

####################################################################################################
@expose
def GetTMDbMetadata(id, lang):

  return PerformTMDbMovieUpdate(id, lang)

####################################################################################################
def GetTMDBJSON(url, cache_time=CACHE_1MONTH):

  tmdb_dict = None

  try:
    tmdb_dict = JSON.ObjectFromURL(TMDB_BASE_URL % String.Quote(url, True), sleep=2.0, headers={'Accept': 'application/json'}, cacheTime=cache_time)
  except:
    Log('Error fetching JSON from The Movie Database: %s' % (TMDB_BASE_URL % String.Quote(url, True)))

  return tmdb_dict

####################################################################################################
def AppendSearchResult(results, id, name=None, year=-1, score=0, lang=None):

  new_result = dict(id=str(id), name=name, year=int(year), score=score, lang=lang)

  if isinstance(results, list):

    results.append(new_result)

  else:

    results.Append(MetadataSearchResult(**new_result))

####################################################################################################
def DictToMovieMetadataObj(metadata_dict, metadata):

  try:
    if not metadata or not metadata.attrs:
      return
  except AttributeError:
    Log('WARNING: Framework not new enough to use One True Agent')  # TODO: add a more official log message about version number when available
    return

  for attr_name, attr_obj in metadata.attrs.iteritems():

    if attr_name not in metadata_dict:
      continue

    dict_value = metadata_dict[attr_name]

    if isinstance(dict_value, list):

      attr_obj.clear()
      for val in dict_value:
        attr_obj.add(val)

    elif isinstance(dict_value, dict):

      if attr_name in ['posters', 'art', 'themes']:  # Can't access MapObject, so have to write these out

        for k, v in dict_value.iteritems():
          if isinstance(v, tuple):
            try: attr_obj[k] = Proxy.Preview(HTTP.Request(v[0]).content, sort_order=v[1])
            except: pass
          else:
            attr_obj[k] = v

        attr_obj.validate_keys(dict_value.keys())

      else:
        for k, v in dict_value.iteritems():
          attr_obj[k] = v

    elif attr_name is 'originally_available_at':

      try: attr_obj.setcontent(Datetime.ParseDate(dict_value).date())
      except: pass

    else:
      attr_obj.setcontent(dict_value)

  # The following are special kind of objects
  if 'roles' in metadata_dict:
    metadata.roles.clear()
    for role in metadata_dict['roles']:
      meta_role = metadata.roles.new()
      if 'role' in role:
        meta_role.role = role['role']
      if 'name' in role:
        meta_role.name = role['name']
      if 'photo' in role:
        meta_role.photo = role['photo']

  if 'directors' in metadata_dict:
    metadata.directors.clear()
    for director in metadata_dict['directors']:
      meta_director = metadata.directors.new()
      if 'name' in director:
        meta_director.name = director['name']
      if 'photo' in director:
        meta_director.photo = director['photo']

  if 'writers' in metadata_dict:
    metadata.writers.clear()
    for writer in metadata_dict['writers']:
      meta_writer = metadata.writers.new()
      if 'name' in writer:
        meta_writer.name = writer['name']
      if 'photo' in writer:
        meta_writer.photo = writer['photo']

  if 'producers' in metadata_dict:
    metadata.producers.clear()
    for producer in metadata_dict['producers']:
      meta_producer = metadata.producers.new()
      if 'name' in producer:
        meta_producer.name = producer['name']
      if 'photo' in producer:
        meta_producer.photo = producer['photo']


####################################################################################################
def PerformTMDbMovieSearch(results, media, lang, manual, get_imdb_id=False):

  # If search is initiated by a different, primary metadata agent.
  # This requires the other agent to use the IMDb id as key.
  if media.primary_metadata is not None and RE_IMDB_ID.search(media.primary_metadata.id):
    AppendSearchResult(results=results, id=media.primary_metadata.id, score=100)

  else:
    # If this a manual search (Fix Incorrect Match) and we get an IMDb id as input.
    if manual and RE_IMDB_ID.search(media.name):
      tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE % (media.name, lang))

      if isinstance(tmdb_dict, dict) and 'id' in tmdb_dict:

        if get_imdb_id:
          if 'imdb_id' in tmdb_dict and RE_IMDB_ID.search(tmdb_dict['imdb_id']):
            id = str(tmdb_dict['imdb_id'])
          else:
            id = GetImdbId(tmdb_dict['id'], lang) or tmdb_dict['id']
        else:
          id = tmdb_dict['id']

        AppendSearchResult(results=results,
                           id=id,
                           name=tmdb_dict['title'],
                           year=int(tmdb_dict['release_date'].split('-')[0]),
                           score=100,
                           lang=lang)

    # If this is an automatic search and The Movie Database agent is used as a primary agent.
    else:
      if media.year and int(media.year) > 1900:
        year = media.year
      else:
        year = ''

      include_adult = 'false'
      if Prefs['adult']:
        include_adult = 'true'

      # Historically we've StrippedDiacritics() here, but this is a pretty aggressive function that won't pass
      # anything that can't be encoded to ASCII, and as such has a tendency to nuke whole titles in, e.g., Asian
      # languages (See GHI #26).  If we have a string that was modified by StripDiacritics() and we get no results,
      # try the search again with the original.
      #
      stripped_name = String.StripDiacritics(media.name)
      tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE_SEARCH % (String.Quote(stripped_name, True), year, lang, include_adult))
      if media.name != stripped_name and (tmdb_dict is None or len(tmdb_dict['results']) == 0):
        Log('No results for title modified by strip diacritics, searching again with the original: ' + media.name)
        tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE_SEARCH % (String.Quote(media.name, True), year, lang, include_adult))

      if isinstance(tmdb_dict, dict) and 'results' in tmdb_dict:

        for i, movie in enumerate(sorted(tmdb_dict['results'], key=lambda k: k['popularity'], reverse=True)):
          score = 90
          score = score - abs(String.LevenshteinDistance(movie['title'].lower(), media.name.lower()))

          # Adjust score slightly for 'popularity' (helpful for similar or identical titles when no media.year is present)
          score = score - (5 * i)

          if 'release_date' in movie and movie['release_date']:
            release_year = int(movie['release_date'].split('-')[0])
          else:
            release_year = -1

          if media.year and int(media.year) > 1900 and release_year:
            year_diff = abs(int(media.year) - release_year)

            if year_diff <= 1:
              score = score + 10
            else:
              score = score - (5 * year_diff)

          if score <= 0:
            continue
          else:

            if get_imdb_id:
              if 'imdb_id' in movie and RE_IMDB_ID.search(movie['imdb_id']):
                id = str(movie['imdb_id'])
              else:
                id = GetImdbId(movie['id'], lang) or movie['id']
            else:
              id = movie['id']

            AppendSearchResult(results=results,
                               id=id,
                               name=movie['title'],
                               year=release_year,
                               score=score,
                               lang=lang)

####################################################################################################
def PerformTMDbMovieUpdate(metadata_id, lang, existing_metadata):

  metadata = dict(id=metadata_id)

  config_dict = GetTMDBJSON(url=TMDB_CONFIG, cache_time=CACHE_1WEEK * 2)
  if config_dict is None or 'images' not in config_dict or 'base_url' not in config_dict['images']:
    config_dict = dict(images=dict(base_url=''))

  tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE % (metadata_id, lang))

  if not isinstance(tmdb_dict, dict) or 'overview' not in tmdb_dict or tmdb_dict['overview'] in (None, ''):
    # Retry the query with no language specified if we didn't get anything from the initial request.
    tmdb_dict = GetTMDBJSON(url=TMDB_MOVIE % (metadata_id, ''))

  # This additional request is necessary since full art/poster lists are not returned if they don't exactly match the language
  tmdb_images_dict = GetTMDBJSON(url=TMDB_MOVIE_IMAGES % metadata_id)

  if not isinstance(tmdb_dict, dict) or not isinstance(tmdb_images_dict, dict):
    return None

  # Rating.
  votes = tmdb_dict.get('vote_count') or 0
  rating = tmdb_dict.get('vote_average') or 0.0
  if votes > 3:
    metadata['rating'] = rating
    metadata['audience_rating'] = 0.0
    metadata['rating_image'] = None
    metadata['audience_rating_image'] = None

  # Title of the film.
  metadata['title'] = tmdb_dict.get('title')

  if 'original_title' in tmdb_dict and tmdb_dict['original_title'] != metadata['title']:
    metadata['original_title'] = tmdb_dict['original_title']

  # Tagline.
  metadata['tagline'] = tmdb_dict.get('tagline')

  # Release date.
  try:
    metadata['originally_available_at'] = tmdb_dict['release_date']
    metadata['year'] = Datetime.ParseDate(tmdb_dict['release_date']).date().year
  except:
    pass

  if Prefs['country'] != '':
    c = Prefs['country']

    try:
      for country in tmdb_dict['releases']['countries']:
        if country['iso_3166_1'] == Locale.CountryCodes.MatchToCode(c):

          # Content rating.
          if 'certification' in country and country['certification'] != '':
            if Locale.CountryCodes.MatchToCode(c) == 'US':
              metadata['content_rating'] = country['certification']
            else:
              metadata['content_rating'] = '%s/%s' % (Locale.CountryCodes.MatchToCode(c).lower(), country['certification'])

          # Release date (country specific).
          try:
            metadata['originally_available_at'] = country['release_date']
            metadata['year'] = Datetime.ParseDate(country['release_date']).date().year
          except:
            pass

          break
    except: pass

  # Summary.
  metadata['summary'] = tmdb_dict['overview']
  if metadata['summary'] == 'No overview found.':
    metadata['summary'] = ""

  # Runtime.
  try: metadata['duration'] = int(tmdb_dict['runtime']) * 60 * 1000
  except: pass

  # Genres.
  metadata['genres'] = []
  for genre in (tmdb_dict.get('genres') or list()):
    metadata['genres'].append(genre.get('name', '').strip())

  # Collections.
  metadata['collections'] = []
  if Prefs['collections'] and isinstance(tmdb_dict.get('belongs_to_collection', None), dict):
    metadata['collections'].append((tmdb_dict['belongs_to_collection'].get('name') or '').replace(' Collection',''))

  # Studio.
  if 'production_companies' in tmdb_dict and len(tmdb_dict['production_companies']) > 0:
    try: index = tmdb_dict['production_companies'][0]['id']
    except: index = ''  # All numbers are less than an empty string
    company = None

    for studio in tmdb_dict['production_companies']:
      if (studio.get('id') or '') <= index:  # All numbers are less than an empty string
        index = studio['id']
        company = studio['name'].strip()

    metadata['studio'] = company

  else:
    metadata['studio'] = None

  # Country.
  metadata['countries'] = []
  if 'production_countries' in tmdb_dict:
    for country in tmdb_dict['production_countries']:
      country = country['name'].replace('United States of America', 'USA')
      metadata['countries'].append(country)

  # Crew.
  metadata['directors'] = list()
  metadata['writers'] = list()
  metadata['producers'] = list()

  try:
    for member in tmdb_dict['credits']['crew']:
      try:
        if member['job'] == 'Director':
          director = dict()
          director['name'] = member['name']
          if member['profile_path'] is not None:
            director['photo'] = config_dict['images']['base_url'] + 'original' + member['profile_path']
          metadata['directors'].append(director)
        elif member['job'] in ('Writer', 'Screenplay', 'Author'):
          writer = dict()
          writer['name'] = member['name']
          if member['profile_path'] is not None:
            writer['photo'] = config_dict['images']['base_url'] + 'original' + member['profile_path']
          metadata['writers'].append(writer)
        elif member['job'] == 'Producer':
          producer = dict()
          producer['name'] = member['name']
          if member['profile_path'] is not None:
            producer['photo'] = config_dict['images']['base_url'] + 'original' + member['profile_path']
          metadata['producers'].append(producer)
      except: pass
  except: pass

  # Cast.
  metadata['roles'] = list()

  try:
    for member in sorted((tmdb_dict.get('credits') or dict()).get('cast', list()), key=lambda k: k['order']):
      try:
        role = dict()
        role['role'] = member['character']
        role['name'] = member['name']
        if member['profile_path'] is not None:
          role['photo'] = config_dict['images']['base_url'] + 'original' + member['profile_path']
        metadata['roles'].append(role)
      except:pass
  except: pass
  # Note: for TMDB artwork, number of votes is a good predictor of poster quality. Ratings are assigned
  # using a Baysean average that appears to be poorly calibrated, so ratings are almost always between
  # 5 and 6 or zero.  Consider both of these, weighting them according to the POSTER_SCORE_RATIO.

  # No votes get zero, use TMDB's apparent initial Baysean prior mean of 5 instead.
  valid_names = list()

  metadata['posters'] = dict()

  if tmdb_images_dict['posters']:
    try:
      max_average = max([(lambda p: p['vote_average'] or 5)(p) for p in tmdb_images_dict['posters']])
      max_count = max([(lambda p: p['vote_count'])(p) for p in tmdb_images_dict['posters']]) or 1

      for i, poster in enumerate(tmdb_images_dict['posters']):
        try:
          score = (poster['vote_average'] / max_average) * POSTER_SCORE_RATIO
          score += (poster['vote_count'] / max_count) * (1 - POSTER_SCORE_RATIO)
          tmdb_images_dict['posters'][i]['score'] = score

          # Boost the score for localized posters (according to the preference).
          if Prefs['localart']:
            if poster['iso_639_1'] == lang:
              tmdb_images_dict['posters'][i]['score'] = poster['score'] + 1

          # Discount score for foreign posters.
          if poster['iso_639_1'] != lang and poster['iso_639_1'] is not None and poster['iso_639_1'] != 'en':
            tmdb_images_dict['posters'][i]['score'] = poster['score'] - 1
        except: pass

      for i, poster in enumerate(sorted(tmdb_images_dict['posters'], key=lambda k: k['score'], reverse=True)):
        try:
          if i > ARTWORK_ITEM_LIMIT:
            break
          else:
            poster_url = config_dict['images']['base_url'] + 'original' + poster['file_path']
            thumb_url = config_dict['images']['base_url'] + 'w154' + poster['file_path']
            valid_names.append(poster_url)

            if poster_url not in existing_metadata.posters:
              try: metadata['posters'][poster_url] = (thumb_url, i+1)
              except: pass
        except: pass
    except: pass

  # Backdrops.
  valid_names = list()
  metadata['art'] = {}
  if tmdb_images_dict['backdrops']:
    try:
      max_average = max([(lambda p: p['vote_average'] or 5)(p) for p in tmdb_images_dict['backdrops']])
      max_count = max([(lambda p: p['vote_count'])(p) for p in tmdb_images_dict['backdrops']]) or 1

      for i, backdrop in enumerate(tmdb_images_dict['backdrops']):

        try:
          score = (backdrop['vote_average'] / max_average) * BACKDROP_SCORE_RATIO
          score += (backdrop['vote_count'] / max_count) * (1 - BACKDROP_SCORE_RATIO)
          tmdb_images_dict['backdrops'][i]['score'] = score

          # For backdrops, we prefer "No Language" since they're intended to sit behind text.
          if backdrop['iso_639_1'] == 'xx' or backdrop['iso_639_1'] == 'none':
            tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) + 2

          # Boost the score for localized art (according to the preference).
          if Prefs['localart']:
            if backdrop['iso_639_1'] == lang:
              tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) + 1

          # Discount score for foreign art.
          if backdrop['iso_639_1'] != lang and backdrop['iso_639_1'] is not None and backdrop['iso_639_1'] != 'en':
            tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) - 1
        except: pass

      for i, backdrop in enumerate(sorted(tmdb_images_dict['backdrops'], key=lambda k: k['score'], reverse=True)):
        try:
          if i > ARTWORK_ITEM_LIMIT:
            break
          else:
            backdrop_url = config_dict['images']['base_url'] + 'original' + backdrop['file_path']
            thumb_url = config_dict['images']['base_url'] + 'w300' + backdrop['file_path']
            valid_names.append(backdrop_url)

            if backdrop_url not in existing_metadata.art:
              try: metadata['art'][backdrop_url] = (thumb_url, i+1)
              except: pass
        except: pass
    except: pass

  return metadata

####################################################################################################
class TMDbAgent(Agent.Movies):

  name = 'The Movie Database'
  languages = LANGUAGES
  primary_provider = True
  accepts_from = ['com.plexapp.agents.localmedia']

  def search(self, results, media, lang, manual):

    PerformTMDbMovieSearch(results, media, lang, manual)

  def update(self, metadata, media, lang):

    metadata_dict = PerformTMDbMovieUpdate(metadata.id, lang, metadata)

    if metadata_dict is None:
      Log('TMDb was unable to get any metadata for %s (lang = %s)' % (metadata.id, lang))
      return

    DictToMovieMetadataObj(metadata_dict, metadata)

####################################################################################################
class TMDbAgent(Agent.TV_Shows):

  name = 'The Movie Database'
  languages = LANGUAGES
  primary_provider = True
  accepts_from = ['com.plexapp.agents.localmedia', 'com.plexapp.agents.thetvdb']
  contributes_to = ['com.plexapp.agents.thetvdb']

  def search(self, results, media, lang, manual):

    # If TMDB is used as a secondary agent for TVDB, find the TMDB id
    if media.primary_agent == 'com.plexapp.agents.thetvdb':
      tmdb_dict = GetTMDBJSON(url=TMDB_TV_TVDB % (media.primary_metadata.id))

      if isinstance(tmdb_dict, dict) and 'tv_results' in tmdb_dict and len(tmdb_dict['tv_results']) > 0:
        tmdb_id = tmdb_dict['tv_results'][0]['id']

        results.Append(MetadataSearchResult(
          id = str(tmdb_id),
          score = 100
        ))

      return

    if media.year and int(media.year) > 1900:
      year = media.year
    else:
      year = ''

    include_adult = 'false'
    if Prefs['adult']:
      include_adult = 'true'

    media_show = media.show
    if not manual:
      filename = String.Unquote(media.filename)
      if (' (US)' in filename or ' (UK)' in filename) and (media.show.endswith(' Us') or media.show.endswith(' Uk')):
        media_show = media.show.rsplit(' ', 1)[0]

    # StrippedDiacritics() is a pretty aggressive function that won't pass
    # anything that can't be encoded to ASCII, and as such has a tendency to nuke whole titles in, e.g., Asian
    # languages (See GHI #26).  If we have a string that was modified by StripDiacritics() and we get no results,
    # try the search again with the original.
    #
    stripped_name = String.StripDiacritics(media_show)
    tmdb_dict = GetTMDBJSON(url=TMDB_TV_SEARCH % (String.Quote(stripped_name, True), year, lang, include_adult))

    if media_show != stripped_name and (tmdb_dict == None or len(tmdb_dict['results']) == 0):
      Log('No results for title modified by strip diacritics, searching again with the original: ' + media_show)
      tmdb_dict = GetTMDBJSON(url=TMDB_TV_SEARCH % (String.Quote(media_show, True), year, lang, include_adult))

    if isinstance(tmdb_dict, dict) and 'results' in tmdb_dict:
      for i, show in enumerate(sorted(tmdb_dict['results'], key=lambda k: k['popularity'], reverse=True)):
        score = 90
        score = score - abs(String.LevenshteinDistance(show['name'].lower(), media_show.lower()))

        # Adjust score slightly for 'popularity' (helpful for similar or identical titles when no media.year is present)
        #score = score - (5 * i)

        if 'first_air_date' in show and show['first_air_date']:
          release_year = int(show['first_air_date'].split('-')[0])
          score = score + 5
        else:
          release_year = None
          score = score - 5

        if media.year and int(media.year) > 1900 and release_year:
          year_diff = abs(int(media.year) - release_year)

          if year_diff == 0:
            score = score + 10
          else:
            score = score - (5 * year_diff)

        if score <= 0:
          continue
        else:
          results.Append(MetadataSearchResult(
            id = str(show['id']),
            name = show['name'],
            year = release_year,
            score = score,
            lang = lang
          ))

  def update(self, metadata, media, lang):

    config_dict = GetTMDBJSON(url=TMDB_CONFIG, cache_time=CACHE_1WEEK * 2)
    if config_dict is None or 'images' not in config_dict or 'base_url' not in config_dict['images']:
      config_dict = dict(images=dict(base_url=''))

    tmdb_dict = GetTMDBJSON(url=TMDB_TV % (metadata.id, lang))

    if not isinstance(tmdb_dict, dict) or 'overview' not in tmdb_dict or tmdb_dict['overview'] is None or tmdb_dict['overview'] == "":
      # Retry the query with no language specified if we didn't get anything from the initial request.
      tmdb_dict = GetTMDBJSON(url=TMDB_TV % (metadata.id, ''))

    # This additional request is necessary since full art/poster lists are not returned if they don't exactly match the language
    tmdb_images_dict = GetTMDBJSON(url=TMDB_TV_IMAGES % metadata.id)

    if not isinstance(tmdb_dict, dict) or not isinstance(tmdb_images_dict, dict):
      return None

    # Rating.
    votes = tmdb_dict['vote_count']
    rating = tmdb_dict['vote_average']
    if votes > 3:
      metadata.rating = rating
      metadata.audience_rating = 0.0

    # Name of the show.
    metadata.title = tmdb_dict['name']

    # First air date.
    try:
      metadata.originally_available_at = Datetime.ParseDate(tmdb_dict['first_air_date']).date()
      metadata.year = metadata.originally_available_at.year
    except:
      pass

    # Summary.
    metadata.summary = tmdb_dict['overview']
    if metadata.summary == 'No overview found.':
      metadata.summary = ""

    # Runtime.
    try: metadata.duration = int(tmdb_dict['episode_run_time'][0]) * 60 * 1000
    except: pass

    # Genres.
    metadata.genres.clear()
    for genre in tmdb_dict['genres']:
      metadata.genres.add(genre['name'].strip())

    # Studio.
    if 'production_companies' in tmdb_dict and len(tmdb_dict['production_companies']) > 0:
      index = tmdb_dict['production_companies'][0]['id']

      for studio in tmdb_dict['production_companies']:
        if studio['id'] <= index:
          index = studio['id']
          company = studio['name'].strip()

      metadata.studio = company

    else:
      metadata.studio = None

    # Country.
    metadata.countries.clear()
    if 'origin_country' in tmdb_dict:
      for country in tmdb_dict['origin_country']:
        if len(country) == 2 and Locale.CountryCodes.MatchToCountry(country):
          country = Locale.CountryCodes.MatchToCountry(country)
        else:
          continue

        country = country.replace('United States of America', 'USA')
        metadata.countries.add(country)

    # Cast.
    metadata.roles.clear()

    for member in sorted(tmdb_dict['credits']['cast'], key=lambda k: k['order']):
      role = metadata.roles.new()
      role.role = member['character']
      role.name = member['name']
      if member['profile_path'] is not None:
        role.photo = config_dict['images']['base_url'] + 'original' + member['profile_path']

    # Note: for TMDB artwork, number of votes is a good predictor of poster quality. Ratings are assigned
    # using a Baysean average that appears to be poorly calibrated, so ratings are almost always between
    # 5 and 6 or zero.  Consider both of these, weighting them according to the POSTER_SCORE_RATIO.

    # No votes get zero, use TMDB's apparent initial Baysean prior mean of 5 instead.
    valid_names = list()

    if tmdb_images_dict['posters']:
      max_average = max([(lambda p: p['vote_average'] or 5)(p) for p in tmdb_images_dict['posters']])
      max_count = max([(lambda p: p['vote_count'])(p) for p in tmdb_images_dict['posters']]) or 1

      for i, poster in enumerate(tmdb_images_dict['posters']):

        score = (poster['vote_average'] / max_average) * POSTER_SCORE_RATIO
        score += (poster['vote_count'] / max_count) * (1 - POSTER_SCORE_RATIO)
        tmdb_images_dict['posters'][i]['score'] = score

        # Boost the score for localized posters (according to the preference).
        if Prefs['localart']:
          if poster['iso_639_1'] == lang:
            tmdb_images_dict['posters'][i]['score'] = poster['score'] + 1

        # Discount score for foreign posters.
        if poster['iso_639_1'] != lang and poster['iso_639_1'] is not None and poster['iso_639_1'] != 'en':
          tmdb_images_dict['posters'][i]['score'] = poster['score'] - 1

      for i, poster in enumerate(sorted(tmdb_images_dict['posters'], key=lambda k: k['score'], reverse=True)):
        if i > ARTWORK_ITEM_LIMIT:
          break
        else:
          poster_url = config_dict['images']['base_url'] + 'original' + poster['file_path']
          thumb_url = config_dict['images']['base_url'] + 'w154' + poster['file_path']
          valid_names.append(poster_url)

          if poster_url not in metadata.posters:
            try: metadata.posters[poster_url] = Proxy.Preview(HTTP.Request(thumb_url).content, sort_order=i+1)
            except: pass

    metadata.posters.validate_keys(valid_names)

    # Backdrops.
    valid_names = list()

    if tmdb_images_dict['backdrops']:
      max_average = max([(lambda p: p['vote_average'] or 5)(p) for p in tmdb_images_dict['backdrops']])
      max_count = max([(lambda p: p['vote_count'])(p) for p in tmdb_images_dict['backdrops']]) or 1

      for i, backdrop in enumerate(tmdb_images_dict['backdrops']):

        score = (backdrop['vote_average'] / max_average) * BACKDROP_SCORE_RATIO
        score += (backdrop['vote_count'] / max_count) * (1 - BACKDROP_SCORE_RATIO)
        tmdb_images_dict['backdrops'][i]['score'] = score

        # For backdrops, we prefer "No Language" since they're intended to sit behind text.
        if backdrop['iso_639_1'] == 'xx' or backdrop['iso_639_1'] == 'none':
          tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) + 2

        # Boost the score for localized art (according to the preference).
        if Prefs['localart']:
          if backdrop['iso_639_1'] == lang:
            tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) + 1

        # Discount score for foreign art.
        if backdrop['iso_639_1'] != lang and backdrop['iso_639_1'] is not None and backdrop['iso_639_1'] != 'en':
          tmdb_images_dict['backdrops'][i]['score'] = float(backdrop['score']) - 1

      for i, backdrop in enumerate(sorted(tmdb_images_dict['backdrops'], key=lambda k: k['score'], reverse=True)):
        if i > ARTWORK_ITEM_LIMIT:
          break
        else:
          backdrop_url = config_dict['images']['base_url'] + 'original' + backdrop['file_path']
          thumb_url = config_dict['images']['base_url'] + 'w300' + backdrop['file_path']
          valid_names.append(backdrop_url)

          if backdrop_url not in metadata.art:
            try: metadata.art[backdrop_url] = Proxy.Preview(HTTP.Request(thumb_url).content, sort_order=i+1)
            except: pass

    metadata.art.validate_keys(valid_names)

    # Get episode data.
    @parallelize
    def UpdateEpisodes():

      # Loop over seasons.
      for s in media.seasons:
        season = metadata.seasons[s]

        # Set season metadata.
        @task
        def UpdateSeason(season=season, s=s):

          tmdb_season_dict = GetTMDBJSON(url=TMDB_TV_SEASON % (metadata.id, s, lang))

          if tmdb_season_dict is None:
            return None

          season.summary = tmdb_season_dict['overview']

          # Season poster.
          valid_names = list()

          if 'poster_path' in tmdb_season_dict and tmdb_season_dict['poster_path']:
            poster_url = config_dict['images']['base_url'] + 'original' + tmdb_season_dict['poster_path']
            thumb_url = config_dict['images']['base_url'] + 'w154' + tmdb_season_dict['poster_path']
            valid_names.append(poster_url)

            if poster_url not in season.posters:
              try:
                season.posters[poster_url] = Proxy.Preview(HTTP.Request(thumb_url).content)
              except:
                pass

          season.posters.validate_keys(valid_names)

        # Loop over episodes in a season.
        for e in media.seasons[s].episodes:
          episode = metadata.seasons[s].episodes[e]

          # Set episode metadata.
          @task
          def UpdateEpisode(episode=episode, s=s, e=e):

            tmdb_episode_dict = GetTMDBJSON(url=TMDB_TV_EPISODE % (metadata.id, s, e, lang))

            if not isinstance(tmdb_episode_dict, dict):
              return None

            # Rating.
            votes = tmdb_episode_dict['vote_count']
            rating = tmdb_episode_dict['vote_average']
            if votes > 3:
              episode.rating = rating

            # Name of the episode.
            episode.title = tmdb_episode_dict['name']

            # Summary.
            episode.summary = tmdb_episode_dict['overview']

            # Air date.
            try:
              episode.originally_available_at = Datetime.ParseDate(tmdb_episode_dict['air_date']).date()
            except:
              pass

            # Crew.
            episode.directors.clear()
            episode.writers.clear()
            episode.producers.clear()

            if 'crew' in tmdb_episode_dict['credits']:

              for member in tmdb_episode_dict['credits']['crew']:
                if member['job'] == 'Director':
                  director = episode.directors.new()
                  director.name = member['name']
                  if member['profile_path'] is not None:
                    director.photo = config_dict['images']['base_url'] + 'original' + member['profile_path']
                elif member['job'] in ('Writer', 'Screenplay'):
                  writer = episode.writers.new()
                  writer.name = member['name']
                  if member['profile_path'] is not None:
                    writer.photo = config_dict['images']['base_url'] + 'original' + member['profile_path']
                elif member['job'] == 'Producer':
                  producer = episode.producers.new()
                  producer.name = member['name']
                  if member['profile_path'] is not None:
                    producer.photo = config_dict['images']['base_url'] + 'original' + member['profile_path']

            # TODO:
            # - Actors per episode?
            # - Guest stars?

            # Episode still.
            valid_names = list()

            # Check if there are stills present and then loop through them
            if tmdb_episode_dict['images']['stills']:
              max_average = max([(lambda p: p['vote_average'] or 5)(p) for p in tmdb_episode_dict['images']['stills']])
              max_count = max([(lambda p: p['vote_count'])(p) for p in tmdb_episode_dict['images']['stills']]) or 1

              for i, still in enumerate(tmdb_episode_dict['images']['stills']):

                score = (still['vote_average'] / max_average) * STILLS_SCORE_RATIO
                score += (still['vote_count'] / max_count) * (1 - STILLS_SCORE_RATIO)
                tmdb_episode_dict['images']['stills'][i]['score'] = score

              for i, still in enumerate(sorted(tmdb_episode_dict['images']['stills'], key=lambda k: k['score'], reverse=True)):
                if i > ARTWORK_ITEM_LIMIT:
                  break
                else:
                  still_url = config_dict['images']['base_url'] + 'original' + still['file_path']
                  thumb_url = config_dict['images']['base_url'] + 'w154' + still['file_path']
                  valid_names.append(still_url)

                  if still_url not in episode.thumbs:
                    try: episode.thumbs[still_url] = Proxy.Preview(HTTP.Request(thumb_url).content, sort_order=i+1)
                    except: pass

            episode.thumbs.validate_keys(valid_names)

####################################################################################################
class FakeMediaObj():

  def __init__(self, id, name, year):
    self.name = name
    self.year = year
    self.primary_metadata = FakePrimaryMetadataObj(id)

####################################################################################################
class FakePrimaryMetadataObj():

  def __init__(self, id):
    self.id = id