diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 00000000..8ed7a02a --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,5 @@ +exclude_paths: + - 'resources/lib/watchdog/**' + - 'resources/lib/pathtools/**' + - 'resources/lib/pathtools/**' + - 'resources/lib/defused_etree.py' diff --git a/README.md b/README.md index 9ef65f2c..442392a1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-2.7.10-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.7.10-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![stable version](https://img.shields.io/badge/stable_version-2.7.14-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.7.14-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 9c9ad744..059033d4 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -77,9 +77,32 @@ Нативна інтеграція Plex в Kodi Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик! Використовуйте на свій ризик - version 2.7.10: -- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database -- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes) + version 2.7.14: +- Correctly clear window variables e.g. on user switch +- Reload skin on resetting PKC video nodes +- Fix last-played node value to ensure a playcount greater than zero +- 2.7.11-2.7.13 for everyone + +version 2.7.13 (beta only): +- Fix transcoding not working +- Fix 4k H265 not being transcoded +- Fix some appearance tweak settings +- Fix music and picture nodes pointing to video library +- Fix unequality when comparing sections +- Fix Plex Companion logging error messages + +version 2.7.12 (beta only): +- Fix UnicodeEncodeError on playback startup for direct paths +- Attempt to fix rare Kodi crash on PKC exit + +version 2.7.11 (beta only): +- Fixes to unicode +- Cleanup code, remove some obsolet methods and functions +- Fix FutureWarning + +version 2.7.10: +- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database) +- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes - Fix playback sometimes not being reported for direct paths - Update translations diff --git a/changelog.txt b/changelog.txt index 7d2b621d..8a26ad4a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,29 @@ +version 2.7.14: +- Correctly clear window variables e.g. on user switch +- Reload skin on resetting PKC video nodes +- Fix last-played node value to ensure a playcount greater than zero +- 2.7.11-2.7.13 for everyone + +version 2.7.13 (beta only): +- Fix transcoding not working +- Fix 4k H265 not being transcoded +- Fix some appearance tweak settings +- Fix music and picture nodes pointing to video library +- Fix unequality when comparing sections +- Fix Plex Companion logging error messages + +version 2.7.12 (beta only): +- Fix UnicodeEncodeError on playback startup for direct paths +- Attempt to fix rare Kodi crash on PKC exit + +version 2.7.11 (beta only): +- Fixes to unicode +- Cleanup code, remove some obsolet methods and functions +- Fix FutureWarning + version 2.7.10: -- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database -- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes) +- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database) +- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes - Fix playback sometimes not being reported for direct paths - Update translations diff --git a/default.py b/default.py index bd2f4a7d..131c923a 100644 --- a/default.py +++ b/default.py @@ -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', '') @@ -134,6 +138,10 @@ class Main(): LOG.info('User requested to select Plex libraries') transfer.plex_command('select-libraries') + elif mode == 'refreshplaylist': + LOG.info('User requested to refresh Kodi playlists and nodes') + transfer.plex_command('refreshplaylist') + else: entrypoint.show_main_menu(content_type=params.get('content_type')) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index ba4650ea..b4a82a8b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -971,11 +971,6 @@ msgctxt "#39057" msgid "Customize Paths" msgstr "" -# PKC Settings - Appearance Tweaks -msgctxt "#39058" -msgid "Extend Plex TV Series \"On Deck\" view to all shows" -msgstr "" - # PKC Settings - Appearance Tweaks msgctxt "#39059" msgid "Recently Added: Append show title to episode" @@ -1013,7 +1008,7 @@ msgstr "" # PKC Settings - Appearance Tweaks msgctxt "#39066" -msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)" +msgid "Recently Added: Also show already watched movies" msgstr "" # PKC Settings - Connection @@ -1102,6 +1097,11 @@ msgctxt "#39084" msgid "Enter PMS port" msgstr "" +# PKC settings - Appearance Tweaks +msgctxt "#39085" +msgid "Reload Kodi node files to apply all the settings below" +msgstr "" + msgctxt "#39200" msgid "Log-out Plex Home User " msgstr "" diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index d8a394b9..47fdfd45 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -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: diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index e648ea06..211ca709 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -148,7 +148,8 @@ class ContextMenu(object): playqueue.clear() app.PLAYSTATE.context_menu_play = True handle = self.api.path(force_first_media=False, force_addon=True) - xbmc.executebuiltin('RunPlugin(%s)' % handle) + handle = 'RunPlugin(%s)' % handle + xbmc.executebuiltin(handle.encode('utf-8')) def _extras(self): """ diff --git a/resources/lib/defused_etree.py b/resources/lib/defused_etree.py new file mode 100644 index 00000000..503324f1 --- /dev/null +++ b/resources/lib/defused_etree.py @@ -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'] diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index ae2e8ef3..f5ea2288 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -567,4 +567,4 @@ class ContextMonitor(backgroundthread.KillableThread): else: # Different context menu is displayed app.PLAYSTATE.resume_playback = False - app.APP.monitor.waitForAbort(0.1) + xbmc.sleep(100) diff --git a/resources/lib/library_sync/nodes.py b/resources/lib/library_sync/nodes.py index 0203e964..369f37cb 100644 --- a/resources/lib/library_sync/nodes.py +++ b/resources/lib/library_sync/nodes.py @@ -252,6 +252,17 @@ def node_recent(section, node_name): rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag', 'operator': 'is'}) etree.SubElement(rule, 'value').text = section.name + if ((section.section_type == v.PLEX_TYPE_SHOW and + utils.settings('TVShowWatched') == 'false') or + (section.section_type == v.PLEX_TYPE_MOVIE and + utils.settings('MovieShowWatched') == 'false')): + # Adds an additional rule if user deactivated the PKC setting + # "Recently Added: Also show already watched episodes" + # or + # "Recently Added: Also show already watched episodes" + rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount', + 'operator': 'is'}) + etree.SubElement(rule, 'value').text = '0' etree.SubElement(xml, 'label').text = node_name etree.SubElement(xml, 'icon').text = ICON_PATH etree.SubElement(xml, 'content').text = section.content @@ -362,7 +373,7 @@ def node_lastplayed(section, node_name): etree.SubElement(rule, 'value').text = section.name rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount', 'operator': 'greaterthan'}) - etree.SubElement(rule, 'value').text = 0 + etree.SubElement(rule, 'value').text = '0' etree.SubElement(xml, 'label').text = node_name etree.SubElement(xml, 'icon').text = ICON_PATH etree.SubElement(xml, 'content').text = section.content diff --git a/resources/lib/library_sync/sections.py b/resources/lib/library_sync/sections.py index 22a38f90..5a57be7d 100644 --- a/resources/lib/library_sync/sections.py +++ b/resources/lib/library_sync/sections.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -import urllib import copy from . import nodes @@ -111,7 +110,9 @@ class Section(object): return (self.section_id == section.section_id and self.name == section.name and self.section_type == section.section_type) - __ne__ = not __eq__ + + def __ne__(self, section): + return not self == section @property def section_id(self): @@ -226,7 +227,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): """ @@ -262,8 +263,16 @@ class Section(object): utils.window('%s.type' % self.node, value=self.content) utils.window('%s.content' % self.node, value=path) # .path leads to all elements of this library - utils.window('%s.path' % self.node, - value='ActivateWindow(Videos,%s,return)' % path) + if self.section_type in v.PLEX_VIDEOTYPES: + utils.window('%s.path' % self.node, + value='ActivateWindow(videos,%s,return)' % path) + elif self.section_type == v.PLEX_TYPE_ARTIST: + utils.window('%s.path' % self.node, + value='ActivateWindow(music,%s,return)' % path) + else: + # Pictures + utils.window('%s.path' % self.node, + value='ActivateWindow(pictures,%s,return)' % path) utils.window('%s.id' % self.node, value=str(self.section_id)) # To let the user navigate into this node when selecting widgets utils.window('%s.addon_index' % self.node, value=addon_index) @@ -617,6 +626,9 @@ def _sync_from_pms(pick_libraries): # Remove the section itself old_section.remove() + # Clear all existing window vars because we did NOT remove them with the + # command section.remove() + clear_window_vars() # Time to write the sections to Kodi for section in sections: section.to_kodi() @@ -629,22 +641,40 @@ def _sync_from_pms(pick_libraries): def _clear_window_vars(index): node = 'Plex.nodes.%s' % index + utils.window('%s.index' % node, clear=True) utils.window('%s.title' % node, clear=True) utils.window('%s.type' % node, clear=True) utils.window('%s.content' % node, clear=True) utils.window('%s.path' % node, clear=True) utils.window('%s.id' % node, clear=True) - utils.window('%s.addon_path' % node, clear=True) - for kind in WINDOW_ARGS: - node = 'Plex.nodes.%s.%s' % (index, kind) - utils.window(node, clear=True) + utils.window('%s.addon_index' % node, clear=True) + # Just clear everything here, ignore the plex_type + for typus in (x[0] for y in nodes.NODE_TYPES.values() for x in y): + for kind in WINDOW_ARGS: + node = 'Plex.nodes.%s.%s.%s' % (index, typus, kind) + utils.window(node, clear=True) def clear_window_vars(): """ Removes all references to sections stored in window vars 'Plex.nodes...' """ + LOG.debug('Clearing all the Plex video node variables') number_of_nodes = int(utils.window('Plex.nodes.total') or 0) utils.window('Plex.nodes.total', clear=True) for index in range(number_of_nodes): _clear_window_vars(index) + + +def delete_videonode_files(): + """ + Removes all the PKC video node files under userdata/library/video that + start with 'Plex-' + """ + for root, dirs, _ in path_ops.walk(LIBRARY_PATH): + for directory in dirs: + if directory.startswith('Plex-'): + abs_path = path_ops.path.join(root, directory) + LOG.info('Removing video node directory %s', abs_path) + path_ops.rmtree(abs_path, ignore_errors=True) + break diff --git a/resources/lib/library_sync/time.py b/resources/lib/library_sync/time.py index 55ccb7b2..0a237234 100644 --- a/resources/lib/library_sync/time.py +++ b/resources/lib/library_sync/time.py @@ -23,7 +23,7 @@ def sync_pms_time(): # Get all Plex libraries sections = PF.get_plex_sections() - if not sections: + if sections is None: LOG.error("Error download PMS views, abort sync_pms_time") return False diff --git a/resources/lib/path_ops.py b/resources/lib/path_ops.py index 9ec23687..b915fa1c 100644 --- a/resources/lib/path_ops.py +++ b/resources/lib/path_ops.py @@ -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 '' diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index b8673bc6..5800f0eb 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -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) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 96fd6cfd..a0ba8645 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -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' % diff --git a/resources/lib/playlists/pms.py b/resources/lib/playlists/pms.py index 2d69ee75..b909b8e4 100644 --- a/resources/lib/playlists/pms.py +++ b/resources/lib/playlists/pms.py @@ -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', diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index e5780051..773f9fb8 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -124,8 +124,11 @@ class PlayUtils(): try: resolution = int(videoCodec['resolution']) except (TypeError, ValueError): - LOG.info('No video resolution from PMS, not transcoding.') - return False + if videoCodec['resolution'] == '4k': + resolution = 2160 + else: + LOG.info('No video resolution from PMS, not transcoding.') + return False if 'h265' in codec or 'hevc' in codec: if resolution >= self.getH265(): LOG.info('Option to transcode h265/HEVC enabled. Resolution ' diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index 8d40824b..f8152abf 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -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 """ - 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,16 +1437,16 @@ 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?' + '/video/:/transcode/universal/start.m3u8' args = { 'audioBoost': utils.settings('audioBoost'), 'autoAdjustQuality': 0, '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: @@ -1694,96 +1670,6 @@ class API(object): pass return listitem - def _create_folder_listitem(self, listitem=None): - """ - Use for video items only - Call on a child level of PMS xml response (e.g. in a for loop) - - listitem : existing xbmcgui.ListItem to work with - otherwise, a new one is created - append_show_title : True to append TV show title to episode title - append_sxxexx : True to append SxxExx to episode title - - Returns XBMC listitem for this PMS library item - """ - title = self.title() - typus = self.plex_type() - - if listitem is None: - listitem = ListItem(title) - else: - listitem.setLabel(title) - # Necessary; Kodi won't start video otherwise! - listitem.setProperty('IsPlayable', 'true') - # Video items, e.g. movies and episodes or clips - people = self.people() - userdata = self.userdata() - metadata = { - 'genre': self.genre_list(), - 'country': self.country_list(), - 'year': self.year(), - 'rating': self.audience_rating(), - 'playcount': userdata['PlayCount'], - 'cast': people['Cast'], - 'director': people['Director'], - 'plot': self.plot(), - 'sorttitle': self.sorttitle(), - 'duration': userdata['Runtime'], - 'studio': self.music_studio_list(), - 'tagline': self.tagline(), - 'writer': people.get('Writer'), - 'premiered': self.premiere_date(), - 'dateadded': self.date_created(), - 'lastplayed': userdata['LastPlayedDate'], - 'mpaa': self.content_rating(), - 'aired': self.premiere_date(), - } - # Do NOT set resumetime - otherwise Kodi always resumes at that time - # even if the user chose to start element from the beginning - # listitem.setProperty('resumetime', str(userdata['Resume'])) - listitem.setProperty('totaltime', str(userdata['Runtime'])) - - if typus == v.PLEX_TYPE_EPISODE: - metadata['mediatype'] = 'episode' - _, _, show, season, episode = self.episode_data() - season = -1 if season is None else int(season) - episode = -1 if episode is None else int(episode) - metadata['episode'] = episode - metadata['sortepisode'] = episode - metadata['season'] = season - metadata['sortseason'] = season - metadata['tvshowtitle'] = show - if season and episode: - if append_sxxexx is True: - title = "S%.2dE%.2d - %s" % (season, episode, title) - if append_show_title is True: - title = "%s - %s " % (show, title) - if append_show_title or append_sxxexx: - listitem.setLabel(title) - elif typus == v.PLEX_TYPE_MOVIE: - metadata['mediatype'] = 'movie' - else: - # E.g. clips, trailers, ... - pass - - plex_id = self.plex_id() - listitem.setProperty('plexid', str(plex_id)) - with PlexDB() as plexdb: - db_item = plexdb.item_by_id(plex_id, self.plex_type()) - if db_item: - metadata['dbid'] = db_item['kodi_id'] - metadata['title'] = title - # Expensive operation - listitem.setInfo('video', infoLabels=metadata) - try: - # Add context menu entry for information screen - listitem.addContextMenuItems([(utils.lang(30032), - 'XBMC.Action(Info)',)]) - except TypeError: - # Kodi fuck-up - pass - return listitem - def disc_number(self): """ Returns the song's disc number as an int or None if not found @@ -1878,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 @@ -1928,13 +1814,3 @@ class API(object): # Kodi cannot locate the file #s. Please verify your PKC settings. Stop # syncing? return utils.yesno_dialog(utils.lang(29999), utils.lang(39031) % url) - - @staticmethod - def _set_listitem_artprop(listitem, arttype, path): - if arttype in ( - 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators'): - listitem.setProperty(arttype, path) - else: - listitem.setArt({arttype: path}) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 37fd6e4d..267e42e0 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -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,8 @@ class PlexCompanion(backgroundthread.KillableThread): 'key': '{server}%s' % data.get('key'), 'offset': data.get('offset') } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) + handle = 'RunPlugin(plugin://%s)' % utils.extend_url(v.ADDON_ID, params) + executebuiltin(handle.encode('utf-8')) @staticmethod def _process_playlist(data): diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index c4dcdb45..b70efe8c 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -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) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index d50c2a20..00b0cb4b 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -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 ############################################################################### @@ -52,6 +47,12 @@ class MyHandler(BaseHTTPRequestHandler): self.serverlist = [] BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + def log_message(self, format, *args): + ''' + Mute all requests, don't log 'em + ''' + pass + def do_HEAD(self): LOG.debug("Serving HEAD request...") self.answer_request(0) @@ -102,8 +103,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] diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index fa929595..b00e879b 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -302,6 +302,36 @@ class Service(object): finally: app.APP.resume_threads() + def reset_playlists_and_nodes(self): + """ + Resets the Kodi playlists and nodes for all the PKC libraries by + deleting all of them first, then rewriting everything + """ + app.APP.suspend_threads() + from .library_sync import sections + try: + sections.clear_window_vars() + sections.delete_videonode_files() + # Get newest sections from the PMS + if not sections.sync_from_pms(self, pick_libraries=False): + LOG.warn('We could not successfully reset the playlists!') + # "Plex playlists/nodes refresh failed" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39406), + icon='{plex}', + sound=False) + return + # "Plex playlists/nodes refreshed" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39405), + icon='{plex}', + sound=False) + finally: + app.APP.resume_threads() + xbmc.executebuiltin('ReloadSkin()') + def _do_auth(self): LOG.info('Authenticating user') if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate @@ -449,6 +479,8 @@ class Service(object): app.SYNC.run_lib_scan = 'textures' elif plex_command == 'select-libraries': self.choose_plex_libraries() + elif plex_command == 'refreshplaylist': + self.reset_playlists_and_nodes() elif plex_command == 'RESET-PKC': utils.reset() elif plex_command == 'EXIT-PKC': diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 06c09229..afcafdb1 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -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 diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 9c2018d2..757b66fa 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -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: diff --git a/resources/settings.xml b/resources/settings.xml index d1fc8f1e..79212504 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -157,10 +157,10 @@ --> - - + + + -