Merge pull request #839 from croneter/beta-version

Bump master
This commit is contained in:
croneter 2019-04-21 10:07:23 +02:00 committed by GitHub
commit 71a150ea09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 411 additions and 291 deletions

5
.codacy.yaml Normal file
View file

@ -0,0 +1,5 @@
exclude_paths:
- 'resources/lib/watchdog/**'
- 'resources/lib/pathtools/**'
- 'resources/lib/pathtools/**'
- 'resources/lib/defused_etree.py'

View file

@ -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) [![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.10-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-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) [![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) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.10" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.14" provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
@ -77,9 +77,32 @@
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary> <summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description> <description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer> <disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
<news>version 2.7.10: <news>version 2.7.14:
- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database - Correctly clear window variables e.g. on user switch
- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes) - 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 - Fix playback sometimes not being reported for direct paths
- Update translations - Update translations

View file

@ -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: version 2.7.10:
- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database - 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 UnicodeEncodeError for Direct Paths and some PKC video nodes
- Fix playback sometimes not being reported for direct paths - Fix playback sometimes not being reported for direct paths
- Update translations - Update translations

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', '')
@ -134,6 +138,10 @@ class Main():
LOG.info('User requested to select Plex libraries') LOG.info('User requested to select Plex libraries')
transfer.plex_command('select-libraries') transfer.plex_command('select-libraries')
elif mode == 'refreshplaylist':
LOG.info('User requested to refresh Kodi playlists and nodes')
transfer.plex_command('refreshplaylist')
else: else:
entrypoint.show_main_menu(content_type=params.get('content_type')) entrypoint.show_main_menu(content_type=params.get('content_type'))

View file

@ -971,11 +971,6 @@ msgctxt "#39057"
msgid "Customize Paths" msgid "Customize Paths"
msgstr "" msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr ""
# PKC Settings - Appearance Tweaks # PKC Settings - Appearance Tweaks
msgctxt "#39059" msgctxt "#39059"
msgid "Recently Added: Append show title to episode" msgid "Recently Added: Append show title to episode"
@ -1013,7 +1008,7 @@ msgstr ""
# PKC Settings - Appearance Tweaks # PKC Settings - Appearance Tweaks
msgctxt "#39066" msgctxt "#39066"
msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)" msgid "Recently Added: Also show already watched movies"
msgstr "" msgstr ""
# PKC Settings - Connection # PKC Settings - Connection
@ -1102,6 +1097,11 @@ msgctxt "#39084"
msgid "Enter PMS port" msgid "Enter PMS port"
msgstr "" msgstr ""
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
msgctxt "#39200" msgctxt "#39200"
msgid "Log-out Plex Home User " msgid "Log-out Plex Home User "
msgstr "" msgstr ""

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

@ -148,7 +148,8 @@ class ContextMenu(object):
playqueue.clear() playqueue.clear()
app.PLAYSTATE.context_menu_play = True app.PLAYSTATE.context_menu_play = True
handle = self.api.path(force_first_media=False, force_addon=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): def _extras(self):
""" """

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

@ -567,4 +567,4 @@ class ContextMonitor(backgroundthread.KillableThread):
else: else:
# Different context menu is displayed # Different context menu is displayed
app.PLAYSTATE.resume_playback = False app.PLAYSTATE.resume_playback = False
app.APP.monitor.waitForAbort(0.1) xbmc.sleep(100)

View file

@ -252,6 +252,17 @@ def node_recent(section, node_name):
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag', rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
'operator': 'is'}) 'operator': 'is'})
etree.SubElement(rule, 'value').text = section.name 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, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content etree.SubElement(xml, 'content').text = section.content
@ -362,7 +373,7 @@ def node_lastplayed(section, node_name):
etree.SubElement(rule, 'value').text = section.name etree.SubElement(rule, 'value').text = section.name
rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount', rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount',
'operator': 'greaterthan'}) 'operator': 'greaterthan'})
etree.SubElement(rule, 'value').text = 0 etree.SubElement(rule, 'value').text = '0'
etree.SubElement(xml, 'label').text = node_name etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content etree.SubElement(xml, 'content').text = section.content

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
import urllib
import copy import copy
from . import nodes from . import nodes
@ -111,7 +110,9 @@ class Section(object):
return (self.section_id == section.section_id and return (self.section_id == section.section_id and
self.name == section.name and self.name == section.name and
self.section_type == section.section_type) self.section_type == section.section_type)
__ne__ = not __eq__
def __ne__(self, section):
return not self == section
@property @property
def section_id(self): def section_id(self):
@ -226,7 +227,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):
""" """
@ -262,8 +263,16 @@ class Section(object):
utils.window('%s.type' % self.node, value=self.content) utils.window('%s.type' % self.node, value=self.content)
utils.window('%s.content' % self.node, value=path) utils.window('%s.content' % self.node, value=path)
# .path leads to all elements of this library # .path leads to all elements of this library
if self.section_type in v.PLEX_VIDEOTYPES:
utils.window('%s.path' % self.node, utils.window('%s.path' % self.node,
value='ActivateWindow(Videos,%s,return)' % path) 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)) utils.window('%s.id' % self.node, value=str(self.section_id))
# To let the user navigate into this node when selecting widgets # To let the user navigate into this node when selecting widgets
utils.window('%s.addon_index' % self.node, value=addon_index) utils.window('%s.addon_index' % self.node, value=addon_index)
@ -617,6 +626,9 @@ def _sync_from_pms(pick_libraries):
# Remove the section itself # Remove the section itself
old_section.remove() 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 # Time to write the sections to Kodi
for section in sections: for section in sections:
section.to_kodi() section.to_kodi()
@ -629,14 +641,17 @@ def _sync_from_pms(pick_libraries):
def _clear_window_vars(index): def _clear_window_vars(index):
node = 'Plex.nodes.%s' % index node = 'Plex.nodes.%s' % index
utils.window('%s.index' % node, clear=True)
utils.window('%s.title' % node, clear=True) utils.window('%s.title' % node, clear=True)
utils.window('%s.type' % node, clear=True) utils.window('%s.type' % node, clear=True)
utils.window('%s.content' % node, clear=True) utils.window('%s.content' % node, clear=True)
utils.window('%s.path' % node, clear=True) utils.window('%s.path' % node, clear=True)
utils.window('%s.id' % node, clear=True) utils.window('%s.id' % node, clear=True)
utils.window('%s.addon_path' % 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: for kind in WINDOW_ARGS:
node = 'Plex.nodes.%s.%s' % (index, kind) node = 'Plex.nodes.%s.%s.%s' % (index, typus, kind)
utils.window(node, clear=True) utils.window(node, clear=True)
@ -644,7 +659,22 @@ def clear_window_vars():
""" """
Removes all references to sections stored in window vars 'Plex.nodes...' 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) number_of_nodes = int(utils.window('Plex.nodes.total') or 0)
utils.window('Plex.nodes.total', clear=True) utils.window('Plex.nodes.total', clear=True)
for index in range(number_of_nodes): for index in range(number_of_nodes):
_clear_window_vars(index) _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

View file

@ -23,7 +23,7 @@ def sync_pms_time():
# Get all Plex libraries # Get all Plex libraries
sections = PF.get_plex_sections() sections = PF.get_plex_sections()
if not sections: if sections is None:
LOG.error("Error download PMS views, abort sync_pms_time") LOG.error("Error download PMS views, abort sync_pms_time")
return False return False

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,7 +55,7 @@ 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',
@ -80,7 +79,7 @@ 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,

View file

@ -124,6 +124,9 @@ class PlayUtils():
try: try:
resolution = int(videoCodec['resolution']) resolution = int(videoCodec['resolution'])
except (TypeError, ValueError): except (TypeError, ValueError):
if videoCodec['resolution'] == '4k':
resolution = 2160
else:
LOG.info('No video resolution from PMS, not transcoding.') LOG.info('No video resolution from PMS, not transcoding.')
return False return False
if 'h265' in codec or 'hevc' in codec: if 'h265' in codec or 'hevc' in codec:

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,16 +1437,16 @@ 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 = {
'audioBoost': utils.settings('audioBoost'), 'audioBoost': utils.settings('audioBoost'),
'autoAdjustQuality': 0, 'autoAdjustQuality': 0,
'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:
@ -1694,96 +1670,6 @@ class API(object):
pass pass
return listitem 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): def disc_number(self):
""" """
Returns the song's disc number as an int or None if not found Returns the song's disc number as an int or None if not found
@ -1878,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
@ -1928,13 +1814,3 @@ class API(object):
# Kodi cannot locate the file #s. Please verify your PKC settings. Stop # Kodi cannot locate the file #s. Please verify your PKC settings. Stop
# syncing? # syncing?
return utils.yesno_dialog(utils.lang(29999), utils.lang(39031) % url) 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})

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,8 @@ 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)' handle = 'RunPlugin(plugin://%s)' % utils.extend_url(v.ADDON_ID, params)
% (v.ADDON_ID, urlencode(params))) executebuiltin(handle.encode('utf-8'))
@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
############################################################################### ###############################################################################
@ -52,6 +47,12 @@ class MyHandler(BaseHTTPRequestHandler):
self.serverlist = [] self.serverlist = []
BaseHTTPRequestHandler.__init__(self, *args, **kwargs) BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def log_message(self, format, *args):
'''
Mute all requests, don't log 'em
'''
pass
def do_HEAD(self): def do_HEAD(self):
LOG.debug("Serving HEAD request...") LOG.debug("Serving HEAD request...")
self.answer_request(0) self.answer_request(0)
@ -102,8 +103,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

@ -302,6 +302,36 @@ class Service(object):
finally: finally:
app.APP.resume_threads() 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): def _do_auth(self):
LOG.info('Authenticating user') LOG.info('Authenticating user')
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate 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' app.SYNC.run_lib_scan = 'textures'
elif plex_command == 'select-libraries': elif plex_command == 'select-libraries':
self.choose_plex_libraries() self.choose_plex_libraries()
elif plex_command == 'refreshplaylist':
self.reset_playlists_and_nodes()
elif plex_command == 'RESET-PKC': elif plex_command == 'RESET-PKC':
utils.reset() utils.reset()
elif plex_command == 'EXIT-PKC': elif plex_command == 'EXIT-PKC':

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,9 +74,11 @@ 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
else:
return image.decode('utf-8') return image.decode('utf-8')
@ -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:

View file

@ -157,10 +157,10 @@
--> -->
<category label="39073"><!-- Appearance Tweaks --> <category label="39073"><!-- Appearance Tweaks -->
<setting id="fetch_pms_item_number" label="39077" type="number" default="50" option="int" /> <setting label="[COLOR yellow]$ADDON[plugin.video.plexkodiconnect 39085][/COLOR]" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=refreshplaylist)" option="close" /><!-- Reload Kodi node files to apply all the settings below -->
<setting type="sep" /> <setting type="lsep" />
<setting id="fetch_pms_item_number" label="39077" type="number" default="50" option="int" visible="false" />
<setting type="lsep" label="39074" /><!-- TV Shows --> <setting type="lsep" label="39074" /><!-- TV Shows -->
<setting id="OnDeckTVextended" type="bool" label="39058" default="true" /><!-- Extend Plex TV Series "On Deck" view to all shows -->
<setting id="OnDeckTvAppendShow" type="bool" label="39047" default="false" /><!--On Deck view: Append show title to episode--> <setting id="OnDeckTvAppendShow" type="bool" label="39047" default="false" /><!--On Deck view: Append show title to episode-->
<setting id="OnDeckTvAppendSeason" type="bool" label="39048" default="false" /><!--On Deck view: Append season number to episode--> <setting id="OnDeckTvAppendSeason" type="bool" label="39048" default="false" /><!--On Deck view: Append season number to episode-->
<setting id="TVShowWatched" type="bool" label="39064" default="true" /><!--Recently Added: Also show already watched episodes--> <setting id="TVShowWatched" type="bool" label="39064" default="true" /><!--Recently Added: Also show already watched episodes-->