601 lines
22 KiB
Python
601 lines
22 KiB
Python
#!/usr/bin/python
|
|
# coding: utf-8
|
|
|
|
########################
|
|
|
|
from __future__ import division
|
|
|
|
from resources.lib.helper import *
|
|
from resources.lib.functions import *
|
|
from resources.lib.database import *
|
|
from resources.lib.nfo_updater import *
|
|
|
|
########################
|
|
|
|
RUN_IN_BACKGROUND = ADDON.getSettingBool('update_background')
|
|
BUSYDIALOG = False if RUN_IN_BACKGROUND else True
|
|
OMDB_FALLBACK = ADDON.getSettingBool('omdb_fallback_search')
|
|
OMDB_API = ADDON.getSetting('omdb_api_key')
|
|
COUNTRY_CODE = ADDON.getSetting('country_code')
|
|
SKIP_MPAA = ADDON.getSettingBool('mpaa_skip')
|
|
SKIP_NOT_RATED = ADDON.getSettingBool('mpaa_skip_nr')
|
|
MPAA_FALLBACK = ADDON.getSettingBool('mpaa_fallback')
|
|
TMDB_LANGUAGE = ADDON.getSetting('tmdb_language')
|
|
RATING_DEBUG = ADDON.getSetting('debug_rating_updater')
|
|
|
|
########################
|
|
|
|
def update_ratings(dbid=None,dbtype=None,content=None):
|
|
# no omdb API key message
|
|
if not OMDB_API:
|
|
if not DIALOG.yesno(xbmc.getLocalizedString(14117), ADDON.getLocalizedString(32035)):
|
|
return
|
|
|
|
winprop('UpdatingRatings.bool', True)
|
|
msg_text = xbmc.getLocalizedString(19256)
|
|
|
|
# get database ids
|
|
if isinstance(dbtype, str):
|
|
dbtype = dbtype.split('+')
|
|
|
|
with busy_dialog(force=BUSYDIALOG):
|
|
db = Database(dbid=dbid, append=['episodes'])
|
|
for i in dbtype:
|
|
getattr(db, i)()
|
|
result = db.result()
|
|
|
|
# calc total items to process
|
|
total_items = 0
|
|
for i in result:
|
|
if result.get(i):
|
|
total_items = total_items + len(result[i])
|
|
|
|
if total_items > 1:
|
|
# show progress if 1< will be processed
|
|
progressdialog = ProgressDialog(total_items)
|
|
|
|
for i in result:
|
|
if i == 'movie':
|
|
cat = xbmc.getLocalizedString(20338)
|
|
elif i == 'tvshow':
|
|
cat = xbmc.getLocalizedString(20364)
|
|
elif i == 'episode':
|
|
cat = xbmc.getLocalizedString(20359)
|
|
|
|
for item in result[i]:
|
|
if progressdialog.canceled():
|
|
break
|
|
|
|
if item.get('showtitle') and item.get('label'):
|
|
label = item.get('showtitle') + ' - ' + item.get('label')
|
|
else:
|
|
label = item.get('title')
|
|
|
|
if item.get('year'):
|
|
label = label + ' (' + str(item.get('year')) + ')'
|
|
|
|
progressdialog.update(cat, label)
|
|
|
|
UpdateRating({'dbid': item.get('%sid' % i),
|
|
'type': i})
|
|
#xbmc.sleep(50)
|
|
|
|
if progressdialog.canceled():
|
|
msg_text = ADDON.getLocalizedString(32042)
|
|
break
|
|
|
|
progressdialog.close()
|
|
|
|
elif total_items == 1:
|
|
# process single item
|
|
for i in result:
|
|
UpdateRating({'dbid': result[i][0].get('%sid' % i),
|
|
'type': i})
|
|
|
|
else:
|
|
# error message
|
|
msg_text = ADDON.getLocalizedString(32048)
|
|
|
|
winprop('UpdatingRatings', clear=True)
|
|
notification(ADDON.getLocalizedString(32030), msg_text)
|
|
|
|
|
|
class ProgressDialog(object):
|
|
def __init__(self,total_items):
|
|
if RUN_IN_BACKGROUND:
|
|
self.progressdialog = xbmcgui.DialogProgressBG()
|
|
else:
|
|
self.progressdialog = xbmcgui.DialogProgress()
|
|
|
|
self.progressdialog.create('Updating', '')
|
|
self.total_items = total_items
|
|
self.processed_items = 0
|
|
self.progress = 0
|
|
|
|
def canceled(self):
|
|
if RUN_IN_BACKGROUND:
|
|
return True if winprop('CancelRatingUpdater.bool') else False
|
|
else:
|
|
return True if self.progressdialog.iscanceled() or winprop('CancelRatingUpdater.bool') else False
|
|
|
|
def update(self,cat,label):
|
|
self.processed_items += 1
|
|
progress = int(100 / self.total_items * self.processed_items)
|
|
processed = str(self.processed_items) + ' / ' + str(self.total_items)
|
|
|
|
if RUN_IN_BACKGROUND:
|
|
self.progressdialog.update(progress, processed, cat + ':' + label)
|
|
else:
|
|
self.progressdialog.update(progress, cat + ':[CR]' + label + '[CR]' + processed)
|
|
|
|
def close(self):
|
|
self.progressdialog.close()
|
|
self.progressdialog = None
|
|
winprop('CancelRatingUpdater', clear=True)
|
|
|
|
|
|
class UpdateRating(object):
|
|
def __init__(self,params):
|
|
self.dbid = params.get('dbid')
|
|
self.dbtype = params.get('type')
|
|
self.tmdb_type = 'movie' if self.dbtype == 'movie' else 'tv'
|
|
self.tmdb_tv_status = None
|
|
self.tmdb_mpaa = None
|
|
self.tmdb_mpaa_fallback = None
|
|
self.tmdb_rating = None
|
|
self.imdb_rating = None
|
|
self.omdb_limit = False
|
|
self.update_uniqueid = False
|
|
self.episodeguide = None
|
|
|
|
# collect db data
|
|
self.db = Database(dbid=self.dbid, dbtype=self.dbtype)
|
|
self.get_details()
|
|
|
|
self.uniqueid = self.details.get('uniqueid', {})
|
|
self.ratings = self.details.get('ratings', {})
|
|
self.file = self.details.get('file')
|
|
self.year = self.details.get('year')
|
|
self.premiered = self.details.get('premiered') or self.details.get('firstaired')
|
|
self.title = self.details.get('title')
|
|
self.original_title = self.details.get('originaltitle') or self.title
|
|
self.tags = self.details.get('tag')
|
|
|
|
if any(string in self.details.get('episodeguide', '') for string in ['tvdb', 'tmdb']):
|
|
self.episodeguide = self.details.get('episodeguide')
|
|
else:
|
|
self.episodeguide = None
|
|
|
|
if self.uniqueid:
|
|
self.run()
|
|
|
|
def get_details(self):
|
|
getattr(self.db, self.dbtype)()
|
|
self.details = self.db.result().get(self.dbtype)[0]
|
|
|
|
def run(self):
|
|
log('Run rating updater - %s: %s - ID: %s' % (self.dbtype, self.title, str(self.dbid)), force=RATING_DEBUG)
|
|
self.imdb = self.uniqueid.get('imdb')
|
|
self.tmdb = self.uniqueid.get('tmdb')
|
|
self.tvdb = self.uniqueid.get('tvdb')
|
|
|
|
# don't proceed for episodes if no IMDb is available
|
|
if self.dbtype == 'episode' and not self.imdb:
|
|
log('Episode with no IMDb. Skip.', force=RATING_DEBUG)
|
|
return
|
|
|
|
# get the default used rating
|
|
self.default_rating = None
|
|
for rating in self.ratings:
|
|
if self.ratings[rating].get('default'):
|
|
self.default_rating = rating
|
|
break
|
|
|
|
if self.dbtype != 'episode':
|
|
# get TMDb ID (if not available) by using the ID of IMDb or TVDb
|
|
if not self.tmdb and self.imdb:
|
|
log('No TMDb. Try to get by IMDb ID %s' % self.imdb, force=RATING_DEBUG)
|
|
self.get_tmdb_externalid(self.imdb)
|
|
|
|
elif not self.tmdb and self.tvdb:
|
|
log('No TMDb. Try to get by TVDb ID %s' % str(self.tvdb), force=RATING_DEBUG)
|
|
self.get_tmdb_externalid(self.tvdb)
|
|
|
|
# get TMDb rating and IMDb number if not available
|
|
if self.tmdb:
|
|
log('Fetch data by TMDb ID %s' % str(self.tmdb), force=RATING_DEBUG)
|
|
self.get_tmdb()
|
|
|
|
# get Rotten, Metacritic and IMDb ratings of OMDb
|
|
if not self.omdb_limit:
|
|
log('Fetch OMDb data', force=RATING_DEBUG)
|
|
self.get_omdb()
|
|
|
|
# if no TMDb ID was known before but OMDb return the IMDb ID -> try to get TMDb data again
|
|
if self.dbtype != 'episode' and not self.tmdb and self.imdb:
|
|
log('Try to get TMDb ID by returned IMDb ID %s of OMDb' % self.imdb, force=RATING_DEBUG)
|
|
self.get_tmdb_externalid(self.imdb)
|
|
|
|
if self.tmdb:
|
|
log('Fetch data by TMDb ID %s' % str(self.tmdb), force=RATING_DEBUG)
|
|
self.get_tmdb()
|
|
|
|
# emby <ratings> and <votes>
|
|
if 'default' in self.ratings:
|
|
self.emby_ratings()
|
|
|
|
# update db + nfo
|
|
log('Updating info', force=RATING_DEBUG)
|
|
self.update_info()
|
|
|
|
def emby_ratings(self):
|
|
# Emby For Kodi is storing the rating as 'default'
|
|
if self.imdb_rating:
|
|
self._update_ratings_dict(key='default',
|
|
rating=float(self.imdb_rating),
|
|
votes=int(self.imdb_votes)
|
|
)
|
|
|
|
elif self.tmdb_rating:
|
|
self._update_ratings_dict(key='default',
|
|
rating=float(self.tmdb_rating),
|
|
votes=int(self.tmdb_votes)
|
|
)
|
|
|
|
def get_tmdb(self):
|
|
result = self._tmdb(action=self.tmdb_type,
|
|
call=str(self.tmdb),
|
|
params={'append_to_response': 'release_dates,content_ratings,external_ids'}
|
|
)
|
|
|
|
if not result:
|
|
return
|
|
|
|
self.tmdb_rating = result.get('vote_average')
|
|
self.tmdb_votes = result.get('vote_count')
|
|
|
|
if not self.original_title:
|
|
self.original_title = result.get('original_title') or resultresult.get('original_name')
|
|
|
|
# update original title if missing
|
|
if self.original_title:
|
|
self._set_value('originaltitle', self.original_title)
|
|
|
|
if self.tmdb_type == 'tv':
|
|
premiered = result.get('first_air_date')
|
|
self.tmdb_tv_status = result.get('status')
|
|
|
|
# update TV status as well
|
|
if self.tmdb_tv_status:
|
|
self._set_value('status', self.tmdb_tv_status)
|
|
|
|
else:
|
|
premiered = result.get('release_date')
|
|
|
|
# update the year if not correct
|
|
if premiered and self.premiered != premiered:
|
|
self.year = premiered[:4]
|
|
|
|
if ADDON.getSettingBool('update_premiered') or not self.premiered:
|
|
self._set_value('premiered', premiered)
|
|
|
|
if self.tmdb_rating:
|
|
self._update_ratings_dict(key='themoviedb',
|
|
rating=self.tmdb_rating,
|
|
votes=self.tmdb_votes
|
|
)
|
|
|
|
# set MPAA based on setting
|
|
if not SKIP_MPAA:
|
|
if self.tmdb_type == 'movie':
|
|
release_dates = result['release_dates']['results']
|
|
|
|
for country in release_dates:
|
|
if country.get('iso_3166_1') == COUNTRY_CODE:
|
|
for item in country['release_dates']:
|
|
if item.get('certification'):
|
|
self.tmdb_mpaa = item.get('certification')
|
|
break
|
|
break
|
|
|
|
elif country.get('iso_3166_1') == 'US':
|
|
for item in country['release_dates']:
|
|
if item.get('certification'):
|
|
self.tmdb_mpaa_fallback = item.get('certification')
|
|
break
|
|
|
|
if self.tmdb_type == 'tv':
|
|
content_ratings = result['content_ratings']['results']
|
|
|
|
for country in content_ratings:
|
|
if country.get('iso_3166_1') == COUNTRY_CODE:
|
|
self.tmdb_mpaa = country.get('rating')
|
|
break
|
|
|
|
elif country.get('iso_3166_1') == 'US':
|
|
self.tmdb_mpaa_fallback = country.get('rating')
|
|
|
|
if SKIP_NOT_RATED:
|
|
if self.tmdb_mpaa == 'NR':
|
|
self.tmdb_mpaa = None
|
|
|
|
if self.tmdb_mpaa_fallback == 'NR':
|
|
self.tmdb_mpaa_fallback = None
|
|
|
|
if self.tmdb_mpaa:
|
|
if COUNTRY_CODE == 'DE':
|
|
self.tmdb_mpaa = 'FSK ' + self.tmdb_mpaa
|
|
|
|
self._set_value('mpaa', self.tmdb_mpaa)
|
|
|
|
elif self.tmdb_mpaa_fallback and MPAA_FALLBACK:
|
|
self._set_value('mpaa', self.tmdb_mpaa_fallback)
|
|
|
|
else:
|
|
self._set_value('mpaa', '')
|
|
|
|
# set IMDb ID if not available in the library
|
|
if not self.imdb:
|
|
if self.tmdb_type == 'movie':
|
|
self.imdb = result.get('imdb_id')
|
|
|
|
elif self.tmdb_type == 'tv':
|
|
self.imdb = result['external_ids'].get('imdb_id')
|
|
|
|
if self.imdb:
|
|
self._update_uniqueid_dict('imdb', self.imdb)
|
|
|
|
# add TVDb ID to uniqueid if missing
|
|
if not self.tvdb and self.tmdb_type == 'tv':
|
|
self.tvdb = result['external_ids'].get('tvdb_id')
|
|
|
|
if self.tvdb:
|
|
self._update_uniqueid_dict('tvdb', self.tvdb)
|
|
|
|
def get_tmdb_externalid(self,external_id):
|
|
result = self._tmdb(action='find',
|
|
call=str(external_id),
|
|
params={'external_source': 'imdb_id' if external_id.startswith('tt') else 'tvdb_id'}
|
|
)
|
|
|
|
if self.dbtype == 'movie' and result.get('movie_results'):
|
|
self.tmdb = result['movie_results'][0].get('id')
|
|
|
|
elif self.dbtype == 'tvshow' and result.get('tv_results'):
|
|
self.tmdb = result['tv_results'][0].get('id')
|
|
|
|
if self.tmdb:
|
|
self._update_uniqueid_dict('tmdb', self.tmdb)
|
|
|
|
def get_omdb(self):
|
|
omdb = self._omdb()
|
|
|
|
if not omdb:
|
|
return
|
|
|
|
tree = ET.ElementTree(ET.fromstring(omdb))
|
|
root = tree.getroot()
|
|
|
|
for child in root:
|
|
# imdb ratings
|
|
self.imdb_rating = child.get('imdbRating', '').replace('N/A', '')
|
|
self.imdb_votes = child.get('imdbVotes', '0').replace('N/A', '0').replace(',', '')
|
|
if self.imdb_rating:
|
|
self._update_ratings_dict(key='imdb',
|
|
rating=float(self.imdb_rating),
|
|
votes=int(self.imdb_votes)
|
|
)
|
|
|
|
# regular rotten rating
|
|
tomatometerallcritics = child.get('tomatoMeter', '').replace('N/A', '')
|
|
tomatometerallcritics_avg = child.get('tomatoRating', '').replace('N/A', '')
|
|
tomatometerallcritics_votes = child.get('tomatoReviews', '0').replace('N/A', '0').replace(',', '')
|
|
|
|
if tomatometerallcritics:
|
|
self._update_ratings_dict(key='tomatometerallcritics',
|
|
rating=int(tomatometerallcritics) / 10,
|
|
votes=int(tomatometerallcritics_votes))
|
|
|
|
if tomatometerallcritics_avg:
|
|
self._update_ratings_dict(key='tomatometeravgcritics',
|
|
rating=float(tomatometerallcritics_avg),
|
|
votes=int(tomatometerallcritics_votes))
|
|
|
|
# user rotten rating
|
|
tomatometerallaudience = child.get('tomatoUserMeter', '').replace('N/A', '')
|
|
tomatometerallaudience_avg = child.get('tomatoUserRating', '').replace('N/A', '')
|
|
tomatometerallaudience_votes = child.get('tomatoUserReviews', '0').replace('N/A', '0').replace(',', '')
|
|
|
|
if tomatometerallaudience:
|
|
self._update_ratings_dict(key='tomatometerallaudience',
|
|
rating=int(tomatometerallaudience) / 10,
|
|
votes=int(tomatometerallaudience_votes))
|
|
|
|
if tomatometerallaudience_avg:
|
|
self._update_ratings_dict(key='tomatometeravgaudience',
|
|
rating=float(tomatometerallaudience_avg),
|
|
votes=int(tomatometerallaudience_votes))
|
|
|
|
# metacritic
|
|
metacritic = child.get('metascore', '').replace('N/A', '')
|
|
if metacritic:
|
|
metacritic = int(metacritic) / 10
|
|
self._update_ratings_dict(key='metacritic',
|
|
rating=metacritic,
|
|
votes=0)
|
|
|
|
# set imdb if not set before
|
|
if not self.imdb and child.get('imdbID') and child.get('imdbID') != 'N/A':
|
|
self.imdb = child.get('imdbID')
|
|
self._update_uniqueid_dict('imdb', child.get('imdbID'))
|
|
|
|
break
|
|
|
|
def update_info(self):
|
|
# set at least one default rating if none is set in the library
|
|
if not self.default_rating and self.ratings:
|
|
for item in ['imdb', 'themoviedb', 'tomatometerallcritics', 'tomatometeravgcritics', 'metacritic']:
|
|
if item in self.ratings:
|
|
self.default_rating = item
|
|
break
|
|
|
|
# unkown rating source is stored -> use the first one
|
|
if not self.default_rating:
|
|
for item in self.ratings:
|
|
self.default_rating = item
|
|
break
|
|
|
|
# update to library
|
|
self._set_value(key='ratings', value=self.ratings)
|
|
|
|
if self.update_uniqueid:
|
|
self._set_value(key='uniqueid', value=self.uniqueid)
|
|
|
|
# episode guide verification
|
|
if self.episodeguide:
|
|
if 'thetvdb' in self.episodeguide and 'tvdb' not in self.uniqueid:
|
|
self.episodeguide = None
|
|
elif 'themoviedb' in self.episodeguide and 'tmdb' not in self.uniqueid:
|
|
self.episodeguide = None
|
|
|
|
if self.dbtype == 'tvshow' and not self.episodeguide:
|
|
if 'tvdb' in self.uniqueid:
|
|
value = self.uniqueid.get('tvdb')
|
|
url = 'https://api.thetvdb.com/login?{"apikey":"439DFEBA9D3059C6","id":%s}|Content-Type=application/json' % str(value)
|
|
json_value = '<episodeguide><url post="yes" cache="auth.json"><url>%s</url></episodeguide>' % url
|
|
|
|
elif 'tmdb' in self.uniqueid:
|
|
value = self.uniqueid.get('tmdb')
|
|
cache = 'tmdb-%s-%s.json' % (str(value), TMDB_LANGUAGE)
|
|
url = 'http://api.themoviedb.org/3/tv/%s?api_key=6a5be4999abf74eba1f9a8311294c267&language=%s' % (str(value), TMDB_LANGUAGE)
|
|
json_value = '<episodeguide><url cache="%s"><url>%s</url></episodeguide>' % (cache, url)
|
|
|
|
else:
|
|
json_value = '<episodeguide><url cache=""><url></url></episodeguide>'
|
|
|
|
self.episodeguide = json_value
|
|
self._set_value('episodeguide', json_value)
|
|
|
|
# nfo updating
|
|
if self.file:
|
|
# get updated data
|
|
self.get_details()
|
|
|
|
# TV status cannot be fetched in Leia
|
|
if self.tmdb_tv_status and not self.details.get('status'):
|
|
self.details['status'] = self.tmdb_tv_status
|
|
|
|
update_nfo(file=self.file,
|
|
dbtype=self.dbtype,
|
|
dbid=self.dbid,
|
|
details=self.details
|
|
)
|
|
|
|
def _update_ratings_dict(self,key,rating,votes):
|
|
self.ratings[key] = {'default': True if key == self.default_rating else False,
|
|
'rating': rating,
|
|
'votes': votes}
|
|
|
|
def _update_uniqueid_dict(self,key,value):
|
|
self.uniqueid[key] = str(value)
|
|
self.update_uniqueid = True
|
|
|
|
def _set_value(self,key,value):
|
|
self.db.write(key=key, value=value)
|
|
|
|
def _omdb(self):
|
|
if not OMDB_API:
|
|
log('No OMDb API key configured. Skip.', force=RATING_DEBUG)
|
|
return
|
|
|
|
if self.imdb:
|
|
url = 'http://www.omdbapi.com/?apikey=%s&i=%s&plot=short&r=xml&tomatoes=true' % (OMDB_API, self.imdb)
|
|
|
|
elif OMDB_FALLBACK and self.dbtype != 'episode' and self.original_title and self.year:
|
|
# urllib has issues with some asian letters
|
|
try:
|
|
title = urllib.quote(self.original_title)
|
|
except KeyError:
|
|
return
|
|
|
|
url = 'http://www.omdbapi.com/?apikey=%s&t=%s&year=%s&plot=short&r=xml&tomatoes=true' % (OMDB_API, title, self.year)
|
|
|
|
else:
|
|
return
|
|
|
|
error_msg = 'OMDb error for "%s (%s)" IMDBd "%s". Error --> ' % (self.original_title, self.year, self.imdb)
|
|
|
|
for i in range(1,4): # loop if heavy server load
|
|
log('OMDb call try %s/3' % str(i), force=RATING_DEBUG)
|
|
try:
|
|
request = requests.get(url, timeout=5)
|
|
if not str(request.status_code).startswith('5'):
|
|
break
|
|
elif i == 1:
|
|
notification('OMDb', ADDON.getLocalizedString(32024))
|
|
|
|
except Exception:
|
|
if i < 3:
|
|
xbmc.sleep(500)
|
|
else:
|
|
log(error_msg + '408', WARNING)
|
|
return
|
|
|
|
if request.status_code == 401:
|
|
log('OMDb error --> API limit reached', WARNING)
|
|
if DIALOG.yesno(xbmc.getLocalizedString(257), ADDON.getLocalizedString(32033)):
|
|
log('OMDb limit reached and disabled for next calls', force=RATING_DEBUG)
|
|
self.omdb_limit = True
|
|
else:
|
|
log('OMDb limit reached and rating updater canceled', force=RATING_DEBUG)
|
|
winprop('CancelRatingUpdater.bool', True)
|
|
return
|
|
|
|
elif not request.ok:
|
|
log(error_msg + str(request.status_code), WARNING)
|
|
return
|
|
|
|
result = request.text
|
|
|
|
if not result or '<root response="False">' in result:
|
|
log(error_msg + 'Result = ' + str(result), WARNING)
|
|
return
|
|
|
|
return result
|
|
|
|
def _tmdb(self,action,call=None,get=None,params=None):
|
|
result = {}
|
|
args = {}
|
|
args['api_key'] = 'fc168650632c6597038cf7072a7c20da'
|
|
|
|
if params:
|
|
args.update(params)
|
|
|
|
call = '/' + str(call) if call else ''
|
|
get = '/' + get if get else ''
|
|
|
|
url = 'https://api.themoviedb.org/3/' + action + call + get
|
|
url = '{0}?{1}'.format(url, urlencode(args))
|
|
|
|
for i in range(1,4): # loop if heavy server load
|
|
log('TMDb call try %s/3' % str(i), force=RATING_DEBUG)
|
|
try:
|
|
request = requests.get(url, timeout=5)
|
|
if not str(request.status_code).startswith('5'):
|
|
break
|
|
elif i == 1:
|
|
notification('TMDb', ADDON.getLocalizedString(32024))
|
|
|
|
except Exception:
|
|
if i < 3:
|
|
xbmc.sleep(500)
|
|
else:
|
|
log('TMDb connection error', force=RATING_DEBUG)
|
|
return result
|
|
|
|
if request.ok:
|
|
result = request.json()
|
|
else:
|
|
log('TMDb returned nothing', force=RATING_DEBUG)
|
|
|
|
return result |