429 lines
17 KiB
Python
429 lines
17 KiB
Python
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
from __future__ import absolute_import, division, unicode_literals
|
||
|
from logging import getLogger
|
||
|
from re import sub
|
||
|
|
||
|
from ..kodi_db import KodiVideoDB, KodiMusicDB
|
||
|
from ..downloadutils import DownloadUtils as DU
|
||
|
from .. import utils, variables as v, app
|
||
|
|
||
|
LOG = getLogger('PLEX.api')
|
||
|
|
||
|
|
||
|
class Artwork(object):
|
||
|
def one_artwork(self, art_kind, aspect=None):
|
||
|
"""
|
||
|
aspect can be: 'square', '16:9', 'poster'. Defaults to 'poster'
|
||
|
"""
|
||
|
aspect = 'poster' if not aspect else aspect
|
||
|
if aspect == 'poster':
|
||
|
width = 1000
|
||
|
height = 1500
|
||
|
elif aspect == '16:9':
|
||
|
width = 1920
|
||
|
height = 1080
|
||
|
elif aspect == 'square':
|
||
|
width = 1000
|
||
|
height = 1000
|
||
|
else:
|
||
|
raise NotImplementedError('aspect ratio not yet implemented: %s'
|
||
|
% aspect)
|
||
|
artwork = self.xml.get(art_kind)
|
||
|
if not artwork or artwork.startswith('http'):
|
||
|
return artwork
|
||
|
if '/composite/' in artwork:
|
||
|
try:
|
||
|
# e.g. Plex collections where artwork already contains width and
|
||
|
# height. Need to upscale for better resolution
|
||
|
artwork, args = artwork.split('?')
|
||
|
args = dict(utils.parse_qsl(args))
|
||
|
width = int(args.get('width', 400))
|
||
|
height = int(args.get('height', 400))
|
||
|
# Adjust to 4k resolution 1920x1080
|
||
|
scaling = 1920.0 / float(max(width, height))
|
||
|
width = int(scaling * width)
|
||
|
height = int(scaling * height)
|
||
|
except ValueError:
|
||
|
# e.g. playlists
|
||
|
pass
|
||
|
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
||
|
artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
|
||
|
'minSize=1&upscale=0&url=%s'
|
||
|
% (app.CONN.server, utils.quote(artwork)))
|
||
|
artwork = self.attach_plex_token_to_url(artwork)
|
||
|
return artwork
|
||
|
|
||
|
def artwork_episode(self, full_artwork):
|
||
|
"""
|
||
|
Episodes are special, they only get the thumb, because all the other
|
||
|
artwork will be saved under season and show EXCEPT if you're
|
||
|
constructing a listitem and the item has NOT been synched to the Kodi db
|
||
|
"""
|
||
|
artworks = {}
|
||
|
# Item is currently NOT in the Kodi DB
|
||
|
art = self.one_artwork('thumb')
|
||
|
if art:
|
||
|
artworks['thumb'] = art
|
||
|
if not full_artwork:
|
||
|
# For episodes, only get the thumb. Everything else stemms from
|
||
|
# either the season or the show
|
||
|
return artworks
|
||
|
for kodi_artwork, plex_artwork in \
|
||
|
v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems():
|
||
|
art = self.one_artwork(plex_artwork)
|
||
|
if art:
|
||
|
artworks[kodi_artwork] = art
|
||
|
return artworks
|
||
|
|
||
|
def artwork(self, kodi_id=None, kodi_type=None, full_artwork=False):
|
||
|
"""
|
||
|
Gets the URLs to the Plex artwork. Dict keys will be missing if there
|
||
|
is no corresponding artwork.
|
||
|
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
|
||
|
(thus potentially more artwork, e.g. clearart, discart).
|
||
|
|
||
|
Output ('max' version)
|
||
|
{
|
||
|
'thumb'
|
||
|
'poster'
|
||
|
'banner'
|
||
|
'clearart'
|
||
|
'clearlogo'
|
||
|
'fanart'
|
||
|
}
|
||
|
'landscape' and 'icon' might be implemented later
|
||
|
Passing full_artwork=True returns ALL the artwork for the item, so not
|
||
|
just 'thumb' for episodes, but also season and show artwork
|
||
|
"""
|
||
|
if self.plex_type == v.PLEX_TYPE_EPISODE:
|
||
|
return self.artwork_episode(full_artwork)
|
||
|
artworks = {}
|
||
|
if kodi_id:
|
||
|
# in Kodi database, potentially with additional e.g. clearart
|
||
|
if self.plex_type in v.PLEX_VIDEOTYPES:
|
||
|
with KodiVideoDB(lock=False) as kodidb:
|
||
|
return kodidb.get_art(kodi_id, kodi_type)
|
||
|
else:
|
||
|
with KodiMusicDB(lock=False) as kodidb:
|
||
|
return kodidb.get_art(kodi_id, kodi_type)
|
||
|
|
||
|
for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems():
|
||
|
art = self.one_artwork(plex_artwork)
|
||
|
if art:
|
||
|
artworks[kodi_artwork] = art
|
||
|
if self.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_ALBUM):
|
||
|
# Get parent item artwork if the main item is missing artwork
|
||
|
if 'fanart' not in artworks:
|
||
|
art = self.one_artwork('parentArt')
|
||
|
if art:
|
||
|
artworks['fanart1'] = art
|
||
|
if 'poster' not in artworks:
|
||
|
art = self.one_artwork('parentThumb')
|
||
|
if art:
|
||
|
artworks['poster'] = art
|
||
|
if self.plex_type in (v.PLEX_TYPE_SONG,
|
||
|
v.PLEX_TYPE_ALBUM,
|
||
|
v.PLEX_TYPE_ARTIST):
|
||
|
# need to set poster also as thumb
|
||
|
art = self.one_artwork('thumb')
|
||
|
if art:
|
||
|
artworks['thumb'] = art
|
||
|
if self.plex_type == v.PLEX_TYPE_PLAYLIST:
|
||
|
art = self.one_artwork('composite')
|
||
|
if art:
|
||
|
artworks['thumb'] = art
|
||
|
return artworks
|
||
|
|
||
|
def fanart_artwork(self, artworks):
|
||
|
"""
|
||
|
Downloads additional fanart from third party sources (well, link to
|
||
|
fanart only).
|
||
|
"""
|
||
|
external_id = self.retrieve_external_item_id()
|
||
|
if external_id is not None:
|
||
|
artworks = self.lookup_fanart_tv(external_id[0], artworks)
|
||
|
return artworks
|
||
|
|
||
|
def set_artwork(self):
|
||
|
"""
|
||
|
Gets the URLs to the Plex artwork, or empty string if not found.
|
||
|
Only call on movies!
|
||
|
"""
|
||
|
artworks = {}
|
||
|
# Plex does not get much artwork - go ahead and get the rest from
|
||
|
# fanart tv only for movie or tv show
|
||
|
external_id = self.retrieve_external_item_id(collection=True)
|
||
|
if external_id is not None:
|
||
|
external_id, poster, background = external_id
|
||
|
if poster is not None:
|
||
|
artworks['poster'] = poster
|
||
|
if background is not None:
|
||
|
artworks['fanart'] = background
|
||
|
artworks = self.lookup_fanart_tv(external_id, artworks)
|
||
|
else:
|
||
|
LOG.info('Did not find a set/collection ID on TheMovieDB using %s.'
|
||
|
' Artwork will be missing.', self.title())
|
||
|
return artworks
|
||
|
|
||
|
def retrieve_external_item_id(self, collection=False):
|
||
|
"""
|
||
|
Returns the set
|
||
|
media_id [unicode]: the item's IMDB id for movies or tvdb id for
|
||
|
TV shows
|
||
|
poster [unicode]: path to the item's poster artwork
|
||
|
background [unicode]: path to the item's background artwork
|
||
|
|
||
|
The last two might be None if not found. Generally None is returned
|
||
|
if unsuccessful.
|
||
|
|
||
|
If not found in item's Plex metadata, check themovidedb.org.
|
||
|
"""
|
||
|
item = self.xml.attrib
|
||
|
media_type = self.plex_type
|
||
|
media_id = None
|
||
|
# Return the saved Plex id's, if applicable
|
||
|
# Always seek collection's ids since not provided by PMS
|
||
|
if collection is False:
|
||
|
if media_type == v.PLEX_TYPE_MOVIE:
|
||
|
media_id = self.provider('imdb')
|
||
|
elif media_type == v.PLEX_TYPE_SHOW:
|
||
|
media_id = self.provider('tvdb')
|
||
|
if media_id is not None:
|
||
|
return media_id, None, None
|
||
|
LOG.info('Plex did not provide ID for IMDB or TVDB. Start '
|
||
|
'lookup process')
|
||
|
else:
|
||
|
LOG.debug('Start movie set/collection lookup on themoviedb with %s',
|
||
|
item.get('title', ''))
|
||
|
|
||
|
api_key = utils.settings('themoviedbAPIKey')
|
||
|
if media_type == v.PLEX_TYPE_SHOW:
|
||
|
media_type = 'tv'
|
||
|
title = self.title()
|
||
|
# if the title has the year in remove it as tmdb cannot deal with it...
|
||
|
# replace e.g. 'The Americans (2015)' with 'The Americans'
|
||
|
title = sub(r'\s*\(\d{4}\)$', '', title, count=1)
|
||
|
url = 'https://api.themoviedb.org/3/search/%s' % media_type
|
||
|
parameters = {
|
||
|
'api_key': api_key,
|
||
|
'language': v.KODILANGUAGE,
|
||
|
'query': title.encode('utf-8')
|
||
|
}
|
||
|
data = DU().downloadUrl(url,
|
||
|
authenticate=False,
|
||
|
parameters=parameters,
|
||
|
timeout=7)
|
||
|
try:
|
||
|
data.get('test')
|
||
|
except AttributeError:
|
||
|
LOG.warning('Could not download data from FanartTV')
|
||
|
return
|
||
|
if not data.get('results'):
|
||
|
LOG.info('No match found on themoviedb for type: %s, title: %s',
|
||
|
media_type, title)
|
||
|
return
|
||
|
|
||
|
year = item.get('year')
|
||
|
match_found = None
|
||
|
# find year match
|
||
|
if year:
|
||
|
for entry in data['results']:
|
||
|
if year in entry.get('first_air_date', ''):
|
||
|
match_found = entry
|
||
|
break
|
||
|
elif year in entry.get('release_date', ''):
|
||
|
match_found = entry
|
||
|
break
|
||
|
# find exact match based on title, if we haven't found a year match
|
||
|
if match_found is None:
|
||
|
LOG.info('No themoviedb match found using year %s', year)
|
||
|
replacements = (
|
||
|
' ',
|
||
|
'-',
|
||
|
'&',
|
||
|
',',
|
||
|
':',
|
||
|
';'
|
||
|
)
|
||
|
for entry in data['results']:
|
||
|
name = entry.get('name', entry.get('title', ''))
|
||
|
original_name = entry.get('original_name', '')
|
||
|
title_alt = title.lower()
|
||
|
name_alt = name.lower()
|
||
|
org_name_alt = original_name.lower()
|
||
|
for replace_string in replacements:
|
||
|
title_alt = title_alt.replace(replace_string, '')
|
||
|
name_alt = name_alt.replace(replace_string, '')
|
||
|
org_name_alt = org_name_alt.replace(replace_string, '')
|
||
|
if name == title or original_name == title:
|
||
|
# match found for exact title name
|
||
|
match_found = entry
|
||
|
break
|
||
|
elif (name.split(' (')[0] == title or title_alt == name_alt or
|
||
|
title_alt == org_name_alt):
|
||
|
# match found with substituting some stuff
|
||
|
match_found = entry
|
||
|
break
|
||
|
|
||
|
# if a match was not found, we accept the closest match from TMDB
|
||
|
if match_found is None and data.get('results'):
|
||
|
LOG.info('Using very first match from themoviedb')
|
||
|
match_found = entry = data.get('results')[0]
|
||
|
|
||
|
if match_found is None:
|
||
|
LOG.info('Still no themoviedb match for type: %s, title: %s, '
|
||
|
'year: %s', media_type, title, year)
|
||
|
LOG.debug('themoviedb answer was %s', data['results'])
|
||
|
return
|
||
|
|
||
|
LOG.info('Found themoviedb match for %s: %s',
|
||
|
item.get('title'), match_found)
|
||
|
|
||
|
tmdb_id = str(entry.get('id', ''))
|
||
|
if tmdb_id == '':
|
||
|
LOG.error('No themoviedb ID found, aborting')
|
||
|
return
|
||
|
|
||
|
if media_type == 'multi' and entry.get('media_type'):
|
||
|
media_type = entry.get('media_type')
|
||
|
name = entry.get('name', entry.get('title'))
|
||
|
# lookup external tmdb_id and perform artwork lookup on fanart.tv
|
||
|
parameters = {'api_key': api_key}
|
||
|
if media_type == 'movie':
|
||
|
url = 'https://api.themoviedb.org/3/movie/%s' % tmdb_id
|
||
|
parameters['append_to_response'] = 'videos'
|
||
|
elif media_type == 'tv':
|
||
|
url = 'https://api.themoviedb.org/3/tv/%s' % tmdb_id
|
||
|
parameters['append_to_response'] = 'external_ids,videos'
|
||
|
media_id, poster, background = None, None, None
|
||
|
for language in [v.KODILANGUAGE, 'en']:
|
||
|
parameters['language'] = language
|
||
|
data = DU().downloadUrl(url,
|
||
|
authenticate=False,
|
||
|
parameters=parameters,
|
||
|
timeout=7)
|
||
|
try:
|
||
|
data.get('test')
|
||
|
except AttributeError:
|
||
|
LOG.warning('Could not download %s with parameters %s',
|
||
|
url, parameters)
|
||
|
continue
|
||
|
if collection is False:
|
||
|
if data.get('imdb_id'):
|
||
|
media_id = str(data.get('imdb_id'))
|
||
|
break
|
||
|
if (data.get('external_ids') and
|
||
|
data['external_ids'].get('tvdb_id')):
|
||
|
media_id = str(data['external_ids']['tvdb_id'])
|
||
|
break
|
||
|
else:
|
||
|
if not data.get('belongs_to_collection'):
|
||
|
continue
|
||
|
media_id = data.get('belongs_to_collection').get('id')
|
||
|
if not media_id:
|
||
|
continue
|
||
|
media_id = str(media_id)
|
||
|
LOG.debug('Retrieved collections tmdb id %s for %s',
|
||
|
media_id, title)
|
||
|
url = 'https://api.themoviedb.org/3/collection/%s' % media_id
|
||
|
data = DU().downloadUrl(url,
|
||
|
authenticate=False,
|
||
|
parameters=parameters,
|
||
|
timeout=7)
|
||
|
try:
|
||
|
data.get('poster_path')
|
||
|
except AttributeError:
|
||
|
LOG.debug('Could not find TheMovieDB poster paths for %s'
|
||
|
' in the language %s', title, language)
|
||
|
continue
|
||
|
if not poster and data.get('poster_path'):
|
||
|
poster = ('https://image.tmdb.org/t/p/original%s' %
|
||
|
data.get('poster_path'))
|
||
|
if not background and data.get('backdrop_path'):
|
||
|
background = ('https://image.tmdb.org/t/p/original%s' %
|
||
|
data.get('backdrop_path'))
|
||
|
return media_id, poster, background
|
||
|
|
||
|
def lookup_fanart_tv(self, media_id, artworks):
|
||
|
"""
|
||
|
perform artwork lookup on fanart.tv
|
||
|
|
||
|
media_id: IMDB id for movies, tvdb id for TV shows
|
||
|
"""
|
||
|
api_key = utils.settings('FanArtTVAPIKey')
|
||
|
typus = self.plex_type
|
||
|
if typus == v.PLEX_TYPE_SHOW:
|
||
|
typus = 'tv'
|
||
|
|
||
|
if typus == v.PLEX_TYPE_MOVIE:
|
||
|
url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \
|
||
|
% (media_id, api_key)
|
||
|
elif typus == 'tv':
|
||
|
url = 'http://webservice.fanart.tv/v3/tv/%s?api_key=%s' \
|
||
|
% (media_id, api_key)
|
||
|
else:
|
||
|
# Not supported artwork
|
||
|
return artworks
|
||
|
data = DU().downloadUrl(url, authenticate=False, timeout=15)
|
||
|
try:
|
||
|
data.get('test')
|
||
|
except AttributeError:
|
||
|
LOG.error('Could not download data from FanartTV')
|
||
|
return artworks
|
||
|
|
||
|
fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE)
|
||
|
|
||
|
if typus == v.PLEX_TYPE_ARTIST:
|
||
|
fanart_tv_types.append(("thumb", "folder"))
|
||
|
else:
|
||
|
fanart_tv_types.append(("thumb", "thumb"))
|
||
|
|
||
|
prefixes = (
|
||
|
"hd" + typus,
|
||
|
"hd",
|
||
|
typus,
|
||
|
"",
|
||
|
)
|
||
|
for fanart_tv_type, kodi_type in fanart_tv_types:
|
||
|
# Skip the ones we already have
|
||
|
if kodi_type in artworks:
|
||
|
continue
|
||
|
for prefix in prefixes:
|
||
|
fanarttvimage = prefix + fanart_tv_type
|
||
|
if fanarttvimage not in data:
|
||
|
continue
|
||
|
# select image in preferred language
|
||
|
for entry in data[fanarttvimage]:
|
||
|
if entry.get("lang") == v.KODILANGUAGE:
|
||
|
artworks[kodi_type] = \
|
||
|
entry.get("url", "").replace(' ', '%20')
|
||
|
break
|
||
|
# just grab the first english OR undefinded one as fallback
|
||
|
# (so we're actually grabbing the more popular one)
|
||
|
if kodi_type not in artworks:
|
||
|
for entry in data[fanarttvimage]:
|
||
|
if entry.get("lang") in ("en", "00"):
|
||
|
artworks[kodi_type] = \
|
||
|
entry.get("url", "").replace(' ', '%20')
|
||
|
break
|
||
|
|
||
|
# grab extrafanarts in list
|
||
|
fanartcount = 1 if 'fanart' in artworks else ''
|
||
|
for prefix in prefixes:
|
||
|
fanarttvimage = prefix + 'background'
|
||
|
if fanarttvimage not in data:
|
||
|
continue
|
||
|
for entry in data[fanarttvimage]:
|
||
|
if entry.get("url") is None:
|
||
|
continue
|
||
|
artworks['fanart%s' % fanartcount] = \
|
||
|
entry['url'].replace(' ', '%20')
|
||
|
try:
|
||
|
fanartcount += 1
|
||
|
except TypeError:
|
||
|
fanartcount = 1
|
||
|
if fanartcount >= v.MAX_BACKGROUND_COUNT:
|
||
|
break
|
||
|
return artworks
|