The branch, eden has been updated
via 3b410bcfc3fa931d70b78bf7a29da7057d47e34f (commit)
from b90a9f0b2f886d31c408971c3d9b7167ab722681 (commit)
- Log -----------------------------------------------------------------
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/scripts;a=commit;h=3b410bcfc3fa931d70b78bf7a29da7057d47e34f
commit 3b410bcfc3fa931d70b78bf7a29da7057d47e34f
Author: Martijn Kaijser <[email protected]>
Date: Wed May 21 19:53:49 2014 +0200
[script.module.metahandler] 1.5.0
diff --git a/script.module.metahandler/addon.xml
b/script.module.metahandler/addon.xml
index 3bc0b94..18d8fd0 100644
--- a/script.module.metahandler/addon.xml
+++ b/script.module.metahandler/addon.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.module.metahandler"
name="metahandler"
- version="1.4.0"
+ version="1.5.0"
provider-name="Eldorado (inspired by anarchintosh, westcoast, daledude,
t0mm0)">
<requires>
<import addon="xbmc.python" version="2.0" />
@@ -21,4 +21,4 @@
<email></email>
<source></source>
</extension>
-</addon>
+</addon>
\ No newline at end of file
diff --git a/script.module.metahandler/changelog.txt
b/script.module.metahandler/changelog.txt
index 35b497a..6e309ae 100644
--- a/script.module.metahandler/changelog.txt
+++ b/script.module.metahandler/changelog.txt
@@ -1,3 +1,13 @@
+[B]Version 1.5.0[/B]
+- Rework TMDB api key so that it can be set when initializing metahandlers -
soon will require individual keys
+- Added option in addon settings to delete metahandlers cache db to restart
fresh
+- Fixed refresh episode meta data to hold watched status
+- Updated tvdbapi calls for better accuracy
+- Added new config table to cache db - comes with new _set_config(setting,
value) and _get_config(setting) methods
+- Get and set TMDB config values, cache in config table
+- Properly set TMDB image locations
+- Temporary SQL update added to clean existing DB's of hardcoded TMDB image
url's - keep only filenames
+
[B]Version 1.4.0[/B]
- New TMDB API key
- Updated calls to TMDB so we only make one request per movie
diff --git a/script.module.metahandler/lib/metahandler/TMDB.py
b/script.module.metahandler/lib/metahandler/TMDB.py
index 2c8160f..8bf7a82 100644
--- a/script.module.metahandler/lib/metahandler/TMDB.py
+++ b/script.module.metahandler/lib/metahandler/TMDB.py
@@ -28,25 +28,16 @@ class TMDB(object):
or if there is data missing on TMDB, another call is made to IMDB to fill
in the missing information.
'''
- def __init__(self, api_key='af95ef8a4fe1e697f86b8c194f2e5e11',
view='json', lang='en'):
+ def __init__(self, api_key='', view='json', lang='en'):
#view = yaml json xml
self.view = view
- self.lang = self.__get_language(lang)
+ self.lang = lang
self.api_key = api_key
self.url_prefix = 'http://api.themoviedb.org/3'
- self.poster_prefix = 'http://d3gtl9l2a4fn1j.cloudfront.net/t/p/' +
addon.get_setting('tmdb_poster_size')
- self.backdrop_prefix = 'http://d3gtl9l2a4fn1j.cloudfront.net/t/p/' +
addon.get_setting('tmdb_backdrop_size')
self.imdb_api = 'http://www.imdbapi.com/?i=%s'
self.imdb_name_api = 'http://www.imdbapi.com/?t=%s'
self.imdb_nameyear_api = 'http://www.imdbapi.com/?t=%s&y=%s'
-
- def __get_language(self, lang):
- tmdb_language = addon.get_setting('tmdb_language')
- if tmdb_language:
- return re.sub(".*\((\w+)\).*","\\1",tmdb_language)
- else:
- return lang
-
+
def __clean_name(self, mystring):
newstring = ''
for word in mystring.split(' '):
@@ -139,7 +130,15 @@ class TMDB(object):
except:
return True
+
+ def call_config(self):
+ '''
+ Query TMDB config api for current values
+ '''
+ r = self._do_request('configuration', '')
+ return r
+
def search_imdb(self, name, imdb_id='', year=''):
'''
Search IMDB by either IMDB ID or Name/Year
@@ -396,9 +395,9 @@ class TMDB(object):
trailers = meta['trailers']
if meta.has_key('poster_path') and meta['poster_path']:
- meta['cover_url'] = self.poster_prefix +
meta['poster_path']
+ meta['cover_url'] = meta['poster_path']
if meta.has_key('backdrop_path') and meta['backdrop_path']:
- meta['backdrop_url'] = self.backdrop_prefix +
meta['backdrop_path']
+ meta['backdrop_url'] = meta['backdrop_path']
meta['released'] = meta['release_date']
#Set rating to 0 so that we can force it to be grabbed from
IMDB
meta['tmdb_rating'] = meta['vote_average']
diff --git a/script.module.metahandler/lib/metahandler/metahandlers.py
b/script.module.metahandler/lib/metahandler/metahandlers.py
index 482929f..9c309a1 100644
--- a/script.module.metahandler/lib/metahandler/metahandlers.py
+++ b/script.module.metahandler/lib/metahandler/metahandlers.py
@@ -34,7 +34,7 @@ from thetvdbapi import TheTVDB
import xbmc
import xbmcvfs
-''' Use t0mm0's common library for http calls '''
+''' Use t0mm0.common library for http calls '''
from t0mm0.common.net import Net
net = Net()
@@ -95,15 +95,19 @@ class MetaData:
'''
- def __init__(self, preparezip=False):
+ def __init__(self, preparezip=False,
tmdb_api_key='af95ef8a4fe1e697f86b8c194f2e5e11'):
#Check if a path has been set in the addon settings
settings_path = common.addon.get_setting('meta_folder_location')
+ # TMDB constants
+ self.tmdb_image_url = ''
+ self.tmdb_api_key = tmdb_api_key
+
if settings_path:
self.path = xbmc.translatePath(settings_path)
else:
- self.path =
xbmc.translatePath('special://profile/addon_data/script.module.metahandler')
+ self.path = common.profile_path();
self.cache_path = make_dir(self.path, 'meta_cache')
@@ -147,7 +151,7 @@ class MetaData:
db_user = common.addon.get_setting('db_user')
db_pass = common.addon.get_setting('db_pass')
db_name = common.addon.get_setting('db_name')
- self.dbcon = database.connect(db_name, db_user, db_pass,
db_address, buffered=True)
+ self.dbcon = database.connect(database=db_name, user=db_user,
password=db_pass, host=db_address, buffered=True)
self.dbcur = self.dbcon.cursor(cursor_class=MySQLCursorDict,
buffered=True)
else:
self.dbcon = database.connect(self.videocache)
@@ -156,7 +160,70 @@ class MetaData:
# initialize cache db
self._cache_create_movie_db()
+
+ # Check TMDB configuration, update if necessary
+ self._set_tmdb_config()
+
+ ## !!!!!!!!!!!!!!!!!! Temporary code to update movie_meta columns
cover_url and backdrop_url to store only filename !!!!!!!!!!!!!!!!!!!!!
+
+ ## We have matches with outdated url, so lets strip the url out and
only keep filename
+ try:
+ if DB == 'mysql':
+ sql_select = "SELECT imdb_id, tmdb_id, cover_url, backdrop_url
"\
+ "FROM movie_meta "\
+ "where substring(cover_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net' "\
+ "or substring(backdrop_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net'"
+ self.dbcur.execute(sql_select)
+ matchedrows = self.dbcur.fetchall()[0]
+
+ if matchedrows:
+ sql_update = "UPDATE movie_meta "\
+ "SET cover_url =
SUBSTRING_INDEX(cover_url, '/', -1), "\
+ "backdrop_url =
SUBSTRING_INDEX(backdrop_url, '/', -1) "\
+ "where substring(cover_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net' "\
+ "or substring(backdrop_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net'"
+ self.dbcur.execute(sql_update)
+ self.dbcon.commit()
+ common.addon.log('MySQL rows successfully updated')
+
+ else:
+ common.addon.log('No MySQL rows requiring update')
+
+ else:
+
+ sql_select = "SELECT imdb_id, tmdb_id, cover_url, backdrop_url
"\
+ "FROM movie_meta "\
+ "where substr(cover_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net' "\
+ "or substr(backdrop_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net'"
+ self.dbcur.execute(sql_select)
+ matchedrows = self.dbcur.fetchone()
+
+ if matchedrows:
+ sql_update = "update movie_meta "\
+ "set cover_url = "\
+ "case when substr(cover_url,
length(cover_url) - 31, 1) = '/' "\
+ " then substr(cover_url,
length(cover_url) - 31, 32) "\
+ "else substr(cover_url,
length(cover_url) - 30, 31) "\
+ "end, "\
+ "backdrop_url = "\
+ "case when substr(backdrop_url,
length(backdrop_url) - 31, 1) = '/' "\
+ " then substr(backdrop_url,
length(backdrop_url) - 31, 32) "\
+ "else substr(backdrop_url,
length(backdrop_url) - 30, 31) "\
+ "end "\
+ "where substr(cover_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net' "\
+ "or substr(backdrop_url, 1, 36 ) =
'http://d3gtl9l2a4fn1j.cloudfront.net'"
+ self.dbcur.execute(sql_update)
+ self.dbcon.commit()
+ common.addon.log('SQLite rows successfully updated')
+ else:
+ common.addon.log('No SQLite rows requiring update')
+
+ except Exception, e:
+ common.addon.log('************* Error updating cover and backdrop
columns: %s' % e, 4)
+ pass
+
+ ##
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
def __del__(self):
''' Cleanup db when object destroyed '''
@@ -307,6 +374,21 @@ class MetaData:
self.dbcur.execute(sql_create)
common.addon.log('Table addons initialized', 0)
+ # Create Configuration table
+ sql_create = "CREATE TABLE IF NOT EXISTS config ("\
+ "setting TEXT, "\
+ "value TEXT, "\
+ "UNIQUE(setting)"\
+ ");"
+
+ if DB == 'mysql':
+ sql_create = sql_create.replace("setting TEXT", "setting
VARCHAR(255)")
+ sql_create = sql_create.replace("value TEXT", "value VARCHAR(255)")
+ self.dbcur.execute(sql_create)
+ else:
+ self.dbcur.execute(sql_create)
+ common.addon.log('Table config initialized', 0)
+
def _init_movie_meta(self, imdb_id, tmdb_id, name, year=0):
'''
@@ -444,6 +526,22 @@ class MetaData:
return meta
+ def __get_tmdb_language(self):
+ tmdb_language = common.addon.get_setting('tmdb_language')
+ if tmdb_language:
+ return re.sub(".*\((\w+)\).*","\\1",tmdb_language)
+ else:
+ return 'en'
+
+
+ def __get_tvdb_language(self) :
+ tvdb_language = common.addon.get_setting('tvdb_language')
+ if tvdb_language and tvdb_language!='':
+ return re.sub(".*\((\w+)\).*","\\1",tvdb_language)
+ else:
+ return 'en'
+
+
def _string_compare(self, s1, s2):
""" Method that takes two strings and returns True or False, based
on if they are equal, regardless of case.
@@ -589,6 +687,91 @@ class MetaData:
return 0
+ def _get_config(self, setting):
+ '''
+ Query local Config table for values
+ '''
+
+ #Query local table first for current values
+ sql_select = "SELECT * FROM config where setting = '%s'" % setting
+
+ common.addon.log('Looking up in local cache for config data: %s' %
setting, 2)
+ common.addon.log('SQL Select: %s' % sql_select, 0)
+
+ try:
+ self.dbcur.execute(sql_select)
+ matchedrow = self.dbcur.fetchone()
+ except Exception, e:
+ common.addon.log('************* Error selecting from cache db: %s'
% e, 4)
+ return None
+
+ if matchedrow:
+ common.addon.log('Found config data in cache table for setting: %s
value: %s' % (setting, dict(matchedrow)), 0)
+ return dict(matchedrow)['value']
+ else:
+ common.addon.log('No match in local DB for config setting: %s' %
setting, 0)
+ return None
+
+
+ def _set_config(self, setting, value):
+ '''
+ Set local Config table for values
+ '''
+
+ try:
+ sql_insert = "REPLACE INTO config (setting, value) VALUES(%s,%s)"
+ if DB == 'sqlite':
+ sql_insert = 'INSERT OR ' + sql_insert.replace('%s', '?')
+
+ common.addon.log('Updating local cache for config data: %s value:
%s' % (setting, value), 2)
+ common.addon.log('SQL Insert: %s' % sql_insert, 0)
+
+ self.dbcur.execute(sql_insert, (setting, value))
+ self.dbcon.commit()
+ except Exception, e:
+ common.addon.log('************* Error updating cache db: %s' % e,
4)
+ return None
+
+
+ def _set_tmdb_config(self):
+ '''
+ Query config database for required TMDB config values, set constants
as needed
+ Validate cache timestamp to ensure it is only refreshed once every 7
days
+ '''
+
+ tmdb_image_url = self._get_config('tmdb_image_url')
+ tmdb_config_timestamp = self._get_config('tmdb_config_timestamp')
+
+ #Grab current time in seconds
+ now = time.time()
+ age = 0
+
+ #Cache limit is 7 days: 60 seconds * 60 minutes * 24 hours * 7 days
+ expire = 60 * 60 * 24 * 7
+
+ #Check if image and timestamp values are valid
+ if tmdb_image_url and tmdb_config_timestamp:
+ created = float(tmdb_config_timestamp)
+ age = now - created
+
+ #If cache hasn't expired, set constant values
+ if age < expire:
+ common.addon.log('Cache still valid, setting values', 0)
+ self.tmdb_image_url = tmdb_image_url
+
+ #Either we don't have the values or the cache has expired, so lets
request and set them - update cache in the end
+ elif not tmdb_image_url or not tmdb_config_timestamp or age > expire:
+ common.addon.log('No cached config data found or cache expired,
requesting from TMDB', 0)
+
+ tmdb = TMDB(api_key=self.tmdb_api_key,
lang=self.__get_tmdb_language())
+ config_data = tmdb.call_config()
+
+ if config_data:
+ self.tmdb_image_url = config_data['images']['base_url']
+ self._set_config('tmdb_image_url',
config_data['images']['base_url'])
+ self._set_config('tmdb_config_timestamp', now)
+
+
def check_meta_installed(self, addon_id):
'''
Check if a meta data pack has been installed for a specific addon
@@ -830,7 +1013,15 @@ class MetaData:
banner_path=os.path.join(root_banners,
banner_name[0].lower())
if self.classmode == 'true':
self._downloadimages(meta['banner_url'],
banner_path, banner_name)
- meta['banner_url'] = os.path.join(banner_path,
banner_name)
+ meta['banner_url'] = os.path.join(banner_path,
banner_name)
+
+ #Else - they are online so piece together the full URL from TMDB
+ else:
+ if media_type == self.type_movie:
+ if meta['cover_url'].startswith('/'):
+ meta['cover_url'] = self.tmdb_image_url +
common.addon.get_setting('tmdb_poster_size') + meta['cover_url']
+ if meta['backdrop_url'].startswith('/'):
+ meta['backdrop_url'] = self.tmdb_image_url +
common.addon.get_setting('tmdb_backdrop_size') + meta['backdrop_url']
common.addon.log('Returned Meta: %s' % meta, 0)
return meta
@@ -1129,7 +1320,7 @@ class MetaData:
these "None found" entries otherwise we hit tmdb alot.
'''
- tmdb = TMDB()
+ tmdb = TMDB(api_key=self.tmdb_api_key, lang=self.__get_tmdb_language())
meta = tmdb.tmdb_lookup(name,imdb_id,tmdb_id, year)
if meta is None:
@@ -1270,7 +1461,7 @@ class MetaData:
these "None found" entries otherwise we hit tvdb alot.
'''
common.addon.log('Starting TVDB Lookup', 0)
- tvdb = TheTVDB()
+ tvdb = TheTVDB(language=self.__get_tvdb_language())
tvdb_id = ''
try:
@@ -1615,7 +1806,7 @@ class MetaData:
else:
return None
-
+
def update_episode_meta(self, name, imdb_id, season, episode, tvdb_id='',
new_imdb_id='', new_tvdb_id=''):
'''
Updates and returns meta data for given episode,
@@ -1663,7 +1854,7 @@ class MetaData:
elif not new_tvdb_id:
new_tvdb_id = tvdb_id
- return self.get_episode_meta(name, imdb_id, season, episode, overlay)
+ return self.get_episode_meta(name, imdb_id, season, episode,
overlay=overlay)
def _cache_lookup_episode(self, imdb_id, tvdb_id, season, episode,
air_date=''):
@@ -1777,7 +1968,7 @@ class MetaData:
'''
meta = {}
- tvdb = TheTVDB()
+ tvdb = TheTVDB(language=self._get_tvdb_language())
if air_date:
try:
episode = tvdb.get_episode_by_airdate(tvdb_id, air_date)
@@ -2246,7 +2437,7 @@ class MetaData:
def _get_season_posters(self, tvdb_id, season):
- tvdb = TheTVDB()
+ tvdb = TheTVDB(language=self._get_tvdb_language())
try:
images = tvdb.get_show_image_choices(tvdb_id)
diff --git a/script.module.metahandler/lib/metahandler/thetvdbapi.py
b/script.module.metahandler/lib/metahandler/thetvdbapi.py
index 6f3dabe..6b79567 100644
--- a/script.module.metahandler/lib/metahandler/thetvdbapi.py
+++ b/script.module.metahandler/lib/metahandler/thetvdbapi.py
@@ -1,6 +1,7 @@
"""
thetvdb.com Python API
(c) 2009 James Smith (http://loopj.com)
+(c) 2014 Wayne Davison <[email protected]>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -18,135 +19,189 @@ along with this program. If not, see
<http://www.gnu.org/licenses/>.
import urllib
import datetime
+import random
import re
import copy
-import xml.etree.ElementTree as ET
-#import elementtree.ElementTree as ET
-#from elementtree.ElementTree import ElementTree as ET
+import xml.parsers.expat as expat
from cStringIO import StringIO
+from zipfile import ZipFile
class TheTVDB(object):
- def __init__(self, api_key='2B8557E0CBF7D720'):
-
#http://www.thetvdb.com/api/GetEpisodeByAirDate.php?apikey=1D62F2F90030C444&seriesid=71256&airdate=2010-03-29
+ def __init__(self, api_key='2B8557E0CBF7D720', language = 'en', want_raw =
False):
+ #http://thetvdb.com/api/<apikey>/<request>
self.api_key = api_key
- self.mirror_url = "http://www.thetvdb.com"
+ self.mirror_url = "http://thetvdb.com"
self.base_url = self.mirror_url + "/api"
self.base_key_url = "%s/%s" % (self.base_url, self.api_key)
-
+ self.language = language
+ self.want_raw = want_raw
+
+ # Mirror selection got deprecated a while back, so tell it to skip the
actual fetch.
+ self.select_mirrors(False)
+
+
+ def select_mirrors(self, do_the_fetch = True):
+ #http://thetvdb.com/api/<apikey>/mirrors.xml
+ url = "%s/mirrors.xml" % self.base_key_url
+ self.xml_mirrors = []
+ self.zip_mirrors = []
+ try:
+ filt_func = lambda name, attrs: attrs if name == 'Mirror' else None
+ xml = self._get_xml_data(url, filt_func) if do_the_fetch else {}
+ for mirror in xml.get("Mirror", []):
+ mirrorpath = mirror.get("mirrorpath", None)
+ typemask = mirror.get("typemask", None)
+ if not mirrorpath or not typemask:
+ continue
+ typemask = int(typemask)
+ if typemask & 1:
+ self.xml_mirrors.append(mirrorpath)
+ if typemask & 4:
+ self.zip_mirrors.append(mirrorpath)
+ except:
+ pass
+
+ if not self.xml_mirrors:
+ self.xml_mirrors = [ self.mirror_url ]
+ if not self.zip_mirrors:
+ self.zip_mirrors = [ self.mirror_url ]
+
+ self.xml_mirror_url = random.choice(self.xml_mirrors)
+ self.zip_mirror_url = random.choice(self.zip_mirrors)
+
+ self.base_xml_url = "%s/api/%s" % (self.xml_mirror_url, self.api_key)
+ self.base_zip_url = "%s/api/%s" % (self.zip_mirror_url, self.api_key)
+
+
+ def _2show(self, attrs):
+ return attrs if self.want_raw else TheTVDB.Show(attrs, self.mirror_url)
+
+
+ def _2episode(self, attrs):
+ return attrs if self.want_raw else TheTVDB.Episode(attrs,
self.mirror_url)
+
+
class Show(object):
"""A python object representing a thetvdb.com show record."""
def __init__(self, node, mirror_url):
# Main show details
- #print node
- self.id = node.findtext("id")
- self.name = node.findtext("SeriesName")
- self.overview = node.findtext("Overview")
- self.genre = node.findtext("Genre") #[g for g in
node.findtext("Genre").split("|") if g]
- self.actors = [a for a in node.findtext("Actors").split("|") if a]
- self.network = node.findtext("Network")
- self.content_rating = node.findtext("ContentRating")
- self.rating = node.findtext("Rating")
- self.runtime = node.findtext("Runtime")
- self.status = node.findtext("Status")
- self.language = node.findtext("Language")
-
+ self.id = node.get("id", "")
+ self.name = node.get("SeriesName", "")
+ self.overview = node.get("Overview", "")
+ self.genre = node.get("Genre", "") #[g for g in node.get("Genre",
"").split("|") if g]
+ self.actors = [a for a in node.get("Actors", "").split("|") if a]
+ self.network = node.get("Network", "")
+ self.content_rating = node.get("ContentRating", "")
+ self.rating = node.get("Rating", "")
+ self.runtime = node.get("Runtime", "")
+ self.status = node.get("Status", "")
+ self.language = node.get("Language", "")
+
# Air details
- self.first_aired =
TheTVDB.convert_date(node.findtext("FirstAired"))
- self.airs_day = node.findtext("Airs_DayOfWeek")
- self.airs_time =
node.findtext("Airs_Time")#TheTVDB.convert_time(node.findtext("Airs_Time"))
-
+ self.first_aired = TheTVDB.convert_date(node.get("FirstAired", ""))
+ self.airs_day = node.get("Airs_DayOfWeek", "")
+ self.airs_time = node.get("Airs_Time",
"")#TheTVDB.convert_time(node.get("Airs_Time", ""))
+
# Main show artwork
- temp = node.findtext("banner")
+ temp = node.get("banner", "")
if temp != '' and temp is not None:
self.banner_url = "%s/banners/%s" % (mirror_url, temp)
else:
self.banner_url = ''
- temp = node.findtext("poster")
+ temp = node.get("poster", "")
if temp != '' and temp is not None:
self.poster_url = "%s/banners/%s" % (mirror_url, temp)
else:
self.poster_url = ''
- temp = node.findtext("fanart")
+ temp = node.get("fanart", "")
if temp != '' and temp is not None:
self.fanart_url = "%s/banners/%s" % (mirror_url, temp)
else:
self.fanart_url = ''
# External references
- self.imdb_id = node.findtext("IMDB_ID")
- self.tvcom_id = node.findtext("SeriesID")
- self.zap2it_id = node.findtext("zap2it_id")
+ self.imdb_id = node.get("IMDB_ID", "")
+ self.tvcom_id = node.get("SeriesID", "")
+ self.zap2it_id = node.get("zap2it_id", "")
# When this show was last updated
- self.last_updated =
datetime.datetime.fromtimestamp(int(node.findtext("lastupdated")))
+ self.last_updated_utime =
int(TheTVDB.check(node.get("lastupdated", ""), '0'))
+ self.last_updated =
datetime.datetime.fromtimestamp(self.last_updated_utime)
+
def __str__(self):
import pprint
return pprint.saferepr(self)
+
class Episode(object):
"""A python object representing a thetvdb.com episode record."""
def __init__(self, node, mirror_url):
- self.id = node.findtext("id")
- self.show_id = node.findtext("seriesid")
- self.name = node.findtext("EpisodeName")
- self.overview = node.findtext("Overview")
- self.season_number = node.findtext("SeasonNumber")
- self.episode_number = node.findtext("EpisodeNumber")
- self.director = node.findtext("Director")
- self.guest_stars = node.findtext("GuestStars")
- self.language = node.findtext("Language")
- self.production_code = node.findtext("ProductionCode")
- self.rating = node.findtext("Rating")
- self.writer = node.findtext("Writer")
+ self.id = node.get("id", "")
+ self.show_id = node.get("seriesid", "")
+ self.name = node.get("EpisodeName", "")
+ self.overview = node.get("Overview", "")
+ self.season_number = node.get("SeasonNumber", "")
+ self.episode_number = node.get("EpisodeNumber", "")
+ self.director = node.get("Director", "")
+ self.guest_stars = node.get("GuestStars", "")
+ self.language = node.get("Language", "")
+ self.production_code = node.get("ProductionCode", "")
+ self.rating = node.get("Rating", "")
+ self.writer = node.get("Writer", "")
# Air date
- self.first_aired =
node.findtext("FirstAired")#TheTVDB.convert_date(node.findtext("FirstAired"))
+ self.first_aired = node.get("FirstAired",
"")#TheTVDB.convert_date(node.get("FirstAired", ""))
# DVD Information
- self.dvd_chapter = node.findtext("DVD_chapter")
- self.dvd_disc_id = node.findtext("DVD_discid")
- self.dvd_episode_number = node.findtext("DVD_episodenumber")
- self.dvd_season = node.findtext("DVD_season")
+ self.dvd_chapter = node.get("DVD_chapter", "")
+ self.dvd_disc_id = node.get("DVD_discid", "")
+ self.dvd_episode_number = node.get("DVD_episodenumber", "")
+ self.dvd_season = node.get("DVD_season", "")
# Artwork/screenshot
- temp = node.findtext("filename")
+ temp = node.get("filename", "")
if temp != '' and temp is not None:
self.image = "%s/banners/%s" % (mirror_url, temp)
else:
self.image = ''
- #self.image = 'http://thetvdb.com/banners/' +
node.findtext("filename")
+ #self.image = 'http://thetvdb.com/banners/' + node.get("filename",
"")
# Episode ordering information (normally for specials)
- self.airs_after_season = node.findtext("airsafter_season")
- self.airs_before_season = node.findtext("airsbefore_season")
- self.airs_before_episode = node.findtext("airsbefore_episode")
+ self.airs_after_season = node.get("airsafter_season", "")
+ self.airs_before_season = node.get("airsbefore_season", "")
+ self.airs_before_episode = node.get("airsbefore_episode", "")
# Unknown
- self.combined_episode_number =
node.findtext("combined_episode_number")
- self.combined_season = node.findtext("combined_season")
- self.absolute_number = node.findtext("absolute_number")
- self.season_id = node.findtext("seasonid")
- self.ep_img_flag = node.findtext("EpImgFlag")
+ self.combined_episode_number = node.get("Combined_episodenumber",
"")
+ self.combined_season = node.get("Combined_season", "")
+ self.absolute_number = node.get("absolute_number", "")
+ self.season_id = node.get("seasonid", "")
+ self.ep_img_flag = node.get("EpImgFlag", "")
# External references
- self.imdb_id = node.findtext("IMDB_ID")
+ self.imdb_id = node.get("IMDB_ID", "")
# When this episode was last updated
- self.last_updated =
datetime.datetime.fromtimestamp(int(self.check(node.findtext("lastupdated"),
'0')))
+ self.last_updated_utime =
int(TheTVDB.check(node.get("lastupdated", ""), '0'))
+ self.last_updated =
datetime.datetime.fromtimestamp(self.last_updated_utime)
+
def __str__(self):
return repr(self)
- def check(self, value, ret=None):
- if value is None or value == '':
- if ret == None:
- return ''
- else:
- return ret
+
+ @staticmethod
+ def check(value, ret=None):
+ if value is None or value == '':
+ if ret == None:
+ return ''
else:
- return value
+ return ret
+ else:
+ return value
+
@staticmethod
def convert_time(time_string):
@@ -161,12 +216,16 @@ class TheTVDB(object):
if "hour" in gd and "minute" in gd and gd["minute"] and "ampm"
in gd:
hour = int(gd["hour"])
+ if hour == 12:
+ hour = 0
if gd["ampm"].lower() == "p":
hour += 12
return datetime.time(hour, int(gd["minute"]))
elif "hour" in gd and "ampm" in gd:
hour = int(gd["hour"])
+ if hour == 12:
+ hour = 0
if gd["ampm"].lower() == "p":
hour += 12
@@ -176,6 +235,7 @@ class TheTVDB(object):
return None
+
@staticmethod
def convert_date(date_string):
"""Convert a thetvdb date string into a datetime.date object."""
@@ -187,195 +247,194 @@ class TheTVDB(object):
return first_aired
- def get_matching_shows(self, show_name):
+
+ # language can be "all", "en", "fr", etc.
+ def get_matching_shows(self, show_name, language=None, want_raw=False):
"""Get a list of shows matching show_name."""
- get_args = urllib.urlencode({"seriesname": show_name}, doseq=True)
+ if type(show_name) == type(u''):
+ show_name = show_name.encode('utf-8')
+ get_args = {"seriesname": show_name}
+ if language is not None:
+ get_args['language'] = language
+ get_args = urllib.urlencode(get_args, doseq=True)
url = "%s/GetSeries.php?%s" % (self.base_url, get_args)
- print url
- data = urllib.urlopen(url)
- show_list = []
- if data:
- try:
- tree = ET.parse(data)
- show_list = [(show.findtext("seriesid"),
show.findtext("SeriesName"),show.findtext("IMDB_ID")) for show in
tree.getiterator("Series")]
- except SyntaxError:
- pass
+ if want_raw:
+ filt_func = lambda name, attrs: attrs if name == "Series" else None
+ else:
+ filt_func = lambda name, attrs: (attrs.get("seriesid", ""),
attrs.get("SeriesName", ""), attrs.get("IMDB_ID", "")) if name == "Series" else
None
+ xml = self._get_xml_data(url, filt_func)
+ return xml.get('Series', [])
- return show_list
def get_show(self, show_id):
"""Get the show object matching this show_id."""
- #url = "%s/series/%s/%s.xml" % (self.base_key_url, show_id, "el")
- url = "%s/series/%s/" % (self.base_key_url, show_id)
- data = StringIO(urllib.urlopen(url).read())
- temp_data = data.getvalue()
- show = None
- if temp_data.startswith('<?xml') == False:
- return show
-
- try:
- tree = ET.parse(data)
- show_node = tree.find("Series")
-
- show = TheTVDB.Show(show_node, self.mirror_url)
- except SyntaxError:
- pass
-
- return show
+ url = "%s/series/%s/%s.xml" % (self.base_xml_url, show_id,
self.language)
+ return self._get_show_by_url(url)
def get_show_by_imdb(self, imdb_id):
"""Get the show object matching this show_id."""
#url = "%s/series/%s/%s.xml" % (self.base_key_url, show_id, "el")
url = "%s/GetSeriesByRemoteID.php?imdbid=%s" % (self.base_url, imdb_id)
- data = StringIO(urllib.urlopen(url).read())
- temp_data = data.getvalue()
- tvdb_id = ''
- if temp_data.startswith('<?xml') == False:
- return tvdb_id
- try:
- tree = ET.parse(data)
- for show in tree.getiterator("Series"):
- tvdb_id = show.findtext("seriesid")
+ show = self._get_show_by_url(url)
+ if show:
+ return show['id'] if self.want_raw else show.id
+ return ''
- except SyntaxError:
- pass
- return tvdb_id
+ def _get_show_by_url(self, url):
+ filt_func = lambda name, attrs: self._2show(attrs) if name == "Series"
else None
+ xml = self._get_xml_data(url, filt_func)
+ return xml['Series'][0] if 'Series' in xml else None
+
def get_episode(self, episode_id):
"""Get the episode object matching this episode_id."""
- url = "%s/episodes/%s" % (self.base_key_url, episode_id)
- data = urllib.urlopen(url)
- print url
- episode = None
- try:
- tree = ET.parse(data)
- episode_node = tree.find("Episode")
-
- episode = TheTVDB.Episode(episode_node, self.mirror_url)
- except SyntaxError:
- pass
-
- return episode
+ url = "%s/episodes/%s" % (self.base_xml_url, episode_id)
+ return self._get_episode_by_url(url)
def get_episode_by_airdate(self, show_id, aired):
"""Get the episode object matching this episode_id."""
#url = "%s/series/%s/default/%s/%s" % (self.base_key_url, show_id,
season_num, ep_num)
'''http://www.thetvdb.com/api/GetEpisodeByAirDate.php?apikey=1D62F2F90030C444&seriesid=71256&airdate=2010-03-29'''
-
url =
"%s/GetEpisodeByAirDate.php?apikey=1D62F2F90030C444&seriesid=%s&airdate=%s" %
(self.base_url, show_id, aired)
- data = StringIO(urllib.urlopen(url).read())
-
- print url
- episode = None
-
- #code to check if data has been returned
- temp_data = data.getvalue()
- if temp_data.startswith('<?xml') == False:
- print 'No data returned ', temp_data
- return episode
-
- try:
- tree = ET.parse(data)
- episode_node = tree.find("Episode")
+ return self._get_episode_by_url(url)
- episode = TheTVDB.Episode(episode_node, self.mirror_url)
-
- except SyntaxError:
- pass
-
- return episode
-
def get_episode_by_season_ep(self, show_id, season_num, ep_num):
"""Get the episode object matching this episode_id."""
- url = "%s/series/%s/default/%s/%s" % (self.base_key_url, show_id,
season_num, ep_num)
- data = StringIO(urllib.urlopen(url).read())
-
- episode = None
- print url
-
- #code to check if data has been returned
- temp_data = data.getvalue()
- if temp_data.startswith('<?xml') == False :
- print 'No data returned ', temp_data
- return episode
-
- try:
- tree = ET.parse(data)
- episode_node = tree.find("Episode")
+ url = "%s/series/%s/default/%s/%s" % (self.base_xml_url, show_id,
season_num, ep_num)
+ return self._get_episode_by_url(url)
+
+
+ def _get_episode_by_url(self, url):
+ filt_func = lambda name, attrs: self._2episode(attrs) if name ==
"Episode" else None
+ xml = self._get_xml_data(url, filt_func)
+ return xml['Episode'][0] if 'Episode' in xml else None
+
- episode = TheTVDB.Episode(episode_node, self.mirror_url)
- except SyntaxError:
- pass
-
- return episode
-
def get_show_and_episodes(self, show_id):
"""Get the show object and all matching episode objects for this
show_id."""
- url = "%s/series/%s/all/" % (self.base_key_url, show_id)
- data = urllib.urlopen(url)
-
- show_and_episodes = None
- try:
- tree = ET.parse(data)
- show_node = tree.find("Series")
-
- show = TheTVDB.Show(show_node, self.mirror_url)
- episodes = []
-
- episode_nodes = tree.getiterator("Episode")
- for episode_node in episode_nodes:
- episodes.append(TheTVDB.Episode(episode_node, self.mirror_url))
-
- show_and_episodes = (show, episodes)
- except SyntaxError:
- pass
-
- return show_and_episodes
+ url = "%s/series/%s/all/%s.zip" % (self.base_zip_url, show_id,
self.language)
+ zip_name = '%s.xml' % self.language
+ filt_func = lambda name, attrs: self._2episode(attrs) if name ==
"Episode" else self._2show(attrs) if name == "Series" else None
+ xml = self._get_xml_data(url, filt_func, zip_name=zip_name)
+ if 'Series' not in xml:
+ return None
+ return (xml['Series'][0], xml.get('Episode', []))
+
+
+ def get_show_image_choices(self, show_id):
+ """Get a list of image urls and types relating to this show."""
+ url = "%s/series/%s/banners.xml" % (self.base_xml_url, show_id)
+ filt_func = lambda name, attrs: attrs if name == "Banner" else None
+ xml = self._get_xml_data(url, filt_func)
+
+ images = []
+ for banner in xml.get("Banner", []):
+ banner_type = banner["BannerType"]
+ banner_season = banner["Season"] if banner_type == 'season' else ''
+ banner_url = "%s/banners/%s" % (self.mirror_url,
banner["BannerPath"])
+ images.append((banner_url, banner_type, banner_season))
+
+ return images
+
def get_updated_shows(self, period = "day"):
"""Get a list of show ids which have been updated within this
period."""
- url = "%s/updates/updates_%s.xml" % (self.base_key_url, period)
- data = urllib.urlopen(url)
- tree = ET.parse(data)
-
- series_nodes = tree.getiterator("Series")
+ filt_func = lambda name, attrs: attrs["id"] if name == "Series" else
None
+ xml = self._get_update_info(period, filt_func)
+ return xml.get("Series", [])
- return [x.findtext("id") for x in series_nodes]
def get_updated_episodes(self, period = "day"):
"""Get a list of episode ids which have been updated within this
period."""
- url = "%s/updates/updates_%s.xml" % (self.base_key_url, period)
- data = urllib.urlopen(url)
- tree = ET.parse(data)
+ filt_func = lambda name, attrs: (attrs["Series"], attrs["id"]) if name
== "Episode" else None
+ xml = self._get_update_info(period, filt_func)
+ return xml.get("Episode", [])
- episode_nodes = tree.getiterator("Episode")
- return [(x.findtext("Series"), x.findtext("id")) for x in
episode_nodes]
+ def get_updates(self, callback, period = "day"):
+ """Return all series, episode, and banner updates w/o having to have it
+ all in memory at once. Also returns the Data timestamp. The callback
+ routine should be defined as: my_callback(name, attrs) where name will
+ be "Data", "Series", "Episode", or "Banner", and attrs will be a dict
+ of the values (e.g. id, time, etc)."""
+ self._get_update_info(period, callback=callback)
- def get_show_image_choices(self, show_id):
- """Get a list of image urls and types relating to this show."""
- url = "%s/series/%s/banners.xml" % (self.base_key_url, show_id)
- data = urllib.urlopen(url)
- tree = ET.parse(data)
- print url
-
- images = []
- banner_data = tree.find("Banners")
- banner_nodes = tree.getiterator("Banner")
- for banner in banner_nodes:
- banner_path = banner.findtext("BannerPath")
- banner_type = banner.findtext("BannerType")
- if banner_type == 'season':
- banner_season = banner.findtext("Season")
- else:
- banner_season = ''
- banner_url = "%s/banners/%s" % (self.mirror_url, banner_path)
+ def _get_update_info(self, period, filter_func = None, callback = None):
+ url = "%s/updates/updates_%s.zip" % (self.base_zip_url, period)
+ zip_name = 'updates_%s.xml' % period
+ return self._get_xml_data(url, filter_func, zip_name, callback)
- images.append((banner_url, banner_type, banner_season))
- return images
+ def _get_xml_data(self, url, filter_func = None, zip_name = None, callback
= None):
+ data = urllib.urlopen(url)
+ if zip_name:
+ zipfile = ZipFile(StringIO(data.read()))
+ data = zipfile.open(zip_name)
+ if not data:
+ raise Exception("Failed to get any data")
+
+ e = ExpatParseXml(callback, filter_func)
+ e.parse(data)
+ return e.xml
+
+
+class ExpatParseXml(object):
+ def __init__(self, callback, filter_func):
+ self.el_container = None
+ self.el_name = None
+ self.el_attr_name = None
+ self.el_attrs = None
+ self.el_callback = callback if callback else self.stash_xml
+ self.el_filter_func = filter_func # only used by stash_xml()
+ self.xml = {}
+
+ self.parser = expat.ParserCreate()
+ self.parser.StartElementHandler = self.start_element
+ self.parser.EndElementHandler = self.end_element
+ self.parser.CharacterDataHandler = self.char_data
+
+ def parse(self, fh):
+ # Sadly ParseFile(fh) actually mangles the data, so we parse the file
line by line:
+ for line in fh:
+ self.parser.Parse(line)
+
+ def start_element(self, name, attrs):
+ if not self.el_name:
+ if not self.el_container:
+ self.el_container = name
+ self.el_callback(name, attrs)
+ else:
+ self.el_name = name
+ self.el_attrs = {}
+ elif not self.el_attr_name:
+ self.el_attr_name = name
+
+ def end_element(self, name):
+ if self.el_attr_name and name == self.el_attr_name:
+ self.el_attr_name = None
+ elif self.el_name and name == self.el_name:
+ self.el_callback(self.el_name, self.el_attrs)
+ self.el_name = None
+ self.el_attr_name = None
+
+ def char_data(self, data):
+ if self.el_attr_name:
+ if self.el_attr_name in self.el_attrs:
+ self.el_attrs[self.el_attr_name] += data
+ else:
+ self.el_attrs[self.el_attr_name] = data
+
+ def stash_xml(self, name, attrs):
+ if self.el_filter_func:
+ attrs = self.el_filter_func(name, attrs)
+ if attrs is None:
+ return
+ if name in self.xml:
+ self.xml[name].append(attrs)
+ else:
+ self.xml[name] = [ attrs ]
diff --git a/script.module.metahandler/resources/settings.xml
b/script.module.metahandler/resources/settings.xml
index 23710cd..7110cc7 100644
--- a/script.module.metahandler/resources/settings.xml
+++ b/script.module.metahandler/resources/settings.xml
@@ -2,12 +2,14 @@
<settings>
<category label="Metahandler">
<setting id="meta_folder_location" type="folder" label="Meta Data Save
Location" default="special://profile/addon_data/script.module.metahandler/"/>
- <setting id="use_remote_db" type="bool" label="Use a remote
MySQL DB" default="False"/>
+ <setting id="use_remote_db" type="bool" label="Use a remote
MySQL DB" default="False"/>
<setting id="db_address" type="text" label=" Address"
enable="eq(-1,true)" default="" />
<setting id="db_port" type="integer" label=" Port"
enable="eq(-2,true)" default="" />
<setting id="db_user" type="text" label=" Username"
enable="eq(-3,true)" default="" />
<setting id="db_pass" type="text" label=" Password"
enable="eq(-4,true)" default="" option="hidden"/>
<setting id="db_name" type="text" label=" Database"
enable="eq(-5,true)" default="video_cache" />
+ <setting type="sep" />
+ <setting label="Reset Dababase" type="action"
action="RunScript($CWD/lib/metahandler/rm_DB.py)"/>
</category>
<category label="TMDB (Movies)">
<setting id="tmdb_language" label="Language" type="labelenum"
default="English (en)" values="English (en)|Äeský (cs)|Dansk (da)|Deutsch
(de)|ελληνικά (el)|Español (es)|Français (fr)|Magyar (hu)|Italiano
(it)|Nederlands (nl)|Polski (pl)|Português (pt)|PÑÑÑкий
(ru)|SlovenÅ¡Äina (sk)|Svenska (sv)|Türkçe (tr)"/>
@@ -16,4 +18,7 @@
<setting type="sep" />
<setting id="year_lock" label="Year Lock" type="bool" default="false"/>
</category>
+<category label="TVDB (Series, Mangas and tvshows)">
+ <setting id="tvdb_language" label="Language" type="labelenum"
default="English (en)" values="English (en)|ÄeÅ¡tina (cs)|Dansk (da)|Deutsch
(de)|ελληνικά (el)|Español (es)|Suomeksi (fi)|Français (fr)|
×¢×ר×ת (he)|Hrvatski (hr)|Magyar (hu)|Italiano (it)|æ¥æ¬èª
(ja)|Nederlands (nl)|Norsk (no)|Polski (pl)|Português (pt)|PÑÑÑкий
(ru)|Slovenski (sl)|Svenska (sv)|Türkçe (tr)|䏿 (zh)"/>
+</category>
</settings>
-----------------------------------------------------------------------
Summary of changes:
script.module.metahandler/addon.xml | 4 +-
script.module.metahandler/changelog.txt | 10 +
script.module.metahandler/lib/metahandler/TMDB.py | 27 +-
.../lib/metahandler/metahandlers.py | 213 ++++++++-
script.module.metahandler/lib/metahandler/rm_DB.py | 32 ++
.../lib/metahandler/thetvdbapi.py | 489 +++++++++++---------
script.module.metahandler/resources/settings.xml | 7 +-
7 files changed, 539 insertions(+), 243 deletions(-)
create mode 100644 script.module.metahandler/lib/metahandler/rm_DB.py
hooks/post-receive
--
Scripts
------------------------------------------------------------------------------
"Accelerate Dev Cycles with Automated Cross-Browser Testing - For FREE
Instantly run your Selenium tests across 300+ browser/OS combos.
Get unparalleled scalability from the best Selenium testing platform available
Simple to use. Nothing to install. Get started now for free."
http://p.sf.net/sfu/SauceLabs
_______________________________________________
Xbmc-addons mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/xbmc-addons