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):
LOG.debug('Full sys.argv received: %s', argv)
# Parse parameters
path = unicode_paths.decode(argv[0])
params = dict(parse_qsl(argv[2][1:]))
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', '')
itemid = params.get('id', '')

View file

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

View file

@ -201,3 +201,18 @@ def copy_tree(src, dst, *args, **kwargs):
src = encode_path(src)
dst = encode_path(dst)
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 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from urlparse import parse_qsl
from . import playback
from . import context_entry
from . import transfer
from . import backgroundthread
from . import utils, playback, context_entry, transfer, backgroundthread
###############################################################################
@ -35,7 +30,7 @@ class PlaybackTask(backgroundthread.Task):
LOG.debug('Detected 3rd party add-on call - ignoring')
transfer.send(True)
return
params = dict(parse_qsl(params))
params = dict(utils.parse_qsl(params))
mode = params.get('mode')
resolve = False if params.get('handle') == '-1' else True
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 logging import getLogger
import urllib
from urlparse import parse_qsl, urlsplit
from .plex_api import API
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.file = kodi_item.get('file')
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_type = query.get('itemType')
if item.plex_id is None and item.file is not None:
item.uri = ('library://whatever/item/%s'
% urllib.quote(utils.try_encode(item.file), safe=''))
% utils.quote(item.file, safe=''))
else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
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 logging import getLogger
import urllib
from .common import PlaylistError
from ..plex_api import API
from ..downloadutils import DownloadUtils as DU
from .. import app, variables as v
from .. import utils, app, variables as v
###############################################################################
LOG = getLogger('PLEX.playlists.pms')
@ -56,8 +55,8 @@ def initialize(playlist, plex_id):
'type': v.PLEX_PLAYLIST_TYPE_FROM_KODI[playlist.kodi_type],
'title': playlist.plex_name,
'smart': 0,
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s'
% plex_id, safe='')))
'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
% plex_id, safe='')))
}
xml = DU().downloadUrl(url='{server}/playlists',
action_type='POST',
@ -80,8 +79,8 @@ def add_item(playlist, plex_id):
Raises PlaylistError if that did not work out.
"""
params = {
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s'
% plex_id, safe='')))
'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
% plex_id, safe='')))
}
xml = DU().downloadUrl(url='{server}/playlists/%s/items' % playlist.plex_id,
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 logging import getLogger
from re import sub
from urllib import urlencode, unquote, quote
from urlparse import parse_qsl
from xbmcgui import ListItem
@ -101,10 +99,8 @@ class API(object):
"""
Returns the unique int <ratingKey><updatedAt>
"""
return int('%s%s' % (self.item.get('ratingKey'),
self.item.get('updatedAt',
self.item.get('addedAt',
1541572987))))
return int('%s%s' % (self.plex_id(),
self.updated_at() or self.item.get('addedAt', 1541572987)))
def plex_id(self):
"""
@ -152,9 +148,9 @@ class API(object):
def directory_path(self, section_id=None, plex_type=None, old_key=None,
synched=True):
key = cast(unicode, self.item.get('fastKey'))
key = self.item.get('fastKey')
if not key:
key = cast(unicode, self.item.get('key'))
key = self.item.get('key')
if old_key:
key = '%s/%s' % (old_key, key)
elif not key.startswith('/'):
@ -169,7 +165,7 @@ class API(object):
params['synched'] = 'false'
if self.item.get('prompt'):
# 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:
params['id'] = section_id
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):
"""
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
or None
as unicode or None
force_first_media=True:
will always use 1st media stream, e.g. when several different
@ -221,51 +217,43 @@ class API(object):
return
try:
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:
ans = self.item[0][self.part].attrib['file']
ans = cast(str, self.item[0][self.part].attrib['file'])
except (TypeError, AttributeError, IndexError, KeyError):
ans = None
if ans is not None:
try:
ans = utils.try_decode(unquote(ans))
except UnicodeDecodeError:
# Sometimes, Plex seems to have encoded in latin1
ans = unquote(ans).decode('latin1')
return ans
return
return utils.unquote(ans)
def get_picture_path(self):
"""
Returns the item's picture path (transcode, if necessary) as string.
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:
# Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080
path = app.CONN.server + PF.transcode_image_path(
self.item[0][0].get('key'),
path,
app.ACCOUNT.pms_token,
"%s%s" % (app.CONN.server, self.item[0][0].get('key')),
"%s%s" % (app.CONN.server, path),
1920,
1080)
else:
path = self.attach_plex_token_to_url(
'%s%s' % (app.CONN.server, self.item[0][0].attrib['key']))
path = self.attach_plex_token_to_url('%s%s' % (app.CONN.server, path))
# Attach Plex id to url to let it be picked up by our playqueue agent
# 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):
"""
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
or None
"""
res = None
for child in self.item:
if child.tag == 'Location':
res = child.get('path')
return res
return child.get('path')
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
"""
try:
return int(self.item.attrib['viewCount'])
except (KeyError, ValueError):
return 0
return cast(int, self.item.get('viewCount')) or 0
def userdata(self):
"""
@ -781,8 +766,7 @@ class API(object):
'container': self._data_from_part_or_media('container'),
}
try:
answ['bitDepth'] = self.item[0][self.part][self.mediastream].get(
'bitDepth')
answ['bitDepth'] = self.item[0][self.part][self.mediastream].get('bitDepth')
except (TypeError, AttributeError, KeyError, IndexError):
answ['bitDepth'] = None
return answ
@ -848,7 +832,7 @@ class API(object):
subtitlelanguages = []
try:
# Sometimes, aspectratio is on the "toplevel"
aspect = self.item[0].get('aspectRatio')
aspect = cast(float, self.item[0].get('aspectRatio'))
except IndexError:
# There is no stream info at all, returning empty
return {
@ -860,39 +844,37 @@ class API(object):
for child in self.item[0]:
container = child.get('container')
# Loop over Streams
for grandchild in child:
stream = grandchild.attrib
for stream in child:
media_type = int(stream.get('streamType', 999))
track = {}
if media_type == 1: # Video streams
if 'codec' in stream:
track['codec'] = stream['codec'].lower()
if 'codec' in stream.attrib:
track['codec'] = stream.get('codec').lower()
if "msmpeg4" in track['codec']:
track['codec'] = "divx"
elif "mpeg4" in track['codec']:
# if "simple profile" in profile or profile == "":
# track['codec'] = "xvid"
pass
elif "h264" in track['codec']:
if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1"
track['height'] = stream.get('height')
track['width'] = stream.get('width')
track['height'] = cast(int, stream.get('height'))
track['width'] = cast(int, stream.get('width'))
# track['Video3DFormat'] = item.get('Video3DFormat')
track['aspect'] = stream.get('aspectRatio', aspect)
track['duration'] = self.resume_runtime()[1]
track['aspect'] = cast(float,
stream.get('aspectRatio') or aspect)
track['duration'] = self.runtime()
track['video3DFormat'] = None
videotracks.append(track)
elif media_type == 2: # Audio streams
if 'codec' in stream:
track['codec'] = stream['codec'].lower()
if 'codec' in stream.attrib:
track['codec'] = stream.get('codec').lower()
if ("dca" in track['codec'] and
"ma" in stream.get('profile', '').lower()):
track['codec'] = "dtshd_ma"
track['channels'] = stream.get('channels')
track['channels'] = cast(int, stream.get('channels'))
# 'unknown' if we cannot get language
track['language'] = stream.get(
'languageCode', utils.lang(39310)).lower()
track['language'] = stream.get('languageCode',
utils.lang(39310).lower())
audiotracks.append(track)
elif media_type == 3: # Subtitle streams
# 'unknown' if we cannot get language
@ -925,7 +907,7 @@ class API(object):
# e.g. Plex collections where artwork already contains
# width and height. Need to upscale for better resolution
artwork, args = artwork.split('?')
args = dict(parse_qsl(args))
args = dict(utils.parse_qsl(args))
width = int(args.get('width', 400))
height = int(args.get('height', 400))
# Adjust to 4k resolution 1920x1080
@ -938,7 +920,7 @@ class API(object):
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, quote(artwork)))
% (app.CONN.server, utils.quote(artwork)))
artwork = self.attach_plex_token_to_url(artwork)
return artwork
@ -1297,9 +1279,9 @@ class API(object):
def library_section_id(self):
"""
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):
"""
@ -1345,7 +1327,7 @@ class API(object):
Returns True if the item's 'optimizedForStreaming' is set, False other-
wise
"""
return self.item[0].get('optimizedForStreaming') == '1'
return cast(bool, self.item[0].get('optimizedForStreaming')) or False
def mediastream_number(self):
"""
@ -1371,16 +1353,16 @@ class API(object):
for entry in self.item.iterfind('./Media'):
# Get additional info (filename / languages)
if 'file' in entry[0].attrib:
option = utils.try_decode(entry[0].attrib['file'])
option = path_ops.path.basename(option)
option = entry[0].get('file')
option = path_ops.basename(option)
else:
option = self.title() or ''
# Languages of audio streams
languages = []
for stream in entry[0]:
if (stream.attrib['streamType'] == '1' and
if (cast(int, stream.get('streamType')) == 1 and
'language' in stream.attrib):
language = utils.try_decode(stream.attrib['language'])
language = stream.get('language')
languages.append(language)
languages = ', '.join(languages)
if languages:
@ -1391,19 +1373,19 @@ class API(object):
else:
option = '%s ' % option
if 'videoResolution' in entry.attrib:
res = utils.try_decode(entry.attrib['videoResolution'])
res = entry.get('videoResolution')
option = '%s%sp ' % (option, res)
if 'videoCodec' in entry.attrib:
codec = utils.try_decode(entry.attrib['videoCodec'])
codec = entry.get('videoCodec')
option = '%s%s' % (option, codec)
option = option.strip() + ' - '
if 'audioProfile' in entry.attrib:
profile = utils.try_decode(entry.attrib['audioProfile'])
profile = entry.get('audioProfile')
option = '%s%s ' % (option, profile)
if 'audioCodec' in entry.attrib:
codec = utils.try_decode(entry.attrib['audioCodec'])
codec = entry.get('audioCodec')
option = '%s%s ' % (option, codec)
option = utils.try_encode(option.strip())
option = cast(str, option.strip())
dialoglist.append(option)
media = utils.dialog('select', 'Select stream', dialoglist)
if media == -1:
@ -1437,20 +1419,15 @@ class API(object):
"""
if self.mediastream is None and self.mediastream_number() is None:
return
if quality is None:
quality = {}
quality = {} if quality is None else quality
xargs = clientinfo.getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls
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
# e.g. Trailers already feature an '?'!
if '?' in url:
url += '&' + urlencode(xargs)
else:
url += '?' + urlencode(xargs)
return url
return utils.extend_url(url, xargs)
# For Transcoding
headers = {
@ -1460,7 +1437,7 @@ class API(object):
'X-Plex-Version': '5.8.0.475'
}
# 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 + \
'/video/:/transcode/universal/start.m3u8?'
args = {
@ -1469,7 +1446,7 @@ class API(object):
'directPlay': 0,
'directStream': 1,
'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,
'path': path,
'mediaIndex': self.mediastream,
@ -1478,12 +1455,11 @@ class API(object):
'location': 'lan',
'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)
args.update(quality)
url = transcode_path + urlencode(xargs) + '&' + urlencode(args)
return url
xargs.update(headers)
xargs.update(args)
xargs.update(quality)
return utils.extend_url(transcode_path, xargs)
def cache_external_subs(self):
"""
@ -1500,7 +1476,7 @@ class API(object):
for stream in mediastreams:
# Since plex returns all possible tracks together, have to pull
# only external subtitles - only for these a 'key' exists
if stream.get('streamType') != "3":
if cast(int, stream.get('streamType')) != 3:
# Not a subtitle
continue
# Only set for additional external subtitles NOT lying beside video
@ -1510,11 +1486,11 @@ class API(object):
if key:
# We do know the language - temporarily download
if stream.get('languageCode') is not None:
language = stream.get('languageCode')
codec = stream.get('codec')
path = self.download_external_subtitles(
"{server}%s" % key,
"subtitle%02d.%s.%s" % (fileindex,
stream.attrib['languageCode'],
stream.attrib['codec']))
"subtitle%02d.%s.%s" % (fileindex, language, codec))
fileindex += 1
# We don't know the language - no need to download
else:
@ -1788,7 +1764,7 @@ class API(object):
except ValueError:
pass
else:
args = quote(args)
args = utils.quote(args)
path = '%s:%s:%s' % (protocol, hostname, args)
if (app.SYNC.path_verified and not force_check) or omit_check:
return path

View file

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

View file

@ -2,9 +2,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from urllib import urlencode, quote_plus
from ast import literal_eval
from urlparse import urlparse, parse_qsl
from copy import deepcopy
from time import time
from threading import Thread
@ -57,9 +55,9 @@ def ParseContainerKey(containerKey):
Output hence: library, key, query (str, int, dict)
"""
result = urlparse(containerKey)
library, key = GetPlexKeyNumber(result.path)
query = dict(parse_qsl(result.query))
result = utils.urlparse(containerKey)
library, key = GetPlexKeyNumber(result.path.decode('utf-8'))
query = dict(utils.parse_qsl(result.query))
return library, key, query
@ -480,9 +478,9 @@ def GetPlexMetadata(key, reraise=False):
# 'includePopularLeaves': 1,
# 'includeConcerts': 1
}
url = url + '?' + urlencode(arguments)
try:
xml = DU().downloadUrl(url, reraise=reraise)
xml = DU().downloadUrl(utils.extend_url(url, arguments),
reraise=reraise)
except exceptions.RequestException:
# "PMS offline"
utils.dialog('notification',
@ -556,7 +554,7 @@ def GetAllPlexChildren(key):
Input:
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):
@ -569,9 +567,9 @@ def GetPlexSectionResults(viewId, args=None):
Returns None if something went wrong
"""
url = "{server}/library/sections/%s/all?" % viewId
url = "{server}/library/sections/%s/all" % viewId
if args:
url += urlencode(args) + '&'
url = utils.extend_url(url, args)
return DownloadChunks(url)
@ -726,9 +724,6 @@ class Leaves(DownloadGen):
def DownloadChunks(url):
"""
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.
"""
xml = None
@ -740,13 +735,13 @@ def DownloadChunks(url):
'X-Plex-Container-Start': pos,
'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
try:
xmlpart.attrib
except AttributeError:
LOG.error('Error while downloading chunks: %s',
url + urlencode(args))
LOG.error('Error while downloading chunks: %s, args: %s',
url, args)
pos += CONTAINERSIZE
error_counter += 1
continue
@ -799,16 +794,14 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None):
if updatedAt:
args.append('updatedAt>=%s' % updatedAt)
if args:
url += '?' + '&'.join(args) + '&'
else:
url += '?'
url += '?' + '&'.join(args)
return DownloadChunks(url)
def GetPlexOnDeck(viewId):
"""
"""
return DownloadChunks("{server}/library/sections/%s/onDeck?" % viewId)
return DownloadChunks("{server}/library/sections/%s/onDeck" % viewId)
def get_plex_hub():
@ -843,7 +836,7 @@ def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie',
}
if trailers is True:
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:
xml[0].tag
except (IndexError, TypeError, AttributeError):
@ -976,12 +969,12 @@ def scrobble(ratingKey, state):
'identifier': 'com.plexapp.plugins.library'
}
if state == "watched":
url = "{server}/:/scrobble?" + urlencode(args)
url = '{server}/:/scrobble'
elif state == "unwatched":
url = "{server}/:/unscrobble?" + urlencode(args)
url = '{server}/:/unscrobble'
else:
return
DU().downloadUrl(url)
DU().downloadUrl(utils.extend_url(url, args))
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
else: # internal path, add-on
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
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
# lenient...
path = path.encode('utf-8')
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 = {
'width': width,
'height': height,
@ -1071,4 +1065,4 @@ def transcode_image_path(key, AuthToken, path, width, height):
}
if 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 SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
import xbmc
from .. import companion
from .. import json_rpc as js
from .. import clientinfo
from .. import variables as v
from .. import utils, companion, json_rpc as js, clientinfo, variables as v
from .. import app
###############################################################################
@ -102,8 +97,8 @@ class MyHandler(BaseHTTPRequestHandler):
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
parseresult = utils.urlparse(self.path)
paramarrays = utils.parse_qs(parseresult.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]

View file

@ -10,9 +10,11 @@ from datetime import datetime
from unicodedata import normalize
from threading import Lock
import urllib
import urlparse as _urlparse
# Originally tried faster cElementTree, but does NOT work reliably with Kodi
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 functools import wraps
import hashlib
@ -25,8 +27,6 @@ import xbmcgui
from . import path_ops, variables as v
###############################################################################
LOG = getLogger('PLEX.utils')
WINDOW = xbmcgui.Window(10000)
@ -49,9 +49,6 @@ REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
# Grab Plex id from an URL-encoded string
REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
###############################################################################
# Main methods
def garbageCollect():
gc.collect(2)
@ -325,6 +322,73 @@ def encode_dict(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'):
"""
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 logging import getLogger
import urllib
try:
from multiprocessing.pool import ThreadPool
SUPPORTS_POOL = True
@ -75,10 +74,12 @@ def get_clean_image(image):
image = thumbcache
if image and b"image://" in image:
image = image.replace(b"image://", b"")
image = urllib.unquote(image)
if image.endswith(b"/"):
image = utils.unquote(image)
if image.endswith("/"):
image = image[:-1]
return image.decode('utf-8')
return image
else:
return image.decode('utf-8')
def generate_item(xml_element):
@ -227,7 +228,7 @@ def _generate_content(xml_element):
'key': key,
'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:
url = api.get_picture_path()
else: