Cleanup plex_api.py

This commit is contained in:
croneter 2019-03-30 16:44:35 +01:00
parent 1ac19109ba
commit c1bb083933

View file

@ -33,8 +33,6 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
from re import sub from re import sub
from urllib import urlencode, unquote, quote
from urlparse import parse_qsl
from xbmcgui import ListItem from xbmcgui import ListItem
@ -101,10 +99,8 @@ class API(object):
""" """
Returns the unique int <ratingKey><updatedAt> Returns the unique int <ratingKey><updatedAt>
""" """
return int('%s%s' % (self.item.get('ratingKey'), return int('%s%s' % (self.plex_id(),
self.item.get('updatedAt', self.updated_at() or self.item.get('addedAt', 1541572987)))
self.item.get('addedAt',
1541572987))))
def plex_id(self): def plex_id(self):
""" """
@ -152,9 +148,9 @@ class API(object):
def directory_path(self, section_id=None, plex_type=None, old_key=None, def directory_path(self, section_id=None, plex_type=None, old_key=None,
synched=True): synched=True):
key = cast(unicode, self.item.get('fastKey')) key = self.item.get('fastKey')
if not key: if not key:
key = cast(unicode, self.item.get('key')) key = self.item.get('key')
if old_key: if old_key:
key = '%s/%s' % (old_key, key) key = '%s/%s' % (old_key, key)
elif not key.startswith('/'): elif not key.startswith('/'):
@ -169,7 +165,7 @@ class API(object):
params['synched'] = 'false' params['synched'] = 'false'
if self.item.get('prompt'): if self.item.get('prompt'):
# User input needed, e.g. search for a movie or episode # User input needed, e.g. search for a movie or episode
params['prompt'] = cast(unicode, self.item.get('prompt')) params['prompt'] = self.item.get('prompt')
if section_id: if section_id:
params['id'] = section_id params['id'] = section_id
return utils.extend_url('plugin://%s/' % v.ADDON_ID, params) return utils.extend_url('plugin://%s/' % v.ADDON_ID, params)
@ -210,7 +206,7 @@ class API(object):
def file_path(self, force_first_media=False): def file_path(self, force_first_media=False):
""" """
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv' Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
or None as unicode or None
force_first_media=True: force_first_media=True:
will always use 1st media stream, e.g. when several different will always use 1st media stream, e.g. when several different
@ -221,51 +217,43 @@ class API(object):
return return
try: try:
if force_first_media is False: if force_first_media is False:
ans = self.item[self.mediastream][self.part].attrib['file'] ans = cast(str, self.item[self.mediastream][self.part].attrib['file'])
else: else:
ans = self.item[0][self.part].attrib['file'] ans = cast(str, self.item[0][self.part].attrib['file'])
except (TypeError, AttributeError, IndexError, KeyError): except (TypeError, AttributeError, IndexError, KeyError):
ans = None return
if ans is not None: return utils.unquote(ans)
try:
ans = utils.try_decode(unquote(ans))
except UnicodeDecodeError:
# Sometimes, Plex seems to have encoded in latin1
ans = unquote(ans).decode('latin1')
return ans
def get_picture_path(self): def get_picture_path(self):
""" """
Returns the item's picture path (transcode, if necessary) as string. Returns the item's picture path (transcode, if necessary) as string.
Will always use addon paths, never direct paths Will always use addon paths, never direct paths
""" """
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower() path = self.item[0][0].get('key')
extension = path[path.rfind('.'):].lower()
if app.SYNC.force_transcode_pix or extension not in v.KODI_SUPPORTED_IMAGES: if app.SYNC.force_transcode_pix or extension not in v.KODI_SUPPORTED_IMAGES:
# Let Plex transcode # Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080 # max width/height supported by plex image transcoder is 1920x1080
path = app.CONN.server + PF.transcode_image_path( path = app.CONN.server + PF.transcode_image_path(
self.item[0][0].get('key'), path,
app.ACCOUNT.pms_token, app.ACCOUNT.pms_token,
"%s%s" % (app.CONN.server, self.item[0][0].get('key')), "%s%s" % (app.CONN.server, path),
1920, 1920,
1080) 1080)
else: else:
path = self.attach_plex_token_to_url( path = self.attach_plex_token_to_url('%s%s' % (app.CONN.server, path))
'%s%s' % (app.CONN.server, self.item[0][0].attrib['key']))
# Attach Plex id to url to let it be picked up by our playqueue agent # Attach Plex id to url to let it be picked up by our playqueue agent
# later # later
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id())) return '%s&plex_id=%s' % (path, self.plex_id())
def tv_show_path(self): def tv_show_path(self):
""" """
Returns the direct path to the TV show, e.g. '\\NAS\tv\series' Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
or None or None
""" """
res = None
for child in self.item: for child in self.item:
if child.tag == 'Location': if child.tag == 'Location':
res = child.get('path') return child.get('path')
return res
def season_number(self): def season_number(self):
""" """
@ -295,10 +283,7 @@ class API(object):
""" """
Returns the play count for the item as an int or the int 0 if not found Returns the play count for the item as an int or the int 0 if not found
""" """
try: return cast(int, self.item.get('viewCount')) or 0
return int(self.item.attrib['viewCount'])
except (KeyError, ValueError):
return 0
def userdata(self): def userdata(self):
""" """
@ -781,8 +766,7 @@ class API(object):
'container': self._data_from_part_or_media('container'), 'container': self._data_from_part_or_media('container'),
} }
try: try:
answ['bitDepth'] = self.item[0][self.part][self.mediastream].get( answ['bitDepth'] = self.item[0][self.part][self.mediastream].get('bitDepth')
'bitDepth')
except (TypeError, AttributeError, KeyError, IndexError): except (TypeError, AttributeError, KeyError, IndexError):
answ['bitDepth'] = None answ['bitDepth'] = None
return answ return answ
@ -848,7 +832,7 @@ class API(object):
subtitlelanguages = [] subtitlelanguages = []
try: try:
# Sometimes, aspectratio is on the "toplevel" # Sometimes, aspectratio is on the "toplevel"
aspect = self.item[0].get('aspectRatio') aspect = cast(float, self.item[0].get('aspectRatio'))
except IndexError: except IndexError:
# There is no stream info at all, returning empty # There is no stream info at all, returning empty
return { return {
@ -860,39 +844,37 @@ class API(object):
for child in self.item[0]: for child in self.item[0]:
container = child.get('container') container = child.get('container')
# Loop over Streams # Loop over Streams
for grandchild in child: for stream in child:
stream = grandchild.attrib
media_type = int(stream.get('streamType', 999)) media_type = int(stream.get('streamType', 999))
track = {} track = {}
if media_type == 1: # Video streams if media_type == 1: # Video streams
if 'codec' in stream: if 'codec' in stream.attrib:
track['codec'] = stream['codec'].lower() track['codec'] = stream.get('codec').lower()
if "msmpeg4" in track['codec']: if "msmpeg4" in track['codec']:
track['codec'] = "divx" track['codec'] = "divx"
elif "mpeg4" in track['codec']: elif "mpeg4" in track['codec']:
# if "simple profile" in profile or profile == "":
# track['codec'] = "xvid"
pass pass
elif "h264" in track['codec']: elif "h264" in track['codec']:
if container in ("mp4", "mov", "m4v"): if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1" track['codec'] = "avc1"
track['height'] = stream.get('height') track['height'] = cast(int, stream.get('height'))
track['width'] = stream.get('width') track['width'] = cast(int, stream.get('width'))
# track['Video3DFormat'] = item.get('Video3DFormat') # track['Video3DFormat'] = item.get('Video3DFormat')
track['aspect'] = stream.get('aspectRatio', aspect) track['aspect'] = cast(float,
track['duration'] = self.resume_runtime()[1] stream.get('aspectRatio') or aspect)
track['duration'] = self.runtime()
track['video3DFormat'] = None track['video3DFormat'] = None
videotracks.append(track) videotracks.append(track)
elif media_type == 2: # Audio streams elif media_type == 2: # Audio streams
if 'codec' in stream: if 'codec' in stream.attrib:
track['codec'] = stream['codec'].lower() track['codec'] = stream.get('codec').lower()
if ("dca" in track['codec'] and if ("dca" in track['codec'] and
"ma" in stream.get('profile', '').lower()): "ma" in stream.get('profile', '').lower()):
track['codec'] = "dtshd_ma" track['codec'] = "dtshd_ma"
track['channels'] = stream.get('channels') track['channels'] = cast(int, stream.get('channels'))
# 'unknown' if we cannot get language # 'unknown' if we cannot get language
track['language'] = stream.get( track['language'] = stream.get('languageCode',
'languageCode', utils.lang(39310)).lower() utils.lang(39310).lower())
audiotracks.append(track) audiotracks.append(track)
elif media_type == 3: # Subtitle streams elif media_type == 3: # Subtitle streams
# 'unknown' if we cannot get language # 'unknown' if we cannot get language
@ -925,7 +907,7 @@ class API(object):
# e.g. Plex collections where artwork already contains # e.g. Plex collections where artwork already contains
# width and height. Need to upscale for better resolution # width and height. Need to upscale for better resolution
artwork, args = artwork.split('?') artwork, args = artwork.split('?')
args = dict(parse_qsl(args)) args = dict(utils.parse_qsl(args))
width = int(args.get('width', 400)) width = int(args.get('width', 400))
height = int(args.get('height', 400)) height = int(args.get('height', 400))
# Adjust to 4k resolution 1920x1080 # Adjust to 4k resolution 1920x1080
@ -938,7 +920,7 @@ class API(object):
artwork = '%s?width=%s&height=%s' % (artwork, width, height) artwork = '%s?width=%s&height=%s' % (artwork, width, height)
artwork = ('%s/photo/:/transcode?width=1920&height=1920&' artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
'minSize=1&upscale=0&url=%s' 'minSize=1&upscale=0&url=%s'
% (app.CONN.server, quote(artwork))) % (app.CONN.server, utils.quote(artwork)))
artwork = self.attach_plex_token_to_url(artwork) artwork = self.attach_plex_token_to_url(artwork)
return artwork return artwork
@ -1297,9 +1279,9 @@ class API(object):
def library_section_id(self): def library_section_id(self):
""" """
Returns the id of the Plex library section (for e.g. a movies section) Returns the id of the Plex library section (for e.g. a movies section)
or None as an int or None
""" """
return self.item.get('librarySectionID') return cast(int, self.item.get('librarySectionID'))
def collections_match(self, section_id): def collections_match(self, section_id):
""" """
@ -1345,7 +1327,7 @@ class API(object):
Returns True if the item's 'optimizedForStreaming' is set, False other- Returns True if the item's 'optimizedForStreaming' is set, False other-
wise wise
""" """
return self.item[0].get('optimizedForStreaming') == '1' return cast(bool, self.item[0].get('optimizedForStreaming')) or False
def mediastream_number(self): def mediastream_number(self):
""" """
@ -1371,16 +1353,16 @@ class API(object):
for entry in self.item.iterfind('./Media'): for entry in self.item.iterfind('./Media'):
# Get additional info (filename / languages) # Get additional info (filename / languages)
if 'file' in entry[0].attrib: if 'file' in entry[0].attrib:
option = utils.try_decode(entry[0].attrib['file']) option = entry[0].get('file')
option = path_ops.path.basename(option) option = path_ops.basename(option)
else: else:
option = self.title() or '' option = self.title() or ''
# Languages of audio streams # Languages of audio streams
languages = [] languages = []
for stream in entry[0]: for stream in entry[0]:
if (stream.attrib['streamType'] == '1' and if (cast(int, stream.get('streamType')) == 1 and
'language' in stream.attrib): 'language' in stream.attrib):
language = utils.try_decode(stream.attrib['language']) language = stream.get('language')
languages.append(language) languages.append(language)
languages = ', '.join(languages) languages = ', '.join(languages)
if languages: if languages:
@ -1391,19 +1373,19 @@ class API(object):
else: else:
option = '%s ' % option option = '%s ' % option
if 'videoResolution' in entry.attrib: if 'videoResolution' in entry.attrib:
res = utils.try_decode(entry.attrib['videoResolution']) res = entry.get('videoResolution')
option = '%s%sp ' % (option, res) option = '%s%sp ' % (option, res)
if 'videoCodec' in entry.attrib: if 'videoCodec' in entry.attrib:
codec = utils.try_decode(entry.attrib['videoCodec']) codec = entry.get('videoCodec')
option = '%s%s' % (option, codec) option = '%s%s' % (option, codec)
option = option.strip() + ' - ' option = option.strip() + ' - '
if 'audioProfile' in entry.attrib: if 'audioProfile' in entry.attrib:
profile = utils.try_decode(entry.attrib['audioProfile']) profile = entry.get('audioProfile')
option = '%s%s ' % (option, profile) option = '%s%s ' % (option, profile)
if 'audioCodec' in entry.attrib: if 'audioCodec' in entry.attrib:
codec = utils.try_decode(entry.attrib['audioCodec']) codec = entry.get('audioCodec')
option = '%s%s ' % (option, codec) option = '%s%s ' % (option, codec)
option = utils.try_encode(option.strip()) option = cast(str, option.strip())
dialoglist.append(option) dialoglist.append(option)
media = utils.dialog('select', 'Select stream', dialoglist) media = utils.dialog('select', 'Select stream', dialoglist)
if media == -1: if media == -1:
@ -1437,20 +1419,15 @@ class API(object):
""" """
if self.mediastream is None and self.mediastream_number() is None: if self.mediastream is None and self.mediastream_number() is None:
return return
if quality is None: quality = {} if quality is None else quality
quality = {}
xargs = clientinfo.getXArgsDeviceInfo() xargs = clientinfo.getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed # For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls # trailers are 'clip' with PMS xmls
if action == "DirectStream": if action == "DirectStream":
path = self.item[self.mediastream][self.part].attrib['key'] path = self.item[self.mediastream][self.part].get('key')
url = app.CONN.server + path url = app.CONN.server + path
# e.g. Trailers already feature an '?'! # e.g. Trailers already feature an '?'!
if '?' in url: return utils.extend_url(url, xargs)
url += '&' + urlencode(xargs)
else:
url += '?' + urlencode(xargs)
return url
# For Transcoding # For Transcoding
headers = { headers = {
@ -1460,7 +1437,7 @@ class API(object):
'X-Plex-Version': '5.8.0.475' 'X-Plex-Version': '5.8.0.475'
} }
# Path/key to VIDEO item of xml PMS response is needed, not part # Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key'] path = self.item.get('key')
transcode_path = app.CONN.server + \ transcode_path = app.CONN.server + \
'/video/:/transcode/universal/start.m3u8?' '/video/:/transcode/universal/start.m3u8?'
args = { args = {
@ -1469,7 +1446,7 @@ class API(object):
'directPlay': 0, 'directPlay': 0,
'directStream': 1, 'directStream': 1,
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': utils.window('plex_client_Id'), 'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
'fastSeek': 1, 'fastSeek': 1,
'path': path, 'path': path,
'mediaIndex': self.mediastream, 'mediaIndex': self.mediastream,
@ -1478,12 +1455,11 @@ class API(object):
'location': 'lan', 'location': 'lan',
'subtitleSize': utils.settings('subtitleSize') 'subtitleSize': utils.settings('subtitleSize')
} }
# Look like Android to let the PMS use the transcoding profile
xargs.update(headers)
LOG.debug("Setting transcode quality to: %s", quality) LOG.debug("Setting transcode quality to: %s", quality)
args.update(quality) xargs.update(headers)
url = transcode_path + urlencode(xargs) + '&' + urlencode(args) xargs.update(args)
return url xargs.update(quality)
return utils.extend_url(transcode_path, xargs)
def cache_external_subs(self): def cache_external_subs(self):
""" """
@ -1500,7 +1476,7 @@ class API(object):
for stream in mediastreams: for stream in mediastreams:
# Since plex returns all possible tracks together, have to pull # Since plex returns all possible tracks together, have to pull
# only external subtitles - only for these a 'key' exists # only external subtitles - only for these a 'key' exists
if stream.get('streamType') != "3": if cast(int, stream.get('streamType')) != 3:
# Not a subtitle # Not a subtitle
continue continue
# Only set for additional external subtitles NOT lying beside video # Only set for additional external subtitles NOT lying beside video
@ -1510,11 +1486,11 @@ class API(object):
if key: if key:
# We do know the language - temporarily download # We do know the language - temporarily download
if stream.get('languageCode') is not None: if stream.get('languageCode') is not None:
language = stream.get('languageCode')
codec = stream.get('codec')
path = self.download_external_subtitles( path = self.download_external_subtitles(
"{server}%s" % key, "{server}%s" % key,
"subtitle%02d.%s.%s" % (fileindex, "subtitle%02d.%s.%s" % (fileindex, language, codec))
stream.attrib['languageCode'],
stream.attrib['codec']))
fileindex += 1 fileindex += 1
# We don't know the language - no need to download # We don't know the language - no need to download
else: else:
@ -1878,7 +1854,7 @@ class API(object):
except ValueError: except ValueError:
pass pass
else: else:
args = quote(args) args = utils.quote(args)
path = '%s:%s:%s' % (protocol, hostname, args) path = '%s:%s:%s' % (protocol, hostname, args)
if (app.SYNC.path_verified and not force_check) or omit_check: if (app.SYNC.path_verified and not force_check) or omit_check:
return path return path