PlexKodiConnect/resources/lib/plex_api/artwork.py

429 lines
17 KiB
Python
Raw Normal View History

2019-06-11 05:29:42 +10:00
#!/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