Support for downloading extra artwork from Fanart.tv (resolves #1023).

New image types include:
* clearart
* discart
* extrathumbs
* extrafanart
* logo
* banner
* landscape (16:9 Thumb)

There are a couple things that should be noted:
1. Only English images will be downloaded.
2. The fanart image is now downloaded from Fanart.tv if it can find one, otherwise it uses TMDB
like it used to.  This is because the images on Fanart.tv tend to be higher resolutions &
quality.
3. Since multiple extrathumbs and extrafanarts are downloaded into a subdirectory, subdirectories
are now supported for metadata file names.  The subdirectories will be automatically created if
they don't exist.
4. Bluray discart will always be preferred over DVD.  Ideally, it would prefer DVD versions for
SD quality movies, but I couldn't find an easy way to determine the quality from within the
plugin.  I suspect major changes would be needed to the plugin system in general in order to get
this to work.  If a user cares about the distinction, the best work-around is to not download
these in Couchpotato and run the Artwork Downloader addon from within XBMC.
5. A maximum of 4 extrathumbs and 20 extrafanarts will be downloaded.

Squashed commit of the following:

commit b113a4def197a9ca8545bde9f5081c0591b93b36
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Thu Apr 24 14:24:12 2014 -0500

    Bug-fix and code cleanup.

    Fixed a bug where the movie.info event would crash if there aren't any pictures to scrape in
    fanart.tv.

commit adf7a4675d472e9e95a316c6cccc681a52804f13
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 16:15:03 2014 -0500

    Added support for extrafanart.
    Also, the main fanart will be taken from fanart.tv unless one
    does not exist.

commit 1791e46c8602f40bb56fe0cf7ecb0607f35b4b12
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 15:13:14 2014 -0500

    Couchpotato now downloads extrathumbs from the extra tmdb backdrops if they exist.

    This commit made some major changes to the core image creation functionality that
    makes writing multiple images to folders possible.

commit c0858807873749dbc928c0260037138f51f894ca
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 12:18:53 2014 -0500

    Bug Fix & Implemented functionality to select bluray or dvd disc images.

    Currently, only blurays will be selected, unless there are no blurays.
    However, if a mechanism for determining the quality of the release is
    implemented, it would be simple to make this selection based on the
    quality.

commit 786751371d243f53d0f5c6f2c38d92288d8608ba
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:59:25 2014 -0500

    Fixed a bug where non-HD clearart and logos couldn't be downloaded.

commit feda8df483d13b5a5df3a869f25de8f2c7e6ffe3
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:12:31 2014 -0500

    Fixed some problems that were missed with the previous merge.

commit 5ddab6c40e69a5accc6c0336cd7485920ff82d8f
Merge: 7273abf ff46aa0
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:02:11 2014 -0500

    Merge branch 'develop' into fanarttv

    Conflicts:
    	couchpotato/core/media/movie/providers/info/themoviedb.py
    	couchpotato/core/providers/metadata/xbmc/__init__.py

commit 7273abf827735cf245711c3d3199a6a173a964aa
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Thu Feb 27 13:29:57 2014 -0600

    Downloads extra artwork from fanart.tv

    Downloads occur with correct filenaming when XBMC metadata is generated,
    but the image URLs are selected when the movie.info event is called.

commit 9080d9d749c7e1ddbdc78f7b37a3c5f83195d580
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 16:31:37 2014 -0600

    Added basic functionality for fanarttv provider.

    This should be mostly done and is based on the tvdb provider.

commit 1b39b246c2a9d65f9ef86c4e150a12d893e362c0
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 14:50:17 2014 -0600

    Updated fanarttv library with the correct package hierarchy
    (libs.fanarttv).

commit 8abb7c8f8ad3347900debb9f6a6d5a7acb7df396
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 13:12:48 2014 -0600

    Added fanart.tv API python library (lib.fanarttv).

    The upstream for this library is at
    https://github.com/z4r/python-fanart.
This commit is contained in:
Dan Boehm
2014-04-24 15:00:04 -05:00
parent ff46aa0226
commit 5ce817cee6
13 changed files with 1037 additions and 45 deletions
@@ -26,7 +26,14 @@ class MovieResultModifier(Plugin):
'backdrop': [],
'poster_original': [],
'backdrop_original': [],
'actors': {}
'actors': {},
'landscape': [],
'logo': [],
'clearart': [],
'discart': [],
'banner': [],
'extrathumbs': [],
'extrafanart': []
},
'runtime': 0,
'plot': '',
@@ -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',
},
],
},
],
}]
@@ -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') == '':
@@ -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)
@@ -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
}
],
},
],
+110
View File
@@ -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)
+44
View File
@@ -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))
+15
View File
@@ -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
+46
View File
@@ -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
+68
View File
@@ -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()]
+103
View File
@@ -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),
)
+80
View File
@@ -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),
)
+108
View File
@@ -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),
)