Merge pull request #807 from croneter/fix-unicode

Fixes to unicode
This commit is contained in:
croneter 2019-03-30 17:50:30 +01:00 committed by GitHub
commit bca657ab08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 159 deletions

View file

@ -29,9 +29,13 @@ class Main():
def __init__(self): def __init__(self):
LOG.debug('Full sys.argv received: %s', argv) LOG.debug('Full sys.argv received: %s', argv)
# Parse parameters # Parse parameters
path = unicode_paths.decode(argv[0]) params = dict(parse_qsl(argv[2][1:]))
arguments = unicode_paths.decode(argv[2]) arguments = unicode_paths.decode(argv[2])
params = dict(parse_qsl(arguments[1:])) path = unicode_paths.decode(argv[0])
# Ensure unicode
for key, value in params.iteritems():
params[key.decode('utf-8')] = params.pop(key)
params[key] = value.decode('utf-8')
mode = params.get('mode', '') mode = params.get('mode', '')
itemid = params.get('id', '') itemid = params.get('id', '')

View file

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
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 urllib import quote_plus, unquote
import requests import requests
from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB
@ -20,11 +19,11 @@ BATCH_SIZE = 500
def double_urlencode(text): def double_urlencode(text):
return quote_plus(quote_plus(text)) return utils.quote_plus(utils.quote_plus(text))
def double_urldecode(text): def double_urldecode(text):
return unquote(unquote(text)) return utils.unquote(utils.unquote(text))
class ImageCachingThread(backgroundthread.KillableThread): class ImageCachingThread(backgroundthread.KillableThread):
@ -89,7 +88,7 @@ class ImageCachingThread(backgroundthread.KillableThread):
def cache_url(url): def cache_url(url):
url = double_urlencode(utils.try_encode(url)) url = double_urlencode(url)
sleeptime = 0 sleeptime = 0
while True: while True:
try: try:

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
xml.etree.ElementTree tries to encode with text.encode('ascii') - which is
just plain BS. This etree will always return unicode, not string
"""
from __future__ import absolute_import, division, unicode_literals
# Originally tried faster cElementTree, but does NOT work reliably with Kodi
from defusedxml.ElementTree import DefusedXMLParser, _generate_etree_functions
from xml.etree.ElementTree import TreeBuilder as _TreeBuilder
from xml.etree.ElementTree import parse as _parse
from xml.etree.ElementTree import iterparse as _iterparse
from xml.etree.ElementTree import tostring
class UnicodeXMLParser(DefusedXMLParser):
"""
PKC Hack to ensure we're always receiving unicode, not str
"""
@staticmethod
def _fixtext(text):
"""
Do NOT try to convert every entry to str with entry.encode('ascii')!
"""
return text
# aliases
XMLTreeBuilder = XMLParse = UnicodeXMLParser
parse, iterparse, fromstring = _generate_etree_functions(UnicodeXMLParser,
_TreeBuilder, _parse,
_iterparse)
XML = fromstring
__all__ = ['XML', 'XMLParse', 'XMLTreeBuilder', 'fromstring', 'iterparse',
'parse', 'tostring']

View file

@ -226,7 +226,7 @@ class Section(object):
args = copy.deepcopy(args) args = copy.deepcopy(args)
for key, value in args.iteritems(): for key, value in args.iteritems():
args[key] = value.format(self=self) args[key] = value.format(self=self)
return 'plugin://plugin.video.plexkodiconnect?%s' % urllib.urlencode(args) return utils.extend_url('plugin://%s' % v.ADDON_ID, args)
def to_kodi(self): def to_kodi(self):
""" """

View file

@ -201,3 +201,18 @@ def copy_tree(src, dst, *args, **kwargs):
src = encode_path(src) src = encode_path(src)
dst = encode_path(dst) dst = encode_path(dst)
return dir_util.copy_tree(src, dst, *args, **kwargs) return dir_util.copy_tree(src, dst, *args, **kwargs)
def basename(path):
"""
Returns the filename for path [unicode] or an empty string if not possible.
Safer than using os.path.basename, as we could be expecting \\ for / or
vice versa
"""
try:
return path.rsplit('/', 1)[1]
except IndexError:
try:
return path.rsplit('\\', 1)[1]
except IndexError:
return ''

View file

@ -2,13 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
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 urlparse import parse_qsl
from . import utils, playback, context_entry, transfer, backgroundthread
from . import playback
from . import context_entry
from . import transfer
from . import backgroundthread
############################################################################### ###############################################################################
@ -35,7 +30,7 @@ class PlaybackTask(backgroundthread.Task):
LOG.debug('Detected 3rd party add-on call - ignoring') LOG.debug('Detected 3rd party add-on call - ignoring')
transfer.send(True) transfer.send(True)
return return
params = dict(parse_qsl(params)) params = dict(utils.parse_qsl(params))
mode = params.get('mode') mode = params.get('mode')
resolve = False if params.get('handle') == '-1' else True resolve = False if params.get('handle') == '-1' else True
LOG.debug('Received mode: %s, params: %s', mode, params) LOG.debug('Received mode: %s, params: %s', mode, params)

View file

@ -5,8 +5,6 @@ Collection of functions associated with Kodi and Plex playlists and playqueues
""" """
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
import urllib
from urlparse import parse_qsl, urlsplit
from .plex_api import API from .plex_api import API
from .plex_db import PlexDB from .plex_db import PlexDB
@ -328,12 +326,16 @@ def playlist_item_from_kodi(kodi_item):
item.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-) item.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-)
item.file = kodi_item.get('file') item.file = kodi_item.get('file')
if item.plex_id is None and item.file is not None: if item.plex_id is None and item.file is not None:
query = dict(parse_qsl(urlsplit(item.file).query)) try:
query = item.file.split('?', 1)[1]
except IndexError:
query = ''
query = dict(utils.parse_qsl(query))
item.plex_id = utils.cast(int, query.get('plex_id')) item.plex_id = utils.cast(int, query.get('plex_id'))
item.plex_type = query.get('itemType') item.plex_type = query.get('itemType')
if item.plex_id is None and item.file is not None: if item.plex_id is None and item.file is not None:
item.uri = ('library://whatever/item/%s' item.uri = ('library://whatever/item/%s'
% urllib.quote(utils.try_encode(item.file), safe='')) % utils.quote(item.file, safe=''))
else: else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %

View file

@ -6,13 +6,12 @@ manipulate playlists
""" """
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
import urllib
from .common import PlaylistError from .common import PlaylistError
from ..plex_api import API from ..plex_api import API
from ..downloadutils import DownloadUtils as DU from ..downloadutils import DownloadUtils as DU
from .. import app, variables as v from .. import utils, app, variables as v
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playlists.pms') LOG = getLogger('PLEX.playlists.pms')
@ -56,8 +55,8 @@ def initialize(playlist, plex_id):
'type': v.PLEX_PLAYLIST_TYPE_FROM_KODI[playlist.kodi_type], 'type': v.PLEX_PLAYLIST_TYPE_FROM_KODI[playlist.kodi_type],
'title': playlist.plex_name, 'title': playlist.plex_name,
'smart': 0, 'smart': 0,
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s' 'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
% plex_id, safe=''))) % plex_id, safe='')))
} }
xml = DU().downloadUrl(url='{server}/playlists', xml = DU().downloadUrl(url='{server}/playlists',
action_type='POST', action_type='POST',
@ -80,8 +79,8 @@ def add_item(playlist, plex_id):
Raises PlaylistError if that did not work out. Raises PlaylistError if that did not work out.
""" """
params = { params = {
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s' 'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
% plex_id, safe=''))) % plex_id, safe='')))
} }
xml = DU().downloadUrl(url='{server}/playlists/%s/items' % playlist.plex_id, xml = DU().downloadUrl(url='{server}/playlists/%s/items' % playlist.plex_id,
action_type='PUT', action_type='PUT',

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:
@ -1788,7 +1764,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

View file

@ -8,7 +8,6 @@ from logging import getLogger
from threading import Thread from threading import Thread
from Queue import Empty from Queue import Empty
from socket import SHUT_RDWR from socket import SHUT_RDWR
from urllib import urlencode
from xbmc import executebuiltin from xbmc import executebuiltin
from .plexbmchelper import listener, plexgdm, subscribers, httppersist from .plexbmchelper import listener, plexgdm, subscribers, httppersist
@ -96,7 +95,7 @@ class PlexCompanion(backgroundthread.KillableThread):
transient_token=data.get('token')) transient_token=data.get('token'))
elif data['containerKey'].startswith('/playQueues/'): elif data['containerKey'].startswith('/playQueues/'):
_, container_key, _ = PF.ParseContainerKey(data['containerKey']) _, container_key, _ = PF.ParseContainerKey(data['containerKey'])
xml = PF.DownloadChunks('{server}/playQueues/%s?' % container_key) xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
if xml is None: if xml is None:
# "Play error" # "Play error"
utils.dialog('notification', utils.dialog('notification',
@ -133,8 +132,7 @@ class PlexCompanion(backgroundthread.KillableThread):
'key': '{server}%s' % data.get('key'), 'key': '{server}%s' % data.get('key'),
'offset': data.get('offset') 'offset': data.get('offset')
} }
executebuiltin('RunPlugin(plugin://%s?%s)' executebuiltin('RunPlugin(plugin://%s)' % utils.extend_url(v.ADDON_ID, params))
% (v.ADDON_ID, urlencode(params)))
@staticmethod @staticmethod
def _process_playlist(data): def _process_playlist(data):

View file

@ -2,9 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
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 urllib import urlencode, quote_plus
from ast import literal_eval from ast import literal_eval
from urlparse import urlparse, parse_qsl
from copy import deepcopy from copy import deepcopy
from time import time from time import time
from threading import Thread from threading import Thread
@ -57,9 +55,9 @@ def ParseContainerKey(containerKey):
Output hence: library, key, query (str, int, dict) Output hence: library, key, query (str, int, dict)
""" """
result = urlparse(containerKey) result = utils.urlparse(containerKey)
library, key = GetPlexKeyNumber(result.path) library, key = GetPlexKeyNumber(result.path.decode('utf-8'))
query = dict(parse_qsl(result.query)) query = dict(utils.parse_qsl(result.query))
return library, key, query return library, key, query
@ -480,9 +478,9 @@ def GetPlexMetadata(key, reraise=False):
# 'includePopularLeaves': 1, # 'includePopularLeaves': 1,
# 'includeConcerts': 1 # 'includeConcerts': 1
} }
url = url + '?' + urlencode(arguments)
try: try:
xml = DU().downloadUrl(url, reraise=reraise) xml = DU().downloadUrl(utils.extend_url(url, arguments),
reraise=reraise)
except exceptions.RequestException: except exceptions.RequestException:
# "PMS offline" # "PMS offline"
utils.dialog('notification', utils.dialog('notification',
@ -556,7 +554,7 @@ def GetAllPlexChildren(key):
Input: Input:
key Key to a Plex item, e.g. 12345 key Key to a Plex item, e.g. 12345
""" """
return DownloadChunks("{server}/library/metadata/%s/children?" % key) return DownloadChunks("{server}/library/metadata/%s/children" % key)
def GetPlexSectionResults(viewId, args=None): def GetPlexSectionResults(viewId, args=None):
@ -569,9 +567,9 @@ def GetPlexSectionResults(viewId, args=None):
Returns None if something went wrong Returns None if something went wrong
""" """
url = "{server}/library/sections/%s/all?" % viewId url = "{server}/library/sections/%s/all" % viewId
if args: if args:
url += urlencode(args) + '&' url = utils.extend_url(url, args)
return DownloadChunks(url) return DownloadChunks(url)
@ -726,9 +724,6 @@ class Leaves(DownloadGen):
def DownloadChunks(url): def DownloadChunks(url):
""" """
Downloads PMS url in chunks of CONTAINERSIZE. Downloads PMS url in chunks of CONTAINERSIZE.
url MUST end with '?' (if no other url encoded args are present) or '&'
Returns a stitched-together xml or None. Returns a stitched-together xml or None.
""" """
xml = None xml = None
@ -740,13 +735,13 @@ def DownloadChunks(url):
'X-Plex-Container-Start': pos, 'X-Plex-Container-Start': pos,
'sort': 'id' 'sort': 'id'
} }
xmlpart = DU().downloadUrl(url + urlencode(args)) xmlpart = DU().downloadUrl(utils.extend_url(url, args))
# If something went wrong - skip in the hope that it works next time # If something went wrong - skip in the hope that it works next time
try: try:
xmlpart.attrib xmlpart.attrib
except AttributeError: except AttributeError:
LOG.error('Error while downloading chunks: %s', LOG.error('Error while downloading chunks: %s, args: %s',
url + urlencode(args)) url, args)
pos += CONTAINERSIZE pos += CONTAINERSIZE
error_counter += 1 error_counter += 1
continue continue
@ -799,16 +794,14 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None):
if updatedAt: if updatedAt:
args.append('updatedAt>=%s' % updatedAt) args.append('updatedAt>=%s' % updatedAt)
if args: if args:
url += '?' + '&'.join(args) + '&' url += '?' + '&'.join(args)
else:
url += '?'
return DownloadChunks(url) return DownloadChunks(url)
def GetPlexOnDeck(viewId): def GetPlexOnDeck(viewId):
""" """
""" """
return DownloadChunks("{server}/library/sections/%s/onDeck?" % viewId) return DownloadChunks("{server}/library/sections/%s/onDeck" % viewId)
def get_plex_hub(): def get_plex_hub():
@ -843,7 +836,7 @@ def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie',
} }
if trailers is True: if trailers is True:
args['extrasPrefixCount'] = utils.settings('trailerNumber') args['extrasPrefixCount'] = utils.settings('trailerNumber')
xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST") xml = DU().downloadUrl(utils.extend_url(url, args), action_type="POST")
try: try:
xml[0].tag xml[0].tag
except (IndexError, TypeError, AttributeError): except (IndexError, TypeError, AttributeError):
@ -976,12 +969,12 @@ def scrobble(ratingKey, state):
'identifier': 'com.plexapp.plugins.library' 'identifier': 'com.plexapp.plugins.library'
} }
if state == "watched": if state == "watched":
url = "{server}/:/scrobble?" + urlencode(args) url = '{server}/:/scrobble'
elif state == "unwatched": elif state == "unwatched":
url = "{server}/:/unscrobble?" + urlencode(args) url = '{server}/:/unscrobble'
else: else:
return return
DU().downloadUrl(url) DU().downloadUrl(utils.extend_url(url, args))
LOG.info("Toggled watched state for Plex item %s", ratingKey) LOG.info("Toggled watched state for Plex item %s", ratingKey)
@ -1058,12 +1051,13 @@ def transcode_image_path(key, AuthToken, path, width, height):
path = 'http://127.0.0.1:32400' + key path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key path = 'http://127.0.0.1:32400' + path + '/' + key
path = utils.try_encode(path)
# This is bogus (note the extra path component) but ATV is stupid when it # This is bogus (note the extra path component) but ATV is stupid when it
# comes to caching images, it doesn't use querystrings. Fortunately PMS is # comes to caching images, it doesn't use querystrings. Fortunately PMS is
# lenient... # lenient...
path = path.encode('utf-8')
transcode_path = ('/photo/:/transcode/%sx%s/%s' transcode_path = ('/photo/:/transcode/%sx%s/%s'
% (width, height, quote_plus(path))) % (width, height, utils.quote_plus(path)))
transcode_path = transcode_path.decode('utf-8')
args = { args = {
'width': width, 'width': width,
'height': height, 'height': height,
@ -1071,4 +1065,4 @@ def transcode_image_path(key, AuthToken, path, width, height):
} }
if AuthToken: if AuthToken:
args['X-Plex-Token'] = AuthToken args['X-Plex-Token'] = AuthToken
return transcode_path + '?' + urlencode(args) return utils.extend_url(transcode_path, args)

View file

@ -8,13 +8,8 @@ from logging import getLogger
from re import sub from re import sub
from SocketServer import ThreadingMixIn from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
import xbmc
from .. import companion from .. import utils, companion, json_rpc as js, clientinfo, variables as v
from .. import json_rpc as js
from .. import clientinfo
from .. import variables as v
from .. import app from .. import app
############################################################################### ###############################################################################
@ -102,8 +97,8 @@ class MyHandler(BaseHTTPRequestHandler):
request_path = self.path[1:] request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path) request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path) parseresult = utils.urlparse(self.path)
paramarrays = parse_qs(url.query) paramarrays = utils.parse_qs(parseresult.query)
params = {} params = {}
for key in paramarrays: for key in paramarrays:
params[key] = paramarrays[key][0] params[key] = paramarrays[key][0]

View file

@ -10,9 +10,11 @@ from datetime import datetime
from unicodedata import normalize from unicodedata import normalize
from threading import Lock from threading import Lock
import urllib import urllib
import urlparse as _urlparse
# Originally tried faster cElementTree, but does NOT work reliably with Kodi # Originally tried faster cElementTree, but does NOT work reliably with Kodi
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
import defusedxml.ElementTree as defused_etree # etree parse unsafe # etree parse unsafe; make sure we're always receiving unicode
from . import defused_etree
from xml.etree.ElementTree import ParseError from xml.etree.ElementTree import ParseError
from functools import wraps from functools import wraps
import hashlib import hashlib
@ -25,8 +27,6 @@ import xbmcgui
from . import path_ops, variables as v from . import path_ops, variables as v
###############################################################################
LOG = getLogger('PLEX.utils') LOG = getLogger('PLEX.utils')
WINDOW = xbmcgui.Window(10000) WINDOW = xbmcgui.Window(10000)
@ -49,9 +49,6 @@ REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
# Grab Plex id from an URL-encoded string # Grab Plex id from an URL-encoded string
REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''') REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
###############################################################################
# Main methods
def garbageCollect(): def garbageCollect():
gc.collect(2) gc.collect(2)
@ -325,6 +322,73 @@ def encode_dict(dictionary):
return dictionary return dictionary
def parse_qs(qs, keep_blank_values=0, strict_parsing=0):
"""
unicode-safe way to use urlparse.parse_qs(). Pass in the query string qs
either as str or unicode
Returns a dict with lists as values; all entires unicode
"""
if isinstance(qs, unicode):
qs = qs.encode('utf-8')
qs = _urlparse.parse_qs(qs, keep_blank_values, strict_parsing)
return {k.decode('utf-8'): [e.decode('utf-8') for e in v]
for k, v in qs.iteritems()}
def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
"""
unicode-safe way to use urlparse.parse_qsl(). Pass in either str or unicode
Returns a list of unicode tuples
"""
if isinstance(qs, unicode):
qs = qs.encode('utf-8')
qs = _urlparse.parse_qsl(qs, keep_blank_values, strict_parsing)
return [(x.decode('utf-8'), y.decode('utf-8')) for (x, y) in qs]
def urlparse(url, scheme='', allow_fragments=True):
"""
unicode-safe way to use urlparse.urlparse(). Pass in either str or unicode
CAREFUL: returns an encoded urlparse.ParseResult()!
"""
if isinstance(url, unicode):
url = url.encode('utf-8')
return _urlparse.urlparse(url, scheme, allow_fragments)
def quote(s, safe='/'):
"""
unicode-safe way to use urllib.quote(). Pass in either str or unicode
Returns unicode
"""
if isinstance(s, unicode):
s = s.encode('utf-8')
s = urllib.quote(s, safe)
return s.decode('utf-8')
def quote_plus(s, safe=''):
"""
unicode-safe way to use urllib.quote(). Pass in either str or unicode
Returns unicode
"""
if isinstance(s, unicode):
s = s.encode('utf-8')
s = urllib.quote_plus(s, safe)
return s.decode('utf-8')
def unquote(s):
"""
unicode-safe way to use urllib.unquote(). Pass in either str or unicode
Returns unicode
"""
if isinstance(s, unicode):
s = s.encode('utf-8')
s = urllib.unquote(s)
return s.decode('utf-8')
def try_encode(input_str, encoding='utf-8'): def try_encode(input_str, encoding='utf-8'):
""" """
Will try to encode input_str (in unicode) to encoding. This possibly Will try to encode input_str (in unicode) to encoding. This possibly

View file

@ -8,7 +8,6 @@ e.g. plugin://... calls. Hence be careful to only rely on window variables.
""" """
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
import urllib
try: try:
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
SUPPORTS_POOL = True SUPPORTS_POOL = True
@ -75,10 +74,12 @@ def get_clean_image(image):
image = thumbcache image = thumbcache
if image and b"image://" in image: if image and b"image://" in image:
image = image.replace(b"image://", b"") image = image.replace(b"image://", b"")
image = urllib.unquote(image) image = utils.unquote(image)
if image.endswith(b"/"): if image.endswith("/"):
image = image[:-1] image = image[:-1]
return image.decode('utf-8') return image
else:
return image.decode('utf-8')
def generate_item(xml_element): def generate_item(xml_element):
@ -227,7 +228,7 @@ def _generate_content(xml_element):
'key': key, 'key': key,
'offset': xml_element.attrib.get('viewOffset', '0'), 'offset': xml_element.attrib.get('viewOffset', '0'),
} }
url = "plugin://%s?%s" % (v.ADDON_ID, urllib.urlencode(params)) url = utils.extend_url('plugin://%s' % v.ADDON_ID, params)
elif plex_type == v.PLEX_TYPE_PHOTO: elif plex_type == v.PLEX_TYPE_PHOTO:
url = api.get_picture_path() url = api.get_picture_path()
else: else: