commit
71a150ea09
26 changed files with 411 additions and 291 deletions
5
.codacy.yaml
Normal file
5
.codacy.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
exclude_paths:
|
||||
- 'resources/lib/watchdog/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/defused_etree.py'
|
|
@ -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)
|
||||
|
|
31
addon.xml
31
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?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>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.9.1" />
|
||||
|
@ -77,9 +77,32 @@
|
|||
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
||||
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
||||
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
|
||||
<news>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)
|
||||
<news>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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
12
default.py
12
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'))
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
39
resources/lib/defused_etree.py
Normal file
39
resources/lib/defused_etree.py
Normal 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']
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' %
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 '
|
||||
|
|
|
@ -33,8 +33,6 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from re import sub
|
||||
from urllib import urlencode, unquote, quote
|
||||
from urlparse import parse_qsl
|
||||
|
||||
from xbmcgui import ListItem
|
||||
|
||||
|
@ -101,10 +99,8 @@ class API(object):
|
|||
"""
|
||||
Returns the unique int <ratingKey><updatedAt>
|
||||
"""
|
||||
return int('%s%s' % (self.item.get('ratingKey'),
|
||||
self.item.get('updatedAt',
|
||||
self.item.get('addedAt',
|
||||
1541572987))))
|
||||
return int('%s%s' % (self.plex_id(),
|
||||
self.updated_at() or self.item.get('addedAt', 1541572987)))
|
||||
|
||||
def plex_id(self):
|
||||
"""
|
||||
|
@ -152,9 +148,9 @@ class API(object):
|
|||
|
||||
def directory_path(self, section_id=None, plex_type=None, old_key=None,
|
||||
synched=True):
|
||||
key = cast(unicode, self.item.get('fastKey'))
|
||||
key = self.item.get('fastKey')
|
||||
if not key:
|
||||
key = cast(unicode, self.item.get('key'))
|
||||
key = self.item.get('key')
|
||||
if old_key:
|
||||
key = '%s/%s' % (old_key, key)
|
||||
elif not key.startswith('/'):
|
||||
|
@ -169,7 +165,7 @@ class API(object):
|
|||
params['synched'] = 'false'
|
||||
if self.item.get('prompt'):
|
||||
# User input needed, e.g. search for a movie or episode
|
||||
params['prompt'] = cast(unicode, self.item.get('prompt'))
|
||||
params['prompt'] = self.item.get('prompt')
|
||||
if section_id:
|
||||
params['id'] = section_id
|
||||
return utils.extend_url('plugin://%s/' % v.ADDON_ID, params)
|
||||
|
@ -210,7 +206,7 @@ class API(object):
|
|||
def file_path(self, force_first_media=False):
|
||||
"""
|
||||
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
|
||||
or None
|
||||
as unicode or None
|
||||
|
||||
force_first_media=True:
|
||||
will always use 1st media stream, e.g. when several different
|
||||
|
@ -221,51 +217,43 @@ class API(object):
|
|||
return
|
||||
try:
|
||||
if force_first_media is False:
|
||||
ans = self.item[self.mediastream][self.part].attrib['file']
|
||||
ans = cast(str, self.item[self.mediastream][self.part].attrib['file'])
|
||||
else:
|
||||
ans = self.item[0][self.part].attrib['file']
|
||||
ans = cast(str, self.item[0][self.part].attrib['file'])
|
||||
except (TypeError, AttributeError, IndexError, KeyError):
|
||||
ans = None
|
||||
if ans is not None:
|
||||
try:
|
||||
ans = utils.try_decode(unquote(ans))
|
||||
except UnicodeDecodeError:
|
||||
# Sometimes, Plex seems to have encoded in latin1
|
||||
ans = unquote(ans).decode('latin1')
|
||||
return ans
|
||||
return
|
||||
return utils.unquote(ans)
|
||||
|
||||
def get_picture_path(self):
|
||||
"""
|
||||
Returns the item's picture path (transcode, if necessary) as string.
|
||||
Will always use addon paths, never direct paths
|
||||
"""
|
||||
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower()
|
||||
path = self.item[0][0].get('key')
|
||||
extension = path[path.rfind('.'):].lower()
|
||||
if app.SYNC.force_transcode_pix or extension not in v.KODI_SUPPORTED_IMAGES:
|
||||
# Let Plex transcode
|
||||
# max width/height supported by plex image transcoder is 1920x1080
|
||||
path = app.CONN.server + PF.transcode_image_path(
|
||||
self.item[0][0].get('key'),
|
||||
path,
|
||||
app.ACCOUNT.pms_token,
|
||||
"%s%s" % (app.CONN.server, self.item[0][0].get('key')),
|
||||
"%s%s" % (app.CONN.server, path),
|
||||
1920,
|
||||
1080)
|
||||
else:
|
||||
path = self.attach_plex_token_to_url(
|
||||
'%s%s' % (app.CONN.server, self.item[0][0].attrib['key']))
|
||||
path = self.attach_plex_token_to_url('%s%s' % (app.CONN.server, path))
|
||||
# Attach Plex id to url to let it be picked up by our playqueue agent
|
||||
# later
|
||||
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id()))
|
||||
return '%s&plex_id=%s' % (path, self.plex_id())
|
||||
|
||||
def tv_show_path(self):
|
||||
"""
|
||||
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
|
||||
or None
|
||||
"""
|
||||
res = None
|
||||
for child in self.item:
|
||||
if child.tag == 'Location':
|
||||
res = child.get('path')
|
||||
return res
|
||||
return child.get('path')
|
||||
|
||||
def season_number(self):
|
||||
"""
|
||||
|
@ -295,10 +283,7 @@ class API(object):
|
|||
"""
|
||||
Returns the play count for the item as an int or the int 0 if not found
|
||||
"""
|
||||
try:
|
||||
return int(self.item.attrib['viewCount'])
|
||||
except (KeyError, ValueError):
|
||||
return 0
|
||||
return cast(int, self.item.get('viewCount')) or 0
|
||||
|
||||
def userdata(self):
|
||||
"""
|
||||
|
@ -781,8 +766,7 @@ class API(object):
|
|||
'container': self._data_from_part_or_media('container'),
|
||||
}
|
||||
try:
|
||||
answ['bitDepth'] = self.item[0][self.part][self.mediastream].get(
|
||||
'bitDepth')
|
||||
answ['bitDepth'] = self.item[0][self.part][self.mediastream].get('bitDepth')
|
||||
except (TypeError, AttributeError, KeyError, IndexError):
|
||||
answ['bitDepth'] = None
|
||||
return answ
|
||||
|
@ -848,7 +832,7 @@ class API(object):
|
|||
subtitlelanguages = []
|
||||
try:
|
||||
# Sometimes, aspectratio is on the "toplevel"
|
||||
aspect = self.item[0].get('aspectRatio')
|
||||
aspect = cast(float, self.item[0].get('aspectRatio'))
|
||||
except IndexError:
|
||||
# There is no stream info at all, returning empty
|
||||
return {
|
||||
|
@ -860,39 +844,37 @@ class API(object):
|
|||
for child in self.item[0]:
|
||||
container = child.get('container')
|
||||
# Loop over Streams
|
||||
for grandchild in child:
|
||||
stream = grandchild.attrib
|
||||
for stream in child:
|
||||
media_type = int(stream.get('streamType', 999))
|
||||
track = {}
|
||||
if media_type == 1: # Video streams
|
||||
if 'codec' in stream:
|
||||
track['codec'] = stream['codec'].lower()
|
||||
if 'codec' in stream.attrib:
|
||||
track['codec'] = stream.get('codec').lower()
|
||||
if "msmpeg4" in track['codec']:
|
||||
track['codec'] = "divx"
|
||||
elif "mpeg4" in track['codec']:
|
||||
# if "simple profile" in profile or profile == "":
|
||||
# track['codec'] = "xvid"
|
||||
pass
|
||||
elif "h264" in track['codec']:
|
||||
if container in ("mp4", "mov", "m4v"):
|
||||
track['codec'] = "avc1"
|
||||
track['height'] = stream.get('height')
|
||||
track['width'] = stream.get('width')
|
||||
track['height'] = cast(int, stream.get('height'))
|
||||
track['width'] = cast(int, stream.get('width'))
|
||||
# track['Video3DFormat'] = item.get('Video3DFormat')
|
||||
track['aspect'] = stream.get('aspectRatio', aspect)
|
||||
track['duration'] = self.resume_runtime()[1]
|
||||
track['aspect'] = cast(float,
|
||||
stream.get('aspectRatio') or aspect)
|
||||
track['duration'] = self.runtime()
|
||||
track['video3DFormat'] = None
|
||||
videotracks.append(track)
|
||||
elif media_type == 2: # Audio streams
|
||||
if 'codec' in stream:
|
||||
track['codec'] = stream['codec'].lower()
|
||||
if 'codec' in stream.attrib:
|
||||
track['codec'] = stream.get('codec').lower()
|
||||
if ("dca" in track['codec'] and
|
||||
"ma" in stream.get('profile', '').lower()):
|
||||
track['codec'] = "dtshd_ma"
|
||||
track['channels'] = stream.get('channels')
|
||||
track['channels'] = cast(int, stream.get('channels'))
|
||||
# 'unknown' if we cannot get language
|
||||
track['language'] = stream.get(
|
||||
'languageCode', utils.lang(39310)).lower()
|
||||
track['language'] = stream.get('languageCode',
|
||||
utils.lang(39310).lower())
|
||||
audiotracks.append(track)
|
||||
elif media_type == 3: # Subtitle streams
|
||||
# 'unknown' if we cannot get language
|
||||
|
@ -925,7 +907,7 @@ class API(object):
|
|||
# e.g. Plex collections where artwork already contains
|
||||
# width and height. Need to upscale for better resolution
|
||||
artwork, args = artwork.split('?')
|
||||
args = dict(parse_qsl(args))
|
||||
args = dict(utils.parse_qsl(args))
|
||||
width = int(args.get('width', 400))
|
||||
height = int(args.get('height', 400))
|
||||
# Adjust to 4k resolution 1920x1080
|
||||
|
@ -938,7 +920,7 @@ class API(object):
|
|||
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
||||
artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
|
||||
'minSize=1&upscale=0&url=%s'
|
||||
% (app.CONN.server, quote(artwork)))
|
||||
% (app.CONN.server, utils.quote(artwork)))
|
||||
artwork = self.attach_plex_token_to_url(artwork)
|
||||
return artwork
|
||||
|
||||
|
@ -1297,9 +1279,9 @@ class API(object):
|
|||
def library_section_id(self):
|
||||
"""
|
||||
Returns the id of the Plex library section (for e.g. a movies section)
|
||||
or None
|
||||
as an int or None
|
||||
"""
|
||||
return self.item.get('librarySectionID')
|
||||
return cast(int, self.item.get('librarySectionID'))
|
||||
|
||||
def collections_match(self, section_id):
|
||||
"""
|
||||
|
@ -1345,7 +1327,7 @@ class API(object):
|
|||
Returns True if the item's 'optimizedForStreaming' is set, False other-
|
||||
wise
|
||||
"""
|
||||
return self.item[0].get('optimizedForStreaming') == '1'
|
||||
return cast(bool, self.item[0].get('optimizedForStreaming')) or False
|
||||
|
||||
def mediastream_number(self):
|
||||
"""
|
||||
|
@ -1371,16 +1353,16 @@ class API(object):
|
|||
for entry in self.item.iterfind('./Media'):
|
||||
# Get additional info (filename / languages)
|
||||
if 'file' in entry[0].attrib:
|
||||
option = utils.try_decode(entry[0].attrib['file'])
|
||||
option = path_ops.path.basename(option)
|
||||
option = entry[0].get('file')
|
||||
option = path_ops.basename(option)
|
||||
else:
|
||||
option = self.title() or ''
|
||||
# Languages of audio streams
|
||||
languages = []
|
||||
for stream in entry[0]:
|
||||
if (stream.attrib['streamType'] == '1' and
|
||||
if (cast(int, stream.get('streamType')) == 1 and
|
||||
'language' in stream.attrib):
|
||||
language = utils.try_decode(stream.attrib['language'])
|
||||
language = stream.get('language')
|
||||
languages.append(language)
|
||||
languages = ', '.join(languages)
|
||||
if languages:
|
||||
|
@ -1391,19 +1373,19 @@ class API(object):
|
|||
else:
|
||||
option = '%s ' % option
|
||||
if 'videoResolution' in entry.attrib:
|
||||
res = utils.try_decode(entry.attrib['videoResolution'])
|
||||
res = entry.get('videoResolution')
|
||||
option = '%s%sp ' % (option, res)
|
||||
if 'videoCodec' in entry.attrib:
|
||||
codec = utils.try_decode(entry.attrib['videoCodec'])
|
||||
codec = entry.get('videoCodec')
|
||||
option = '%s%s' % (option, codec)
|
||||
option = option.strip() + ' - '
|
||||
if 'audioProfile' in entry.attrib:
|
||||
profile = utils.try_decode(entry.attrib['audioProfile'])
|
||||
profile = entry.get('audioProfile')
|
||||
option = '%s%s ' % (option, profile)
|
||||
if 'audioCodec' in entry.attrib:
|
||||
codec = utils.try_decode(entry.attrib['audioCodec'])
|
||||
codec = entry.get('audioCodec')
|
||||
option = '%s%s ' % (option, codec)
|
||||
option = utils.try_encode(option.strip())
|
||||
option = cast(str, option.strip())
|
||||
dialoglist.append(option)
|
||||
media = utils.dialog('select', 'Select stream', dialoglist)
|
||||
if media == -1:
|
||||
|
@ -1437,20 +1419,15 @@ class API(object):
|
|||
"""
|
||||
if self.mediastream is None and self.mediastream_number() is None:
|
||||
return
|
||||
if quality is None:
|
||||
quality = {}
|
||||
quality = {} if quality is None else quality
|
||||
xargs = clientinfo.getXArgsDeviceInfo()
|
||||
# For DirectPlay, path/key of PART is needed
|
||||
# trailers are 'clip' with PMS xmls
|
||||
if action == "DirectStream":
|
||||
path = self.item[self.mediastream][self.part].attrib['key']
|
||||
path = self.item[self.mediastream][self.part].get('key')
|
||||
url = app.CONN.server + path
|
||||
# e.g. Trailers already feature an '?'!
|
||||
if '?' in url:
|
||||
url += '&' + urlencode(xargs)
|
||||
else:
|
||||
url += '?' + urlencode(xargs)
|
||||
return url
|
||||
return utils.extend_url(url, xargs)
|
||||
|
||||
# For Transcoding
|
||||
headers = {
|
||||
|
@ -1460,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})
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -157,10 +157,10 @@
|
|||
-->
|
||||
|
||||
<category label="39073"><!-- Appearance Tweaks -->
|
||||
<setting id="fetch_pms_item_number" label="39077" type="number" default="50" option="int" />
|
||||
<setting type="sep" />
|
||||
<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="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 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="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-->
|
||||
|
|
Loading…
Reference in a new issue