diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index bf257272..beb29e74 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -26,7 +26,14 @@ class MovieResultModifier(Plugin): 'backdrop': [], 'poster_original': [], 'backdrop_original': [], - 'actors': {} + 'actors': {}, + 'landscape': [], + 'logo': [], + 'clearart': [], + 'discart': [], + 'banner': [], + 'extrathumbs': [], + 'extrafanart': [] }, 'runtime': 0, 'plot': '', diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py new file mode 100644 index 00000000..07d1a009 --- /dev/null +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -0,0 +1,168 @@ +import os +import traceback + +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media.movie.providers.base import MovieProvider +from couchpotato.core.plugins.quality import QualityPlugin + +from libs.fanarttv.movie import Movie +import libs.fanarttv.errors as fanarttv_errors + + +log = CPLog(__name__) + +autoload = 'FanartTV' + + +class FanartTV(MovieProvider): + MAX_EXTRAFANART = 20 + + def __init__(self): + addEvent('movie.extraart', self.getArt, priority=2) + + # Configure fanarttv API settings + os.environ.setdefault('FANART_APIKEY', self.conf('api_key')) + + def getArt(self, identifier): + # FIXME: I believe I should be registering a cache here... I need to look into that. + log.debug("Getting Extra Artwork from Fanart.tv...") + if not identifier: + return {} + + images = {} + + try: + try: + exists = True + movie = Movie.get(id=identifier) + except (fanarttv_errors.FanartError, IOError): + exists = False + + if exists: + images = self._parseMovie(movie, True) + + except: + log.error('Failed getting extra art for %s: %s', + (identifier, traceback.format_exc())) + return {} + + return images + + def _parseMovie(self, movie, isHD): + images = { + 'landscape': [], + 'logo': [], + 'discart': [], + 'clearart': [], + 'banner': [], + 'extrafanart': [] + } + + images['landscape'] = self._getMultImages(movie.thumbs, 1) + images['banner'] = self._getMultImages(movie.banners, 1) + images['discart'] = self._getMultImages(self._trimDiscs(movie.discs, isHD), 1) + + images['clearart'] = self._getMultImages(movie.hdarts, 1) + if len(images['clearart']) is 0: + images['clearart'] = self._getMultImages(movie.arts, 1) + + images['logo'] = self._getMultImages(movie.hdlogos, 1) + if len(images['logo']) is 0: + images['logo'] = self._getMultImages(movie.logos, 1) + + fanarts = self._getMultImages(movie.backgrounds, self.MAX_EXTRAFANART + 1) + + if fanarts: + images['backdrop_original'] = fanarts[0] + images['extrafanart'] = fanarts[1:] + + # TODO: Add support for extra backgrounds + #extraFanart = self._getMultImages(movie.backgrounds, -1) + + return images + + def _trimDiscs(self, discImages, isHD): + ''' + Return a subset of discImages based on isHD. If isHD is true, only + bluray disc images will be returned. If isHD is false, only dvd disc + images will be returned. If the resulting list would be an empty list, + then the original list is returned instead. + ''' + trimmed = [] + for disc in discImages: + if isHD and disc.disc_type == u'bluray': + trimmed.append(disc) + elif not isHD and disc.disc_type == u'dvd': + trimmed.append(disc) + + if len(trimmed) is 0: + return discImages + else: + return trimmed + + def _getImage(self, images): + image_url = None + highscore = -1 + for image in images: + if image.likes > highscore: + highscore = image.likes + image_url = image.url + + return image_url + + def _getMultImages(self, images, n): + ''' + Chooses the best n images and returns them as a list. + If n<0, all images will be returned. + ''' + image_urls = [] + pool = [] + for image in images: + if image.lang == u'en': + pool.append(image) + origPoolSize = len(pool) + + while len(pool) > 0 and (n < 0 or origPoolSize - len(pool) < n): + best = None + highscore = -1 + for image in pool: + if image.likes > highscore: + highscore = image.likes + best = image + image_urls.append(best.url) + pool.remove(best) + + return image_urls + + def isDisabled(self): + if self.conf('api_key') == '': + log.error('No API key provided.') + return True + return False + + def _determineHD(self, quality): + for qualityDef in QualityPlugin.qualities: + if quality == qualityDef.get('identifier'): + return bool(qualityDef.get('hd')) + return False + +config = [{ + 'name': 'fanarttv', + 'groups': [ + { + 'tab': 'providers', + 'name': 'fanarttv', + 'label': 'fanart.tv', + 'hidden': True, + 'description': 'Used for all calls to fanart.tv.', + 'options': [ + { + 'name': 'api_key', + 'default': 'd788b4822b9e1f44068026e05557e5d9', + 'label': 'API Key', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index b48822cc..7ffba38b 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -1,6 +1,6 @@ import traceback -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog @@ -13,6 +13,7 @@ autoload = 'TheMovieDb' class TheMovieDb(MovieProvider): + MAX_EXTRATHUMBS = 4 def __init__(self): addEvent('movie.info', self.getInfo, priority = 2) @@ -87,6 +88,13 @@ class TheMovieDb(MovieProvider): except: log.error('Failed getting info for %s: %s', (identifier, traceback.format_exc())) + # Get extra artwork via Fanart.TV and merge into images dict + try: + extraArt = fireEvent('movie.extraart', identifier)[0] + result['images'] = dict(result['images'].items() + extraArt.items()) + except IndexError: + pass + return result def parseMovie(self, movie, extended = True): @@ -100,13 +108,15 @@ class TheMovieDb(MovieProvider): poster = self.getImage(movie, type = 'poster', size = 'poster') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') + extrathumbs = self.getMultImages(movie, type='backdrops', size='original', n=self.MAX_EXTRATHUMBS, skipfirst=True) images = { 'poster': [poster] if poster else [], #'backdrop': [backdrop] if backdrop else [], 'poster_original': [poster_original] if poster_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [], - 'actors': {} + 'actors': {}, + 'extrathumbs': extrathumbs } # Genres @@ -171,6 +181,28 @@ class TheMovieDb(MovieProvider): log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) return image_url + + def getMultImages(self, movie, type='backdrops', size='original', n=-1, skipfirst=False): + ''' + If n < 0, return all images. Otherwise return n images. + If n > len(getattr(movie, type)), then return all images. + If skipfirst is True, then it will skip getattr(movie, type)[0]. This + is because backdrops[0] is typically backdrop. + ''' + image_urls = [] + try: + images = getattr(movie, type) + if n < 0 or n > len(images): + numImages = len(images) + else: + numImages = n + for i in range(int(skipfirst), numImages + int(skipfirst)): + image_urls.append(images[i].geturl(size=size)) + + except: + log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie)))) + + return image_urls def isDisabled(self): if self.conf('api_key') == '': diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index b9fc5d71..ec91b062 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -37,76 +37,150 @@ class MovieMetaData(MetaDataBase): root = os.path.dirname(root_name) movie_info = group['media'].get('info') - - for file_type in ['nfo', 'thumbnail', 'fanart']: + + for file_type in ['nfo']: try: - # Get file path - name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root) + self._createType(meta_name, root, movie_info, group, file_type, 0) + except: + log.error('Unable to create %s file: %s', ('nfo', traceback.format_exc())) - if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): - - # Get file content - content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group) - if content: - log.debug('Creating %s file: %s', (file_type, name)) - if os.path.isfile(content): - content = sp(content) - name = sp(name) - - shutil.copy2(content, name) - shutil.copyfile(content, name) - - # Try and copy stats seperately - try: shutil.copystat(content, name) - except: pass - else: - self.createFile(name, content) - group['renamed_files'].append(name) - - try: - os.chmod(sp(name), Env.getPermission('file')) - except: - log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) + for file_type in ['thumbnail', 'fanart', 'banner', 'discart', 'logo', 'clearart', 'landscape', 'extrathumbs', 'extrafanart']: + try: + if file_type == 'thumbnail': + numImages = len(movie_info['images']['poster_original']) + elif file_type == 'fanart': + numImages = len(movie_info['images']['backdrop_original']) + else: + numImages = len(movie_info['images'][file_type]) + for i in range(numImages): + self._createType(meta_name, root, movie_info, group, file_type, i) except: log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) + def _createType(self, meta_name, root, movie_info, group, file_type, i):# Get file path + name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root, i) + + if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): + + # Get file content + content = getattr(self, 'get' + file_type.capitalize())(movie_info=movie_info, data=group, i=i) + if content: + log.debug('Creating %s file: %s', (file_type, name)) + if os.path.isfile(content): + content = sp(content) + name = sp(name) + + if not os.path.exists(os.path.dirname(name)): + os.makedirs(os.path.dirname(name)) + + shutil.copy2(content, name) + shutil.copyfile(content, name) + + # Try and copy stats seperately + try: shutil.copystat(content, name) + except: pass + else: + self.createFile(name, content) + group['renamed_files'].append(name) + + try: + os.chmod(sp(name), Env.getPermission('file')) + except: + log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) + def getRootName(self, data = None): if not data: data = {} return os.path.join(data['destination_dir'], data['filename']) - def getFanartName(self, name, root): + def getFanartName(self, name, root, i): return - def getThumbnailName(self, name, root): + def getThumbnailName(self, name, root, i): return - def getNfoName(self, name, root): + def getBannerName(self, name, root, i): return - def getNfo(self, movie_info = None, data = None): + def getClearartName(self, name, root, i): + return + + def getLogoName(self, name, root, i): + return + + def getDiscartName(self, name, root, i): + return + + def getLandscapeName(self, name, root, i): + return + + def getExtrathumbsName(self, name, root, i): + return + + def getExtrafanartName(self, name, root, i): + return + + def getNfoName(self, name, root, i): + return + + def getNfo(self, movie_info=None, data=None, i=0): if not data: data = {} if not movie_info: movie_info = {} - def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original'): + def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original', i = 0): if not data: data = {} if not movie_info: movie_info = {} # See if it is in current files files = data['media'].get('files') if files.get('image_' + wanted_file_type): - if os.path.isfile(files['image_' + wanted_file_type][0]): - return files['image_' + wanted_file_type][0] + if os.path.isfile(files['image_' + wanted_file_type][i]): + return files['image_' + wanted_file_type][i] # Download using existing info try: images = movie_info['images'][wanted_file_type] - file_path = fireEvent('file.download', url = images[0], single = True) + file_path = fireEvent('file.download', url = images[i], single = True) return file_path except: pass - def getFanart(self, movie_info = None, data = None): + def getFanart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original') + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='backdrop_original', i=i) + + def getBanner(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='banner', i=i) + + def getClearart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='clearart', i=i) + + def getLogo(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='logo', i=i) + + def getDiscart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='discart', i=i) + + def getLandscape(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='landscape', i=i) + + def getExtrathumbs(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrathumbs', i=i) + + def getExtrafanart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrafanart', i=i) diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index 9f6bb7dc..318f79a7 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -17,19 +17,43 @@ autoload = 'XBMC' class XBMC(MovieMetaData): - def getFanartName(self, name, root): + def getFanartName(self, name, root, i): return self.createMetaName(self.conf('meta_fanart_name'), name, root) - def getThumbnailName(self, name, root): + def getThumbnailName(self, name, root, i): return self.createMetaName(self.conf('meta_thumbnail_name'), name, root) - def getNfoName(self, name, root): + def getNfoName(self, name, root, i): return self.createMetaName(self.conf('meta_nfo_name'), name, root) + def getBannerName(self, name, root, i): + return self.createMetaName(self.conf('meta_banner_name'), name, root) + + def getClearartName(self, name, root, i): + return self.createMetaName(self.conf('meta_clearart_name'), name, root) + + def getLogoName(self, name, root, i): + return self.createMetaName(self.conf('meta_logo_name'), name, root) + + def getDiscartName(self, name, root, i): + return self.createMetaName(self.conf('meta_discart_name'), name, root) + + def getLandscapeName(self, name, root, i): + return self.createMetaName(self.conf('meta_landscape_name'), name, root) + + def getExtrathumbsName(self, name, root, i): + return self.createMetaNameMult(self.conf('meta_extrathumbs_name'), name, root, i) + + def getExtrafanartName(self, name, root, i): + return self.createMetaNameMult(self.conf('meta_extrafanart_name'), name, root, i) + def createMetaName(self, basename, name, root): return os.path.join(root, basename.replace('%s', name)) - def getNfo(self, movie_info = None, data = None): + def createMetaNameMult(self, basename, name, root, i): + return os.path.join(root, basename.replace('%s', name).replace('%i', str(i + 1))) + + def getNfo(self, movie_info=None, data=None, i=0): if not data: data = {} if not movie_info: movie_info = {} @@ -133,6 +157,35 @@ class XBMC(MovieMetaData): for image_url in movie_info['images']['backdrop_original']: image = SubElement(fanart, 'thumb') image.text = toUnicode(image_url) + banner = SubElement(nfoxml, 'banner') + for image_url in movie_info['images']['banner']: + image = SubElement(banner, 'thumb') + image.text = toUnicode(image_url) + discart = SubElement(nfoxml, 'discart') + for image_url in movie_info['images']['discart']: + image = SubElement(discart, 'thumb') + image.text = toUnicode(image_url) + logo = SubElement(nfoxml, 'logo') + for image_url in movie_info['images']['logo']: + image = SubElement(logo, 'thumb') + image.text = toUnicode(image_url) + clearart = SubElement(nfoxml, 'clearart') + for image_url in movie_info['images']['clearart']: + image = SubElement(clearart, 'thumb') + image.text = toUnicode(image_url) + landscape = SubElement(nfoxml, 'landscape') + for image_url in movie_info['images']['landscape']: + image = SubElement(landscape, 'thumb') + image.text = toUnicode(image_url) + extrathumb = SubElement(nfoxml, 'extrathumb') + for image_url in movie_info['images']['extrathumbs']: + image = SubElement(extrathumb, 'thumb') + image.text = toUnicode(image_url) + extrafanart = SubElement(nfoxml, 'extrafanart') + for image_url in movie_info['images']['extrafanart']: + image = SubElement(extrafanart, 'thumb') + image.text = toUnicode(image_url) + # Add trailer if found trailer_found = False @@ -239,6 +292,90 @@ config = [{ 'default': '%s.tbn', 'advanced': True, }, + { + 'name': 'meta_banner', + 'label': 'Banner', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_banner_name', + 'label': 'Banner filename', + 'default': 'banner.jpg', + 'advanced': True, + }, + { + 'name': 'meta_clearart', + 'label': 'ClearArt', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_clearart_name', + 'label': 'ClearArt filename', + 'default': 'clearart.png', + 'advanced': True, + }, + { + 'name': 'meta_disc', + 'label': 'DiscArt', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_discart_name', + 'label': 'DiscArt filename', + 'default': 'disc.png', + 'advanced': True, + }, + { + 'name': 'meta_landscape', + 'label': 'Landscape', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_landscape_name', + 'label': 'Landscape filename', + 'default': 'landscape.jpg', + 'advanced': True, + }, + { + 'name': 'meta_logo', + 'label': 'ClearLogo', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_logo_name', + 'label': 'ClearLogo filename', + 'default': 'logo.png', + 'advanced': True, + }, + { + 'name': 'meta_extrathumbs', + 'label': 'Extrathumbs', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_extrathumbs_name', + 'label': 'Extrathumbs filename (%i is the image number, and must be included to have multiple images).', + 'default': 'extrathumbs/thumb%i.jpg', + 'advanced': True + }, + { + 'name': 'meta_extrafanart', + 'lavel': 'Extrafanart', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_extrafanart_name', + 'label': 'Extrafanart filename (%i is the image number, and must be included to have multiple images).', + 'default': 'extrafanart/extrafanart%i.jpg', + 'advanced': True + } ], }, ], diff --git a/couchpotato/core/notifications/xbmc.py b/couchpotato/core/notifications/xbmc.py index 28439a75..db343068 100644 --- a/couchpotato/core/notifications/xbmc.py +++ b/couchpotato/core/notifications/xbmc.py @@ -3,6 +3,8 @@ import json import socket import traceback import urllib +import time +import os from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog @@ -36,7 +38,7 @@ class XBMC(Notification): if self.use_json_notifications.get(host): calls = [ - ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), + ('GUI.ShowNotification', None, {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ] if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): @@ -44,7 +46,7 @@ class XBMC(Notification): if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])): param = {'directory': data['destination_dir']} - calls.append(('VideoLibrary.Scan', param)) + calls.append(('VideoLibrary.Scan', None, param)) max_successful += len(calls) response = self.request(host, calls) @@ -66,6 +68,50 @@ class XBMC(Notification): except: log.error('Failed parsing results: %s', traceback.format_exc()) + + if self.conf('run_artwork_downloader') and data and self.use_json_notifications.get(host): + time.sleep(self.conf('run_artwork_downloader_delay')) + + if self.conf('force_full_scan'): + calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader'})] + max_successful += len(calls) + response = self.request(host, calls) + + try: + if response[0].get('result') and result['result'] == 'OK': + successful += 1 + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) + else: + calls = [('VideoLibrary.GetMovies', 'libMovies', {"filter":{"field": "title", "operator": "is", "value": data['media']['title'], "year": data['media']['info']['year']}})] + max_successful += len(calls) + response = self.request(host, calls) + + dbid = None + try: + if response[0].get('result'): + successful += 1 + dbid = response[0]['result']['movies'][-1]['movieid'] + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) + + if dbid is not None: + calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader', 'params':{'media_type': 'movie', 'dbid': str(dbid)}})] + + max_successful += len(calls) + response = self.request(host, calls) + + try: + if response[0].get('result') and result['result'] == 'OK': + successful += 1 + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) return successful == max_successful @@ -75,7 +121,7 @@ class XBMC(Notification): # XBMC JSON-RPC version request response = self.request(host, [ - ('JSONRPC.Version', {}) + ('JSONRPC.Version', None, {}) ]) for result in response: if result.get('result') and type(result['result']['version']).__name__ == 'int': @@ -112,7 +158,7 @@ class XBMC(Notification): self.use_json_notifications[host] = True # send the text message - resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) + resp = self.request(host, [('GUI.ShowNotification', None, {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) for r in resp: if r.get('result') and r['result'] == 'OK': log.debug('Message delivered successfully!') @@ -184,12 +230,16 @@ class XBMC(Notification): data = [] for req in do_requests: - method, kwargs = req + method, id, kwargs = req + + if id is None: + id = method + data.append({ 'method': method, 'params': kwargs, 'jsonrpc': '2.0', - 'id': method, + 'id': id, }) data = json.dumps(data) @@ -273,6 +323,22 @@ config = [{ 'advanced': True, 'description': 'Also send message when movie is snatched.', }, + { + 'name': 'run_artwork_downloader', + 'label': 'Run the Artwork Downloader', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Runs the Artwork Downloader script to initialize/download artwork. (Requires the Artwork Downloader addon and XBMC Frodo or later)' + }, + { + 'name': 'run_artwork_downloader_delay', + 'label': 'Artwork Downloader delay', + 'default': 5, + 'type': 'int', + 'advanced': True, + 'description': 'Number of seconds to wait to start the Artwork Downloader script after notifying XBMC.', + }, ], } ], diff --git a/libs/fanarttv/__init__.py b/libs/fanarttv/__init__.py new file mode 100644 index 00000000..773703ca --- /dev/null +++ b/libs/fanarttv/__init__.py @@ -0,0 +1,110 @@ +__author__ = 'Andrea De Marco <24erre@gmail.com>' +__version__ = '1.4.0' +__classifiers__ = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Software Development :: Libraries', +] +__copyright__ = "2012, %s " % __author__ +__license__ = """ + Copyright %s. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" % __copyright__ + +__docformat__ = 'restructuredtext en' + +__doc__ = """ +:abstract: Python interface to fanart.tv API +:version: %s +:author: %s +:contact: http://z4r.github.com/ +:date: 2012-04-04 +:copyright: %s +""" % (__version__, __author__, __license__) + + +def values(obj): + return [v for k, v in obj.__dict__.iteritems() if not k.startswith('_')] + +BASEURL = 'http://api.fanart.tv/webservice' + + +class FORMAT(object): + JSON = 'JSON' + XML = 'XML' + PHP = 'PHP' + + +class WS(object): + MUSIC = 'artist' + MOVIE = 'movie' + TV = 'series' + + +class TYPE(object): + ALL = 'all' + + class TV(object): + ART = 'clearart' + LOGO = 'clearlogo' + CHARACTER = 'characterart' + THUMB = 'tvthumb' + SEASONTHUMB = 'seasonthumb' + BACKGROUND = 'showbackground' + HDLOGO = 'hdtvlogo' + HDART = 'hdclearart' + POSTER = 'tvposter' + BANNER = 'tvbanner' + + class MUSIC(object): + DISC = 'cdart' + LOGO = 'musiclogo' + BACKGROUND = 'artistbackground' + COVER = 'albumcover' + THUMB = 'artistthumb' + + class MOVIE(object): + ART = 'movieart' + LOGO = 'movielogo' + DISC = 'moviedisc' + POSTER = 'movieposter' + BACKGROUND = 'moviebackground' + HDLOGO = 'hdmovielogo' + HDART = 'hdmovieclearart' + BANNER = 'moviebanner' + THUMB = 'moviethumb' + + +class SORT(object): + POPULAR = 1 + NEWEST = 2 + OLDEST = 3 + + +class LIMIT(object): + ONE = 1 + ALL = 2 + +FORMAT_LIST = values(FORMAT) +WS_LIST = values(WS) +TYPE_LIST = values(TYPE.MUSIC) + values(TYPE.TV) + values(TYPE.MOVIE) + [TYPE.ALL] +MUSIC_TYPE_LIST = values(TYPE.MUSIC) + [TYPE.ALL] +TV_TYPE_LIST = values(TYPE.TV) + [TYPE.ALL] +MOVIE_TYPE_LIST = values(TYPE.MOVIE) + [TYPE.ALL] +SORT_LIST = values(SORT) +LIMIT_LIST = values(LIMIT) diff --git a/libs/fanarttv/core.py b/libs/fanarttv/core.py new file mode 100644 index 00000000..9cd1cad7 --- /dev/null +++ b/libs/fanarttv/core.py @@ -0,0 +1,44 @@ +import libs.requests as requests +import libs.fanarttv as fanart +from libs.fanarttv.errors import RequestFanartError, ResponseFanartError + + +class Request(object): + def __init__(self, apikey, id, ws, type=None, sort=None, limit=None): + self._apikey = apikey + self._id = id + self._ws = ws + self._type = type or fanart.TYPE.ALL + self._sort = sort or fanart.SORT.POPULAR + self._limit = limit or fanart.LIMIT.ALL + self.validate() + self._response = None + + def validate(self): + for attribute_name in ('ws', 'type', 'sort', 'limit'): + attribute = getattr(self, '_' + attribute_name) + choices = getattr(fanart, attribute_name.upper() + '_LIST') + if attribute not in choices: + raise RequestFanartError('Not allowed {0}: {1} [{2}]'.format(attribute_name, attribute, ', '.join(choices))) + + def __str__(self): + return '/'.join(map(str, [ + fanart.BASEURL, + self._ws, + self._apikey, + self._id, + fanart.FORMAT.JSON, + self._type, + self._sort, + self._limit, + ])) + + def response(self): + try: + response = requests.get(str(self)) + rjson = response.json() + if not isinstance(rjson, dict): + raise Exception(response.text) + return rjson + except Exception as e: + raise ResponseFanartError(str(e)) diff --git a/libs/fanarttv/errors.py b/libs/fanarttv/errors.py new file mode 100644 index 00000000..95a71e35 --- /dev/null +++ b/libs/fanarttv/errors.py @@ -0,0 +1,15 @@ +class FanartError(Exception): + def __str__(self): + return ', '.join(map(str, self.args)) + + def __repr__(self): + name = self.__class__.__name__ + return '%s%r' % (name, self.args) + + +class ResponseFanartError(FanartError): + pass + + +class RequestFanartError(FanartError): + pass diff --git a/libs/fanarttv/immutable.py b/libs/fanarttv/immutable.py new file mode 100644 index 00000000..170de370 --- /dev/null +++ b/libs/fanarttv/immutable.py @@ -0,0 +1,46 @@ +class Immutable(object): + _mutable = False + + def __setattr__(self, name, value): + if self._mutable or name == '_mutable': + super(Immutable, self).__setattr__(name, value) + else: + raise TypeError("Can't modify immutable instance") + + def __delattr__(self, name): + if self._mutable: + super(Immutable, self).__delattr__(name) + else: + raise TypeError("Can't modify immutable instance") + + def __eq__(self, other): + return hash(self) == hash(other) + + def __hash__(self): + return hash(repr(self)) + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + ', '.join(['{0}={1}'.format(k, repr(v)) for k, v in self]) + ) + + def __iter__(self): + l = self.__dict__.keys() + l.sort() + for k in l: + if not k.startswith('_'): + yield k, getattr(self, k) + + @staticmethod + def mutablemethod(f): + def func(self, *args, **kwargs): + if isinstance(self, Immutable): + old_mutable = self._mutable + self._mutable = True + res = f(self, *args, **kwargs) + self._mutable = old_mutable + else: + res = f(self, *args, **kwargs) + return res + return func diff --git a/libs/fanarttv/items.py b/libs/fanarttv/items.py new file mode 100644 index 00000000..64848076 --- /dev/null +++ b/libs/fanarttv/items.py @@ -0,0 +1,68 @@ +import json +import os +import libs.requests as requests +from libs.fanarttv.core import Request +from libs.fanarttv.immutable import Immutable + + +class LeafItem(Immutable): + KEY = NotImplemented + + @Immutable.mutablemethod + def __init__(self, id, url, likes): + self.id = int(id) + self.url = url + self.likes = int(likes) + self._content = None + + @classmethod + def from_dict(cls, resource): + return cls(**dict([(str(k), v) for k, v in resource.iteritems()])) + + @classmethod + def extract(cls, resource): + return [cls.from_dict(i) for i in resource.get(cls.KEY, {})] + + @Immutable.mutablemethod + def content(self): + if not self._content: + self._content = requests.get(self.url).content + return self._content + + def __str__(self): + return self.url + + +class ResourceItem(Immutable): + WS = NotImplemented + request_cls = Request + + @classmethod + def from_dict(cls, map): + raise NotImplementedError + + @classmethod + def get(cls, id): + map = cls.request_cls( + apikey=os.environ.get('FANART_APIKEY'), + id=id, + ws=cls.WS + ).response() + return cls.from_dict(map) + + def json(self, **kw): + return json.dumps( + self, + default=lambda o: dict([(k, v) for k, v in o.__dict__.items() if not k.startswith('_')]), + **kw + ) + + +class CollectableItem(Immutable): + @classmethod + def from_dict(cls, key, map): + raise NotImplementedError + + @classmethod + def collection_from_dict(cls, map): + return [cls.from_dict(k, v) for k, v in map.iteritems()] diff --git a/libs/fanarttv/movie.py b/libs/fanarttv/movie.py new file mode 100644 index 00000000..fe473d4d --- /dev/null +++ b/libs/fanarttv/movie.py @@ -0,0 +1,103 @@ +import libs.fanarttv as fanart +from libs.fanarttv.items import LeafItem, Immutable, ResourceItem +__all__ = ( + 'ArtItem', + 'DiscItem', + 'LogoItem', + 'PosterItem', + 'BackgroundItem', + 'HdLogoItem', + 'HdArtItem', + 'BannerItem', + 'ThumbItem', + 'Movie', +) + + +class MovieItem(LeafItem): + + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang): + super(MovieItem, self).__init__(id, url, likes) + self.lang = lang + + +class DiscItem(MovieItem): + KEY = fanart.TYPE.MOVIE.DISC + + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang, disc, disc_type): + super(DiscItem, self).__init__(id, url, likes, lang) + self.disc = int(disc) + self.disc_type = disc_type + + +class ArtItem(MovieItem): + KEY = fanart.TYPE.MOVIE.ART + + +class LogoItem(MovieItem): + KEY = fanart.TYPE.MOVIE.LOGO + + +class PosterItem(MovieItem): + KEY = fanart.TYPE.MOVIE.POSTER + + +class BackgroundItem(MovieItem): + KEY = fanart.TYPE.MOVIE.BACKGROUND + + +class HdLogoItem(MovieItem): + KEY = fanart.TYPE.MOVIE.HDLOGO + + +class HdArtItem(MovieItem): + KEY = fanart.TYPE.MOVIE.HDART + + +class BannerItem(MovieItem): + KEY = fanart.TYPE.MOVIE.BANNER + + +class ThumbItem(MovieItem): + KEY = fanart.TYPE.MOVIE.THUMB + + +class Movie(ResourceItem): + WS = fanart.WS.MOVIE + + @Immutable.mutablemethod + def __init__(self, name, imdbid, tmdbid, arts, logos, discs, posters, backgrounds, hdlogos, hdarts, + banners, thumbs): + self.name = name + self.imdbid = imdbid + self.tmdbid = tmdbid + self.arts = arts + self.posters = posters + self.logos = logos + self.discs = discs + self.backgrounds = backgrounds + self.hdlogos = hdlogos + self.hdarts = hdarts + self.banners = banners + self.thumbs = thumbs + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + imdbid=resource['imdb_id'], + tmdbid=resource['tmdb_id'], + arts=ArtItem.extract(resource), + logos=LogoItem.extract(resource), + discs=DiscItem.extract(resource), + posters=PosterItem.extract(resource), + backgrounds=BackgroundItem.extract(resource), + hdlogos=HdLogoItem.extract(resource), + hdarts=HdArtItem.extract(resource), + banners=BannerItem.extract(resource), + thumbs=ThumbItem.extract(resource), + ) diff --git a/libs/fanarttv/music.py b/libs/fanarttv/music.py new file mode 100644 index 00000000..df634c67 --- /dev/null +++ b/libs/fanarttv/music.py @@ -0,0 +1,80 @@ +from libs.fanarttv.items import Immutable, LeafItem, ResourceItem, CollectableItem +import libs.fanarttv as fanart +__all__ = ( + 'BackgroundItem', + 'CoverItem', + 'LogoItem', + 'ThumbItem', + 'DiscItem', + 'Artist', + 'Album', +) + + +class BackgroundItem(LeafItem): + KEY = fanart.TYPE.MUSIC.BACKGROUND + + +class CoverItem(LeafItem): + KEY = fanart.TYPE.MUSIC.COVER + + +class LogoItem(LeafItem): + KEY = fanart.TYPE.MUSIC.LOGO + + +class ThumbItem(LeafItem): + KEY = fanart.TYPE.MUSIC.THUMB + + +class DiscItem(LeafItem): + KEY = fanart.TYPE.MUSIC.DISC + + @Immutable.mutablemethod + def __init__(self, id, url, likes, disc, size): + super(DiscItem, self).__init__(id, url, likes) + self.disc = int(disc) + self.size = int(size) + + +class Artist(ResourceItem): + WS = fanart.WS.MUSIC + + @Immutable.mutablemethod + def __init__(self, name, mbid, albums, backgrounds, logos, thumbs): + self.name = name + self.mbid = mbid + self.albums = albums + self.backgrounds = backgrounds + self.logos = logos + self.thumbs = thumbs + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + mbid=resource['mbid_id'], + albums=Album.collection_from_dict(resource.get('albums', {})), + backgrounds=BackgroundItem.extract(resource), + thumbs=ThumbItem.extract(resource), + logos=LogoItem.extract(resource), + ) + + +class Album(CollectableItem): + + @Immutable.mutablemethod + def __init__(self, mbid, covers, arts): + self.mbid = mbid + self.covers = covers + self.arts = arts + + @classmethod + def from_dict(cls, key, resource): + return cls( + mbid=key, + covers=CoverItem.extract(resource), + arts=DiscItem.extract(resource), + ) diff --git a/libs/fanarttv/tv.py b/libs/fanarttv/tv.py new file mode 100644 index 00000000..9b1b08a7 --- /dev/null +++ b/libs/fanarttv/tv.py @@ -0,0 +1,108 @@ +import libs.fanarttv as fanart +from libs.fanarttv.items import LeafItem, Immutable, ResourceItem +__all__ = ( + 'CharacterItem', + 'ArtItem', + 'LogoItem', + 'BackgroundItem', + 'SeasonItem', + 'ThumbItem', + 'HdLogoItem', + 'HdArtItem', + 'PosterItem', + 'BannerItem', + 'TvShow', +) + + +class TvItem(LeafItem): + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang): + super(TvItem, self).__init__(id, url, likes) + self.lang = lang + + +class SeasonedTvItem(TvItem): + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang, season): + super(SeasonedTvItem, self).__init__(id, url, likes, lang) + self.season = 0 if season == 'all' else int(season or 0) + + +class CharacterItem(TvItem): + KEY = fanart.TYPE.TV.CHARACTER + + +class ArtItem(TvItem): + KEY = fanart.TYPE.TV.ART + + +class LogoItem(TvItem): + KEY = fanart.TYPE.TV.LOGO + + +class BackgroundItem(SeasonedTvItem): + KEY = fanart.TYPE.TV.BACKGROUND + + +class SeasonItem(SeasonedTvItem): + KEY = fanart.TYPE.TV.SEASONTHUMB + + +class ThumbItem(TvItem): + KEY = fanart.TYPE.TV.THUMB + + +class HdLogoItem(TvItem): + KEY = fanart.TYPE.TV.HDLOGO + + +class HdArtItem(TvItem): + KEY = fanart.TYPE.TV.HDART + + +class PosterItem(TvItem): + KEY = fanart.TYPE.TV.POSTER + + +class BannerItem(TvItem): + KEY = fanart.TYPE.TV.BANNER + + +class TvShow(ResourceItem): + WS = fanart.WS.TV + + @Immutable.mutablemethod + def __init__(self, name, tvdbid, backgrounds, characters, arts, logos, seasons, thumbs, hdlogos, hdarts, posters, + banners): + self.name = name + self.tvdbid = tvdbid + self.backgrounds = backgrounds + self.characters = characters + self.arts = arts + self.logos = logos + self.seasons = seasons + self.thumbs = thumbs + self.hdlogos = hdlogos + self.hdarts = hdarts + self.posters = posters + self.banners = banners + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + tvdbid=resource['thetvdb_id'], + backgrounds=BackgroundItem.extract(resource), + characters=CharacterItem.extract(resource), + arts=ArtItem.extract(resource), + logos=LogoItem.extract(resource), + seasons=SeasonItem.extract(resource), + thumbs=ThumbItem.extract(resource), + hdlogos=HdLogoItem.extract(resource), + hdarts=HdArtItem.extract(resource), + posters=PosterItem.extract(resource), + banners=BannerItem.extract(resource), + )