Merge branch 'beta-version'

This commit is contained in:
croneter 2018-06-23 19:22:12 +02:00
commit 7ee68937bc
68 changed files with 2247 additions and 2068 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-2.1.1-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.2.1-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.2.2-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)

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.1" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.2" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.4" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.4" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.5" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.5" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides>
@ -67,7 +67,17 @@
<summary lang="ru_RU">Нативная интеграция сервера Plex в Kodi</summary>
<description lang="ru_RU">Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы данных музыки и видео в Kodi (так как плагин напрямую их изменяет). Используйте на свой страх и риск</description>
<disclaimer lang="ru_RU">Используйте на свой страх и риск</disclaimer>
<news>version 2.2.1 (beta only):
<news>version 2.2.2 (beta only):
- Fixes to locking mechanisms which resulted in weird behavior in some cases
- Switch to Python __future__ unicode_literals and absolute paths
- Fix UnboundLocalError for playlists
- Check all Kodi database versions before starting PKC
- Fix KeyError on non-PKC playback startup
- Speed up subtitle download to Kodi
- Update translations
- PEP-8 stuff
version 2.2.1 (beta only):
- Fix library sync crash due to PMS sending string, not unicode
- Fix playback from playlists for add-on paths
- Detect playback from a Kodi playlist for add-on paths - because we need some hacks due to Kodi bugs

View file

@ -1,3 +1,13 @@
version 2.2.2 (beta only):
- Fixes to locking mechanisms which resulted in weird behavior in some cases
- Switch to Python __future__ unicode_literals and absolute paths
- Fix UnboundLocalError for playlists
- Check all Kodi database versions before starting PKC
- Fix KeyError on non-PKC playback startup
- Speed up subtitle download to Kodi
- Update translations
- PEP-8 stuff
version 2.2.1 (beta only):
- Fix library sync crash due to PMS sending string, not unicode
- Fix playback from playlists for add-on paths

View file

@ -1,47 +1,20 @@
# -*- coding: utf-8 -*-
###############################################################################
from __future__ import absolute_import, division, unicode_literals
import logging
from os import path as os_path
from sys import path as sys_path, argv
from sys import argv
from urlparse import parse_qsl
from xbmc import translatePath, sleep, executebuiltin
from xbmcaddon import Addon
from xbmc import sleep, executebuiltin
from xbmcgui import ListItem
from xbmcplugin import setResolvedUrl
_addon = Addon(id='plugin.video.plexkodiconnect')
try:
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
_addon_path = _addon.getAddonInfo('path').decode()
try:
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode()
sys_path.append(_base_resource)
from resources.lib import entrypoint, utils, pickler, pkc_listitem, \
variables as v, loghandler
from resources.lib.watchdog.utils import unicode_paths
###############################################################################
import entrypoint
from utils import window, reset, passwords_xml, language as lang, dialog, \
plex_command
from pickler import unpickle_me, pickl_window
from PKC_listitem import convert_PKC_to_listitem
import variables as v
###############################################################################
import loghandler
loghandler.config()
log = logging.getLogger('PLEX.default')
@ -54,9 +27,11 @@ class Main():
# MAIN ENTRY POINT
# @utils.profiling()
def __init__(self):
log.debug('Full sys.argv received: %s' % argv)
log.debug('Full sys.argv received: %s', argv)
# Parse parameters
params = dict(parse_qsl(argv[2][1:]))
path = unicode_paths.decode(argv[0])
arguments = unicode_paths.decode(argv[2])
params = dict(parse_qsl(arguments[1:]))
mode = params.get('mode', '')
itemid = params.get('id', '')
@ -104,7 +79,7 @@ class Main():
entrypoint.create_new_pms()
elif mode == 'reset':
reset()
utils.reset()
elif mode == 'togglePlexTV':
entrypoint.toggle_plex_tv_sign_in()
@ -113,51 +88,51 @@ class Main():
entrypoint.reset_authorization()
elif mode == 'passwords':
passwords_xml()
utils.passwords_xml()
elif mode == 'switchuser':
entrypoint.switch_plex_user()
elif mode in ('manualsync', 'repair'):
if window('plex_online') != 'true':
if pickler.pickl_window('plex_online') != 'true':
# Server is not online, do not run the sync
dialog('ok', lang(29999), lang(39205))
utils.dialog('ok', utils.lang(29999), utils.lang(39205))
log.error('Not connected to a PMS.')
else:
if mode == 'repair':
log.info('Requesting repair lib sync')
plex_command('RUN_LIB_SCAN', 'repair')
utils.plex_command('RUN_LIB_SCAN', 'repair')
elif mode == 'manualsync':
log.info('Requesting full library scan')
plex_command('RUN_LIB_SCAN', 'full')
utils.plex_command('RUN_LIB_SCAN', 'full')
elif mode == 'texturecache':
log.info('Requesting texture caching of all textures')
plex_command('RUN_LIB_SCAN', 'textures')
utils.plex_command('RUN_LIB_SCAN', 'textures')
elif mode == 'chooseServer':
entrypoint.choose_pms_server()
elif mode == 'refreshplaylist':
log.info('Requesting playlist/nodes refresh')
plex_command('RUN_LIB_SCAN', 'views')
utils.plex_command('RUN_LIB_SCAN', 'views')
elif mode == 'deviceid':
self.deviceid()
elif mode == 'fanart':
log.info('User requested fanarttv refresh')
plex_command('RUN_LIB_SCAN', 'fanart')
utils.plex_command('RUN_LIB_SCAN', 'fanart')
elif '/extrafanart' in argv[0]:
plexpath = argv[2][1:]
elif '/extrafanart' in path:
plexpath = arguments[1:]
plexid = itemid
entrypoint.extra_fanart(plexid, plexpath)
entrypoint.get_video_files(plexid, plexpath)
# Called by e.g. 3rd party plugin video extras
elif ('/Extras' in argv[0] or '/VideoFiles' in argv[0] or
'/Extras' in argv[2]):
elif ('/Extras' in path or '/VideoFiles' in path or
'/Extras' in arguments):
plexId = itemid or None
entrypoint.get_video_files(plexId, params)
@ -171,40 +146,40 @@ class Main():
"""
request = '%s&handle=%s' % (argv[2], HANDLE)
# Put the request into the 'queue'
plex_command('PLAY', request)
utils.plex_command('PLAY', request)
if HANDLE == -1:
# Handle -1 received, not waiting for main thread
return
# Wait for the result
while not pickl_window('plex_result'):
while not pickler.pickl_window('plex_result'):
sleep(50)
result = unpickle_me()
result = pickler.unpickle_me()
if result is None:
log.error('Error encountered, aborting')
dialog('notification',
heading='{plex}',
message=lang(30128),
icon='{error}',
time=3000)
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30128),
icon='{error}',
time=3000)
setResolvedUrl(HANDLE, False, ListItem())
elif result.listitem:
listitem = convert_PKC_to_listitem(result.listitem)
listitem = pkc_listitem.convert_pkc_to_listitem(result.listitem)
setResolvedUrl(HANDLE, True, listitem)
@staticmethod
def deviceid():
deviceId_old = window('plex_client_Id')
deviceId_old = pickler.pickl_window('plex_client_Id')
from clientinfo import getDeviceId
try:
deviceId = getDeviceId(reset=True)
except Exception as e:
log.error('Failed to generate a new device Id: %s' % e)
dialog('ok', lang(29999), lang(33032))
utils.dialog('ok', utils.lang(29999), utils.lang(33032))
else:
log.info('Successfully removed old device ID: %s New deviceId:'
'%s' % (deviceId_old, deviceId))
# 'Kodi will now restart to apply the changes'
dialog('ok', lang(29999), lang(33033))
utils.dialog('ok', utils.lang(29999), utils.lang(33033))
executebuiltin('RestartApp')

View file

@ -3,27 +3,24 @@
###############################################################################
from logging import getLogger
from Queue import Queue, Empty
from shutil import rmtree
from urllib import quote_plus, unquote
from threading import Thread
from os import makedirs
import requests
import xbmc
from xbmcvfs import exists
from utils import settings, language as lang, kodi_sql, try_encode, try_decode,\
thread_methods, dialog, exists_dir
import state
from . import path_ops
from . import utils
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.artwork')
# Disable annoying requests warnings
requests.packages.urllib3.disable_warnings()
ARTWORK_QUEUE = Queue()
IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC']
if not settings('imageSyncDuringPlayback') == 'true':
if not utils.settings('imageSyncDuringPlayback') == 'true':
IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC')
###############################################################################
@ -37,7 +34,7 @@ def double_urldecode(text):
return unquote(unquote(text))
@thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
@utils.thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
class Image_Cache_Thread(Thread):
sleep_between = 50
# Potentially issues with limited number of threads
@ -73,20 +70,20 @@ class Image_Cache_Thread(Thread):
'Window.IsVisible(DialogAddonSettings.xml)'):
# Avoid saving '0' all the time
set_zero = True
settings('caching_artwork_count', value='0')
utils.settings('caching_artwork_count', value='0')
xbmc.sleep(1000)
continue
set_zero = False
if isinstance(url, ArtworkSyncMessage):
if state.IMAGE_SYNC_NOTIFICATIONS:
dialog('notification',
heading=lang(29999),
message=url.message,
icon='{plex}',
sound=False)
utils.dialog('notification',
heading=utils.lang(29999),
message=url.message,
icon='{plex}',
sound=False)
queue.task_done()
continue
url = double_urlencode(try_encode(url))
url = double_urlencode(utils.try_encode(url))
sleeptime = 0
while True:
try:
@ -133,14 +130,15 @@ class Image_Cache_Thread(Thread):
if (counter > 20 and not xbmc.getCondVisibility(
'Window.IsVisible(DialogAddonSettings.xml)')):
counter = 0
settings('caching_artwork_count', value=str(queue.qsize()))
utils.settings('caching_artwork_count',
value=str(queue.qsize()))
# Sleep for a bit to reduce CPU strain
xbmc.sleep(sleep_between)
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
class Artwork():
enableTextureCache = settings('enableTextureCache') == "true"
enableTextureCache = utils.settings('enableTextureCache') == "true"
if enableTextureCache:
queue = ARTWORK_QUEUE
@ -156,7 +154,7 @@ class Artwork():
artworks = list()
# Get all posters and fanart/background for video and music
for kind in ('video', 'music'):
connection = kodi_sql(kind)
connection = utils.kodi_sql(kind)
cursor = connection.cursor()
for typus in ('poster', 'fanart'):
cursor.execute('SELECT url FROM art WHERE type == ?',
@ -164,7 +162,7 @@ class Artwork():
artworks.extend(cursor.fetchall())
connection.close()
artworks_to_cache = list()
connection = kodi_sql('texture')
connection = utils.kodi_sql('texture')
cursor = connection.cursor()
for url in artworks:
query = 'SELECT url FROM texture WHERE url == ? LIMIT 1'
@ -175,40 +173,40 @@ class Artwork():
if not artworks_to_cache:
LOG.info('Caching of major images to Kodi texture cache done')
# Set to "None"
settings('caching_artwork_count', value=lang(30069))
utils.settings('caching_artwork_count', value=utils.lang(30069))
return
length = len(artworks_to_cache)
LOG.info('Caching has not been completed - caching %s major images',
length)
settings('caching_artwork_count', value=str(length))
utils.settings('caching_artwork_count', value=str(length))
# Caching %s Plex images
self.queue.put(ArtworkSyncMessage(lang(30006) % length))
self.queue.put(ArtworkSyncMessage(utils.lang(30006) % length))
for i, url in enumerate(artworks_to_cache):
self.queue.put(url[0])
# Plex image caching done
self.queue.put(ArtworkSyncMessage(lang(30007)))
self.queue.put(ArtworkSyncMessage(utils.lang(30007)))
def fullTextureCacheSync(self):
"""
This method will sync all Kodi artwork to textures13.db
and cache them locally. This takes diskspace!
"""
if not dialog('yesno', "Image Texture Cache", lang(39250)):
if not utils.dialog('yesno', "Image Texture Cache", utils.lang(39250)):
return
LOG.info("Doing Image Cache Sync")
# ask to rest all existing or not
if dialog('yesno', "Image Texture Cache", lang(39251)):
if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)):
LOG.info("Resetting all cache data first")
# Remove all existing textures first
path = try_decode(xbmc.translatePath("special://thumbnails/"))
if exists_dir(path):
rmtree(path, ignore_errors=True)
path = path_ops.translate_path('special://thumbnails/')
if path_ops.exists(path):
path_ops.rmtree(path, ignore_errors=True)
self.restore_cache_directories()
# remove all existing data from texture DB
connection = kodi_sql('texture')
connection = utils.kodi_sql('texture')
cursor = connection.cursor()
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
cursor.execute(query, ('table', ))
@ -221,7 +219,7 @@ class Artwork():
connection.close()
# Cache all entries in video DB
connection = kodi_sql('video')
connection = utils.kodi_sql('video')
cursor = connection.cursor()
# dont include actors
query = "SELECT url FROM art WHERE media_type != ?"
@ -234,7 +232,7 @@ class Artwork():
for url in result:
self.cache_texture(url[0])
# Cache all entries in music DB
connection = kodi_sql('music')
connection = utils.kodi_sql('music')
cursor = connection.cursor()
cursor.execute("SELECT url FROM art")
result = cursor.fetchall()
@ -309,7 +307,7 @@ class Artwork():
"""
Deleted the cached artwork with path url (if it exists)
"""
connection = kodi_sql('texture')
connection = utils.kodi_sql('texture')
cursor = connection.cursor()
try:
cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1",
@ -320,10 +318,11 @@ class Artwork():
pass
else:
# Delete thumbnail as well as the entry
path = xbmc.translatePath("special://thumbnails/%s" % cachedurl)
path = path_ops.translate_path("special://thumbnails/%s"
% cachedurl)
LOG.debug("Deleting cached thumbnail: %s", path)
if exists(path):
rmtree(try_decode(path), ignore_errors=True)
if path_ops.exists(path):
path_ops.rmtree(path, ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
finally:
@ -336,8 +335,8 @@ class Artwork():
"a", "b", "c", "d", "e", "f",
"Video", "plex")
for path in paths:
makedirs(try_decode(xbmc.translatePath("special://thumbnails/%s"
% path)))
new_path = path_ops.translate_path("special://thumbnails/%s" % path)
path_ops.makedirs(utils.encode_path(new_path))
class ArtworkSyncMessage(object):

View file

@ -3,12 +3,12 @@
###############################################################################
from logging import getLogger
from utils import window, settings
import variables as v
from . import utils
from . import variables as v
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger('PLEX.clientinfo')
###############################################################################
@ -43,8 +43,8 @@ def getXArgsDeviceInfo(options=None, include_token=True):
'X-Plex-Client-Identifier': getDeviceId(),
'X-Plex-Provides': 'client,controller,player,pubsub-player',
}
if include_token and window('pms_token'):
xargs['X-Plex-Token'] = window('pms_token')
if include_token and utils.window('pms_token'):
xargs['X-Plex-Token'] = utils.window('pms_token')
if options is not None:
xargs.update(options)
return xargs
@ -60,26 +60,26 @@ def getDeviceId(reset=False):
"""
if reset is True:
v.PKC_MACHINE_IDENTIFIER = None
window('plex_client_Id', clear=True)
settings('plex_client_Id', value="")
utils.window('plex_client_Id', clear=True)
utils.settings('plex_client_Id', value="")
client_id = v.PKC_MACHINE_IDENTIFIER
if client_id:
return client_id
client_id = settings('plex_client_Id')
client_id = utils.settings('plex_client_Id')
# Because Kodi appears to cache file settings!!
if client_id != "" and reset is False:
v.PKC_MACHINE_IDENTIFIER = client_id
window('plex_client_Id', value=client_id)
log.info("Unique device Id plex_client_Id loaded: %s", client_id)
utils.window('plex_client_Id', value=client_id)
LOG.info("Unique device Id plex_client_Id loaded: %s", client_id)
return client_id
log.info("Generating a new deviceid.")
LOG.info("Generating a new deviceid.")
from uuid import uuid4
client_id = str(uuid4())
settings('plex_client_Id', value=client_id)
utils.settings('plex_client_Id', value=client_id)
v.PKC_MACHINE_IDENTIFIER = client_id
window('plex_client_Id', value=client_id)
log.info("Unique device Id plex_client_Id generated: %s", client_id)
utils.window('plex_client_Id', value=client_id)
LOG.info("Unique device Id plex_client_Id generated: %s", client_id)
return client_id

View file

@ -2,18 +2,17 @@
###############################################################################
import logging
from threading import Thread
from xbmc import sleep
from utils import window, thread_methods
import state
from . import utils
from . import state
###############################################################################
LOG = logging.getLogger("PLEX." + __name__)
LOG = logging.getLogger('PLEX.command_pipeline')
###############################################################################
@thread_methods
@utils.thread_methods
class Monitor_Window(Thread):
"""
Monitors window('plex_command') for new entries that we need to take care
@ -26,9 +25,9 @@ class Monitor_Window(Thread):
queue = state.COMMAND_PIPELINE_QUEUE
LOG.info("----===## Starting Kodi_Play_Client ##===----")
while not stopped():
if window('plex_command'):
value = window('plex_command')
window('plex_command', clear=True)
if utils.window('plex_command'):
value = utils.window('plex_command')
utils.window('plex_command', clear=True)
if value.startswith('PLAY-'):
queue.put(value.replace('PLAY-', ''))
elif value == 'SUSPEND_LIBRARY_THREAD-True':

View file

@ -2,18 +2,17 @@
Processes Plex companion inputs from the plexbmchelper to Kodi commands
"""
from logging import getLogger
from xbmc import Player
from variables import ALEXA_TO_COMPANION
import playqueue as PQ
from PlexFunctions import GetPlexKeyNumber
import json_rpc as js
import state
from . import playqueue as PQ
from . import plex_functions as PF
from . import json_rpc as js
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.companion')
###############################################################################
@ -25,7 +24,7 @@ def skip_to(params):
Does not seem to be implemented yet by Plex!
"""
playqueue_item_id = params.get('playQueueItemID')
_, plex_id = GetPlexKeyNumber(params.get('key'))
_, plex_id = PF.GetPlexKeyNumber(params.get('key'))
LOG.debug('Skipping to playQueueItemID %s, plex_id %s',
playqueue_item_id, plex_id)
found = True
@ -51,8 +50,8 @@ def convert_alexa_to_companion(dictionary):
The params passed by Alexa must first be converted to Companion talk
"""
for key in dictionary:
if key in ALEXA_TO_COMPANION:
dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key]
if key in v.ALEXA_TO_COMPANION:
dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key]
del dictionary[key]

View file

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from os.path import join
import xbmcgui
from xbmcaddon import Addon
from utils import window
from . import utils
from . import path_ops
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
ADDON = Addon('plugin.video.plexkodiconnect')
LOG = getLogger('PLEX.context')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
@ -44,8 +42,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
return self.selected_option
def onInit(self):
if window('PlexUserImage'):
self.getControl(USER_IMAGE).setImage(window('PlexUserImage'))
if utils.window('PlexUserImage'):
self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage'))
height = 479 + (len(self._options) * 55)
LOG.debug("options: %s", self._options)
self.list_ = self.getControl(LIST)
@ -66,10 +64,11 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
self.close()
def _add_editcontrol(self, x, y, height, width, password=None):
media = join(ADDON.getAddonInfo('path'),
'resources', 'skins', 'default', 'media')
media = path_ops.path.join(
v.ADDON_PATH, 'resources', 'skins', 'default', 'media')
filename = utils.try_encode(path_ops.path.join(media, 'white.png'))
control = xbmcgui.ControlImage(0, 0, 0, 0,
filename=join(media, "white.png"),
filename=filename,
aspectRatio=0,
colorDiffuse="ff111111")
control.setPosition(x, y)

View file

@ -1,35 +1,32 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from xbmcaddon import Addon
import xbmc
import xbmcplugin
import xbmcgui
import context
import plexdb_functions as plexdb
from utils import window, settings, dialog, language as lang
import PlexFunctions as PF
from PlexAPI import API
import playqueue as PQ
import variables as v
import state
from . import context
from . import plexdb_functions as plexdb
from . import utils
from . import plex_functions as PF
from .plex_api import API
from . import playqueue as PQ
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.context_entry')
OPTIONS = {
'Refresh': lang(30410),
'Delete': lang(30409),
'Addon': lang(30408),
# 'AddFav': lang(30405),
# 'RemoveFav': lang(30406),
# 'RateSong': lang(30407),
'Transcode': lang(30412),
'PMS_Play': lang(30415), # Use PMS to start playback
'Extras': lang(30235)
'Refresh': utils.lang(30410),
'Delete': utils.lang(30409),
'Addon': utils.lang(30408),
# 'AddFav': utils.lang(30405),
# 'RemoveFav': utils.lang(30406),
# 'RateSong': utils.lang(30407),
'Transcode': utils.lang(30412),
'PMS_Play': utils.lang(30415), # Use PMS to start playback
'Extras': utils.lang(30235)
}
###############################################################################
@ -98,14 +95,14 @@ class ContextMenu(object):
options.append(OPTIONS['Transcode'])
# Delete item, only if the Plex Home main user is logged in
if (window('plex_restricteduser') != 'true' and
window('plex_allows_mediaDeletion') == 'true'):
if (utils.window('plex_restricteduser') != 'true' and
utils.window('plex_allows_mediaDeletion') == 'true'):
options.append(OPTIONS['Delete'])
# Addon settings
options.append(OPTIONS['Addon'])
context_menu = context.ContextMenu(
"script-plex-context.xml",
Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
utils.try_encode(v.ADDON_PATH),
"default",
"1080i")
context_menu.set_options(options)
@ -138,14 +135,14 @@ class ContextMenu(object):
Delete item on PMS
"""
delete = True
if settings('skipContextMenu') != "true":
if not dialog("yesno", heading="{plex}", line1=lang(33041)):
if utils.settings('skipContextMenu') != "true":
if not utils.dialog("yesno", heading="{plex}", line1=utils.lang(33041)):
LOG.info("User skipped deletion for: %s", self.plex_id)
delete = False
if delete:
LOG.info("Deleting Plex item with id %s", self.plex_id)
if PF.delete_item_from_pms(self.plex_id) is False:
dialog("ok", heading="{plex}", line1=lang(30414))
utils.dialog("ok", heading="{plex}", line1=utils.lang(30414))
def _PMS_play(self):
"""

View file

@ -5,10 +5,9 @@ from logging import getLogger
import xml.etree.ElementTree as etree
import requests
from utils import window, language as lang, dialog
import clientinfo as client
import state
from . import utils
from . import clientinfo
from . import state
###############################################################################
@ -16,7 +15,7 @@ import state
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.downloadutils')
###############################################################################
@ -75,22 +74,22 @@ class DownloadUtils():
# Start session
self.s = requests.Session()
self.deviceId = client.getDeviceId()
self.deviceId = clientinfo.getDeviceId()
# Attach authenticated header to the session
self.s.headers = client.getXArgsDeviceInfo()
self.s.headers = clientinfo.getXArgsDeviceInfo()
self.s.encoding = 'utf-8'
# Set SSL settings
self.setSSL()
# Set other stuff
self.setServer(window('pms_server'))
self.setServer(utils.window('pms_server'))
# Counters to declare PMS dead or unauthorized
# Use window variables because start of movies will be called with a
# new plugin instance - it's impossible to share data otherwise
if reset is True:
window('countUnauthorized', value='0')
window('countError', value='0')
utils.window('countUnauthorized', value='0')
utils.window('countError', value='0')
# Retry connections to the server
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
@ -110,7 +109,7 @@ class DownloadUtils():
LOG.info('Request session stopped')
def getHeader(self, options=None):
header = client.getXArgsDeviceInfo()
header = clientinfo.getXArgsDeviceInfo()
if options is not None:
header.update(options)
return header
@ -227,9 +226,9 @@ class DownloadUtils():
else:
# We COULD contact the PMS, hence it ain't dead
if authenticate is True:
window('countError', value='0')
utils.window('countError', value='0')
if r.status_code != 401:
window('countUnauthorized', value='0')
utils.window('countUnauthorized', value='0')
if r.status_code == 204:
# No body in the response
@ -247,9 +246,10 @@ class DownloadUtils():
LOG.info(r.text)
if '401 Unauthorized' in r.text:
# Truly unauthorized
window('countUnauthorized',
value=str(int(window('countUnauthorized')) + 1))
if (int(window('countUnauthorized')) >=
utils.window(
'countUnauthorized',
value=str(int(utils.window('countUnauthorized')) + 1))
if (int(utils.window('countUnauthorized')) >=
self.unauthorizedAttempts):
LOG.warn('We seem to be truly unauthorized for PMS'
' %s ', url)
@ -258,11 +258,11 @@ class DownloadUtils():
LOG.debug('Setting PMS server status to '
'unauthorized')
state.PMS_STATUS = '401'
window('plex_serverStatus', value="401")
dialog('notification',
lang(29999),
lang(30017),
icon='{error}')
utils.window('plex_serverStatus', value="401")
utils.dialog('notification',
utils.lang(29999),
utils.lang(30017),
icon='{error}')
else:
# there might be other 401 where e.g. PMS under strain
LOG.info('PMS might only be under strain')
@ -312,12 +312,12 @@ class DownloadUtils():
if authenticate is True:
# Make the addon aware of status
try:
window('countError',
value=str(int(window('countError')) + 1))
if int(window('countError')) >= self.connectionAttempts:
utils.window('countError',
value=str(int(utils.window('countError')) + 1))
if int(utils.window('countError')) >= self.connectionAttempts:
LOG.warn('Failed to connect to %s too many times. '
'Declare PMS dead', url)
window('plex_online', value="false")
utils.window('plex_online', value="false")
except ValueError:
# 'countError' not yet set
pass

View file

@ -5,32 +5,26 @@
#
###############################################################################
from logging import getLogger
from shutil import copyfile
from os import walk, makedirs
from os.path import basename, join
from sys import argv
from urllib import urlencode
import xbmcplugin
from xbmc import sleep, executebuiltin, translatePath
from xbmc import sleep, executebuiltin
from xbmcgui import ListItem
from utils import window, settings, language as lang, dialog, try_encode, \
catch_exceptions, exists_dir, plex_command, try_decode
import downloadutils
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
GetMachineIdentifier
from PlexAPI import API
import json_rpc as js
import variables as v
from . import utils
from . import path_ops
from .downloadutils import DownloadUtils as DU
from .plex_api import API
from . import plex_functions as PF
from . import json_rpc as js
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.entrypoint')
try:
HANDLE = int(argv[1])
ARGV_0 = argv[0]
ARGV_0 = path_ops.decode_path(argv[0])
except IndexError:
pass
###############################################################################
@ -47,8 +41,8 @@ def choose_pms_server():
server = setup.pick_pms(showDialog=True)
if server is None:
LOG.error('We did not connect to a new PMS, aborting')
plex_command('SUSPEND_USER_CLIENT', 'False')
plex_command('SUSPEND_LIBRARY_THREAD', 'False')
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
return
LOG.info("User chose server %s", server['name'])
@ -65,12 +59,12 @@ def choose_pms_server():
_log_in()
LOG.info("Choosing new PMS complete")
# '<PMS> connected'
dialog('notification',
lang(29999),
'%s %s' % (server['name'], lang(39220)),
icon='{plex}',
time=3000,
sound=False)
utils.dialog('notification',
utils.lang(29999),
'%s %s' % (server['name'], utils.lang(39220)),
icon='{plex}',
time=3000,
sound=False)
def toggle_plex_tv_sign_in():
@ -78,38 +72,38 @@ def toggle_plex_tv_sign_in():
Signs out of Plex.tv if there was a token saved and thus deletes the token.
Or signs in to plex.tv if the user was not logged in before.
"""
if settings('plexToken'):
if utils.settings('plexToken'):
LOG.info('Reseting plex.tv credentials in settings')
settings('plexLogin', value="")
settings('plexToken', value="")
settings('plexid', value="")
settings('plexHomeSize', value="1")
settings('plexAvatar', value="")
settings('plex_status', value=lang(39226))
utils.settings('plexLogin', value="")
utils.settings('plexToken', value="")
utils.settings('plexid', value="")
utils.settings('plexHomeSize', value="1")
utils.settings('plexAvatar', value="")
utils.settings('plex_status', value=utils.lang(39226))
window('plex_token', clear=True)
plex_command('PLEX_TOKEN', '')
plex_command('PLEX_USERNAME', '')
utils.window('plex_token', clear=True)
utils.plex_command('PLEX_TOKEN', '')
utils.plex_command('PLEX_USERNAME', '')
else:
LOG.info('Login to plex.tv')
import initialsetup
initialsetup.InitialSetup().plex_tv_sign_in()
dialog('notification',
lang(29999),
lang(39221),
icon='{plex}',
time=3000,
sound=False)
utils.dialog('notification',
utils.lang(29999),
utils.lang(39221),
icon='{plex}',
time=3000,
sound=False)
def reset_authorization():
"""
User tried login and failed too many times. Reset # of logins
"""
resp = dialog('yesno', heading="{plex}", line1=lang(39206))
resp = utils.dialog('yesno', heading="{plex}", line1=utils.lang(39206))
if resp == 1:
LOG.info("Reset login attempts.")
plex_command('PMS_STATUS', 'Auth')
utils.plex_command('PMS_STATUS', 'Auth')
else:
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
@ -138,17 +132,17 @@ def show_main_menu(content_type=None):
LOG.debug('Do main listing with content_type: %s', content_type)
xbmcplugin.setContent(HANDLE, 'files')
# Get emby nodes from the window props
plexprops = window('Plex.nodes.total')
plexprops = utils.window('Plex.nodes.total')
if plexprops:
totalnodes = int(plexprops)
for i in range(totalnodes):
path = window('Plex.nodes.%s.index' % i)
path = utils.window('Plex.nodes.%s.index' % i)
if not path:
path = window('Plex.nodes.%s.content' % i)
path = utils.window('Plex.nodes.%s.content' % i)
if not path:
continue
label = window('Plex.nodes.%s.title' % i)
node_type = window('Plex.nodes.%s.type' % i)
label = utils.window('Plex.nodes.%s.title' % i)
node_type = utils.window('Plex.nodes.%s.type' % i)
# because we do not use seperate entrypoints for each content type,
# we need to figure out which items to show in each listing. for
# now we just only show picture nodes in the picture library video
@ -161,21 +155,19 @@ def show_main_menu(content_type=None):
# Plex Watch later
if content_type not in ('image', 'audio'):
directory_item(lang(39211),
directory_item(utils.lang(39211),
"plugin://%s?mode=watchlater" % v.ADDON_ID)
# Plex Channels
directory_item(lang(30173),
"plugin://%s?mode=channels" % v.ADDON_ID)
directory_item(utils.lang(30173), "plugin://%s?mode=channels" % v.ADDON_ID)
# Plex user switch
directory_item('%s%s' % (lang(39200), settings('username')),
directory_item('%s%s' % (utils.lang(39200), utils.settings('username')),
"plugin://%s?mode=switchuser" % v.ADDON_ID)
# some extra entries for settings and stuff
directory_item(lang(39201),
"plugin://%s?mode=settings" % v.ADDON_ID)
directory_item(lang(39203),
directory_item(utils.lang(39201), "plugin://%s?mode=settings" % v.ADDON_ID)
directory_item(utils.lang(39203),
"plugin://%s?mode=refreshplaylist" % v.ADDON_ID)
directory_item(lang(39204),
directory_item(utils.lang(39204),
"plugin://%s?mode=manualsync" % v.ADDON_ID)
xbmcplugin.endOfDirectory(HANDLE)
@ -188,7 +180,7 @@ def switch_plex_user():
# Guess these user avatars are a future feature. Skipping for now
# Delete any userimages. Since there's always only 1 user: position = 0
# position = 0
# window('EmbyAdditionalUserImage.%s' % position, clear=True)
# utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
LOG.info("Plex home user switch requested")
if not _log_out():
return
@ -258,7 +250,8 @@ def create_listitem(item, append_show_title=False, append_sxxexx=False):
listitem.setArt({'icon': 'DefaultTVShows.png'})
listitem.setProperty('fanart_image', item['art'].get('tvshow.fanart', ''))
try:
listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
listitem.addContextMenuItems([(utils.lang(30032),
'XBMC.Action(Info)',)])
except TypeError:
# Kodi fuck-up
pass
@ -287,7 +280,7 @@ def next_up_episodes(tagname, limit):
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
}
for item in js.get_tv_shows(params):
if settings('ignoreSpecialsNextEpisodes') == "true":
if utils.settings('ignoreSpecialsNextEpisodes') == "true":
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
@ -383,8 +376,8 @@ def recent_episodes(mediatype, tagname, limit):
# if the addon is called with recentepisodes parameter,
# we return the recentepisodes list of the given tagname
xbmcplugin.setContent(HANDLE, 'episodes')
append_show_title = settings('RecentTvAppendShow') == 'true'
append_sxxexx = settings('RecentTvAppendSeason') == 'true'
append_show_title = utils.settings('RecentTvAppendShow') == 'true'
append_sxxexx = utils.settings('RecentTvAppendSeason') == 'true'
# First we get a list of all the TV shows - filtered by tag
show_ids = set()
params = {
@ -401,7 +394,7 @@ def recent_episodes(mediatype, tagname, limit):
"dateadded", "lastplayed"],
"limits": {"end": limit}
}
if settings('TVShowWatched') == 'false':
if utils.settings('TVShowWatched') == 'false':
params['filter'] = {
'operator': "lessthan",
'field': "playcount",
@ -444,9 +437,9 @@ def get_video_files(plex_id, params):
LOG.info('No Plex ID found, abort getting Extras')
return xbmcplugin.endOfDirectory(HANDLE)
item = GetPlexMetadata(plex_id)
item = PF.GetPlexMetadata(plex_id)
try:
path = item[0][0][0].attrib['file']
path = utils.try_decode(item[0][0][0].attrib['file'])
except (TypeError, IndexError, AttributeError, KeyError):
LOG.error('Could not get file path for item %s', plex_id)
return xbmcplugin.endOfDirectory(HANDLE)
@ -458,18 +451,19 @@ def get_video_files(plex_id, params):
elif '\\' in path:
path = path.replace('\\', '\\\\')
# Directory only, get rid of filename
path = path.replace(basename(path), '')
if exists_dir(path):
for root, dirs, files in walk(path):
path = path.replace(path_ops.path.basename(path), '')
if path_ops.exists(path):
for root, dirs, files in path_ops.walk(path):
for directory in dirs:
item_path = try_encode(join(root, directory))
item_path = utils.try_encode(path_ops.path.join(root,
directory))
listitem = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=item_path,
listitem=listitem,
isFolder=True)
for file in files:
item_path = try_encode(join(root, file))
item_path = utils.try_encode(path_ops.path.join(root, file))
listitem = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=file,
@ -480,7 +474,7 @@ def get_video_files(plex_id, params):
xbmcplugin.endOfDirectory(HANDLE)
@catch_exceptions(warnuser=False)
@utils.catch_exceptions(warnuser=False)
def extra_fanart(plex_id, plex_path):
"""
Get extrafanart for listitem
@ -497,12 +491,12 @@ def extra_fanart(plex_id, plex_path):
# We need to store the images locally for this to work
# because of the caching system in xbmc
fanart_dir = try_decode(translatePath(
"special://thumbnails/plex/%s/" % plex_id))
if not exists_dir(fanart_dir):
fanart_dir = path_ops.translate_path("special://thumbnails/plex/%s/"
% plex_id)
if not path_ops.exists(fanart_dir):
# Download the images to the cache directory
makedirs(fanart_dir)
xml = GetPlexMetadata(plex_id)
path_ops.makedirs(fanart_dir)
xml = PF.GetPlexMetadata(plex_id)
if xml is None:
LOG.error('Could not download metadata for %s', plex_id)
return xbmcplugin.endOfDirectory(HANDLE)
@ -511,19 +505,23 @@ def extra_fanart(plex_id, plex_path):
backdrops = api.artwork()['Backdrop']
for count, backdrop in enumerate(backdrops):
# Same ordering as in artwork
art_file = try_encode(join(fanart_dir, "fanart%.3d.jpg" % count))
art_file = utils.try_encode(path_ops.path.join(
fanart_dir, "fanart%.3d.jpg" % count))
listitem = ListItem("%.3d" % count, path=art_file)
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=art_file,
listitem=listitem)
copyfile(backdrop, try_decode(art_file))
path_ops.copyfile(backdrop, utils.try_decode(art_file))
else:
LOG.info("Found cached backdrop.")
# Use existing cached images
for root, _, files in walk(fanart_dir):
fanart_dir = utils.try_decode(fanart_dir)
for root, _, files in path_ops.walk(fanart_dir):
root = utils.decode_path(root)
for file in files:
art_file = try_encode(join(root, file))
file = utils.decode_path(file)
art_file = utils.try_encode(path_ops.path.join(root, file))
listitem = ListItem(file, path=art_file)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=art_file,
@ -541,13 +539,13 @@ def on_deck_episodes(viewid, tagname, limit):
limit: Max. number of items to retrieve, e.g. 50
"""
xbmcplugin.setContent(HANDLE, 'episodes')
append_show_title = settings('OnDeckTvAppendShow') == 'true'
append_sxxexx = settings('OnDeckTvAppendSeason') == 'true'
if settings('OnDeckTVextended') == 'false':
append_show_title = utils.settings('OnDeckTvAppendShow') == 'true'
append_sxxexx = utils.settings('OnDeckTvAppendSeason') == 'true'
if utils.settings('OnDeckTVextended') == 'false':
# Chances are that this view is used on Kodi startup
# Wait till we've connected to a PMS. At most 30s
counter = 0
while window('plex_authenticated') != 'true':
while utils.window('plex_authenticated') != 'true':
counter += 1
if counter == 300:
LOG.error('Aborting On Deck view, we were not authenticated '
@ -555,13 +553,12 @@ def on_deck_episodes(viewid, tagname, limit):
xbmcplugin.endOfDirectory(HANDLE, False)
return
sleep(100)
xml = downloadutils.DownloadUtils().downloadUrl(
'{server}/library/sections/%s/onDeck' % viewid)
xml = DU().downloadUrl('{server}/library/sections/%s/onDeck' % viewid)
if xml in (None, 401):
LOG.error('Could not download PMS xml for view %s', viewid)
xbmcplugin.endOfDirectory(HANDLE, False)
return
direct_paths = settings('useDirectPaths') == '1'
direct_paths = utils.settings('useDirectPaths') == '1'
counter = 0
for item in xml:
api = API(item)
@ -580,7 +577,7 @@ def on_deck_episodes(viewid, tagname, limit):
break
xbmcplugin.endOfDirectory(
handle=HANDLE,
cacheToDisc=settings('enableTextureCache') == 'true')
cacheToDisc=utils.settings('enableTextureCache') == 'true')
return
# if the addon is called with nextup parameter,
@ -610,7 +607,7 @@ def on_deck_episodes(viewid, tagname, limit):
"dateadded", "lastplayed"
],
}
if settings('ignoreSpecialsNextEpisodes') == "true":
if utils.settings('ignoreSpecialsNextEpisodes') == "true":
params['filter'] = {
'and': [
{'operator': "lessthan", 'field': "playcount", 'value': "1"},
@ -663,37 +660,34 @@ def watchlater():
"""
Listing for plex.tv Watch Later section (if signed in to plex.tv)
"""
if window('plex_token') == '':
if utils.window('plex_token') == '':
LOG.error('No watch later - not signed in to plex.tv')
return xbmcplugin.endOfDirectory(HANDLE, False)
if window('plex_restricteduser') == 'true':
if utils.window('plex_restricteduser') == 'true':
LOG.error('No watch later - restricted user')
return xbmcplugin.endOfDirectory(HANDLE, False)
xml = downloadutils.DownloadUtils().downloadUrl(
'https://plex.tv/pms/playlists/queue/all',
authenticate=False,
headerOptions={'X-Plex-Token': window('plex_token')})
xml = DU().downloadUrl('https://plex.tv/pms/playlists/queue/all',
authenticate=False,
headerOptions={'X-Plex-Token': utils.window('plex_token')})
if xml in (None, 401):
LOG.error('Could not download watch later list from plex.tv')
return xbmcplugin.endOfDirectory(HANDLE, False)
LOG.info('Displaying watch later plex.tv items')
xbmcplugin.setContent(HANDLE, 'movies')
direct_paths = settings('useDirectPaths') == '1'
direct_paths = utils.settings('useDirectPaths') == '1'
for item in xml:
__build_item(item, direct_paths)
xbmcplugin.endOfDirectory(
handle=HANDLE,
cacheToDisc=settings('enableTextureCache') == 'true')
cacheToDisc=utils.settings('enableTextureCache') == 'true')
def channels():
"""
Listing for Plex Channels
"""
xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all')
xml = DU().downloadUrl('{server}/channels/all')
try:
xml[0].attrib
except (ValueError, AttributeError, IndexError, TypeError):
@ -708,7 +702,7 @@ def channels():
__build_folder(item)
xbmcplugin.endOfDirectory(
handle=HANDLE,
cacheToDisc=settings('enableTextureCache') == 'true')
cacheToDisc=utils.settings('enableTextureCache') == 'true')
def browse_plex(key=None, plex_section_id=None):
@ -717,9 +711,9 @@ def browse_plex(key=None, plex_section_id=None):
be used directly for PMS url {server}<key>) or the plex_section_id
"""
if key:
xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % key)
xml = DU().downloadUrl('{server}%s' % key)
else:
xml = GetPlexSectionResults(plex_section_id)
xml = PF.GetPlexSectionResults(plex_section_id)
try:
xml[0].attrib
except (ValueError, AttributeError, IndexError, TypeError):
@ -735,7 +729,7 @@ def browse_plex(key=None, plex_section_id=None):
artists = False
albums = False
musicvideos = False
direct_paths = settings('useDirectPaths') == '1'
direct_paths = utils.settings('useDirectPaths') == '1'
for item in xml:
if item.tag == 'Directory':
__build_folder(item, plex_section_id=plex_section_id)
@ -802,7 +796,7 @@ def browse_plex(key=None, plex_section_id=None):
xbmcplugin.endOfDirectory(
handle=HANDLE,
cacheToDisc=settings('enableTextureCache') == 'true')
cacheToDisc=utils.settings('enableTextureCache') == 'true')
def __build_folder(xml_element, plex_section_id=None):
@ -854,7 +848,7 @@ def extras(plex_id):
Lists all extras for plex_id
"""
xbmcplugin.setContent(HANDLE, 'movies')
xml = GetPlexMetadata(plex_id)
xml = PF.GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, KeyError):
@ -874,41 +868,47 @@ def create_new_pms():
Opens dialogs for the user the plug in the PMS details
"""
# "Enter your Plex Media Server's IP or URL. Examples are:"
dialog('ok', lang(29999), lang(39215), '192.168.1.2', 'plex.myServer.org')
address = dialog('input', "Enter PMS IP or URL")
utils.dialog('ok',
utils.lang(29999),
utils.lang(39215),
'192.168.1.2',
'plex.myServer.org')
address = utils.dialog('input', "Enter PMS IP or URL")
if address == '':
return
port = dialog('input', "Enter PMS port", '32400', type='{numeric}')
port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}')
if port == '':
return
url = '%s:%s' % (address, port)
# "Does your Plex Media Server support SSL connections?
# (https instead of http)"
https = dialog('yesno', lang(29999), lang(39217))
https = utils.dialog('yesno', utils.lang(29999), utils.lang(39217))
if https:
url = 'https://%s' % url
else:
url = 'http://%s' % url
https = 'true' if https else 'false'
machine_identifier = GetMachineIdentifier(url)
machine_identifier = PF.GetMachineIdentifier(url)
if machine_identifier is None:
# "Error contacting url
# Abort (Yes) or save address anyway (No)"
if dialog('yesno',
lang(29999),
'%s %s. %s' % (lang(39218), url, lang(39219))):
if utils.dialog('yesno',
utils.lang(29999),
'%s %s. %s' % (utils.lang(39218),
url,
utils.lang(39219))):
return
else:
settings('plex_machineIdentifier', '')
utils.settings('plex_machineIdentifier', '')
else:
settings('plex_machineIdentifier', machine_identifier)
utils.settings('plex_machineIdentifier', machine_identifier)
LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s',
https, address, port, machine_identifier)
settings('https', value=https)
settings('ipaddress', value=address)
settings('port', value=port)
utils.settings('https', value=https)
utils.settings('ipaddress', value=address)
utils.settings('port', value=port)
# Chances are this is a local PMS, so disable SSL certificate check
settings('sslverify', value='false')
utils.settings('sslverify', value='false')
# Sign out to trigger new login
if _log_out():
@ -923,9 +923,9 @@ def _log_in():
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
out!
"""
plex_command('RUN_LIB_SCAN', 'full')
utils.plex_command('RUN_LIB_SCAN', 'full')
# Restart user client
plex_command('SUSPEND_USER_CLIENT', 'False')
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
def _log_out():
@ -935,22 +935,22 @@ def _log_out():
Returns True if successfully signed out, False otherwise
"""
# Resetting, please wait
dialog('notification',
lang(29999),
lang(39207),
icon='{plex}',
time=3000,
sound=False)
utils.dialog('notification',
utils.lang(29999),
utils.lang(39207),
icon='{plex}',
time=3000,
sound=False)
# Pause library sync thread
plex_command('SUSPEND_LIBRARY_THREAD', 'True')
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'True')
# Wait max for 10 seconds for all lib scans to shutdown
counter = 0
while window('plex_dbScan') == 'true':
while utils.window('plex_dbScan') == 'true':
if counter > 200:
# Failed to reset PMS and plex.tv connects. Try to restart Kodi.
dialog('ok', lang(29999), lang(39208))
utils.dialog('ok', utils.lang(29999), utils.lang(39208))
# Resuming threads, just in case
plex_command('SUSPEND_LIBRARY_THREAD', 'False')
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
LOG.error("Could not stop library sync, aborting")
return False
counter += 1
@ -959,17 +959,17 @@ def _log_out():
counter = 0
# Log out currently signed in user:
window('plex_serverStatus', value='401')
plex_command('PMS_STATUS', '401')
utils.window('plex_serverStatus', value='401')
utils.plex_command('PMS_STATUS', '401')
# Above method needs to have run its course! Hence wait
while window('plex_serverStatus') == "401":
while utils.window('plex_serverStatus') == "401":
if counter > 100:
# 'Failed to reset PKC. Try to restart Kodi.'
dialog('ok', lang(29999), lang(39208))
utils.dialog('ok', utils.lang(29999), utils.lang(39208))
LOG.error("Could not sign out user, aborting")
return False
counter += 1
sleep(50)
# Suspend the user client during procedure
plex_command('SUSPEND_USER_CLIENT', 'True')
utils.plex_command('SUSPEND_USER_CLIENT', 'True')
return True

View file

@ -6,26 +6,29 @@ import xml.etree.ElementTree as etree
from xbmc import executebuiltin, translatePath
from utils import settings, window, language as lang, try_decode, dialog, \
XmlKodiSetting, reboot_kodi
from migration import check_migration
from downloadutils import DownloadUtils as DU
from userclient import UserClient
from clientinfo import getDeviceId
import PlexFunctions as PF
import plex_tv
import json_rpc as js
import playqueue as PQ
from videonodes import VideoNodes
import state
import variables as v
from . import utils
from . import path_ops
from . import migration
from .downloadutils import DownloadUtils as DU
from . import videonodes
from . import userclient
from . import clientinfo
from . import plex_functions as PF
from . import plex_tv
from . import json_rpc as js
from . import playqueue as PQ
from . import state
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.initialsetup')
###############################################################################
if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
WINDOW_PROPERTIES = (
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
@ -48,27 +51,28 @@ def reload_pkc():
reload(state)
# Reset window props
for prop in WINDOW_PROPERTIES:
window(prop, clear=True)
utils.window(prop, clear=True)
# Clear video nodes properties
VideoNodes().clearProperties()
videonodes.VideoNodes().clearProperties()
# Initializing
state.VERIFY_SSL_CERT = settings('sslverify') == 'true'
state.SSL_CERT_PATH = settings('sslcert') \
if settings('sslcert') != 'None' else None
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
state.BACKGROUND_SYNC_DISABLED = settings(
state.VERIFY_SSL_CERT = utils.settings('sslverify') == 'true'
state.SSL_CERT_PATH = utils.settings('sslcert') \
if utils.settings('sslcert') != 'None' else None
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
state.SYNC_DIALOG = utils.settings('dbSyncIndicator') == 'true'
state.ENABLE_MUSIC = utils.settings('enableMusic') == 'true'
state.BACKGROUND_SYNC_DISABLED = utils.settings(
'enableBackgroundSync') == 'false'
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
settings('backgroundsync_saftyMargin'))
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
state.REMAP_PATH = settings('remapSMB') == 'true'
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number')
state.FORCE_RELOAD_SKIN = settings('forceReloadSkinOnPlaybackStop') == 'true'
utils.settings('backgroundsync_saftyMargin'))
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
state.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset'))
state.FETCH_PMS_ITEM_NUMBER = utils.settings('fetch_pms_item_number')
state.FORCE_RELOAD_SKIN = \
utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
# Init some Queues()
state.COMMAND_PIPELINE_QUEUE = Queue()
state.COMPANION_QUEUE = Queue(maxsize=100)
@ -76,9 +80,9 @@ def reload_pkc():
set_replace_paths()
set_webserver()
# To detect Kodi profile switches
window('plex_kodiProfile',
value=try_decode(translatePath("special://profile")))
getDeviceId()
utils.window('plex_kodiProfile',
value=utils.try_decode(translatePath("special://profile")))
clientinfo.getDeviceId()
# Initialize the PKC playqueues
PQ.init_playqueues()
LOG.info('Done (re-)loading PKC settings')
@ -92,7 +96,7 @@ def set_replace_paths():
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
for arg in ('Org', 'New'):
key = 'remapSMB%s%s' % (typus, arg)
value = settings(key)
value = utils.settings(key)
if '://' in value:
protocol = value.split('://', 1)[0]
value = value.replace(protocol, protocol.lower())
@ -129,8 +133,8 @@ def _write_pms_settings(url, token):
for entry in xml:
if entry.attrib.get('id', '') == 'allowMediaDeletion':
value = 'true' if entry.get('value', '1') == '1' else 'false'
settings('plex_allows_mediaDeletion', value=value)
window('plex_allows_mediaDeletion', value=value)
utils.settings('plex_allows_mediaDeletion', value=value)
utils.window('plex_allows_mediaDeletion', value=value)
class InitialSetup(object):
@ -140,8 +144,8 @@ class InitialSetup(object):
"""
def __init__(self):
LOG.debug('Entering initialsetup class')
self.server = UserClient().get_server()
self.serverid = settings('plex_machineIdentifier')
self.server = userclient.UserClient().get_server()
self.serverid = utils.settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = PF.GetPlexLoginFromSettings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
@ -149,7 +153,7 @@ class InitialSetup(object):
self.plex_token = plexdict['plexToken']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.pms_token = settings('accessToken')
self.pms_token = utils.settings('accessToken')
if self.plex_token:
LOG.debug('Found a plex.tv token in the settings')
@ -179,20 +183,20 @@ class InitialSetup(object):
# HTTP Error: unauthorized. Token is no longer valid
LOG.info('plex.tv connection returned HTTP %s', str(chk))
# Delete token in the settings
settings('plexToken', value='')
settings('plexLogin', value='')
utils.settings('plexToken', value='')
utils.settings('plexLogin', value='')
# Could not login, please try again
dialog('ok', lang(29999), lang(39009))
utils.dialog('ok', utils.lang(29999), utils.lang(39009))
answer = self.plex_tv_sign_in()
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
LOG.info('Problems connecting to plex.tv; connection returned '
'HTTP %s', str(chk))
dialog('ok', lang(29999), lang(39010))
utils.dialog('ok', utils.lang(29999), utils.lang(39010))
answer = False
else:
LOG.info('plex.tv connection with token successful')
settings('plex_status', value=lang(39227))
utils.settings('plex_status', value=utils.lang(39227))
# Refresh the info from Plex.tv
xml = DU().downloadUrl('https://plex.tv/users/account',
authenticate=False,
@ -202,11 +206,12 @@ class InitialSetup(object):
except (AttributeError, KeyError):
LOG.error('Failed to update Plex info from plex.tv')
else:
settings('plexLogin', value=self.plex_login)
utils.settings('plexLogin', value=self.plex_login)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
settings('plexhome', value=home)
settings('plexAvatar', value=xml.attrib.get('thumb'))
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
utils.settings('plexhome', value=home)
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
utils.settings('plexHomeSize',
value=xml.attrib.get('homeSize', '1'))
LOG.info('Updated Plex info from plex.tv')
return answer
@ -233,7 +238,7 @@ class InitialSetup(object):
LOG.warn('Could not retrieve machineIdentifier')
answer = False
else:
settings('plex_machineIdentifier', value=self.serverid)
utils.settings('plex_machineIdentifier', value=self.serverid)
elif answer is True:
temp_server_id = PF.GetMachineIdentifier(self.server)
if temp_server_id != self.serverid:
@ -325,7 +330,7 @@ class InitialSetup(object):
if item.get('machineIdentifier') == self.serverid:
server = item
if server is None:
name = settings('plex_servername')
name = utils.settings('plex_servername')
LOG.warn('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline', self.serverid, name)
@ -356,18 +361,18 @@ class InitialSetup(object):
"""
https_updated = False
# Searching for PMS
dialog('notification',
heading='{plex}',
message=lang(30001),
icon='{plex}',
time=5000)
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30001),
icon='{plex}',
time=5000)
while True:
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
# Exit if no servers found
if not serverlist:
LOG.warn('No plex media servers found!')
dialog('ok', lang(29999), lang(39011))
utils.dialog('ok', utils.lang(29999), utils.lang(39011))
return
# Get a nicer list
dialoglist = []
@ -375,10 +380,10 @@ class InitialSetup(object):
if server['local']:
# server is in the same network as client.
# Add"local"
msg = lang(39022)
msg = utils.lang(39022)
else:
# Add 'remote'
msg = lang(39054)
msg = utils.lang(39054)
if server.get('ownername'):
# Display username if its not our PMS
dialoglist.append('%s (%s, %s)'
@ -389,7 +394,7 @@ class InitialSetup(object):
dialoglist.append('%s (%s)'
% (server['name'], msg))
# Let user pick server from a list
resp = dialog('select', lang(39012), dialoglist)
resp = utils.dialog('select', utils.lang(39012), dialoglist)
if resp == -1:
# User cancelled
return
@ -406,17 +411,19 @@ class InitialSetup(object):
LOG.warn('Not yet authorized for Plex server %s',
server['name'])
# Please sign in to plex.tv
dialog('ok',
lang(29999),
lang(39013) + server['name'],
lang(39014))
utils.dialog('ok',
utils.lang(29999),
utils.lang(39013) + server['name'],
utils.lang(39014))
if self.plex_tv_sign_in() is False:
# Exit while loop if user cancels
return
# Problems connecting
elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server?
answ = dialog('yesno', lang(29999), lang(39015))
answ = utils.dialog('yesno',
utils.lang(29999),
utils.lang(39015))
# Exit while loop if user chooses No
if not answ:
return
@ -429,30 +436,31 @@ class InitialSetup(object):
"""
Saves server to file settings
"""
settings('plex_machineIdentifier', server['machineIdentifier'])
settings('plex_servername', server['name'])
settings('plex_serverowned', 'true' if server['owned'] else 'false')
utils.settings('plex_machineIdentifier', server['machineIdentifier'])
utils.settings('plex_servername', server['name'])
utils.settings('plex_serverowned',
'true' if server['owned'] else 'false')
# Careful to distinguish local from remote PMS
if server['local']:
scheme = server['scheme']
settings('ipaddress', server['ip'])
settings('port', server['port'])
utils.settings('ipaddress', server['ip'])
utils.settings('port', server['port'])
LOG.debug("Setting SSL verify to false, because server is "
"local")
settings('sslverify', 'false')
utils.settings('sslverify', 'false')
else:
baseURL = server['baseURL'].split(':')
scheme = baseURL[0]
settings('ipaddress', baseURL[1].replace('//', ''))
settings('port', baseURL[2])
utils.settings('ipaddress', baseURL[1].replace('//', ''))
utils.settings('port', baseURL[2])
LOG.debug("Setting SSL verify to true, because server is not "
"local")
settings('sslverify', 'true')
utils.settings('sslverify', 'true')
if scheme == 'https':
settings('https', 'true')
utils.settings('https', 'true')
else:
settings('https', 'false')
utils.settings('https', 'false')
# And finally do some logging
LOG.debug("Writing to Kodi user settings file")
LOG.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s ",
@ -468,9 +476,9 @@ class InitialSetup(object):
"""
LOG.info("Initial setup called.")
try:
with XmlKodiSetting('advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml:
with utils.XmlKodiSetting('advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml:
# Get current Kodi video cache setting
cache = xml.get_setting(['cache', 'memorysize'])
# Disable foreground "Loading media information from files"
@ -493,13 +501,13 @@ class InitialSetup(object):
# Kodi default cache if no setting is set
cache = str(cache.text) if cache is not None else '20971520'
LOG.info('Current Kodi video memory cache in bytes: %s', cache)
settings('kodi_video_cache', value=cache)
utils.settings('kodi_video_cache', value=cache)
# Hack to make PKC Kodi master lock compatible
try:
with XmlKodiSetting('sources.xml',
force_create=True,
top_element='sources') as xml:
with utils.XmlKodiSetting('sources.xml',
force_create=True,
top_element='sources') as xml:
root = xml.set_setting(['video'])
count = 2
for source in root.findall('.//path'):
@ -526,21 +534,21 @@ class InitialSetup(object):
pass
# Do we need to migrate stuff?
check_migration()
migration.check_migration()
# Reload the server IP cause we might've deleted it during migration
self.server = UserClient().get_server()
self.server = userclient.UserClient().get_server()
# Display a warning if Kodi puts ALL movies into the queue, basically
# breaking playback reporting for PKC
if js.settings_getsettingvalue('videoplayer.autoplaynextitem'):
LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!')
if settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
if utils.settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
# Only warn once
settings('warned_setting_videoplayer.autoplaynextitem',
value='true')
utils.settings('warned_setting_videoplayer.autoplaynextitem',
value='true')
# Warning: Kodi setting "Play next video automatically" is
# enabled. This could break PKC. Deactivate?
if dialog('yesno', lang(29999), lang(30003)):
if utils.dialog('yesno', utils.lang(29999), utils.lang(30003)):
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
False)
# Set any video library updates to happen in the background in order to
@ -556,7 +564,7 @@ class InitialSetup(object):
self.server, self.serverid)
_write_pms_settings(self.server, self.pms_token)
if reboot is True:
reboot_kodi()
utils.reboot_kodi()
return
# If not already retrieved myplex info, optionally let user sign in
@ -570,78 +578,91 @@ class InitialSetup(object):
self.write_pms_to_settings(server)
# User already answered the installation questions
if settings('InstallQuestionsAnswered') == 'true':
if utils.settings('InstallQuestionsAnswered') == 'true':
if reboot is True:
reboot_kodi()
utils.reboot_kodi()
return
# Additional settings where the user needs to choose
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
goto_settings = False
if dialog('yesno',
lang(29999),
lang(39027),
lang(39028),
nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"):
if utils.dialog('yesno',
utils.lang(29999),
utils.lang(39027),
utils.lang(39028),
nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"):
LOG.debug("User opted to use direct paths.")
settings('useDirectPaths', value="1")
utils.settings('useDirectPaths', value="1")
state.DIRECT_PATHS = True
# Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if dialog('yesno', heading=lang(29999), line1=lang(39033)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39033)):
LOG.debug("User chose to replace paths with smb")
else:
settings('replaceSMB', value="false")
utils.settings('replaceSMB', value="false")
# complete replace all original Plex library paths with custom SMB
if dialog('yesno', heading=lang(29999), line1=lang(39043)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39043)):
LOG.debug("User chose custom smb paths")
settings('remapSMB', value="true")
utils.settings('remapSMB', value="true")
# Please enter your custom smb paths in the settings under
# "Sync Options" and then restart Kodi
dialog('ok', heading=lang(29999), line1=lang(39044))
utils.dialog('ok',
heading=utils.lang(29999),
line1=utils.lang(39044))
goto_settings = True
# Go to network credentials?
if dialog('yesno',
heading=lang(29999),
line1=lang(39029),
line2=lang(39030)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39029),
line2=utils.lang(39030)):
LOG.debug("Presenting network credentials dialog.")
from utils import passwords_xml
passwords_xml()
# Disable Plex music?
if dialog('yesno', heading=lang(29999), line1=lang(39016)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39016)):
LOG.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false")
utils.settings('enableMusic', value="false")
# Download additional art from FanArtTV
if dialog('yesno', heading=lang(29999), line1=lang(39061)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39061)):
LOG.debug("User opted to use FanArtTV")
settings('FanartTV', value="true")
utils.settings('FanartTV', value="true")
# Do you want to replace your custom user ratings with an indicator of
# how many versions of a media item you posses?
if dialog('yesno', heading=lang(29999), line1=lang(39718)):
if utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39718)):
LOG.debug("User opted to replace user ratings with version number")
settings('indicate_media_versions', value="true")
utils.settings('indicate_media_versions', value="true")
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
# dialog.ok(heading=lang(29999), line1=lang(39076))
# dialog.ok(heading=utils.lang(29999), line1=utils.lang(39076))
# Need to tell about our image source for collections: themoviedb.org
# dialog.ok(heading=lang(29999), line1=lang(39717))
# dialog.ok(heading=utils.lang(29999), line1=utils.lang(39717))
# Make sure that we only ask these questions upon first installation
settings('InstallQuestionsAnswered', value='true')
utils.settings('InstallQuestionsAnswered', value='true')
if goto_settings is False:
# Open Settings page now? You will need to restart!
goto_settings = dialog('yesno',
heading=lang(29999),
line1=lang(39017))
goto_settings = utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39017))
if goto_settings:
state.PMS_STATUS = 'Stop'
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
executebuiltin(
'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
elif reboot is True:
reboot_kodi()
utils.reboot_kodi()

View file

@ -4,18 +4,17 @@ from logging import getLogger
from ntpath import dirname
from datetime import datetime
from artwork import Artwork
from utils import window, kodi_sql, catch_exceptions
import plexdb_functions as plexdb
import kodidb_functions as kodidb
from PlexAPI import API
from PlexFunctions import GetPlexMetadata
import variables as v
import state
from . import artwork
from . import utils
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from .plex_api import API
from . import plex_functions as PF
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.itemtypes')
# Note: always use same order of URL arguments, NOT urlencode:
# plex_id=<plex_id>&plex_type=<plex_type>&mode=play
@ -32,8 +31,8 @@ class Items(object):
kodiType: optional argument; e.g. 'video' or 'music'
"""
def __init__(self):
self.artwork = Artwork()
self.server = window('pms_server')
self.artwork = artwork.Artwork()
self.server = utils.window('pms_server')
self.plexconn = None
self.plexcursor = None
self.kodiconn = None
@ -45,9 +44,9 @@ class Items(object):
"""
Open DB connections and cursors
"""
self.plexconn = kodi_sql('plex')
self.plexconn = utils.kodi_sql('plex')
self.plexcursor = self.plexconn.cursor()
self.kodiconn = kodi_sql('video')
self.kodiconn = utils.kodi_sql('video')
self.kodicursor = self.kodiconn.cursor()
self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor)
self.kodi_db = kodidb.KodiDBMethods(self.kodicursor)
@ -63,7 +62,7 @@ class Items(object):
self.kodiconn.close()
return self
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def getfanart(self, plex_id, refresh=False):
"""
Tries to get additional fanart for movies (+sets) and TV shows.
@ -95,7 +94,7 @@ class Items(object):
LOG.debug('Already got all fanart for Plex id %s', plex_id)
return True
xml = GetPlexMetadata(plex_id)
xml = PF.GetPlexMetadata(plex_id)
if xml is None:
# Did not receive a valid XML - skip that item for now
LOG.error("Could not get metadata for %s. Skipping that item "
@ -183,7 +182,7 @@ class Movies(Items):
"""
Used for plex library-type movies
"""
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None):
"""
Process single movie
@ -513,7 +512,7 @@ class TVShows(Items):
"""
For Plex library-type TV shows
"""
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None):
"""
Process a single show
@ -722,7 +721,7 @@ class TVShows(Items):
tags.extend(collections)
self.kodi_db.modify_tags(showid, v.KODI_TYPE_SHOW, tags)
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_updateSeason(self, item, viewtag=None, viewid=None):
"""
Process a single season of a certain tv show
@ -768,7 +767,7 @@ class TVShows(Items):
view_id=viewid,
checksum=checksum)
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_updateEpisode(self, item, viewtag=None, viewid=None):
"""
Process single episode
@ -998,7 +997,7 @@ class TVShows(Items):
runtime,
playcount,
dateplayed,
None) # Do send None, we check here
None) # Do send None, we check here
if not state.DIRECT_PATHS:
# need to set a SECOND file entry for a path without plex show id
filename = api.file_name(force_first_media=True)
@ -1014,9 +1013,9 @@ class TVShows(Items):
runtime,
playcount,
dateplayed,
None) # Do send None - 2nd entry
None) # Do send None - 2nd entry
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def remove(self, plex_id):
"""
Remove the entire TV shows object (show, season or episode) including
@ -1139,16 +1138,16 @@ class Music(Items):
OVERWRITE this method, because we need to open another DB.
Open DB connections and cursors
"""
self.plexconn = kodi_sql('plex')
self.plexconn = utils.kodi_sql('plex')
self.plexcursor = self.plexconn.cursor()
# Here it is, not 'video' but 'music'
self.kodiconn = kodi_sql('music')
self.kodiconn = utils.kodi_sql('music')
self.kodicursor = self.kodiconn.cursor()
self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor)
self.kodi_db = kodidb.KodiDBMethods(self.kodicursor)
return self
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_updateArtist(self, item, viewtag=None, viewid=None):
"""
Adds a single artist
@ -1236,7 +1235,7 @@ class Music(Items):
v.KODI_TYPE_ARTIST,
kodicursor)
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None,
scan_children=True):
"""
@ -1362,7 +1361,7 @@ class Music(Items):
artist_id = plex_db.getItem_byId(parent_id)[0]
except TypeError:
LOG.info('Artist %s does not yet exist in Plex DB', parent_id)
artist = GetPlexMetadata(parent_id)
artist = PF.GetPlexMetadata(parent_id)
try:
artist[0].attrib
except (TypeError, IndexError, AttributeError):
@ -1393,13 +1392,16 @@ class Music(Items):
self.genres,
v.KODI_TYPE_ALBUM)
# Update artwork
artwork.modify_artwork(artworks, album_id, v.KODI_TYPE_ALBUM, kodicursor)
artwork.modify_artwork(artworks,
album_id,
v.KODI_TYPE_ALBUM,
kodicursor)
# Add all children - all tracks
if scan_children:
for child in children:
self.add_updateSong(child, viewtag, viewid)
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def add_updateSong(self, item, viewtag=None, viewid=None):
"""
Process single song
@ -1459,7 +1461,7 @@ class Music(Items):
if disc == 1:
track = tracknumber
else:
track = disc*2**16 + tracknumber
track = disc * 2 ** 16 + tracknumber
year = api.year()
_, duration = api.resume_runtime()
rating = userdata['UserRating']
@ -1573,7 +1575,7 @@ class Music(Items):
# No album found. Let's create it
LOG.info("Album database entry missing.")
plex_album_id = api.parent_plex_id()
album = GetPlexMetadata(plex_album_id)
album = PF.GetPlexMetadata(plex_album_id)
if album is None or album == 401:
LOG.error('Could not download album, abort')
return
@ -1664,7 +1666,8 @@ class Music(Items):
idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
VALUES (?, ?, ?, ?, ?)
'''
kodicursor.execute(query, (songid, albumid, track, title, duration))
kodicursor.execute(query,
(songid, albumid, track, title, duration))
# Link song to artists
artist_loop = [{
'Name': api.grandparent_title(),
@ -1680,7 +1683,7 @@ class Music(Items):
artistid = artist_edb[0]
except TypeError:
# Artist is missing from plex database, add it.
artist_xml = GetPlexMetadata(artist_eid)
artist_xml = PF.GetPlexMetadata(artist_eid)
if artist_xml is None or artist_xml == 401:
LOG.error('Error getting artist, abort')
return
@ -1718,9 +1721,12 @@ class Music(Items):
artwork.modify_artwork(artworks, songid, v.KODI_TYPE_SONG, kodicursor)
if item.get('parentKey') is None:
# Update album artwork
artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor)
artwork.modify_artwork(artworks,
albumid,
v.KODI_TYPE_ALBUM,
kodicursor)
@catch_exceptions(warnuser=True)
@utils.catch_exceptions(warnuser=True)
def remove(self, plex_id):
"""
Completely remove the item with plex_id from the Kodi and Plex DBs.
@ -1768,7 +1774,8 @@ class Music(Items):
##### IF ARTIST #####
elif kodi_type == v.KODI_TYPE_ARTIST:
# Delete songs, album, artist
albums = self.plex_db.getItem_byParentId(kodi_id, v.KODI_TYPE_ALBUM)
albums = self.plex_db.getItem_byParentId(kodi_id,
v.KODI_TYPE_ALBUM)
for album in albums:
songs = self.plex_db.getItem_byParentId(album[1],
v.KODI_TYPE_SONG)

View file

@ -3,9 +3,10 @@ Collection of functions using the Kodi JSON RPC interface.
See http://kodi.wiki/view/JSON-RPC_API
"""
from json import loads, dumps
from utils import millis_to_kodi_time
from xbmc import executeJSONRPC
from . import utils
class JsonRPC(object):
"""
@ -152,7 +153,7 @@ def seek_to(offset):
for playerid in get_player_ids():
JsonRPC("Player.Seek").execute(
{"playerid": playerid,
"value": millis_to_kodi_time(offset)})
"value": utils.millis_to_kodi_time(offset)})
def smallforward():

View file

@ -7,14 +7,14 @@ from logging import getLogger
from ntpath import dirname
from sqlite3 import IntegrityError
import artwork
from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi
import variables as v
import state
from . import artwork
from . import utils
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.kodidb_functions')
###############################################################################
@ -35,7 +35,7 @@ class GetKodiDB(object):
self.db_type = db_type
def __enter__(self):
self.kodiconn = kodi_sql(self.db_type)
self.kodiconn = utils.kodi_sql(self.db_type)
kodi_db = KodiDBMethods(self.kodiconn.cursor())
return kodi_db
@ -118,7 +118,7 @@ class KodiDBMethods(object):
if pathid is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
pathid = self.cursor.fetchone()[0] + 1
datetime = unix_date_to_kodi(unix_timestamp())
datetime = utils.unix_date_to_kodi(utils.unix_timestamp())
query = '''
INSERT INTO path(idPath, strPath, dateAdded)
VALUES (?, ?, ?)
@ -209,13 +209,14 @@ class KodiDBMethods(object):
INSERT INTO files(idFile, idPath, strFilename, dateAdded)
VALUES (?, ?, ?, ?)
'''
self.cursor.execute(query, (file_id, path_id, filename, date_added))
self.cursor.execute(query,
(file_id, path_id, filename, date_added))
return file_id
def obsolete_file_ids(self):
"""
Returns a list of (idFile,) tuples (ints) of all Kodi file ids that do
not have a dateAdded set (dateAdded is NULL) and the filename start with
not have a dateAdded set (dateAdded NULL) and the filename start with
'plugin://plugin.video.plexkodiconnect'
These entries should be deleted as they're created falsely by Kodi.
"""
@ -653,7 +654,7 @@ class KodiDBMethods(object):
movie_id = self.cursor.fetchone()[0]
typus = v.KODI_TYPE_EPISODE
except TypeError:
LOG.warn('Unexpectantly did not find a match!')
LOG.debug('Did not find a video DB match')
return
return movie_id, typus
@ -1236,7 +1237,7 @@ def kodiid_from_filename(path, kodi_type=None, db_type=None):
Returns None, <kodi_type> if not possible
"""
kodi_id = None
path = try_decode(path)
path = utils.try_decode(path)
try:
filename = path.rsplit('/', 1)[1]
path = path.rsplit('/', 1)[0] + '/'

View file

@ -5,29 +5,25 @@ from logging import getLogger
from json import loads
from threading import Thread
import copy
import xbmc
from xbmcgui import Window
import plexdb_functions as plexdb
import kodidb_functions as kodidb
from utils import window, settings, plex_command, thread_methods, try_encode, \
kodi_time_to_millis, unix_date_to_kodi, unix_timestamp
from PlexFunctions import scrobble
from downloadutils import DownloadUtils as DU
from kodidb_functions import kodiid_from_filename
from plexbmchelper.subscribers import LOCKER
from playback import playback_triage
from initialsetup import set_replace_paths
import playqueue as PQ
import json_rpc as js
import playlist_func as PL
import state
import variables as v
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from . import utils
from . import plex_functions as PF
from .downloadutils import DownloadUtils as DU
from . import playback
from . import initialsetup
from . import playqueue as PQ
from . import json_rpc as js
from . import playlist_func as PL
from . import state
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.kodimonitor')
# settings: window-variable
WINDOW_SETTINGS = {
@ -65,6 +61,7 @@ class KodiMonitor(xbmc.Monitor):
def __init__(self):
self.xbmcplayer = xbmc.Player()
self._already_slept = False
self.hack_replay = None
xbmc.Monitor.__init__(self)
for playerid in state.PLAYER_STATES:
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
@ -91,14 +88,14 @@ class KodiMonitor(xbmc.Monitor):
changed = False
# Reset the window variables from the settings variables
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
if window(window_value) != settings(settings_value):
if utils.window(window_value) != utils.settings(settings_value):
changed = True
LOG.debug('PKC window settings changed: %s is now %s',
settings_value, settings(settings_value))
window(window_value, value=settings(settings_value))
settings_value, utils.settings(settings_value))
utils.window(window_value, value=utils.settings(settings_value))
# Reset the state variables in state.py
for settings_value, state_name in STATE_SETTINGS.iteritems():
new = settings(settings_value)
new = utils.settings(settings_value)
if new == 'true':
new = True
elif new == 'false':
@ -110,19 +107,17 @@ class KodiMonitor(xbmc.Monitor):
setattr(state, state_name, new)
if state_name == 'FETCH_PMS_ITEM_NUMBER':
LOG.info('Requesting playlist/nodes refresh')
plex_command('RUN_LIB_SCAN', 'views')
utils.plex_command('RUN_LIB_SCAN', 'views')
# Special cases, overwrite all internal settings
set_replace_paths()
state.BACKGROUND_SYNC_DISABLED = settings(
initialsetup.set_replace_paths()
state.BACKGROUND_SYNC_DISABLED = utils.settings(
'enableBackgroundSync') == 'false'
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
settings('backgroundsync_saftyMargin'))
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
state.SSL_CERT_PATH = settings('sslcert') \
if settings('sslcert') != 'None' else None
# Never set through the user
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
utils.settings('backgroundsync_saftyMargin'))
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
state.SSL_CERT_PATH = utils.settings('sslcert') \
if utils.settings('sslcert') != 'None' else None
if changed is True:
# Assume that the user changed the settings so that we can now find
# the path to all media files
@ -137,28 +132,43 @@ class KodiMonitor(xbmc.Monitor):
data = loads(data, 'utf-8')
LOG.debug("Method: %s Data: %s", method, data)
# Hack
if not method == 'Player.OnStop':
self.hack_replay = None
if method == "Player.OnPlay":
state.SUSPEND_SYNC = True
self.PlayBackStart(data)
with state.LOCK_PLAYQUEUES:
self.PlayBackStart(data)
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()')
if data.get('end'):
if (self.hack_replay and not data.get('end') and
self.hack_replay == data['item']):
# Hack for add-on paths
self.hack_replay = None
with state.LOCK_PLAYQUEUES:
self._hack_addon_paths_replay_video()
elif data.get('end'):
if state.PKC_CAUSED_STOP is True:
state.PKC_CAUSED_STOP = False
LOG.debug('PKC caused this playback stop - ignoring')
else:
_playback_cleanup(ended=True)
with state.LOCK_PLAYQUEUES:
_playback_cleanup(ended=True)
else:
_playback_cleanup()
with state.LOCK_PLAYQUEUES:
_playback_cleanup()
state.PKC_CAUSED_STOP_DONE = True
state.SUSPEND_SYNC = False
elif method == 'Playlist.OnAdd':
self._playlist_onadd(data)
with state.LOCK_PLAYQUEUES:
self._playlist_onadd(data)
elif method == 'Playlist.OnRemove':
self._playlist_onremove(data)
elif method == 'Playlist.OnClear':
self._playlist_onclear(data)
with state.LOCK_PLAYQUEUES:
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
playcount = data.get('playcount')
@ -182,28 +192,58 @@ class KodiMonitor(xbmc.Monitor):
else:
# notify the server
if playcount > 0:
scrobble(itemid, 'watched')
PF.scrobble(itemid, 'watched')
else:
scrobble(itemid, 'unwatched')
PF.scrobble(itemid, 'unwatched')
elif method == "VideoLibrary.OnRemove":
pass
elif method == "System.OnSleep":
# Connection is going to sleep
LOG.info("Marking the server as offline. SystemOnSleep activated.")
window('plex_online', value="sleep")
utils.window('plex_online', value="sleep")
elif method == "System.OnWake":
# Allow network to wake up
xbmc.sleep(10000)
window('plex_online', value="false")
utils.window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
if utils.settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000)
plex_command('RUN_LIB_SCAN', 'full')
utils.plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down')
state.STOP_PKC = True
@LOCKER.lockthis
@staticmethod
def _hack_addon_paths_replay_video():
"""
Hack we need for RESUMABLE items because Kodi lost the path of the
last played item that is now being replayed (see playback.py's
Player().play()) Also see playqueue.py _compare_playqueues()
Needed if user re-starts the same video from the library using addon
paths. (Video is only added to playqueue, then immediately stoppen.
There is no playback initialized by Kodi.) Log excerpts:
Method: Playlist.OnAdd Data:
{u'item': {u'type': u'movie', u'id': 4},
u'playlistid': 1,
u'position': 0}
Now we would hack!
Method: Player.OnStop Data:
{u'item': {u'type': u'movie', u'id': 4},
u'end': False}
(within the same micro-second!)
"""
LOG.info('Detected re-start of playback of last item')
old = state.OLD_PLAYER_STATES[1]
kwargs = {
'plex_id': old['plex_id'],
'plex_type': old['plex_type'],
'path': old['file'],
'resolve': False
}
thread = Thread(target=playback.playback_triage, kwargs=kwargs)
thread.start()
def _playlist_onadd(self, data):
"""
Called if an item is added to a Kodi playlist. Example data dict:
@ -219,23 +259,12 @@ class KodiMonitor(xbmc.Monitor):
if 'id' not in data['item']:
return
old = state.OLD_PLAYER_STATES[data['playlistid']]
if (not state.DIRECT_PATHS and data['position'] == 0 and
if (not state.DIRECT_PATHS and
data['position'] == 0 and data['playlistid'] == 1 and
not PQ.PLAYQUEUES[data['playlistid']].items and
data['item']['type'] == old['kodi_type'] and
data['item']['id'] == old['kodi_id']):
# Hack we need for RESUMABLE items because Kodi lost the path of the
# last played item that is now being replayed (see playback.py's
# Player().play()) Also see playqueue.py _compare_playqueues()
LOG.info('Detected re-start of playback of last item')
kwargs = {
'plex_id': old['plex_id'],
'plex_type': old['plex_type'],
'path': old['file'],
'resolve': False
}
thread = Thread(target=playback_triage, kwargs=kwargs)
thread.start()
return
self.hack_replay = data['item']
def _playlist_onremove(self, data):
"""
@ -247,7 +276,6 @@ class KodiMonitor(xbmc.Monitor):
"""
pass
@LOCKER.lockthis
def _playlist_onclear(self, data):
"""
Called if a Kodi playlist is cleared. Example data dict:
@ -271,7 +299,7 @@ class KodiMonitor(xbmc.Monitor):
plex_type = None
# If using direct paths and starting playback from a widget
if not kodi_id and kodi_type and path:
kodi_id, _ = kodiid_from_filename(path, kodi_type)
kodi_id, _ = kodidb.kodiid_from_filename(path, kodi_type)
if kodi_id:
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type)
@ -313,13 +341,16 @@ class KodiMonitor(xbmc.Monitor):
# element otherwise
self._already_slept = True
xbmc.sleep(1000)
json_item = js.get_item(playerid)
try:
json_item = js.get_item(playerid)
except KeyError:
LOG.debug('No playing item returned by Kodi')
return None, None, None
LOG.debug('Kodi playing item properties: %s', json_item)
return (json_item.get('id'),
json_item.get('type'),
json_item.get('file'))
@LOCKER.lockthis
def PlayBackStart(self, data):
"""
Called whenever playback is started. Example data:
@ -426,7 +457,7 @@ class KodiMonitor(xbmc.Monitor):
LOG.debug('Set the player state: %s', status)
@thread_methods
@utils.thread_methods
class SpecialMonitor(Thread):
"""
Detect the resume dialog for widgets.
@ -435,8 +466,8 @@ class SpecialMonitor(Thread):
def run(self):
LOG.info("----====# Starting Special Monitor #====----")
# "Start from beginning", "Play from beginning"
strings = (try_encode(xbmc.getLocalizedString(12021)),
try_encode(xbmc.getLocalizedString(12023)))
strings = (utils.try_encode(xbmc.getLocalizedString(12021)),
utils.try_encode(xbmc.getLocalizedString(12023)))
while not self.stopped():
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
@ -457,7 +488,6 @@ class SpecialMonitor(Thread):
LOG.info("#====---- Special Monitor Stopped ----====#")
@LOCKER.lockthis
def _playback_cleanup(ended=False):
"""
PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi
@ -501,12 +531,12 @@ def _record_playstate(status, ended):
# Item not (yet) in Kodi library
LOG.debug('No playstate update due to Plex id not found: %s', status)
return
totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000
totaltime = float(utils.kodi_time_to_millis(status['totaltime'])) / 1000
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
time = float(kodi_time_to_millis(status['time'])) / 1000
time = float(utils.kodi_time_to_millis(status['time'])) / 1000
try:
progress = time / totaltime
except ZeroDivisionError:
@ -514,7 +544,7 @@ def _record_playstate(status, ended):
LOG.debug('Playback progress %s (%s of %s seconds)',
progress, time, totaltime)
playcount = status['playcount']
last_played = unix_date_to_kodi(unix_timestamp())
last_played = utils.unix_date_to_kodi(utils.unix_timestamp())
if playcount is None:
LOG.debug('playcount not found, looking it up in the Kodi DB')
with kodidb.GetKodiDB('video') as kodi_db:

View file

@ -2,15 +2,14 @@
from logging import getLogger
from threading import Thread
from Queue import Empty
import xbmc
from utils import thread_methods, settings, language as lang, dialog
import plexdb_functions as plexdb
import itemtypes
from artwork import ArtworkSyncMessage
import variables as v
import state
from .. import utils
from .. import plexdb_functions as plexdb
from .. import itemtypes
from .. import artwork
from .. import variables as v
from .. import state
###############################################################################
@ -19,10 +18,10 @@ LOG = getLogger("PLEX." + __name__)
###############################################################################
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'DB_SCAN',
'STOP_SYNC',
'SUSPEND_SYNC'])
@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'DB_SCAN',
'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedProcessFanart(Thread):
"""
Threaded download of additional fanart in the background
@ -68,17 +67,17 @@ class ThreadedProcessFanart(Thread):
'Window.IsVisible(DialogAddonSettings.xml)'):
# Avoid saving '0' all the time
set_zero = True
settings('fanarttv_lookups', value='0')
utils.settings('fanarttv_lookups', value='0')
xbmc.sleep(200)
continue
set_zero = False
if isinstance(item, ArtworkSyncMessage):
if isinstance(item, artwork.ArtworkSyncMessage):
if state.IMAGE_SYNC_NOTIFICATIONS:
dialog('notification',
heading=lang(29999),
message=item.message,
icon='{plex}',
sound=False)
utils.dialog('notification',
heading=utils.lang(29999),
message=item.message,
icon='{plex}',
sound=False)
queue.task_done()
continue
@ -96,6 +95,6 @@ class ThreadedProcessFanart(Thread):
if (counter > 20 and not xbmc.getCondVisibility(
'Window.IsVisible(DialogAddonSettings.xml)')):
counter = 0
settings('fanarttv_lookups', value=str(queue.qsize()))
utils.settings('fanarttv_lookups', value=str(queue.qsize()))
queue.task_done()
LOG.debug("---===### Stopped FanartSync ###===---")

View file

@ -2,12 +2,11 @@
from logging import getLogger
from threading import Thread
from Queue import Empty
from xbmc import sleep
from utils import thread_methods, window
from PlexFunctions import GetPlexMetadata, GetAllPlexChildren
import sync_info
from .. import utils
from .. import plex_functions as PF
from . import sync_info
###############################################################################
@ -16,9 +15,9 @@ LOG = getLogger("PLEX." + __name__)
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedGetMetadata(Thread):
"""
Threaded download of Plex XML metadata for a certain library item.
@ -79,7 +78,7 @@ class ThreadedGetMetadata(Thread):
sleep(20)
continue
# Download Metadata
xml = GetPlexMetadata(item['plex_id'])
xml = PF.GetPlexMetadata(item['plex_id'])
if xml is None:
# Did not receive a valid XML - skip that item for now
LOG.error("Could not get metadata for %s. Skipping that item "
@ -93,14 +92,14 @@ class ThreadedGetMetadata(Thread):
elif xml == 401:
LOG.error('HTTP 401 returned by PMS. Too much strain? '
'Cancelling sync for now')
window('plex_scancrashed', value='401')
utils.window('plex_scancrashed', value='401')
# Kill remaining items in queue (for main thread to cont.)
queue.task_done()
break
item['xml'] = xml
if item.get('get_children') is True:
children_xml = GetAllPlexChildren(item['plex_id'])
children_xml = PF.GetAllPlexChildren(item['plex_id'])
try:
children_xml[0].attrib
except (TypeError, IndexError, AttributeError):

View file

@ -2,12 +2,11 @@
from logging import getLogger
from threading import Thread
from Queue import Empty
from xbmc import sleep
from utils import thread_methods
import itemtypes
import sync_info
from .. import utils
from .. import itemtypes
from . import sync_info
###############################################################################
LOG = getLogger("PLEX." + __name__)
@ -15,9 +14,9 @@ LOG = getLogger("PLEX." + __name__)
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedProcessMetadata(Thread):
"""
Not yet implemented for more than 1 thread - if ever. Only to be called by

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from threading import Thread, Lock
from xbmc import sleep
from xbmcgui import DialogProgressBG
from utils import thread_methods, language as lang
from .. import utils
###############################################################################
@ -19,9 +18,9 @@ LOCK = Lock()
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedShowSyncInfo(Thread):
"""
Threaded class to show the Kodi statusbar of the metadata download.
@ -44,7 +43,10 @@ class ThreadedShowSyncInfo(Thread):
total = self.total
dialog = DialogProgressBG('dialoglogProgressBG')
dialog.create("%s %s: %s %s"
% (lang(39714), self.item_type, str(total), lang(39715)))
% (utils.lang(39714),
self.item_type,
unicode(total),
utils.lang(39715)))
total = 2 * total
total_progress = 0
@ -55,15 +57,15 @@ class ThreadedShowSyncInfo(Thread):
view_name = PROCESSING_VIEW_NAME
total_progress = get_progress + process_progress
try:
percentage = int(float(total_progress) / float(total)*100.0)
percentage = int(float(total_progress) / float(total) * 100.0)
except ZeroDivisionError:
percentage = 0
dialog.update(percentage,
message="%s %s. %s %s: %s"
% (get_progress,
lang(39712),
utils.lang(39712),
process_progress,
lang(39713),
utils.lang(39713),
view_name))
# Sleep for x milliseconds
sleep(200)

View file

@ -5,34 +5,27 @@ from threading import Thread
import Queue
from random import shuffle
import copy
import xbmc
from xbmcvfs import exists
import utils
from utils import window, settings, dialog, language as lang, try_decode, \
try_encode
from downloadutils import DownloadUtils as DU
import itemtypes
import plexdb_functions as plexdb
import kodidb_functions as kodidb
import artwork
import videonodes
import variables as v
import PlexFunctions as PF
import PlexAPI
from library_sync.get_metadata import ThreadedGetMetadata
from library_sync.process_metadata import ThreadedProcessMetadata
import library_sync.sync_info as sync_info
from library_sync.fanart import ThreadedProcessFanart
import music
import playlists
import state
from . import utils
from .downloadutils import DownloadUtils as DU
from . import itemtypes
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from . import artwork
from . import videonodes
from . import plex_functions as PF
from .plex_api import API
from .library_sync import get_metadata, process_metadata, fanart, sync_info
from . import music
from . import playlists
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.librarysync')
###############################################################################
@ -47,10 +40,10 @@ class LibrarySync(Thread):
self.views = []
self.session_keys = {}
self.fanartqueue = Queue.Queue()
self.fanartthread = ThreadedProcessFanart(self.fanartqueue)
self.fanartthread = fanart.ThreadedProcessFanart(self.fanartqueue)
# How long should we wait at least to process new/changed PMS items?
self.vnodes = videonodes.VideoNodes()
self.install_sync_done = settings('SyncInstallRunDone') == 'true'
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
# Show sync dialog even if user deactivated?
self.force_dialog = True
# Need to be set accordingly later
@ -91,16 +84,16 @@ class LibrarySync(Thread):
if state.SYNC_DIALOG is not True and self.force_dialog is not True:
return
if icon == "plex":
dialog('notification',
heading='{plex}',
message=message,
icon='{plex}',
sound=False)
utils.dialog('notification',
heading='{plex}',
message=message,
icon='{plex}',
sound=False)
elif icon == "error":
dialog('notification',
heading='{plex}',
message=message,
icon='{error}')
utils.dialog('notification',
heading='{plex}',
message=message,
icon='{error}')
@staticmethod
def sync_pms_time():
@ -200,7 +193,8 @@ class LibrarySync(Thread):
# Calculate time offset Kodi-PMS
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
settings('kodiplextimeoffset', value=str(state.KODI_PLEX_TIME_OFFSET))
utils.settings('kodiplextimeoffset',
value=str(state.KODI_PLEX_TIME_OFFSET))
LOG.info("Time offset Koditime - Plextime in seconds: %s",
str(state.KODI_PLEX_TIME_OFFSET))
return True
@ -288,15 +282,15 @@ class LibrarySync(Thread):
if state.ENABLE_MUSIC:
xbmc.executebuiltin('UpdateLibrary(music)')
if window('plex_scancrashed') == 'true':
if utils.window('plex_scancrashed') == 'true':
# Show warning if itemtypes.py crashed at some point
dialog('ok', heading='{plex}', line1=lang(39408))
window('plex_scancrashed', clear=True)
elif window('plex_scancrashed') == '401':
window('plex_scancrashed', clear=True)
utils.dialog('ok', heading='{plex}', line1=utils.lang(39408))
utils.window('plex_scancrashed', clear=True)
elif utils.window('plex_scancrashed') == '401':
utils.window('plex_scancrashed', clear=True)
if state.PMS_STATUS not in ('401', 'Auth'):
# Plex server had too much and returned ERROR
dialog('ok', heading='{plex}', line1=lang(39409))
utils.dialog('ok', heading='{plex}', line1=utils.lang(39409))
return True
def _process_view(self, folder_item, kodi_db, plex_db, totalnodes):
@ -495,7 +489,7 @@ class LibrarySync(Thread):
# totalnodes += 1
# Save total
window('Plex.nodes.total', str(totalnodes))
utils.window('Plex.nodes.total', str(totalnodes))
# Get rid of old items (view has been deleted on Plex side)
if self.old_views:
@ -524,11 +518,11 @@ class LibrarySync(Thread):
elif item['kodi_type'] in v.KODI_AUDIOTYPES:
delete_music.append(item)
dialog('notification',
heading='{plex}',
message=lang(30052),
icon='{plex}',
sound=False)
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30052),
icon='{plex}',
sound=False)
for item in delete_movies:
with itemtypes.Movies() as movie_db:
movie_db.remove(item['plex_id'])
@ -638,8 +632,8 @@ class LibrarySync(Thread):
def process_updatelist(self, item_class):
"""
Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes them
by then calling item_classs.<item_class>()
Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes
them by then calling item_classs.<item_class>()
Input:
item_class: 'Movies', 'TVShows', ...
@ -665,13 +659,15 @@ class LibrarySync(Thread):
# Spawn GetMetadata threads for downloading
threads = []
for _ in range(min(state.SYNC_THREAD_NUMBER, item_number)):
thread = ThreadedGetMetadata(download_queue, process_queue)
thread = get_metadata.ThreadedGetMetadata(download_queue,
process_queue)
thread.setDaemon(True)
thread.start()
threads.append(thread)
LOG.debug("%s download threads spawned", len(threads))
# Spawn one more thread to process Metadata, once downloaded
thread = ThreadedProcessMetadata(process_queue, item_class)
thread = process_metadata.ThreadedProcessMetadata(process_queue,
item_class)
thread.setDaemon(True)
thread.start()
threads.append(thread)
@ -702,7 +698,7 @@ class LibrarySync(Thread):
except:
pass
LOG.debug("Sync threads finished")
if (settings('FanartTV') == 'true' and
if (utils.settings('FanartTV') == 'true' and
item_class in ('Movies', 'TVShows')):
for item in self.updatelist:
if item['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW):
@ -1029,23 +1025,11 @@ class LibrarySync(Thread):
do with "process_" methods
"""
if message['type'] == 'playing':
try:
self.process_playing(message['PlaySessionStateNotification'])
except KeyError:
LOG.error('Received invalid PMS message for playstate: %s',
message)
self.process_playing(message['PlaySessionStateNotification'])
elif message['type'] == 'timeline':
try:
self.process_timeline(message['TimelineEntry'])
except (KeyError, ValueError):
LOG.error('Received invalid PMS message for timeline: %s',
message)
self.process_timeline(message['TimelineEntry'])
elif message['type'] == 'activity':
try:
self.process_activity(message['ActivityNotification'])
except KeyError:
LOG.error('Received invalid PMS message for activity: %s',
message)
self.process_activity(message['ActivityNotification'])
def multi_delete(self, liste, delete_list):
"""
@ -1099,7 +1083,7 @@ class LibrarySync(Thread):
continue
else:
successful = self.process_newitems(item)
if successful and settings('FanartTV') == 'true':
if successful and utils.settings('FanartTV') == 'true':
if item['type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW):
self.fanartqueue.put({
'plex_id': item['ratingKey'],
@ -1200,7 +1184,7 @@ class LibrarySync(Thread):
continue
playlists.process_websocket(plex_id=str(item['itemID']),
updated_at=str(item['updatedAt']),
state=status)
status=status)
elif status == 9:
# Immediately and always process deletions (as the PMS will
# send additional message with other codes)
@ -1294,7 +1278,7 @@ class LibrarySync(Thread):
if kodi_info is None:
# Item not (yet) in Kodi library
continue
if settings('plex_serverowned') == 'false':
if utils.settings('plex_serverowned') == 'false':
# Not our PMS, we are not authorized to get the sessions
# On the bright side, it must be us playing :-)
self.session_keys[session_key] = {}
@ -1312,7 +1296,7 @@ class LibrarySync(Thread):
self.session_keys[session_key]['file_id'] = kodi_info[1]
self.session_keys[session_key]['kodi_type'] = kodi_info[4]
session = self.session_keys[session_key]
if settings('plex_serverowned') != 'false':
if utils.settings('plex_serverowned') != 'false':
# Identify the user - same one as signed on with PKC? Skip
# update if neither session's username nor userid match
# (Owner sometime's returns id '1', not always)
@ -1339,7 +1323,7 @@ class LibrarySync(Thread):
LOG.error('Could not get up-to-date xml for item %s',
plex_id)
continue
api = PlexAPI.API(xml[0])
api = API(xml[0])
userdata = api.userdata()
session['duration'] = userdata['Runtime']
session['viewCount'] = userdata['PlayCount']
@ -1378,7 +1362,8 @@ class LibrarySync(Thread):
resume,
session['duration'],
session['file_id'],
utils.unix_date_to_kodi(utils.unix_timestamp()),
utils.unix_date_to_kodi(
utils.unix_timestamp()),
plex_type)
def sync_fanart(self, missing_only=True, refresh=False):
@ -1389,7 +1374,7 @@ class LibrarySync(Thread):
missing_only=True False will start look-up for EVERY item
refresh=False True will force refresh all external fanart
"""
if settings('FanartTV') == 'false':
if utils.settings('FanartTV') == 'false':
return
with plexdb.Get_Plex_DB() as plex_db:
if missing_only:
@ -1407,7 +1392,8 @@ class LibrarySync(Thread):
# Shuffle the list to not always start out identically
shuffle(items)
# Checking FanartTV for %s items
self.fanartqueue.put(artwork.ArtworkSyncMessage(lang(30018) % len(items)))
self.fanartqueue.put(artwork.ArtworkSyncMessage(
utils.lang(30018) % len(items)))
for i, item in enumerate(items):
self.fanartqueue.put({
'plex_id': item['plex_id'],
@ -1415,7 +1401,7 @@ class LibrarySync(Thread):
'refresh': refresh
})
# FanartTV lookup completed
self.fanartqueue.put(artwork.ArtworkSyncMessage(lang(30019)))
self.fanartqueue.put(artwork.ArtworkSyncMessage(utils.lang(30019)))
def triage_lib_scans(self):
"""
@ -1424,27 +1410,27 @@ class LibrarySync(Thread):
"""
if state.RUN_LIB_SCAN in ("full", "repair"):
LOG.info('Full library scan requested, starting')
window('plex_dbScan', value="true")
utils.window('plex_dbScan', value="true")
state.DB_SCAN = True
success = self.maintain_views()
if success and state.RUN_LIB_SCAN == "full":
success = self.full_sync()
elif success:
success = self.full_sync(repair=True)
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
if success:
# Full library sync finished
self.show_kodi_note(lang(39407))
self.show_kodi_note(utils.lang(39407))
elif not self.suspend_item_sync():
self.force_dialog = True
# ERROR in library sync
self.show_kodi_note(lang(39410), icon='error')
self.show_kodi_note(utils.lang(39410), icon='error')
self.force_dialog = False
# Reset views was requested from somewhere else
elif state.RUN_LIB_SCAN == "views":
LOG.info('Refresh playlist and nodes requested, starting')
window('plex_dbScan', value="true")
utils.window('plex_dbScan', value="true")
state.DB_SCAN = True
# First remove playlists
utils.delete_playlists()
@ -1455,28 +1441,28 @@ class LibrarySync(Thread):
# Ran successfully
LOG.info("Refresh playlists/nodes completed")
# "Plex playlists/nodes refreshed"
self.show_kodi_note(lang(39405))
self.show_kodi_note(utils.lang(39405))
else:
# Failed
LOG.error("Refresh playlists/nodes failed")
# "Plex playlists/nodes refresh failed"
self.show_kodi_note(lang(39406), icon="error")
window('plex_dbScan', clear=True)
self.show_kodi_note(utils.lang(39406), icon="error")
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif state.RUN_LIB_SCAN == 'fanart':
# Only look for missing fanart (No)
# or refresh all fanart (Yes)
refresh = dialog('yesno',
heading='{plex}',
line1=lang(39223),
nolabel=lang(39224),
yeslabel=lang(39225))
refresh = utils.dialog('yesno',
heading='{plex}',
line1=utils.lang(39223),
nolabel=utils.lang(39224),
yeslabel=utils.lang(39225))
self.sync_fanart(missing_only=not refresh, refresh=refresh)
elif state.RUN_LIB_SCAN == 'textures':
state.DB_SCAN = True
window('plex_dbScan', value="true")
utils.window('plex_dbScan', value="true")
artwork.Artwork().fullTextureCacheSync()
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
else:
raise NotImplementedError('Library scan not defined: %s'
@ -1489,12 +1475,12 @@ class LibrarySync(Thread):
self._run_internal()
except Exception as e:
state.DB_SCAN = False
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
LOG.error('LibrarySync thread crashed. Error message: %s', e)
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
# Library sync thread has crashed
dialog('ok', heading='{plex}', line1=lang(39400))
utils.dialog('ok', heading='{plex}', line1=utils.lang(39400))
raise
def _run_internal(self):
@ -1504,18 +1490,21 @@ class LibrarySync(Thread):
last_sync = 0
last_processing = 0
last_time_sync = 0
one_day_in_seconds = 60*60*24
one_day_in_seconds = 60 * 60 * 24
# Link to Websocket queue
queue = state.WEBSOCKET_QUEUE
if not exists(try_encode(v.DB_VIDEO_PATH)):
if (not exists(utils.try_encode(v.DB_VIDEO_PATH)) or
not exists(utils.try_encode(v.DB_TEXTURE_PATH)) or
(state.ENABLE_MUSIC and
not exists(utils.try_encode(v.DB_MUSIC_PATH)))):
# Database does not exists
LOG.error("The current Kodi version is incompatible "
"to know which Kodi versions are supported.")
LOG.error('Current Kodi version: %s', try_decode(
LOG.error('Current Kodi version: %s', utils.try_decode(
xbmc.getInfoLabel('System.BuildVersion')))
# "Current Kodi version is unsupported, cancel lib sync"
dialog('ok', heading='{plex}', line1=lang(39403))
utils.dialog('ok', heading='{plex}', line1=utils.lang(39403))
return
# Do some initializing
@ -1523,14 +1512,14 @@ class LibrarySync(Thread):
self.initialize_plex_db()
# Run start up sync
state.DB_SCAN = True
window('plex_dbScan', value="true")
LOG.info("Db version: %s", settings('dbCreatedWithVersion'))
utils.window('plex_dbScan', value="true")
LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion'))
LOG.info('Refreshing video nodes and playlists now')
# Setup the paths for addon-paths (even when using direct paths)
with kodidb.GetKodiDB('video') as kodi_db:
kodi_db.setup_path_table()
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
playlist_monitor = None
@ -1546,7 +1535,7 @@ class LibrarySync(Thread):
if not self.install_sync_done:
# Very first sync upon installation or reset of Kodi DB
state.DB_SCAN = True
window('plex_dbScan', value='true')
utils.window('plex_dbScan', value='true')
# Initialize time offset Kodi - PMS
self.sync_pms_time()
last_time_sync = utils.unix_timestamp()
@ -1559,9 +1548,9 @@ class LibrarySync(Thread):
LOG.error('Initial maintain_views not successful')
elif self.full_sync():
LOG.info('Initial start-up full sync successful')
settings('SyncInstallRunDone', value='true')
utils.settings('SyncInstallRunDone', value='true')
self.install_sync_done = True
settings('dbCreatedWithVersion', v.ADDON_VERSION)
utils.settings('dbCreatedWithVersion', v.ADDON_VERSION)
self.force_dialog = False
initial_sync_done = True
kodi_db_version_checked = True
@ -1573,27 +1562,29 @@ class LibrarySync(Thread):
else:
LOG.error('Initial start-up full sync unsuccessful')
xbmc.executebuiltin('InhibitIdleShutdown(false)')
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif not kodi_db_version_checked:
# Install sync was already done, don't force-show dialogs
self.force_dialog = False
# Verify the validity of the database
current_version = settings('dbCreatedWithVersion')
if not utils.compare_version(current_version, v.MIN_DB_VERSION):
current_version = utils.settings('dbCreatedWithVersion')
if not utils.compare_version(current_version,
v.MIN_DB_VERSION):
LOG.warn("Db version out of date: %s minimum version "
"required: %s", current_version, v.MIN_DB_VERSION)
# DB out of date. Proceed to recreate?
resp = dialog('yesno',
heading=lang(29999),
line1=lang(39401))
resp = utils.dialog('yesno',
heading=utils.lang(29999),
line1=utils.lang(39401))
if not resp:
LOG.warn("Db version out of date! USER IGNORED!")
# PKC may not work correctly until reset
dialog('ok',
heading='{plex}',
line1=lang(29999) + lang(39402))
utils.dialog('ok',
heading='{plex}',
line1='%s%s' % (utils.lang(29999),
utils.lang(39402)))
else:
utils.reset(ask_user=False)
break
@ -1603,7 +1594,7 @@ class LibrarySync(Thread):
# First sync upon PKC restart. Skipped if very first sync upon
# PKC installation has been completed
state.DB_SCAN = True
window('plex_dbScan', value="true")
utils.window('plex_dbScan', value="true")
LOG.info('Doing initial sync on Kodi startup')
if state.SUSPEND_SYNC:
LOG.warning('Forcing startup sync even if Kodi is playing')
@ -1624,7 +1615,7 @@ class LibrarySync(Thread):
playlist_monitor = playlists.kodi_playlist_monitor()
else:
LOG.info('Startup sync has not yet been successful')
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Currently no db scan, so we can start a new scan
@ -1643,23 +1634,23 @@ class LibrarySync(Thread):
not self.suspend_item_sync()):
LOG.info('Doing scheduled full library scan')
state.DB_SCAN = True
window('plex_dbScan', value="true")
utils.window('plex_dbScan', value="true")
success = self.maintain_views()
if success:
success = self.full_sync()
if not success and not self.suspend_item_sync():
LOG.error('Could not finish scheduled full sync')
self.force_dialog = True
self.show_kodi_note(lang(39410),
self.show_kodi_note(utils.lang(39410),
icon='error')
self.force_dialog = False
elif success:
last_sync = now
# Full library sync finished successfully
self.show_kodi_note(lang(39407))
self.show_kodi_note(utils.lang(39407))
else:
LOG.info('Full sync interrupted')
window('plex_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif now - last_time_sync > one_day_in_seconds:
LOG.info('Starting daily time sync')

View file

@ -1,29 +1,31 @@
from logging import getLogger
import variables as v
from utils import compare_version, settings
from . import variables as v
from . import utils
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger('PLEX.migration')
def check_migration():
log.info('Checking whether we need to migrate something')
last_migration = settings('last_migrated_PKC_version')
LOG.info('Checking whether we need to migrate something')
last_migration = utils.settings('last_migrated_PKC_version')
if last_migration == v.ADDON_VERSION:
log.info('Already migrated to PKC version %s' % v.ADDON_VERSION)
LOG.info('Already migrated to PKC version %s' % v.ADDON_VERSION)
# Ensure later migration if user downgraded PKC!
settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
return
if not compare_version(last_migration, '1.8.2'):
log.info('Migrating to version 1.8.1')
if not utils.compare_version(last_migration, '1.8.2'):
LOG.info('Migrating to version 1.8.1')
# Set the new PKC theMovieDB key
settings('themoviedbAPIKey', value='19c90103adb9e98f2172c6a6a3d85dc4')
utils.settings('themoviedbAPIKey',
value='19c90103adb9e98f2172c6a6a3d85dc4')
if not compare_version(last_migration, '2.0.25'):
log.info('Migrating to version 2.0.24')
if not utils.compare_version(last_migration, '2.0.25'):
LOG.info('Migrating to version 2.0.24')
# Need to re-connect with PMS to pick up on plex.direct URIs
settings('ipaddress', value='')
settings('port', value='')
utils.settings('ipaddress', value='')
utils.settings('port', value='')
settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)

View file

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from re import compile as re_compile
from xml.etree.ElementTree import ParseError
from utils import XmlKodiSetting, reboot_kodi, language as lang
from PlexAPI import API
import variables as v
from . import utils
from .plex_api import API
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
LOG = getLogger('PLEX.music')
###############################################################################
@ -37,9 +34,10 @@ def excludefromscan_music_folders(xml):
omit_check=True)
paths.append(__turn_to_regex(path))
try:
with XmlKodiSetting('advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml_file:
with utils.XmlKodiSetting(
'advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml_file:
parent = xml_file.set_setting(['audio', 'excludefromscan'])
for path in paths:
for element in parent:
@ -71,7 +69,7 @@ def excludefromscan_music_folders(xml):
if reboot is True:
# 'New Plex music library detected. Sorry, but we need to
# restart Kodi now due to the changes made.'
reboot_kodi(lang(39711))
utils.reboot_kodi(utils.lang(39711))
def __turn_to_regex(path):

192
resources/lib/path_ops.py Normal file
View file

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# File and Path operations
#
# Kodi xbmc*.*() functions usually take utf-8 encoded commands, thus try_encode
# works.
# Unfortunatly, working with filenames and paths seems to require an encoding in
# the OS' getfilesystemencoding - it will NOT always work with unicode paths.
# However, sys.getfilesystemencoding might return None.
# Feed unicode to all the functions below and you're fine.
import shutil
import os
from os import path # allows to use path_ops.path.join, for example
from distutils import dir_util
import xbmc
import xbmcvfs
from .watchdog.utils import unicode_paths
# Kodi seems to encode in utf-8 in ALL cases (unlike e.g. the OS filesystem)
KODI_ENCODING = 'utf-8'
def encode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.encode(path)
def decode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.decode(path)
def translate_path(path):
"""
Returns the XBMC translated path [unicode]
e.g. Converts 'special://masterprofile/script_data'
-> '/home/user/XBMC/UserData/script_data' on Linux.
"""
translated = xbmc.translatePath(path.encode(KODI_ENCODING, 'strict'))
return translated.decode(KODI_ENCODING, 'strict')
def exists(path):
"""Returns True if the path [unicode] exists"""
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict'))
def rmtree(path, *args, **kwargs):
"""Recursively delete a directory tree.
If ignore_errors is set, errors are ignored; otherwise, if onerror
is set, it is called to handle the error with arguments (func,
path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
path is the argument to that function that caused it to fail; and
exc_info is a tuple returned by sys.exc_info(). If ignore_errors
is false and onerror is None, an exception is raised.
"""
return shutil.rmtree(encode_path(path), *args, **kwargs)
def copyfile(src, dst):
"""Copy data from src to dst"""
return shutil.copyfile(encode_path(src), encode_path(dst))
def makedirs(path, *args, **kwargs):
"""makedirs(path [, mode=0777])
Super-mkdir; create a leaf directory and all intermediate ones. Works like
mkdir, except that any intermediate path segment (not just the rightmost)
will be created if it does not exist. This is recursive.
"""
return os.makedirs(encode_path(path), *args, **kwargs)
def remove(path):
"""
Remove (delete) the file path. If path is a directory, OSError is raised;
see rmdir() below to remove a directory. This is identical to the unlink()
function documented below. On Windows, attempting to remove a file that is
in use causes an exception to be raised; on Unix, the directory entry is
removed but the storage allocated to the file is not made available until
the original file is no longer in use.
"""
return os.remove(encode_path(path))
def walk(top, topdown=True, onerror=None, followlinks=False):
"""
Directory tree generator.
For each directory in the directory tree rooted at top (including top
itself, but excluding '.' and '..'), yields a 3-tuple
dirpath, dirnames, filenames
dirpath is a string, the path to the directory. dirnames is a list of
the names of the subdirectories in dirpath (excluding '.' and '..').
filenames is a list of the names of the non-directory files in dirpath.
Note that the names in the lists are just names, with no path components.
To get a full path (which begins with top) to a file or directory in
dirpath, do os.path.join(dirpath, name).
If optional arg 'topdown' is true or not specified, the triple for a
directory is generated before the triples for any of its subdirectories
(directories are generated top down). If topdown is false, the triple
for a directory is generated after the triples for all of its
subdirectories (directories are generated bottom up).
When topdown is true, the caller can modify the dirnames list in-place
(e.g., via del or slice assignment), and walk will only recurse into the
subdirectories whose names remain in dirnames; this can be used to prune the
search, or to impose a specific order of visiting. Modifying dirnames when
topdown is false is ineffective, since the directories in dirnames have
already been generated by the time dirnames itself is generated. No matter
the value of topdown, the list of subdirectories is retrieved before the
tuples for the directory and its subdirectories are generated.
By default errors from the os.listdir() call are ignored. If
optional arg 'onerror' is specified, it should be a function; it
will be called with one argument, an os.error instance. It can
report the error to continue with the walk, or raise the exception
to abort the walk. Note that the filename is available as the
filename attribute of the exception object.
By default, os.walk does not follow symbolic links to subdirectories on
systems that support them. In order to get this functionality, set the
optional argument 'followlinks' to true.
Caution: if you pass a relative pathname for top, don't change the
current working directory between resumptions of walk. walk never
changes the current directory, and assumes that the client doesn't
either.
Example:
import os
from os.path import join, getsize
for root, dirs, files in os.walk('python/Lib/email'):
print root, "consumes",
print sum([getsize(join(root, name)) for name in files]),
print "bytes in", len(files), "non-directory files"
if 'CVS' in dirs:
dirs.remove('CVS') # don't visit CVS directories
"""
# Get all the results from os.walk and store them in a list
walker = list(os.walk(encode_path(top),
topdown,
onerror,
followlinks))
for top, dirs, nondirs in walker:
yield (decode_path(top),
[decode_path(x) for x in dirs],
[decode_path(x) for x in nondirs])
def copy_tree(src, dst, *args, **kwargs):
"""
Copy an entire directory tree 'src' to a new location 'dst'.
Both 'src' and 'dst' must be directory names. If 'src' is not a
directory, raise DistutilsFileError. If 'dst' does not exist, it is
created with 'mkpath()'. The end result of the copy is that every
file in 'src' is copied to 'dst', and directories under 'src' are
recursively copied to 'dst'. Return the list of files that were
copied or might have been copied, using their output name. The
return value is unaffected by 'update' or 'dry_run': it is simply
the list of all files under 'src', with the names changed to be
under 'dst'.
'preserve_mode' and 'preserve_times' are the same as for
'copy_file'; note that they only apply to regular files, not to
directories. If 'preserve_symlinks' is true, symlinks will be
copied as symlinks (on platforms that support them!); otherwise
(the default), the destination of the symlink will be copied.
'update' and 'verbose' are the same as for 'copy_file'.
"""
src = encode_path(src)
dst = encode_path(dst)
return dir_util.copy_tree(src, dst, *args, **kwargs)

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
###############################################################################
from cPickle import dumps, loads
from xbmcgui import Window
from xbmc import log, LOGDEBUG
###############################################################################
WINDOW = Window(10000)
PREFIX = 'PLEX.%s: ' % __name__
PREFIX = 'PLEX.pickler: '
###############################################################################

View file

@ -3,11 +3,11 @@
from xbmcgui import ListItem
def convert_PKC_to_listitem(PKC_listitem):
def convert_pkc_to_listitem(pkc_listitem):
"""
Insert a PKC_listitem and you will receive a valid XBMC listitem
Insert a PKCListItem() and you will receive a valid XBMC listitem
"""
data = PKC_listitem.data
data = pkc_listitem.data
listitem = ListItem(label=data.get('label'),
label2=data.get('label2'),
path=data.get('path'))
@ -26,7 +26,7 @@ def convert_PKC_to_listitem(PKC_listitem):
return listitem
class PKC_ListItem(object):
class PKCListItem(object):
"""
Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data
when pickling!

View file

@ -3,42 +3,36 @@ Used to kick off Kodi playback
"""
from logging import getLogger
from threading import Thread
from os.path import join
from xbmc import Player, sleep
from PlexAPI import API
from PlexFunctions import GetPlexMetadata, init_plex_playqueue
from downloadutils import DownloadUtils as DU
import plexdb_functions as plexdb
import kodidb_functions as kodidb
import playlist_func as PL
import playqueue as PQ
from playutils import PlayUtils
from PKC_listitem import PKC_ListItem
from pickler import pickle_me, Playback_Successful
import json_rpc as js
from utils import settings, dialog, language as lang, try_encode
from plexbmchelper.subscribers import LOCKER
import variables as v
import state
from .plex_api import API
from . import plex_functions as PF
from . import utils
from .downloadutils import DownloadUtils as DU
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from . import playlist_func as PL
from . import playqueue as PQ
from . import json_rpc as js
from . import pickler
from .playutils import PlayUtils
from .pkc_listitem import PKCListItem
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.playback')
# Do we need to return ultimately with a setResolvedUrl?
RESOLVE = True
# We're "failing" playback with a video of 0 length
NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4')
###############################################################################
@LOCKER.lockthis
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
"""
Hit this function for addon path playback, Plex trailers, etc.
Will setup playback first, then on second call complete playback.
Will set Playback_Successful() with potentially a PKC_ListItem() attached
Will set Playback_Successful() with potentially a PKCListItem() attached
(to be consumed by setResolvedURL in default.py)
If trailers or additional (movie-)parts are added, default.py is released
@ -50,14 +44,14 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
service.py Python instance
"""
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s,', plex_id, plex_type, path, resolve)
'resolve %s', plex_id, plex_type, path, resolve)
global RESOLVE
# If started via Kodi context menu, we never resolve
RESOLVE = resolve if not state.CONTEXT_MENU_PLAY else False
if not state.AUTHENTICATED:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
dialog('notification', lang(29999), lang(30017))
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
_ensure_resolve(abort=True)
return
playqueue = PQ.get_playqueue_from_type(
@ -74,7 +68,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
except KeyError:
LOG.error('Still no position - abort')
# "Play error"
dialog('notification', lang(29999), lang(30128), icon='{error}')
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
# HACK to detect playback of playlists for add-on paths
@ -97,14 +94,21 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
try:
item = playqueue.items[pos]
except IndexError:
LOG.debug('PKC playqueue yet empty, need to initialize playback')
initiate = True
else:
initiate = True if item.plex_id != plex_id else False
if initiate:
_playback_init(plex_id, plex_type, playqueue, pos)
else:
# kick off playback on second pass
_conclude_playback(playqueue, pos)
if item.plex_id != plex_id:
LOG.debug('Received new plex_id %s, expected %s. Init playback',
plex_id, item.plex_id)
initiate = True
else:
initiate = False
with state.LOCK_PLAYQUEUES:
if initiate:
_playback_init(plex_id, plex_type, playqueue, pos)
else:
# kick off playback on second pass
_conclude_playback(playqueue, pos)
def _playlist_playback(plex_id, plex_type):
@ -124,13 +128,16 @@ def _playlist_playback(plex_id, plex_type):
for the next item in line :-)
(by the way: trying to get active Kodi player id will return [])
"""
xml = GetPlexMetadata(plex_id)
xml = PF.GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
dialog('notification', lang(29999), lang(30128), icon='{error}')
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
@ -150,13 +157,16 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
Playback setup if Kodi starts playing an item for the first time.
"""
LOG.info('Initializing PKC playback')
xml = GetPlexMetadata(plex_id)
xml = PF.GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
dialog('notification', lang(29999), lang(30128), icon='{error}')
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
if playqueue.kodi_pl.size() > 1:
@ -179,10 +189,12 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
api = API(xml[0])
trailers = False
if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and
settings('enableCinema') == "true"):
if settings('askCinema') == "true":
utils.settings('enableCinema') == "true"):
if utils.settings('askCinema') == "true":
# "Play trailers?"
trailers = dialog('yesno', lang(29999), lang(33016))
trailers = utils.dialog('yesno',
utils.lang(29999),
utils.lang(33016))
trailers = True if trailers else False
else:
trailers = True
@ -198,15 +210,18 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
playqueue.clear()
if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion
xml = init_plex_playqueue(plex_id,
xml.attrib.get('librarySectionUUID'),
mediatype=plex_type,
trailers=trailers)
xml = PF.init_plex_playqueue(plex_id,
xml.attrib.get('librarySectionUUID'),
mediatype=plex_type,
trailers=trailers)
if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
plex_id, xml.attrib.get('librarySectionUUID'))
# "Play error"
dialog('notification', lang(29999), lang(30128), icon='{error}')
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
# Do NOT use _ensure_resolve() because we resolved above already
state.CONTEXT_MENU_PLAY = False
state.FORCE_TRANSCODE = False
@ -253,9 +268,13 @@ def _ensure_resolve(abort=False):
# Because playback won't start with context menu play
state.PKC_CAUSED_STOP = True
state.PKC_CAUSED_STOP_DONE = False
result = Playback_Successful()
result.listitem = PKC_ListItem(path=NULL_VIDEO)
pickle_me(result)
if not abort:
result = pickler.Playback_Successful()
result.listitem = PKCListItem(path=v.NULL_VIDEO)
pickler.pickle_me(result)
else:
# Shows PKC error message
pickler.pickle_me(None)
if abort:
# Reset some playback variables
state.CONTEXT_MENU_PLAY = False
@ -308,7 +327,7 @@ def _prep_playlist_stack(xml):
# Need to redirect again to PKC to conclude playback
path = api.path()
listitem = api.create_listitem()
listitem.setPath(try_encode(path))
listitem.setPath(utils.try_encode(path))
else:
# Will add directly via the Kodi DB
path = None
@ -373,8 +392,8 @@ def _conclude_playback(playqueue, pos):
return PKC listitem attached to result
"""
LOG.info('Concluding playback for playqueue position %s', pos)
result = Playback_Successful()
listitem = PKC_ListItem()
result = pickler.Playback_Successful()
listitem = PKCListItem()
item = playqueue.items[pos]
if item.xml is not None:
# Got a Plex element
@ -385,7 +404,7 @@ def _conclude_playback(playqueue, pos):
playurl = playutils.getPlayUrl()
else:
playurl = item.file
listitem.setPath(try_encode(playurl))
listitem.setPath(utils.try_encode(playurl))
if item.playmethod == 'DirectStream':
listitem.setSubtitles(api.cache_external_subs())
elif item.playmethod == 'Transcode':
@ -405,7 +424,7 @@ def _conclude_playback(playqueue, pos):
listitem.setProperty('resumetime', str(item.offset))
# Reset the resumable flag
result.listitem = listitem
pickle_me(result)
pickler.pickle_me(result)
LOG.info('Done concluding playback')
@ -424,7 +443,7 @@ def process_indirect(key, offset, resolve=True):
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
result = Playback_Successful()
result = pickler.Playback_Successful()
if key.startswith('http') or key.startswith('{server}'):
xml = DU().downloadUrl(key)
elif key.startswith('/system/services'):
@ -441,7 +460,7 @@ def process_indirect(key, offset, resolve=True):
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
# Todo: implement offset
api = API(xml[0])
listitem = PKC_ListItem()
listitem = PKCListItem()
api.create_listitem(listitem)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
@ -462,14 +481,14 @@ def process_indirect(key, offset, resolve=True):
return
playurl = xml[0].attrib['key']
item.file = playurl
listitem.setPath(try_encode(playurl))
listitem.setPath(utils.try_encode(playurl))
playqueue.items.append(item)
if resolve is True:
result.listitem = listitem
pickle_me(result)
pickler.pickle_me(result)
else:
thread = Thread(target=Player().play,
args={'item': try_encode(playurl),
args={'item': utils.try_encode(playurl),
'listitem': listitem})
thread.setDaemon(True)
LOG.info('Done initializing PKC playback, starting Kodi player')

View file

@ -4,16 +4,16 @@ from logging import getLogger
from threading import Thread
from urlparse import parse_qsl
import playback
from context_entry import ContextMenu
import state
import json_rpc as js
from pickler import pickle_me, Playback_Successful
import kodidb_functions as kodidb
from . import playback
from . import context_entry
from . import json_rpc as js
from . import pickler
from . import kodidb_functions as kodidb
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.playback_starter')
###############################################################################
@ -29,7 +29,7 @@ class PlaybackStarter(Thread):
except ValueError:
# E.g. other add-ons scanning for Extras folder
LOG.debug('Detected 3rd party add-on call - ignoring')
pickle_me(Playback_Successful())
pickler.pickle_me(pickler.Playback_Successful())
return
params = dict(parse_qsl(params))
mode = params.get('mode')
@ -54,10 +54,10 @@ class PlaybackStarter(Thread):
else:
LOG.error('Could not find tv show id for %s', item)
if resolve:
pickle_me(Playback_Successful())
pickler.pickle_me(pickler.Playback_Successful())
elif mode == 'context_menu':
ContextMenu(kodi_id=params.get('kodi_id'),
kodi_type=params.get('kodi_type'))
context_entry.ContextMenu(kodi_id=params.get('kodi_id'),
kodi_type=params.get('kodi_type'))
def run(self):
queue = state.COMMAND_PIPELINE_QUEUE

View file

@ -3,25 +3,23 @@
Collection of functions associated with Kodi and Plex playlists and playqueues
"""
from logging import getLogger
import os
import urllib
from urlparse import parse_qsl, urlsplit
from re import compile as re_compile
import plexdb_functions as plexdb
from downloadutils import DownloadUtils as DU
from utils import try_decode, try_encode
from PlexAPI import API
from PlexFunctions import GetPlexMetadata
from kodidb_functions import kodiid_from_filename
import json_rpc as js
import variables as v
from .plex_api import API
from . import plex_functions as PF
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from .downloadutils import DownloadUtils as DU
from . import utils
from . import path_ops
from . import json_rpc as js
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.playlist_func')
REGEX = re_compile(r'''metadata%2F(\d+)''')
###############################################################################
@ -42,22 +40,23 @@ class PlaylistObjectBaseclase(object):
def __repr__(self):
"""
Print the playlist, e.g. to log. Returns utf-8 encoded string
Print the playlist, e.g. to log. Returns unicode
"""
answ = u'{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
answ = '{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
# For some reason, can't use dir directly
for key in self.__dict__:
if key in ('id', 'kodi_pl'):
continue
if isinstance(getattr(self, key), str):
answ += '\'%s\': \'%s\', ' % (key,
try_decode(getattr(self, key)))
utils.try_decode(getattr(self,
key)))
elif isinstance(getattr(self, key), unicode):
answ += '\'%s\': \'%s\', ' % (key, getattr(self, key))
else:
# e.g. int
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
return try_encode(answ + '}}')
return answ + '}}'
class Playlist_Object(PlaylistObjectBaseclase):
@ -81,7 +80,9 @@ class Playlist_Object(PlaylistObjectBaseclase):
@kodi_path.setter
def kodi_path(self, path):
file = os.path.basename(path)
if not isinstance(path, unicode):
raise RuntimeError('Path is %s, not unicode!' % type(path))
file = path_ops.path.basename(path)
try:
self.kodi_filename, self.kodi_extension = file.split('.', 1)
except ValueError:
@ -219,16 +220,17 @@ class Playlist_Item(object):
def __repr__(self):
"""
Print the playlist item, e.g. to log. Returns utf-8 encoded string
Print the playlist item, e.g. to log. Returns unicode
"""
answ = (u'{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
answ = ('{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
% (self.__class__.__name__, self.id, self.plex_id))
for key in self.__dict__:
if key in ('id', 'plex_id', 'xml'):
continue
if isinstance(getattr(self, key), str):
answ += '\'%s\': \'%s\', ' % (key,
try_decode(getattr(self, key)))
utils.try_decode(getattr(self,
key)))
elif isinstance(getattr(self, key), unicode):
answ += '\'%s\': \'%s\', ' % (key, getattr(self, key))
else:
@ -238,7 +240,7 @@ class Playlist_Item(object):
answ += '\'xml\': None}}'
else:
answ += '\'xml\': \'%s\'}}' % self.xml.tag
return try_encode(answ)
return answ
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
@ -317,7 +319,7 @@ def playlist_item_from_kodi(kodi_item):
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(try_encode(item.file), safe=''))
% urllib.quote(utils.try_encode(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' %
@ -343,17 +345,20 @@ def verify_kodi_item(plex_id, kodi_item):
# Need more info since we don't have kodi_id nor type. Use file path.
if (kodi_item['file'].startswith('plugin') or
kodi_item['file'].startswith('http')):
raise PlaylistError('kodi_item cannot be used for Plex playback')
LOG.error('kodi_item %s cannot be used for Plex playback', kodi_item)
raise PlaylistError
LOG.debug('Starting research for Kodi id since we didnt get one: %s',
kodi_item)
kodi_id, _ = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_MOVIE)
kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_MOVIE)
kodi_item['type'] = v.KODI_TYPE_MOVIE
if kodi_id is None:
kodi_id, _ = kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_EPISODE)
kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_EPISODE)
kodi_item['type'] = v.KODI_TYPE_EPISODE
if kodi_id is None:
kodi_id, _ = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_SONG)
kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_SONG)
kodi_item['type'] = v.KODI_TYPE_SONG
kodi_item['id'] = kodi_id
kodi_item['type'] = None if kodi_id is None else kodi_item['type']
@ -519,8 +524,9 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
# Need to get the details for the playlist item
item = playlist_item_from_xml(xml[0])
except (KeyError, IndexError, TypeError):
raise PlaylistError('Could not init Plex playlist with plex_id %s and '
'kodi_item %s' % (plex_id, kodi_item))
LOG.error('Could not init Plex playlist: plex_id %s, kodi_item %s',
plex_id, kodi_item)
raise PlaylistError
playlist.items.append(item)
LOG.debug('Initialized the playqueue on the Plex side: %s', playlist)
return item
@ -683,7 +689,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
item = playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None:
xml = GetPlexMetadata(item.plex_id)
xml = PF.GetPlexMetadata(item.plex_id)
item.xml = xml[-1]
playlist.items.insert(pos, item)
return item
@ -859,11 +865,12 @@ def get_plextype_from_xml(xml):
returns None if unsuccessful
"""
try:
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0]
plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
xml.attrib['playQueueSourceURI'])[0]
except IndexError:
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return
new_xml = GetPlexMetadata(plex_id)
new_xml = PF.GetPlexMetadata(plex_id)
try:
new_xml[0].attrib
except (TypeError, IndexError, AttributeError):

View file

@ -1,29 +1,20 @@
# -*- coding: utf-8 -*-
from logging import getLogger
import os
import sys
from threading import Lock
from xbmcvfs import exists
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
import playlist_func as PL
from PlexAPI import API
import kodidb_functions as kodidb
import plexdb_functions as plexdb
import utils
import variables as v
import state
from .watchdog.events import FileSystemEventHandler
from .watchdog.observers import Observer
from . import playlist_func as PL
from .plex_api import API
from . import kodidb_functions as kodidb
from . import plexdb_functions as plexdb
from . import utils
from . import path_ops
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
# Necessary to temporarily hold back librarysync/websocket listener when doing
# a full sync
LOCK = Lock()
LOCKER = utils.LockFunction(LOCK)
LOG = getLogger('PLEX.playlists')
# Which playlist formates are supported by PKC?
SUPPORTED_FILETYPES = (
@ -39,12 +30,6 @@ EVENT_TYPE_DELETED = 'deleted'
EVENT_TYPE_CREATED = 'created'
EVENT_TYPE_MODIFIED = 'modified'
# m3u files do not have encoding specified
if v.PLATFORM == 'Windows':
ENCODING = 'mbcs'
else:
ENCODING = sys.getdefaultencoding()
def create_plex_playlist(playlist):
"""
@ -106,20 +91,21 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
playlist.plex_updatedat = updated_at
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
name = utils.valid_filename(playlist.plex_name)
path = os.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
while exists(path) or playlist_object_from_db(path=path):
path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
while path_ops.exists(path) or playlist_object_from_db(path=path):
# In case the Plex playlist names are not unique
occurance = utils.REGEX_FILE_NUMBERING.search(path)
if not occurance:
path = os.path.join(v.PLAYLIST_PATH,
playlist.type,
'%s_01.m3u' % name[:min(len(name), 248)])
path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type,
'%s_01.m3u' % name[:min(len(name), 248)])
else:
occurance = int(occurance.group(1)) + 1
path = os.path.join(v.PLAYLIST_PATH,
playlist.type,
'%s_%02d.m3u' % (name[:min(len(name), 248)],
occurance))
path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type,
'%s_%02d.m3u' % (name[:min(len(name),
248)],
occurance))
LOG.debug('Kodi playlist path: %s', path)
playlist.kodi_path = path
# Derive filename close to Plex playlist name
@ -137,7 +123,7 @@ def delete_kodi_playlist(playlist):
Returns None or raises PL.PlaylistError
"""
try:
os.remove(playlist.kodi_path)
path_ops.remove(playlist.kodi_path)
except (OSError, IOError) as err:
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
playlist, err.errno, err.strerror)
@ -187,10 +173,10 @@ def m3u_to_plex_ids(playlist):
Adapter to process *.m3u playlist files. Encoding is not uniform!
"""
plex_ids = list()
with open(playlist.kodi_path, 'rb') as f:
with open(path_ops.encode_path(playlist.kodi_path), 'rb') as f:
text = f.read()
try:
text = text.decode(ENCODING)
text = text.decode(v.M3U_ENCODING)
except UnicodeDecodeError:
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
text = text.decode('ISO-8859-1')
@ -217,15 +203,15 @@ def _write_playlist_to_file(playlist, xml):
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
Returns None or raises PL.PlaylistError
"""
text = u'#EXTCPlayListM3U::M3U\n'
text = '#EXTCPlayListM3U::M3U\n'
for element in xml:
api = API(element)
text += (u'#EXTINF:%s,%s\n%s\n'
text += ('#EXTINF:%s,%s\n%s\n'
% (api.runtime(), api.title(), api.path()))
text += '\n'
text = text.encode(ENCODING, 'ignore')
text = text.encode(v.M3U_ENCODING, 'strict')
try:
with open(playlist.kodi_path, 'wb') as f:
with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
f.write(text)
except (OSError, IOError) as err:
LOG.error('Could not write Kodi playlist file: %s', playlist)
@ -274,41 +260,49 @@ def _kodi_playlist_identical(xml_element):
pass
@LOCKER.lockthis
def process_websocket(plex_id, updated_at, state):
def process_websocket(plex_id, updated_at, status):
"""
Hit by librarysync to process websocket messages concerning playlists
"""
create = False
playlist = playlist_object_from_db(plex_id=plex_id)
try:
if playlist and state == 9:
LOG.debug('Plex deletion of playlist detected: %s', playlist)
delete_kodi_playlist(playlist)
elif playlist and playlist.plex_updatedat == updated_at:
LOG.debug('Playlist with id %s already synced: %s',
plex_id, playlist)
elif playlist:
LOG.debug('Change of Plex playlist detected: %s', playlist)
delete_kodi_playlist(playlist)
create = True
elif not playlist and not state == 9:
LOG.debug('Creation of new Plex playlist detected: %s', plex_id)
create = True
# To the actual work
if create:
create_kodi_playlist(plex_id=plex_id, updated_at=updated_at)
except PL.PlaylistError:
pass
with state.LOCK_PLAYLISTS:
playlist = playlist_object_from_db(plex_id=plex_id)
try:
if playlist and status == 9:
LOG.debug('Plex deletion of playlist detected: %s', playlist)
delete_kodi_playlist(playlist)
elif playlist and playlist.plex_updatedat == updated_at:
LOG.debug('Playlist with id %s already synced: %s',
plex_id, playlist)
elif playlist:
LOG.debug('Change of Plex playlist detected: %s', playlist)
delete_kodi_playlist(playlist)
create = True
elif not playlist and not status == 9:
LOG.debug('Creation of new Plex playlist detected: %s',
plex_id)
create = True
# To the actual work
if create:
create_kodi_playlist(plex_id=plex_id, updated_at=updated_at)
except PL.PlaylistError:
pass
@LOCKER.lockthis
def full_sync():
"""
Full sync of playlists between Kodi and Plex. Returns True is successful,
False otherwise
"""
LOG.info('Starting playlist full sync')
with state.LOCK_PLAYLISTS:
return _full_sync()
def _full_sync():
"""
Need to lock because we're messing with playlists
"""
# Get all Plex playlists
xml = PL.get_all_playlists()
if xml is None:
@ -332,7 +326,7 @@ def full_sync():
elif playlist.plex_updatedat != api.updated_at():
LOG.debug('Detected changed Plex playlist %s: %s',
api.plex_id(), api.title())
if exists(playlist.kodi_path):
if path_ops.exists(playlist.kodi_path):
delete_kodi_playlist(playlist)
else:
update_plex_table(playlist, delete=True)
@ -345,7 +339,7 @@ def full_sync():
pass
# Get rid of old Plex playlists that were deleted on the Plex side
for plex_id in old_plex_ids:
playlist = playlist_object_from_db(plex_id=api.plex_id())
playlist = playlist_object_from_db(plex_id=plex_id)
if playlist:
LOG.debug('Removing outdated Plex playlist %s from %s',
playlist.plex_name, playlist.kodi_path)
@ -360,7 +354,7 @@ def full_sync():
if state.ENABLE_MUSIC:
master_paths.append(v.PLAYLIST_PATH_MUSIC)
for master_path in master_paths:
for root, _, files in os.walk(master_path):
for root, _, files in path_ops.walk(master_path):
for file in files:
try:
extension = file.rsplit('.', 1)[1]
@ -368,7 +362,7 @@ def full_sync():
continue
if extension not in SUPPORTED_FILETYPES:
continue
path = os.path.join(root, file)
path = path_ops.path.join(root, file)
kodi_hash = utils.generate_file_md5(path)
playlist = playlist_object_from_db(kodi_hash=kodi_hash)
playlist_2 = playlist_object_from_db(path=path)
@ -438,7 +432,7 @@ class PlaylistEventhandler(FileSystemEventHandler):
EVENT_TYPE_DELETED: self.on_deleted,
}
event_type = event.event_type
with LOCK:
with state.LOCK_PLAYLISTS:
_method_map[event_type](event)
def on_created(self, event):

View file

@ -3,25 +3,20 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
"""
from logging import getLogger
from threading import Thread
from re import compile as re_compile
import xbmc
from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep
from utils import thread_methods
import playlist_func as PL
from PlexFunctions import GetAllPlexChildren
from PlexAPI import API
from plexbmchelper.subscribers import LOCK
from playback import play_xml
import json_rpc as js
import variables as v
import state
from . import utils
from . import playlist_func as PL
from . import plex_functions as PF
from .plex_api import API
from . import json_rpc as js
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.playqueue')
PLUGIN = 'plugin://%s' % v.ADDON_ID
REGEX = re_compile(r'''plex_id=(\d+)''')
# Our PKC playqueues (3 instances of Playqueue_Object())
PLAYQUEUES = []
@ -37,7 +32,7 @@ def init_playqueues():
LOG.debug('Playqueues have already been initialized')
return
# Initialize Kodi playqueues
with LOCK:
with state.LOCK_PLAYQUEUES:
for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists():
@ -48,12 +43,12 @@ def init_playqueues():
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == v.KODI_TYPE_AUDIO:
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
elif playqueue.type == v.KODI_TYPE_VIDEO:
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
else:
# Currently, only video or audio playqueues available
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Overwrite 'picture' with 'photo'
playqueue.type = v.KODI_TYPE_PHOTO
PLAYQUEUES.append(playqueue)
@ -65,14 +60,13 @@ def get_playqueue_from_type(kodi_playlist_type):
Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in
"""
with LOCK:
for playqueue in PLAYQUEUES:
if playqueue.type == kodi_playlist_type:
break
else:
raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
return playqueue
for playqueue in PLAYQUEUES:
if playqueue.type == kodi_playlist_type:
break
else:
raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
return playqueue
def init_playqueue_from_plex_children(plex_id, transient_token=None):
@ -81,7 +75,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
Returns the Playlist_Object
"""
xml = GetAllPlexChildren(plex_id)
xml = PF.GetAllPlexChildren(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
@ -95,41 +89,11 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player')
Player().play(playqueue.kodi_pl, None, False, 0)
xbmc.Player().play(playqueue.kodi_pl, None, False, 0)
return playqueue
def update_playqueue_from_PMS(playqueue,
playqueue_id=None,
repeat=None,
offset=None,
transient_token=None):
"""
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue
repeat = 0, 1, 2
offset = time offset in Plextime (milliseconds)
"""
LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s', playqueue_id, offset, repeat)
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
with LOCK:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
playqueue.clear()
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except PL.PlaylistError:
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token
play_xml(playqueue, xml, offset)
@thread_methods(add_suspends=['PMS_STATUS'])
@utils.thread_methods(add_suspends=['PMS_STATUS'])
class PlayqueueMonitor(Thread):
"""
Unfortunately, Kodi does not tell if items within a Kodi playqueue
@ -167,7 +131,7 @@ class PlayqueueMonitor(Thread):
old_item.kodi_type == new_item['type'])
else:
try:
plex_id = REGEX.findall(new_item['file'])[0]
plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0]
except IndexError:
LOG.debug('Comparing paths directly as a fallback')
identical = old_item.file == new_item['file']
@ -222,8 +186,8 @@ class PlayqueueMonitor(Thread):
while suspended():
if stopped():
break
sleep(1000)
with LOCK:
xbmc.sleep(1000)
with state.LOCK_PLAYQUEUES:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
if playqueue.old_kodi_pl != kodi_pl:
@ -236,5 +200,5 @@ class PlayqueueMonitor(Thread):
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl)
sleep(200)
xbmc.sleep(200)
LOG.info("----===## PlayqueueMonitor stopped ##===----")

View file

@ -2,13 +2,13 @@
###############################################################################
from logging import getLogger
from downloadutils import DownloadUtils as DU
from utils import window, settings, language as lang, dialog, try_encode
import variables as v
from .downloadutils import DownloadUtils as DU
from . import utils
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.playutils')
###############################################################################
@ -46,7 +46,8 @@ class PlayUtils():
'maxVideoBitrate': self.get_bitrate(),
'videoResolution': self.get_resolution(),
'videoQuality': '100',
'mediaBufferSize': int(settings('kodi_video_cache'))/1024,
'mediaBufferSize': int(
utils.settings('kodi_video_cache')) / 1024,
})
self.item.playmethod = 'Transcode'
LOG.info("The playurl is: %s", playurl)
@ -71,15 +72,15 @@ class PlayUtils():
return playurl
# set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0'
if settings('playType') != "0":
if utils.settings('playType') != "0":
# User forcing to play via HTTP
LOG.info("User chose to not direct play")
return
if self.mustTranscode():
return
return self.api.validate_playurl(path,
self.api.plex_type(),
force_check=True)
self.api.plex_type(),
force_check=True)
def mustTranscode(self):
"""
@ -106,7 +107,7 @@ class PlayUtils():
# e.g. trailers. Avoids TypeError with "'h265' in codec"
LOG.info('No codec from PMS, not transcoding.')
return False
if ((settings('transcodeHi10P') == 'true' and
if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
LOG.info('Option to transcode 10bit h264 video content enabled.')
@ -139,7 +140,7 @@ class PlayUtils():
if self.api.plex_type() == 'track':
return True
# set to 'Transcode=2'
if settings('playType') == "2":
if utils.settings('playType') == "2":
# User forcing to play via HTTP
LOG.info("User chose to transcode")
return False
@ -149,7 +150,7 @@ class PlayUtils():
def get_max_bitrate(self):
# get the addon video quality
videoQuality = settings('maxVideoQualities')
videoQuality = utils.settings('maxVideoQualities')
bitrate = {
'0': 320,
'1': 720,
@ -180,13 +181,13 @@ class PlayUtils():
'2': 720,
'3': 1080
}
return H265[settings('transcodeH265')]
return H265[utils.settings('transcodeH265')]
def get_bitrate(self):
"""
Get the desired transcoding bitrate from the settings
"""
videoQuality = settings('transcoderVideoQualities')
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 320,
'1': 720,
@ -207,7 +208,7 @@ class PlayUtils():
"""
Get the desired transcoding resolutions from the settings
"""
chosen = settings('transcoderVideoQualities')
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '420x420',
'1': '576x320',
@ -244,7 +245,7 @@ class PlayUtils():
audio_streams = []
subtitle_streams_list = []
# No subtitles as an option
subtitle_streams = [lang(39706)]
subtitle_streams = [utils.lang(39706)]
downloadable_streams = []
download_subs = []
# selectAudioIndex = ""
@ -264,35 +265,35 @@ class PlayUtils():
codec = stream.attrib.get('codec')
channellayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb+1,
track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'],
codec,
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb+1,
lang(39707), # unknown
track = "%s %s - %s %s" % (audio_numb + 1,
utils.lang(39707), # unknown
codec,
channellayout)
audio_streams_list.append(index)
audio_streams.append(try_encode(track))
audio_streams.append(utils.try_encode(track))
audio_numb += 1
# Subtitles
elif typus == "3":
try:
track = "%s %s" % (sub_num+1, stream.attrib['language'])
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
except KeyError:
track = "%s %s (%s)" % (sub_num+1,
lang(39707), # unknown
track = "%s %s (%s)" % (sub_num + 1,
utils.lang(39707), # unknown
stream.attrib.get('codec'))
default = stream.attrib.get('default')
forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key')
if default:
track = "%s - %s" % (track, lang(39708)) # Default
track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced:
track = "%s - %s" % (track, lang(39709)) # Forced
track = "%s - %s" % (track, utils.lang(39709)) # Forced
if downloadable:
# We do know the language - temporarily download
if 'language' in stream.attrib:
@ -303,23 +304,23 @@ class PlayUtils():
# We don't know the language - no need to download
else:
path = self.api.attach_plex_token_to_url(
"%s%s" % (window('pms_server'),
"%s%s" % (utils.window('pms_server'),
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(try_encode(path))
download_subs.append(utils.try_encode(path))
else:
track = "%s (%s)" % (track, lang(39710)) # burn-in
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
if stream.attrib.get('selected') == '1' and downloadable:
# Only show subs without asking user if they can be
# turned off
default_sub = index
subtitle_streams_list.append(index)
subtitle_streams.append(try_encode(track))
subtitle_streams.append(utils.try_encode(track))
sub_num += 1
if audio_numb > 1:
resp = dialog('select', lang(33013), audio_streams)
resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp > -1:
# User selected some audio track
args = {
@ -335,14 +336,14 @@ class PlayUtils():
return
select_subs_index = None
if (settings('pickPlexSubtitles') == 'true' and
if (utils.settings('pickPlexSubtitles') == 'true' and
default_sub is not None):
LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub
else:
resp = dialog('select', lang(33014), subtitle_streams)
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp > 0:
select_subs_index = subtitle_streams_list[resp-1]
select_subs_index = subtitle_streams_list[resp - 1]
else:
# User selected no subtitles or backed out of dialog
select_subs_index = ''

View file

@ -30,29 +30,23 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt
(and others...)
"""
from logging import getLogger
from re import compile as re_compile, sub
from re import sub
from urllib import urlencode, unquote
from os.path import basename, join
from os import makedirs
from xbmcgui import ListItem
from xbmcvfs import exists
import clientinfo as client
from downloadutils import DownloadUtils as DU
from utils import window, settings, language as lang, try_decode, try_encode, \
unix_date_to_kodi, exists_dir, slugify, dialog, escape_html
import PlexFunctions as PF
import plexdb_functions as plexdb
import kodidb_functions as kodidb
import variables as v
import state
from .downloadutils import DownloadUtils as DU
from . import clientinfo
from . import utils
from . import path_ops
from . import plex_functions as PF
from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
REGEX_IMDB = re_compile(r'''/(tt\d+)''')
REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''')
LOG = getLogger('PLEX.plex_api')
###############################################################################
@ -70,7 +64,7 @@ class API(object):
# which media part in the XML response shall we look at?
self.part = 0
self.mediastream = None
self.server = window('pms_server')
self.server = utils.window('pms_server')
def set_part_number(self, number=None):
"""
@ -203,7 +197,7 @@ class API(object):
ans = None
if ans is not None:
try:
ans = try_decode(unquote(ans))
ans = utils.try_decode(unquote(ans))
except UnicodeDecodeError:
# Sometimes, Plex seems to have encoded in latin1
ans = unquote(ans).decode('latin1')
@ -215,23 +209,23 @@ class API(object):
Will always use addon paths, never direct paths
"""
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower()
if (window('plex_force_transcode_pix') == 'true' or
if (utils.window('plex_force_transcode_pix') == 'true' or
extension not in v.KODI_SUPPORTED_IMAGES):
# Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080
path = self.server + PF.transcode_image_path(
self.item[0][0].get('key'),
window('pms_token'),
utils.window('pms_token'),
"%s%s" % (self.server, self.item[0][0].get('key')),
1920,
1080)
else:
path = self.attach_plex_token_to_url(
'%s%s' % (window('pms_server'),
'%s%s' % (utils.window('pms_server'),
self.item[0][0].attrib['key']))
# Attach Plex id to url to let it be picked up by our playqueue agent
# later
return try_encode('%s&plex_id=%s' % (path, self.plex_id()))
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id()))
def tv_show_path(self):
"""
@ -258,7 +252,7 @@ class API(object):
"""
res = self.item.get('addedAt')
if res is not None:
res = unix_date_to_kodi(res)
res = utils.unix_date_to_kodi(res)
else:
res = '2000-01-01 10:00:00'
return res
@ -295,7 +289,7 @@ class API(object):
played = True if playcount else False
try:
last_played = unix_date_to_kodi(int(item['lastViewedAt']))
last_played = utils.unix_date_to_kodi(int(item['lastViewedAt']))
except (KeyError, ValueError):
last_played = None
@ -423,7 +417,7 @@ class API(object):
"""
answ = self.item.get('guid')
if answ is not None:
answ = escape_html(answ)
answ = utils.escape_html(answ)
return answ
def provider(self, providername=None):
@ -438,10 +432,10 @@ class API(object):
return None
if providername == 'imdb':
regex = REGEX_IMDB
regex = utils.REGEX_IMDB
elif providername == 'tvdb':
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
regex = REGEX_TVDB
regex = utils.REGEX_TVDB
else:
return None
@ -456,7 +450,7 @@ class API(object):
"""
Returns the title of the element as unicode or 'Missing Title Name'
"""
return try_decode(self.item.get('title', 'Missing Title Name'))
return utils.try_decode(self.item.get('title', 'Missing Title Name'))
def titles(self):
"""
@ -657,12 +651,12 @@ class API(object):
url may or may not already contain a '?'
"""
if window('pms_token') == '':
if utils.window('pms_token') == '':
return url
if '?' not in url:
url = "%s?X-Plex-Token=%s" % (url, window('pms_token'))
url = "%s?X-Plex-Token=%s" % (url, utils.window('pms_token'))
else:
url = "%s&X-Plex-Token=%s" % (url, window('pms_token'))
url = "%s&X-Plex-Token=%s" % (url, utils.window('pms_token'))
return url
def item_id(self):
@ -808,12 +802,12 @@ class API(object):
track['channels'] = stream.get('channels')
# 'unknown' if we cannot get language
track['language'] = stream.get(
'languageCode', lang(39310)).lower()
'languageCode', utils.lang(39310)).lower()
audiotracks.append(track)
elif media_type == 3: # Subtitle streams
# 'unknown' if we cannot get language
subtitlelanguages.append(
stream.get('languageCode', lang(39310)).lower())
stream.get('languageCode', utils.lang(39310)).lower())
return {
'video': videotracks,
'audio': audiotracks,
@ -966,7 +960,7 @@ class API(object):
LOG.info('Start movie set/collection lookup on themoviedb with %s',
item.get('title', ''))
api_key = settings('themoviedbAPIKey')
api_key = utils.settings('themoviedbAPIKey')
if media_type == v.PLEX_TYPE_SHOW:
media_type = 'tv'
title = item.get('title', '')
@ -977,7 +971,7 @@ class API(object):
parameters = {
'api_key': api_key,
'language': v.KODILANGUAGE,
'query': try_encode(title)
'query': utils.try_encode(title)
}
data = DU().downloadUrl(url,
authenticate=False,
@ -1103,8 +1097,8 @@ class API(object):
try:
data.get('poster_path')
except AttributeError:
LOG.debug('Could not find TheMovieDB poster paths for %s in '
'the language %s', title, language)
LOG.debug('Could not find TheMovieDB poster paths for %s'
' in the language %s', title, language)
continue
if not poster and data.get('poster_path'):
poster = ('https://image.tmdb.org/t/p/original%s' %
@ -1120,7 +1114,7 @@ class API(object):
media_id: IMDB id for movies, tvdb id for TV shows
"""
api_key = settings('FanArtTVAPIKey')
api_key = utils.settings('FanArtTVAPIKey')
typus = self.plex_type()
if typus == v.PLEX_TYPE_SHOW:
typus = 'tv'
@ -1236,17 +1230,17 @@ class API(object):
count += 1
if (count > 1 and (
(self.plex_type() != 'clip' and
settings('bestQuality') == 'false')
utils.settings('bestQuality') == 'false')
or
(self.plex_type() == 'clip' and
settings('bestTrailer') == 'false'))):
utils.settings('bestTrailer') == 'false'))):
# Several streams/files available.
dialoglist = []
for entry in self.item.iterfind('./Media'):
# Get additional info (filename / languages)
filename = None
if 'file' in entry[0].attrib:
filename = basename(entry[0].attrib['file'])
filename = path_ops.path.basename(entry[0].attrib['file'])
# Languages of audio streams
languages = []
for stream in entry[0]:
@ -1255,12 +1249,13 @@ class API(object):
languages.append(stream.attrib['language'])
languages = ', '.join(languages)
if filename:
option = try_encode(filename)
option = utils.try_encode(filename)
if languages:
if option:
option = '%s (%s): ' % (option, try_encode(languages))
option = '%s (%s): ' % (option,
utils.try_encode(languages))
else:
option = '%s: ' % try_encode(languages)
option = '%s: ' % utils.try_encode(languages)
if 'videoResolution' in entry.attrib:
option = '%s%sp ' % (option,
entry.get('videoResolution'))
@ -1275,7 +1270,7 @@ class API(object):
option = '%s%s ' % (option,
entry.get('audioCodec'))
dialoglist.append(option)
media = dialog('select', 'Select stream', dialoglist)
media = utils.dialog('select', 'Select stream', dialoglist)
else:
media = 0
self.mediastream = media
@ -1306,7 +1301,7 @@ class API(object):
self.mediastream_number()
if quality is None:
quality = {}
xargs = client.getXArgsDeviceInfo()
xargs = clientinfo.getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls
if action == "DirectStream":
@ -1331,19 +1326,19 @@ class API(object):
transcode_path = self.server + \
'/video/:/transcode/universal/start.m3u8?'
args = {
'audioBoost': settings('audioBoost'),
'audioBoost': utils.settings('audioBoost'),
'autoAdjustQuality': 0,
'directPlay': 0,
'directStream': 1,
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': window('plex_client_Id'),
'session': utils.window('plex_client_Id'),
'fastSeek': 1,
'path': path,
'mediaIndex': self.mediastream,
'partIndex': self.part,
'hasMDE': 1,
'location': 'lan',
'subtitleSize': settings('subtitleSize')
'subtitleSize': utils.settings('subtitleSize')
}
# Look like Android to let the PMS use the transcoding profile
xargs.update(headers)
@ -1400,9 +1395,7 @@ class API(object):
Returns the path to the downloaded subtitle or None
"""
if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH):
makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
path = path_ops.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
response = DU().downloadUrl(url, return_response=True)
try:
response.status_code
@ -1411,14 +1404,8 @@ class API(object):
return
else:
LOG.debug('Writing temp subtitle to %s', path)
try:
with open(path, 'wb') as filer:
filer.write(response.content)
except UnicodeEncodeError:
LOG.debug('Need to slugify the filename %s', path)
path = slugify(path)
with open(path, 'wb') as filer:
filer.write(response.content)
with open(path_ops.encode_path(path), 'wb') as filer:
filer.write(response.content)
return path
def kodi_premiere_date(self):
@ -1560,7 +1547,8 @@ class API(object):
listitem.setInfo('video', infoLabels=metadata)
try:
# Add context menu entry for information screen
listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
listitem.addContextMenuItems([(utils.lang(30032),
'XBMC.Action(Info)',)])
except TypeError:
# Kodi fuck-up
pass
@ -1606,20 +1594,21 @@ class API(object):
# exist() needs a / or \ at the end to work for directories
if folder is False:
# files
check = exists(try_encode(path))
check = exists(utils.try_encode(path))
else:
# directories
if "\\" in path:
if not path.endswith('\\'):
checkpath = utils.try_encode(path)
if b"\\" in checkpath:
if not checkpath.endswith('\\'):
# Add the missing backslash
check = exists_dir(path + "\\")
check = utils.exists_dir(checkpath + "\\")
else:
check = exists_dir(path)
check = utils.exists_dir(checkpath)
else:
if not path.endswith('/'):
check = exists_dir(path + "/")
if not checkpath.endswith('/'):
check = utils.exists_dir(checkpath + "/")
else:
check = exists_dir(path)
check = utils.exists_dir(checkpath)
if not check:
if force_check is False:
@ -1648,25 +1637,11 @@ class API(object):
LOG.warn('Cannot access file: %s', url)
# Kodi cannot locate the file #s. Please verify your PKC settings. Stop
# syncing?
resp = dialog('yesno', heading='{plex}', line1=lang(39031) % url)
resp = utils.dialog('yesno',
heading='{plex}',
line1=utils.lang(39031) % url)
return resp
def set_listitem_artwork(self, listitem):
"""
Set all artwork to the listitem
"""
allartwork = self.artwork()
listitem.setArt(self.artwork())
for arttype in arttypes:
art = arttypes[arttype]
if art == "Backdrop":
# Backdrop is a list, grab the first backdrop
self._set_listitem_artprop(listitem,
arttype,
allartwork[art][0])
else:
self._set_listitem_artprop(listitem, arttype, allartwork[art])
@staticmethod
def _set_listitem_artprop(listitem, arttype, path):
if arttype in (

View file

@ -6,30 +6,57 @@ from threading import Thread
from Queue import Empty
from socket import SHUT_RDWR
from urllib import urlencode
from xbmc import sleep, executebuiltin, Player
from utils import settings, thread_methods, language as lang, dialog
from plexbmchelper import listener, plexgdm, subscribers, httppersist
from plexbmchelper.subscribers import LOCKER
from PlexFunctions import ParseContainerKey, GetPlexMetadata, DownloadChunks
from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml, \
get_playlist_details_from_xml
from playback import playback_triage, play_xml
import json_rpc as js
import variables as v
import state
import playqueue as PQ
from .plexbmchelper import listener, plexgdm, subscribers, httppersist
from .plex_api import API
from . import utils
from . import plex_functions as PF
from . import playlist_func as PL
from . import playback
from . import json_rpc as js
from . import playqueue as PQ
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.plex_companion')
###############################################################################
@thread_methods(add_suspends=['PMS_STATUS'])
def update_playqueue_from_PMS(playqueue,
playqueue_id=None,
repeat=None,
offset=None,
transient_token=None):
"""
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue
repeat = 0, 1, 2
offset = time offset in Plextime (milliseconds)
"""
LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s', playqueue_id, offset, repeat)
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
with state.LOCK_PLAYQUEUES:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
playqueue.clear()
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except PL.PlaylistError:
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token
playback.play_xml(playqueue, xml, offset)
@utils.thread_methods(add_suspends=['PMS_STATUS'])
class PlexCompanion(Thread):
"""
Plex Companion monitoring class. Invoke only once
@ -47,9 +74,8 @@ class PlexCompanion(Thread):
self.subscription_manager = None
Thread.__init__(self)
@LOCKER.lockthis
def _process_alexa(self, data):
xml = GetPlexMetadata(data['key'])
xml = PF.GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
@ -62,28 +88,33 @@ class PlexCompanion(Thread):
api.plex_id(),
transient_token=data.get('token'))
elif data['containerKey'].startswith('/playQueues/'):
_, container_key, _ = ParseContainerKey(data['containerKey'])
xml = DownloadChunks('{server}/playQueues/%s?' % container_key)
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
xml = PF.DownloadChunks('{server}/playQueues/%s?' % container_key)
if xml is None:
# "Play error"
dialog('notification', lang(29999), lang(30128), icon='{error}')
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
return
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
playqueue.clear()
get_playlist_details_from_xml(playqueue, xml)
PL.get_playlist_details_from_xml(playqueue, xml)
playqueue.plex_transient_token = data.get('token')
if data.get('offset') != '0':
offset = float(data['offset']) / 1000.0
else:
offset = None
play_xml(playqueue, xml, offset)
playback.play_xml(playqueue, xml, offset)
else:
state.PLEX_TRANSIENT_TOKEN = data.get('token')
if data.get('offset') != '0':
state.RESUMABLE = True
state.RESUME_PLAYBACK = True
playback_triage(api.plex_id(), api.plex_type(), resolve=False)
playback.playback_triage(api.plex_id(),
api.plex_type(),
resolve=False)
@staticmethod
def _process_node(data):
@ -99,17 +130,16 @@ class PlexCompanion(Thread):
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
@LOCKER.lockthis
def _process_playlist(self, data):
# Get the playqueue ID
_, container_key, query = ParseContainerKey(data['containerKey'])
_, container_key, query = PF.ParseContainerKey(data['containerKey'])
try:
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
except KeyError:
# E.g. Plex web does not supply the media type
# Still need to figure out the type (video vs. music vs. pix)
xml = GetPlexMetadata(data['key'])
xml = PF.GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
@ -118,14 +148,12 @@ class PlexCompanion(Thread):
api = API(xml[0])
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
PQ.update_playqueue_from_PMS(
playqueue,
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=data.get('offset'),
transient_token=data.get('token'))
update_playqueue_from_PMS(playqueue,
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=data.get('offset'),
transient_token=data.get('token'))
@LOCKER.lockthis
def _process_streams(self, data):
"""
Plex Companion client adjusted audio or subtitle stream
@ -147,17 +175,16 @@ class PlexCompanion(Thread):
else:
LOG.error('Unknown setStreams command: %s', data)
@LOCKER.lockthis
def _process_refresh(self, data):
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
xml = get_pms_playqueue(data['playQueueID'])
xml = PL.get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml)
plex_type = PL.get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = PQ.get_playqueue_from_type(
@ -166,7 +193,7 @@ class PlexCompanion(Thread):
return
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
PQ.update_playqueue_from_PMS(playqueue, data['playQueueID'])
update_playqueue_from_PMS(playqueue, data['playQueueID'])
def _process_tasks(self, task):
"""
@ -186,14 +213,17 @@ class PlexCompanion(Thread):
LOG.debug('Processing: %s', task)
data = task['data']
if task['action'] == 'alexa':
self._process_alexa(data)
with state.LOCK_PLAYQUEUES:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
self._process_playlist(data)
with state.LOCK_PLAYQUEUES:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
self._process_refresh(data)
with state.LOCK_PLAYQUEUES:
self._process_refresh(data)
elif task['action'] == 'setStreams':
try:
self._process_streams(data)
@ -231,7 +261,7 @@ class PlexCompanion(Thread):
self.player)
self.subscription_manager = subscription_manager
if settings('plexCompanion') == 'true':
if utils.settings('plexCompanion') == 'true':
# Start up httpd
start_count = 0
while True:

View file

@ -3,24 +3,20 @@ from logging import getLogger
from urllib import urlencode, quote_plus
from ast import literal_eval
from urlparse import urlparse, parse_qsl
from re import compile as re_compile
from copy import deepcopy
from time import time
from threading import Thread
from xbmc import sleep
from downloadutils import DownloadUtils as DU
from utils import settings, try_encode, try_decode
from variables import PLEX_TO_KODI_TIMEFACTOR
import plex_tv
from .downloadutils import DownloadUtils as DU
from . import utils
from . import plex_tv
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.plex_functions')
CONTAINERSIZE = int(settings('limitindex'))
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_DIRECT = re_compile(r'''\.plex\.direct:\d+$''')
CONTAINERSIZE = int(utils.settings('limitindex'))
# For discovery of PMS in the local LAN
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
@ -36,7 +32,7 @@ def ConvertPlexToKodiTime(plexTime):
"""
if plexTime is None:
return None
return int(float(plexTime) * PLEX_TO_KODI_TIMEFACTOR)
return int(float(plexTime) * v.PLEX_TO_KODI_TIMEFACTOR)
def GetPlexKeyNumber(plexKey):
@ -48,7 +44,7 @@ def GetPlexKeyNumber(plexKey):
Returns ('','') if nothing is found
"""
try:
result = REGEX_PLEX_KEY.findall(plexKey)[0]
result = utils.REGEX_END_DIGITS.findall(plexKey)[0]
except IndexError:
result = ('', '')
return result
@ -90,13 +86,13 @@ def GetMethodFromPlexType(plexType):
def GetPlexLoginFromSettings():
"""
Returns a dict:
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
Returns strings or unicode
@ -106,13 +102,13 @@ def GetPlexLoginFromSettings():
plexhome is 'true' if plex home is used (the default)
"""
return {
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'),
'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
}
@ -140,7 +136,7 @@ def check_connection(url, token=None, verifySSL=None):
if token is not None:
header_options = {'X-Plex-Token': token}
if verifySSL is True:
verifySSL = None if settings('sslverify') == 'true' else False
verifySSL = None if utils.settings('sslverify') == 'true' else False
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
LOG.debug("Checking connection to server %s with verifySSL=%s",
@ -303,11 +299,11 @@ def _plex_gdm():
}
for line in response['data'].split('\n'):
if 'Content-Type:' in line:
pms['product'] = try_decode(line.split(':')[1].strip())
pms['product'] = utils.try_decode(line.split(':')[1].strip())
elif 'Host:' in line:
pms['baseURL'] = line.split(':')[1].strip()
elif 'Name:' in line:
pms['name'] = try_decode(line.split(':')[1].strip())
pms['name'] = utils.try_decode(line.split(':')[1].strip())
elif 'Port:' in line:
pms['port'] = line.split(':')[1].strip()
elif 'Resource-Identifier:' in line:
@ -336,7 +332,7 @@ def _pms_list_from_plex_tv(token):
queue = Queue()
thread_queue = []
max_age_in_seconds = 2*60*60*24
max_age_in_seconds = 2 * 60 * 60 * 24
for device in xml.findall('Device'):
if 'server' not in device.get('provides'):
# No PMS - skip
@ -355,7 +351,7 @@ def _pms_list_from_plex_tv(token):
'token': device.get('accessToken'),
'ownername': device.get('sourceTitle'),
'product': device.get('product'), # e.g. 'Plex Media Server'
'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e...'
'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e..'
'device': device.get('device'), # e.g. 'PC' or 'Windows'
'platform': device.get('platform'), # e.g. 'Windows', 'Android'
'local': device.get('publicAddressMatches') == '1',
@ -412,7 +408,7 @@ def _pms_list_from_plex_tv(token):
def _poke_pms(pms, queue):
data = pms['connections'][0].attrib
url = data['uri']
if data['local'] == '1' and REGEX_PLEX_DIRECT.findall(url):
if data['local'] == '1' and utils.REGEX_PLEX_DIRECT.findall(url):
# In case DNS resolve of plex.direct does not work, append a new
# connection that will directly access the local IP (e.g. internet down)
conn = deepcopy(pms['connections'][0])
@ -634,7 +630,7 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
'repeat': '0'
}
if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber')
args['extrasPrefixCount'] = utils.settings('trailerNumber')
xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST")
try:
xml[0].tag
@ -791,7 +787,7 @@ def GetUserArtworkURL(username):
Returns the URL for the user's Avatar. Or False if something went
wrong.
"""
users = plex_tv.list_home_users(settings('plexToken'))
users = plex_tv.list_home_users(utils.settings('plexToken'))
url = ''
# If an error is encountered, set to False
if not users:
@ -818,13 +814,13 @@ def transcode_image_path(key, AuthToken, path, width, height):
final path to image file
"""
# external address - can we get a transcoding request for external images?
if key.startswith('http://') or key.startswith('https://'):
if key.startswith('http://') or key.startswith('https://'):
path = key
elif key.startswith('/'): # internal full path.
path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key
path = try_encode(path)
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...

View file

@ -1,15 +1,14 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from xbmc import sleep, executebuiltin
from downloadutils import DownloadUtils as DU
from utils import dialog, language as lang, settings, try_encode
import variables as v
import state
from .downloadutils import DownloadUtils as DU
from . import utils
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.plex_tx')
###############################################################################
@ -39,7 +38,7 @@ def choose_home_user(token):
username = user['title']
userlist.append(username)
# To take care of non-ASCII usernames
userlist_coded.append(try_encode(username))
userlist_coded.append(utils.try_encode(username))
usernumber = len(userlist)
username = ''
usertoken = ''
@ -47,12 +46,14 @@ def choose_home_user(token):
while trials < 3:
if usernumber > 1:
# Select user
user_select = dialog('select', lang(29999) + lang(39306),
userlist_coded)
user_select = utils.dialog(
'select',
'%s%s' % (utils.lang(29999), utils.lang(39306)),
userlist_coded)
if user_select == -1:
LOG.info("No user selected.")
settings('username', value='')
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
utils.settings('username', value='')
executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID)
return False
# Only 1 user received, choose that one
else:
@ -64,11 +65,11 @@ def choose_home_user(token):
pin = None
if user['protected'] == '1':
LOG.debug('Asking for users PIN')
pin = dialog('input',
lang(39307) + selected_user,
'',
type='{numeric}',
option='{hide}')
pin = utils.dialog('input',
'%s%s' % (utils.lang(39307), selected_user),
'',
type='{numeric}',
option='{hide}')
# User chose to cancel
# Plex bug: don't call url for protected user with empty PIN
if not pin:
@ -78,7 +79,7 @@ def choose_home_user(token):
result = switch_home_user(user['id'],
pin,
token,
settings('plex_machineIdentifier'))
utils.settings('plex_machineIdentifier'))
if result:
# Successfully retrieved username: break out of while loop
username = result['username']
@ -88,15 +89,16 @@ def choose_home_user(token):
else:
trials += 1
# Could not login user, please try again
if not dialog('yesno',
heading='{plex}',
line1=lang(39308) + selected_user,
line2=lang(39309)):
if not utils.dialog('yesno',
heading='{plex}',
line1='%s%s' % (utils.lang(39308),
selected_user),
line2=utils.lang(39309)):
# User chose to cancel
break
if not username:
LOG.error('Failed signing in a user to plex.tv')
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID)
return False
return {
'username': username,
@ -123,7 +125,7 @@ def switch_home_user(userid, pin, token, machineIdentifier):
for the machineIdentifier that was chosen
}
settings('userid') and settings('username') with new plex token
utils.settings('userid') and utils.settings('username') with new plex token
"""
LOG.info('Switching to user %s', userid)
url = 'https://plex.tv/api/home/users/' + userid + '/switch'
@ -143,12 +145,12 @@ def switch_home_user(userid, pin, token, machineIdentifier):
token = answer.attrib.get('authenticationToken', '')
# Write to settings file
settings('username', username)
settings('accessToken', token)
settings('userid', answer.attrib.get('id', ''))
settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1'
else 'false')
utils.settings('username', username)
utils.settings('accessToken', token)
utils.settings('userid', answer.attrib.get('id', ''))
utils.settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1'
else 'false')
state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False
@ -239,15 +241,15 @@ def sign_in_with_pin():
code, identifier = get_pin()
if not code:
# Problems trying to contact plex.tv. Try again later
dialog('ok', heading='{plex}', line1=lang(39303))
utils.dialog('ok', heading='{plex}', line1=utils.lang(39303))
return False
# Go to https://plex.tv/pin and enter the code:
# Or press No to cancel the sign in.
answer = dialog('yesno',
heading='{plex}',
line1=lang(39304) + "\n\n",
line2=code + "\n\n",
line3=lang(39311))
answer = utils.dialog('yesno',
heading='{plex}',
line1='%s%s' % (utils.lang(39304), "\n\n"),
line2='%s%s' % (code, "\n\n"),
line3=utils.lang(39311))
if not answer:
return False
count = 0
@ -261,7 +263,7 @@ def sign_in_with_pin():
count += 1
if xml is False:
# Could not sign in to plex.tv Try again later
dialog('ok', heading='{plex}', line1=lang(39305))
utils.dialog('ok', heading='{plex}', line1=utils.lang(39305))
return False
# Parse xml
userid = xml.attrib.get('id')
@ -282,15 +284,15 @@ def sign_in_with_pin():
'plexid': userid,
'homesize': home_size
}
settings('plexLogin', username)
settings('plexToken', token)
settings('plexhome', home)
settings('plexid', userid)
settings('plexAvatar', avatar)
settings('plexHomeSize', home_size)
utils.settings('plexLogin', username)
utils.settings('plexToken', token)
utils.settings('plexhome', home)
utils.settings('plexid', userid)
utils.settings('plexAvatar', avatar)
utils.settings('plexHomeSize', home_size)
# Let Kodi log into plex.tv on startup from now on
settings('myplexlogin', 'true')
settings('plex_status', value=lang(39227))
utils.settings('myplexlogin', 'true')
utils.settings('plex_status', value=utils.lang(39227))
return result

View file

@ -7,7 +7,7 @@ from socket import error as socket_error
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.httppersist')
###############################################################################

View file

@ -6,19 +6,18 @@ from re import sub
from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
import xbmc
from xbmc import sleep, Player, Monitor
from companion import process_command
import json_rpc as js
from clientinfo import getXArgsDeviceInfo
import variables as v
from .. import companion
from .. import json_rpc as js
from .. import clientinfo
from .. import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
PLAYER = Player()
MONITOR = Monitor()
LOG = getLogger('PLEX.listener')
PLAYER = xbmc.Player()
MONITOR = xbmc.Monitor()
# Hack we need in order to keep track of the open connections from Plex Web
CLIENT_DICT = {}
@ -122,7 +121,7 @@ class MyHandler(BaseHTTPRequestHandler):
RESOURCES_XML.format(
title=v.DEVICENAME,
machineIdentifier=v.PKC_MACHINE_IDENTIFIER),
getXArgsDeviceInfo(include_token=False))
clientinfo.getXArgsDeviceInfo(include_token=False))
elif request_path == 'player/timeline/poll':
# Plex web does polling if connected to PKC via Companion
# Only reply if there is indeed something playing
@ -188,7 +187,7 @@ class MyHandler(BaseHTTPRequestHandler):
code=500)
elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False))
clientinfo.getXArgsDeviceInfo(include_token=False))
protocol = params.get('protocol')
host = self.client_address[0]
port = params.get('port')
@ -201,14 +200,14 @@ class MyHandler(BaseHTTPRequestHandler):
command_id)
elif "/unsubscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False))
clientinfo.getXArgsDeviceInfo(include_token=False))
uuid = self.headers.get('X-Plex-Client-Identifier') \
or self.client_address[0]
sub_mgr.remove_subscriber(uuid)
else:
# Throw it to companion.py
process_command(request_path, params)
self.response('', getXArgsDeviceInfo(include_token=False))
companion.process_command(request_path, params)
self.response('', clientinfo.getXArgsDeviceInfo(include_token=False))
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):

View file

@ -25,16 +25,15 @@ import logging
import socket
import threading
import time
from xbmc import sleep
import downloadutils
from utils import window, settings, dialog, language
import variables as v
from ..downloadutils import DownloadUtils as DU
from .. import utils
from .. import variables as v
###############################################################################
log = logging.getLogger("PLEX."+__name__)
log = logging.getLogger('PLEX.plexgdm')
###############################################################################
@ -49,7 +48,7 @@ class plexgdm:
self._multicast_address = '239.0.0.250'
self.discover_group = (self._multicast_address, 32414)
self.client_register_group = (self._multicast_address, 32413)
self.client_update_port = int(settings('companionUpdatePort'))
self.client_update_port = int(utils.settings('companionUpdatePort'))
self.server_list = []
self.discovery_interval = 120
@ -58,7 +57,7 @@ class plexgdm:
self._registration_is_running = False
self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl
self.download = DU().downloadUrl
def clientDetails(self):
self.client_data = (
@ -120,14 +119,15 @@ class plexgdm:
log.error("Unable to bind to port [%s] - Plex Companion will not "
"be registered. Change the Plex Companion update port!"
% self.client_update_port)
if settings('companion_show_gdm_port_warning') == 'true':
if dialog('yesno',
language(29999),
'Port %s' % self.client_update_port,
language(39079),
yeslabel=language(30013), # Never show again
nolabel=language(30012)): # OK
settings('companion_show_gdm_port_warning', value='false')
if utils.settings('companion_show_gdm_port_warning') == 'true':
if utils.dialog('yesno',
utils.lang(29999),
'Port %s' % self.client_update_port,
utils.lang(39079),
yeslabel=utils.lang(30013), # Never show again
nolabel=utils.lang(30012)): # OK
utils.settings('companion_show_gdm_port_warning',
value='false')
from xbmc import executebuiltin
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
@ -189,7 +189,7 @@ class plexgdm:
log.info("Server list is empty. Unable to check")
return False
for server in self.server_list:
if server['uuid'] == window('plex_machineIdentifier'):
if server['uuid'] == utils.window('plex_machineIdentifier'):
media_server = server['server']
media_port = server['port']
scheme = server['protocol']
@ -223,7 +223,7 @@ class plexgdm:
return self.server_list
def discover(self):
currServer = window('pms_server')
currServer = utils.window('pms_server')
if not currServer:
return
currServerProt, currServerIP, currServerPort = \
@ -240,9 +240,9 @@ class plexgdm:
'owned': '1',
'role': 'master',
'server': currServerIP,
'serverName': window('plex_servername'),
'serverName': utils.window('plex_servername'),
'updated': int(time.time()),
'uuid': window('plex_machineIdentifier'),
'uuid': utils.window('plex_machineIdentifier'),
'version': 'irrelevant'
}]
@ -305,5 +305,5 @@ class plexgdm:
def start_all(self, daemon=False):
self.start_discovery(daemon)
if settings('plexCompanion') == 'true':
if utils.settings('plexCompanion') == 'true':
self.start_registration(daemon)

View file

@ -3,22 +3,17 @@ Manages getting playstate from Kodi and sending it to the PMS as well as
subscribed Plex Companion clients.
"""
from logging import getLogger
from threading import Thread, RLock
from threading import Thread
from downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis, LockFunction
import state
import variables as v
import json_rpc as js
import playqueue as PQ
from ..downloadutils import DownloadUtils as DU
from .. import utils
from .. import state
from .. import variables as v
from .. import json_rpc as js
from .. import playqueue as PQ
###############################################################################
LOG = getLogger("PLEX." + __name__)
# Need to lock all methods and functions messing with subscribers or state
LOCK = RLock()
LOCKER = LockFunction(LOCK)
LOG = getLogger('PLEX.subscribers')
###############################################################################
# What is Companion controllable?
@ -150,7 +145,6 @@ class SubscriptionMgr(object):
position = info['position']
return position
@LOCKER.lockthis
def msg(self, players):
"""
Returns a timeline xml as str
@ -190,94 +184,98 @@ class SubscriptionMgr(object):
return answ
def _timeline_dict(self, player, ptype):
playerid = player['playerid']
info = state.PLAYER_STATES[playerid]
playqueue = PQ.PLAYQUEUES[playerid]
position = self._get_correct_position(info, playqueue)
try:
item = playqueue.items[position]
except IndexError:
# E.g. for direct path playback for single item
return {
with state.LOCK_PLAYQUEUES:
playerid = player['playerid']
info = state.PLAYER_STATES[playerid]
playqueue = PQ.PLAYQUEUES[playerid]
position = self._get_correct_position(info, playqueue)
try:
item = playqueue.items[position]
except IndexError:
# E.g. for direct path playback for single item
return {
'controllable': CONTROLLABLE[ptype],
'type': ptype,
'state': 'stopped'
}
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
v.PLEX_PLAYLIST_TYPE_PHOTO):
self.location = 'fullScreenVideo'
self.stop_sent_to_web = False
pbmc_server = utils.window('pms_server')
if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
status = 'paused' if int(info['speed']) == 0 else 'playing'
duration = utils.kodi_time_to_millis(info['totaltime'])
shuffle = '1' if info['shuffled'] else '0'
mute = '1' if info['muted'] is True else '0'
answ = {
'controllable': CONTROLLABLE[ptype],
'protocol': self.protocol,
'address': self.server,
'port': self.port,
'machineIdentifier': utils.window('plex_machineIdentifier'),
'state': status,
'type': ptype,
'state': 'stopped'
'itemType': ptype,
'time': utils.kodi_time_to_millis(info['time']),
'duration': duration,
'seekRange': '0-%s' % duration,
'shuffle': shuffle,
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
'volume': info['volume'],
'mute': mute,
'mediaIndex': 0, # Still to implement from here
'partIndex': 0,
'partCount': 1,
'providerIdentifier': 'com.plexapp.plugins.library',
}
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO):
self.location = 'fullScreenVideo'
self.stop_sent_to_web = False
pbmc_server = window('pms_server')
if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
status = 'paused' if int(info['speed']) == 0 else 'playing'
duration = kodi_time_to_millis(info['totaltime'])
shuffle = '1' if info['shuffled'] else '0'
mute = '1' if info['muted'] is True else '0'
answ = {
'controllable': CONTROLLABLE[ptype],
'protocol': self.protocol,
'address': self.server,
'port': self.port,
'machineIdentifier': window('plex_machineIdentifier'),
'state': status,
'type': ptype,
'itemType': ptype,
'time': kodi_time_to_millis(info['time']),
'duration': duration,
'seekRange': '0-%s' % duration,
'shuffle': shuffle,
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
'volume': info['volume'],
'mute': mute,
'mediaIndex': 0, # Still to implement from here
'partIndex':0,
'partCount': 1,
'providerIdentifier': 'com.plexapp.plugins.library',
}
# Get the plex id from the PKC playqueue not info, as Kodi jumps to next
# playqueue element way BEFORE kodi monitor onplayback is called
if item.plex_id:
answ['key'] = '/library/metadata/%s' % item.plex_id
answ['ratingKey'] = item.plex_id
# PlayQueue stuff
if info['container_key']:
answ['containerKey'] = info['container_key']
if (info['container_key'] is not None and
info['container_key'].startswith('/playQueues')):
answ['playQueueID'] = playqueue.id
answ['playQueueVersion'] = playqueue.version
answ['playQueueItemID'] = item.id
if playqueue.items[position].guid:
answ['guid'] = item.guid
# Temp. token set?
if state.PLEX_TRANSIENT_TOKEN:
answ['token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id:
answ['audioStreamID'] = strm_id
else:
LOG.error('We could not select a Plex audiostream')
strm_id = self._plex_stream_index(playerid, 'video')
if strm_id:
answ['videoStreamID'] = strm_id
else:
LOG.error('We could not select a Plex videostream')
if info['subtitleenabled']:
try:
strm_id = self._plex_stream_index(playerid, 'subtitle')
except KeyError:
# subtitleenabled can be True while currentsubtitle can
# still be {}
strm_id = None
if strm_id is not None:
# If None, then the subtitle is only present on Kodi side
answ['subtitleStreamID'] = strm_id
return answ
# Get the plex id from the PKC playqueue not info, as Kodi jumps to
# next playqueue element way BEFORE kodi monitor onplayback is
# called
if item.plex_id:
answ['key'] = '/library/metadata/%s' % item.plex_id
answ['ratingKey'] = item.plex_id
# PlayQueue stuff
if info['container_key']:
answ['containerKey'] = info['container_key']
if (info['container_key'] is not None and
info['container_key'].startswith('/playQueues')):
answ['playQueueID'] = playqueue.id
answ['playQueueVersion'] = playqueue.version
answ['playQueueItemID'] = item.id
if playqueue.items[position].guid:
answ['guid'] = item.guid
# Temp. token set?
if state.PLEX_TRANSIENT_TOKEN:
answ['token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id:
answ['audioStreamID'] = strm_id
else:
LOG.error('We could not select a Plex audiostream')
strm_id = self._plex_stream_index(playerid, 'video')
if strm_id:
answ['videoStreamID'] = strm_id
else:
LOG.error('We could not select a Plex videostream')
if info['subtitleenabled']:
try:
strm_id = self._plex_stream_index(playerid, 'subtitle')
except KeyError:
# subtitleenabled can be True while currentsubtitle can
# still be {}
strm_id = None
if strm_id is not None:
# If None, then the subtitle is only present on Kodi
# side
answ['subtitleStreamID'] = strm_id
return answ
def signal_stop(self):
"""
@ -302,14 +300,14 @@ class SubscriptionMgr(object):
return playqueue.items[position].plex_stream_index(
info[STREAM_DETAILS[stream_type]]['index'], stream_type)
@LOCKER.lockthis
def update_command_id(self, uuid, command_id):
"""
Updates the Plex Companien client with the machine identifier uuid with
command_id
"""
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
with state.LOCK_SUBSCRIBER:
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
def _playqueue_init_done(self, players):
"""
@ -320,8 +318,6 @@ class SubscriptionMgr(object):
for player in players.values():
info = state.PLAYER_STATES[player['playerid']]
playqueue = PQ.PLAYQUEUES[player['playerid']]
LOG.debug('playqueue is: %s', playqueue)
LOG.debug('info is: %s', info)
position = self._get_correct_position(info, playqueue)
try:
item = playqueue.items[position]
@ -334,34 +330,32 @@ class SubscriptionMgr(object):
return False
return True
@LOCKER.lockthis
def notify(self):
"""
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
self._cleanup()
# Get all the active/playing Kodi players (video, audio, pictures)
players = js.get_players()
# Update the PKC info with what's playing on the Kodi side
for player in players.values():
update_player_info(player['playerid'])
# Check whether we can use the CURRENT info or whether PKC is still
# initializing
if self._playqueue_init_done(players) is False:
LOG.debug('PKC playqueue is still initializing - skipping update')
return
self._notify_server(players)
if self.subscribers:
msg = self.msg(players)
for subscriber in self.subscribers.values():
subscriber.send_update(msg)
self.lastplayers = players
with state.LOCK_SUBSCRIBER:
self._cleanup()
# Get all the active/playing Kodi players (video, audio, pictures)
players = js.get_players()
# Update the PKC info with what's playing on the Kodi side
for player in players.values():
update_player_info(player['playerid'])
# Check whether we can use the CURRENT info or whether PKC is still
# initializing
if self._playqueue_init_done(players) is False:
LOG.debug('PKC playqueue is still initializing - skip update')
return
self._notify_server(players)
if self.subscribers:
msg = self.msg(players)
for subscriber in self.subscribers.values():
subscriber.send_update(msg)
self.lastplayers = players
def _notify_server(self, players):
for typus, player in players.iteritems():
LOG.debug('player is %s', player)
LOG.debug('typus is %s', typus)
self._send_pms_notification(
player['playerid'], self._get_pms_params(player['playerid']))
try:
@ -386,8 +380,8 @@ class SubscriptionMgr(object):
'state': status,
'ratingKey': item.plex_id,
'key': '/library/metadata/%s' % item.plex_id,
'time': kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime'])
'time': utils.kodi_time_to_millis(info['time']),
'duration': utils.kodi_time_to_millis(info['totaltime'])
}
if info['container_key'] is not None:
# params['containerKey'] = info['container_key']
@ -419,7 +413,6 @@ class SubscriptionMgr(object):
LOG.debug("Sent server notification with parameters: %s to %s",
xargs, url)
@LOCKER.lockthis
def add_subscriber(self, protocol, host, port, uuid, command_id):
"""
Adds a new Plex Companion subscriber to PKC.
@ -431,20 +424,21 @@ class SubscriptionMgr(object):
command_id,
self,
self.request_mgr)
self.subscribers[subscriber.uuid] = subscriber
with state.LOCK_SUBSCRIBER:
self.subscribers[subscriber.uuid] = subscriber
return subscriber
@LOCKER.lockthis
def remove_subscriber(self, uuid):
"""
Removes a connected Plex Companion subscriber with machine identifier
uuid from PKC notifications.
(Calls the cleanup() method of the subscriber)
"""
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
with state.LOCK_SUBSCRIBER:
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
def _cleanup(self):
for subscriber in self.subscribers.values():

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
###############################################################################
from utils import kodi_sql
import variables as v
from . import utils
from . import variables as v
###############################################################################
@ -17,7 +15,7 @@ class Get_Plex_DB():
and the db gets closed
"""
def __enter__(self):
self.plexconn = kodi_sql('plex')
self.plexconn = utils.kodi_sql('plex')
return Plex_DB_Functions(self.plexconn.cursor())
def __exit__(self, type, value, traceback):

View file

@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import sys
import xbmc
from . import utils
from . import userclient
from . import initialsetup
from . import kodimonitor
from . import librarysync
from . import websocket_client
from . import plex_companion
from . import plex_functions as PF
from . import command_pipeline
from . import playback_starter
from . import playqueue
from . import artwork
from . import variables as v
from . import state
from . import loghandler
###############################################################################
loghandler.config()
LOG = logging.getLogger("PLEX.service_entry")
###############################################################################
class Service():
server_online = True
warn_auth = True
user = None
ws = None
library = None
plexcompanion = None
user_running = False
ws_running = False
alexa_running = False
library_running = False
plexcompanion_running = False
kodimonitor_running = False
playback_starter_running = False
image_cache_thread_running = False
def __init__(self):
# Initial logging
LOG.info("======== START %s ========", v.ADDON_NAME)
LOG.info("Platform: %s", v.PLATFORM)
LOG.info("KODI Version: %s", v.KODILONGVERSION)
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
LOG.info("PKC Direct Paths: %s",
utils.settings('useDirectPaths') == '1')
LOG.info("Number of sync threads: %s",
utils.settings('syncThreadNumber'))
LOG.info('Playlist m3u encoding: %s', v.M3U_ENCODING)
LOG.info("Full sys.argv received: %s", sys.argv)
self.monitor = xbmc.Monitor()
# Load/Reset PKC entirely - important for user/Kodi profile switch
initialsetup.reload_pkc()
def _stop_pkc(self):
"""
Kodi's abortRequested is really unreliable :-(
"""
return self.monitor.abortRequested() or state.STOP_PKC
def ServiceEntryPoint(self):
# Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once.
_stop_pkc = self._stop_pkc
monitor = self.monitor
# Server auto-detect
initialsetup.InitialSetup().setup()
# Detect playback start early on
self.command_pipeline = command_pipeline.Monitor_Window()
self.command_pipeline.start()
# Initialize important threads, handing over self for callback purposes
self.user = userclient.UserClient()
self.ws = websocket_client.PMS_Websocket()
self.alexa = websocket_client.Alexa_Websocket()
self.library = librarysync.LibrarySync()
self.plexcompanion = plex_companion.PlexCompanion()
self.specialmonitor = kodimonitor.SpecialMonitor()
self.playback_starter = playback_starter.PlaybackStarter()
self.playqueue = playqueue.PlayqueueMonitor()
if utils.settings('enableTextureCache') == "true":
self.image_cache_thread = artwork.Image_Cache_Thread()
welcome_msg = True
counter = 0
while not _stop_pkc():
if utils.window('plex_kodiProfile') != v.KODI_PROFILE:
# Profile change happened, terminate this thread and others
LOG.info("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread.",
v.KODI_PROFILE, utils.window('plex_kodiProfile'))
break
# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
# 3. User has access to the server
if utils.window('plex_online') == "true":
# Plex server is online
# Verify if user is set and has access to the server
if (self.user.user is not None) and self.user.has_access:
if not self.kodimonitor_running:
# Start up events
self.warn_auth = True
if welcome_msg is True:
# Reset authentication warnings
welcome_msg = False
utils.dialog('notification',
utils.lang(29999),
"%s %s" % (utils.lang(33000),
self.user.user),
icon='{plex}',
time=2000,
sound=False)
# Start monitoring kodi events
self.kodimonitor_running = kodimonitor.KodiMonitor()
self.specialmonitor.start()
# Start the Websocket Client
if not self.ws_running:
self.ws_running = True
self.ws.start()
# Start the Alexa thread
if (not self.alexa_running and
utils.settings('enable_alexa') == 'true'):
self.alexa_running = True
self.alexa.start()
# Start the syncing thread
if not self.library_running:
self.library_running = True
self.library.start()
# Start the Plex Companion thread
if not self.plexcompanion_running:
self.plexcompanion_running = True
self.plexcompanion.start()
if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
self.playqueue.start()
if (not self.image_cache_thread_running and
utils.settings('enableTextureCache') == "true"):
self.image_cache_thread_running = True
self.image_cache_thread.start()
else:
if (self.user.user is None) and self.warn_auth:
# Alert user is not authenticated and suppress future
# warning
self.warn_auth = False
LOG.warn("Not authenticated yet.")
# User access is restricted.
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
while self.user.has_access is False:
# Verify access with an API call
self.user.check_access()
if utils.window('plex_online') != "true":
# Server went offline
break
if monitor.waitForAbort(3):
# Abort was requested while waiting. We should exit
break
else:
# Wait until Plex server is online
# or Kodi is shut down.
while not self._stop_pkc():
server = self.user.get_server()
if server is False:
# No server info set in add-on settings
pass
elif PF.check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online:
self.server_online = False
utils.window('plex_online', value="false")
# Suspend threads
state.SUSPEND_LIBRARY_THREAD = True
LOG.error("Plex Media Server went offline")
if utils.settings('show_pms_offline') == 'true':
utils.dialog('notification',
utils.lang(33001),
"%s %s" % (utils.lang(29999),
utils.lang(33002)),
icon='{plex}',
sound=False)
counter += 1
# Periodically check if the IP changed, e.g. per minute
if counter > 20:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.pick_pms()
if tmp is not None:
setup.write_pms_to_settings(tmp)
else:
# Server is online
counter = 0
if not self.server_online:
# Server was offline when Kodi started.
# Wait for server to be fully established.
if monitor.waitForAbort(5):
# Abort was requested while waiting.
break
self.server_online = True
# Alert the user that server is online.
if (welcome_msg is False and
utils.settings('show_pms_offline') == 'true'):
utils.dialog('notification',
utils.lang(29999),
utils.lang(33003),
icon='{plex}',
time=5000,
sound=False)
LOG.info("Server %s is online and ready.", server)
utils.window('plex_online', value="true")
if state.AUTHENTICATED:
# Server got offline when we were authenticated.
# Hence resume threads
state.SUSPEND_LIBRARY_THREAD = False
# Start the userclient thread
if not self.user_running:
self.user_running = True
self.user.start()
break
if monitor.waitForAbort(3):
# Abort was requested while waiting.
break
if monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit
break
# Terminating PlexKodiConnect
# Tell all threads to terminate (e.g. several lib sync threads)
state.STOP_PKC = True
utils.window('plex_service_started', clear=True)
LOG.info("======== STOP %s ========", v.ADDON_NAME)
def start():
# Safety net - Kody starts PKC twice upon first installation!
if utils.window('plex_service_started') == 'true':
EXIT = True
else:
utils.window('plex_service_started', value='true')
EXIT = False
# Delay option
DELAY = int(utils.settings('startupDelay'))
LOG.info("Delaying Plex startup by: %s sec...", DELAY)
if EXIT:
LOG.error('PKC service.py already started - exiting this instance')
elif DELAY and xbmc.Monitor().waitForAbort(DELAY):
# Start the service
LOG.info("Abort requested while waiting. PKC not started.")
else:
Service().ServiceEntryPoint()

View file

@ -1,5 +1,17 @@
# -*- coding: utf-8 -*-
# THREAD SAFE
from threading import Lock, RLock
# LOCKS
####################
# Need to lock all methods and functions messing with Plex Companion subscribers
LOCK_SUBSCRIBER = RLock()
# Need to lock everything messing with Kodi/PKC playqueues
LOCK_PLAYQUEUES = RLock()
# Necessary to temporarily hold back librarysync/websocket listener when doing
# a full sync
LOCK_PLAYLISTS = Lock()
# Quit PKC
STOP_PKC = False

View file

@ -3,24 +3,24 @@
from logging import getLogger
from threading import Thread
from xbmc import sleep, executebuiltin, translatePath
import xbmcaddon
from xbmcvfs import exists
from xbmc import sleep, executebuiltin
from utils import window, settings, language as lang, thread_methods, dialog
from downloadutils import DownloadUtils as DU
import plex_tv
import PlexFunctions as PF
import state
from .downloadutils import DownloadUtils as DU
from . import utils
from . import path_ops
from . import plex_tv
from . import plex_functions as PF
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.userclient')
###############################################################################
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
@utils.thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
class UserClient(Thread):
"""
Manage Plex users
@ -44,7 +44,6 @@ class UserClient(Thread):
self.ssl = None
self.sslcert = None
self.addon = xbmcaddon.Addon()
self.do_utils = None
Thread.__init__(self)
@ -54,11 +53,11 @@ class UserClient(Thread):
Get the current PMS' URL
"""
# Original host
self.server_name = settings('plex_servername')
https = settings('https') == "true"
host = settings('ipaddress')
port = settings('port')
self.machine_identifier = settings('plex_machineIdentifier')
self.server_name = utils.settings('plex_servername')
https = utils.settings('https') == "true"
host = utils.settings('ipaddress')
port = utils.settings('port')
self.machine_identifier = utils.settings('plex_machineIdentifier')
if not host:
LOG.debug("No server information saved.")
return False
@ -74,7 +73,8 @@ class UserClient(Thread):
self.machine_identifier = PF.GetMachineIdentifier(server)
if not self.machine_identifier:
self.machine_identifier = ''
settings('plex_machineIdentifier', value=self.machine_identifier)
utils.settings('plex_machineIdentifier',
value=self.machine_identifier)
LOG.debug('Returning active server: %s', server)
return server
@ -84,15 +84,15 @@ class UserClient(Thread):
Do we need to verify the SSL certificate? Return None if that is the
case, else False
"""
return None if settings('sslverify') == 'true' else False
return None if utils.settings('sslverify') == 'true' else False
@staticmethod
def get_ssl_certificate():
"""
Client side certificate
"""
return None if settings('sslcert') == 'None' \
else settings('sslcert')
return None if utils.settings('sslcert') == 'None' \
else utils.settings('sslcert')
def set_user_prefs(self):
"""
@ -103,7 +103,7 @@ class UserClient(Thread):
if self.token:
url = PF.GetUserArtworkURL(self.user)
if url:
window('PlexUserImage', value=url)
utils.window('PlexUserImage', value=url)
@staticmethod
def check_access():
@ -141,29 +141,32 @@ class UserClient(Thread):
state.PLEX_USER_ID = user_id or None
state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '')
window('pms_token', value=usertoken)
utils.window('pms_token', value=usertoken)
state.PMS_TOKEN = usertoken
# This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv
window('plex_token', value=settings('plexToken'))
state.PLEX_TOKEN = settings('plexToken') or None
window('plex_restricteduser', value=settings('plex_restricteduser'))
utils.window('plex_token', value=utils.settings('plexToken'))
state.PLEX_TOKEN = utils.settings('plexToken') or None
utils.window('plex_restricteduser',
value=utils.settings('plex_restricteduser'))
state.RESTRICTED_USER = True \
if settings('plex_restricteduser') == 'true' else False
window('pms_server', value=self.server)
window('plex_machineIdentifier', value=self.machine_identifier)
window('plex_servername', value=self.server_name)
window('plex_authenticated', value='true')
if utils.settings('plex_restricteduser') == 'true' else False
utils.window('pms_server', value=self.server)
utils.window('plex_machineIdentifier', value=self.machine_identifier)
utils.window('plex_servername', value=self.server_name)
utils.window('plex_authenticated', value='true')
state.AUTHENTICATED = True
window('useDirectPaths', value='true'
if settings('useDirectPaths') == "1" else 'false')
state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \
utils.window('useDirectPaths',
value='true' if utils.settings('useDirectPaths') == "1"
else 'false')
state.DIRECT_PATHS = True if utils.settings('useDirectPaths') == "1" \
else False
state.INDICATE_MEDIA_VERSIONS = True \
if settings('indicate_media_versions') == "true" else False
window('plex_force_transcode_pix', value='true'
if settings('force_transcode_pix') == "1" else 'false')
if utils.settings('indicate_media_versions') == "true" else False
utils.window('plex_force_transcode_pix',
value='true' if utils.settings('force_transcode_pix') == "1"
else 'false')
# Start DownloadUtils session
self.do_utils = DU()
@ -173,9 +176,9 @@ class UserClient(Thread):
self.set_user_prefs()
# Writing values to settings file
settings('username', value=username)
settings('userid', value=user_id)
settings('accessToken', value=usertoken)
utils.settings('username', value=username)
utils.settings('userid', value=user_id)
utils.settings('accessToken', value=usertoken)
return True
def authenticate(self):
@ -188,16 +191,13 @@ class UserClient(Thread):
if self.retry >= 2:
LOG.error("Too many retries to login.")
state.PMS_STATUS = 'Stop'
dialog('ok', lang(33001), lang(39023))
utils.dialog('ok', utils.lang(33001), utils.lang(39023))
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
return False
# Get /profile/addon_data
addondir = translatePath(self.addon.getAddonInfo('profile'))
# If there's no settings.xml
if not exists("%ssettings.xml" % addondir):
if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE):
LOG.error("Error, no settings.xml found.")
self.auth = False
return False
@ -209,10 +209,10 @@ class UserClient(Thread):
return False
# If there is a username in the settings, try authenticating
username = settings('username')
userId = settings('userid')
usertoken = settings('accessToken')
enforceLogin = settings('enforceUserLogin')
username = utils.settings('username')
userId = utils.settings('userid')
usertoken = utils.settings('accessToken')
enforceLogin = utils.settings('enforceUserLogin')
# Found a user in the settings, try to authenticate
if username and enforceLogin == 'false':
LOG.debug('Trying to authenticate with old settings')
@ -225,15 +225,15 @@ class UserClient(Thread):
return True
elif answ == 401:
LOG.error("User token no longer valid. Sign user out")
settings('username', value='')
settings('userid', value='')
settings('accessToken', value='')
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('accessToken', value='')
else:
LOG.debug("Could not yet authenticate user")
return False
# Could not use settings - try to get Plex user list from plex.tv
plextoken = settings('plexToken')
plextoken = utils.settings('plexToken')
if plextoken:
LOG.info("Trying to connect to plex.tv to get a user list")
userInfo = plex_tv.choose_home_user(plextoken)
@ -268,24 +268,24 @@ class UserClient(Thread):
self.do_utils.stopSession()
except AttributeError:
pass
window('plex_authenticated', clear=True)
utils.window('plex_authenticated', clear=True)
state.AUTHENTICATED = False
window('pms_token', clear=True)
utils.window('pms_token', clear=True)
state.PLEX_TOKEN = None
state.PLEX_TRANSIENT_TOKEN = None
state.PMS_TOKEN = None
window('plex_token', clear=True)
window('pms_server', clear=True)
window('plex_machineIdentifier', clear=True)
window('plex_servername', clear=True)
utils.window('plex_token', clear=True)
utils.window('pms_server', clear=True)
utils.window('plex_machineIdentifier', clear=True)
utils.window('plex_servername', clear=True)
state.PLEX_USER_ID = None
state.PLEX_USERNAME = None
window('plex_restricteduser', clear=True)
utils.window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False
settings('username', value='')
settings('userid', value='')
settings('accessToken', value='')
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('accessToken', value='')
self.token = None
self.auth = True
@ -313,7 +313,7 @@ class UserClient(Thread):
elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token
state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth')
utils.window('plex_serverStatus', value='Auth')
self.reset_client()
sleep(3000)
@ -330,7 +330,7 @@ class UserClient(Thread):
LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True)
utils.window('plex_serverStatus', clear=True)
state.PMS_STATUS = False
if not self.auth and (self.user is None):

View file

@ -4,7 +4,6 @@ Various functions and decorators for PKC
"""
###############################################################################
from logging import getLogger
import os
from cProfile import Profile
from pstats import Stats
from sqlite3 import connect, OperationalError
@ -14,31 +13,38 @@ from time import localtime, strftime
from unicodedata import normalize
import xml.etree.ElementTree as etree
from functools import wraps, partial
from shutil import rmtree
from urllib import quote_plus
import hashlib
import re
import unicodedata
import xbmc
import xbmcaddon
import xbmcgui
from xbmcvfs import exists, delete
import variables as v
import state
from . import path_ops
from . import variables as v
from . import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.utils')
WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
EPOCH = datetime.utcfromtimestamp(0)
# Grab Plex id from '...plex_id=XXXX....'
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
# Return the numbers at the end of an url like '.../.../XXXX'
REGEX_END_DIGITS = re.compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_DIRECT = re.compile(r'''\.plex\.direct:\d+$''')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
# Plex API
REGEX_IMDB = re.compile(r'''/(tt\d+)''')
REGEX_TVDB = re.compile(r'''thetvdb:\/\/(.+?)\?''')
# Plex music
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
@ -51,7 +57,7 @@ def reboot_kodi(message=None):
Set optional custom message
"""
message = message or language(33033)
message = message or lang(33033)
dialog('ok', heading='{plex}', line1=message)
xbmc.executebuiltin('RestartApp')
@ -106,31 +112,7 @@ def settings(setting, value=None):
return try_decode(addon.getSetting(setting))
def exists_dir(path):
"""
Safe way to check whether the directory path exists already (broken in Kodi
<17)
Feed with encoded string or unicode
"""
if v.KODIVERSION >= 17:
answ = exists(try_encode(path))
else:
dummyfile = os.path.join(try_decode(path), 'dummyfile.txt')
try:
with open(dummyfile, 'w') as filer:
filer.write('text')
except IOError:
# folder does not exist yet
answ = 0
else:
# Folder exists. Delete file again.
delete(try_encode(dummyfile))
answ = 1
return answ
def language(stringid):
def lang(stringid):
"""
Central string retrieval from strings.po
"""
@ -194,7 +176,7 @@ def dialog(typus, *args, **kwargs):
kwargs['option'] = types[kwargs['option']]
if 'heading' in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999))
lang(29999))
dia = xbmcgui.Dialog()
types = {
'yesno': dia.yesno,
@ -313,10 +295,6 @@ def valid_filename(text):
else:
# Linux
text = re.sub(r'/', '', text)
if not os.path.supports_unicode_filenames:
text = unicodedata.normalize('NFKD', text)
text = text.encode('ascii', 'ignore')
text = text.decode('ascii')
# Ensure that filename length is at most 255 chars (including 3 chars for
# filename extension and 1 dot to separate the extension)
text = text[:min(len(text), 251)]
@ -465,15 +443,15 @@ def wipe_database():
# Delete all synced playlists
for path in playlist_paths:
try:
os.remove(path)
path_ops.remove(path)
except (OSError, IOError):
pass
LOG.info("Resetting all cached artwork.")
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/")
if exists(path):
rmtree(try_decode(path), ignore_errors=True)
path = path_ops.translate_path("special://thumbnails/")
if path_ops.exists(path):
path_ops.rmtree(path, ignore_errors=True)
# remove all existing data from texture DB
connection = kodi_sql('texture')
cursor = connection.cursor()
@ -487,8 +465,8 @@ def wipe_database():
connection.commit()
cursor.close()
# Reset the artwork sync status in the PKC settings
settings('caching_artwork_count', value=language(39310))
settings('fanarttv_lookups', value=language(39310))
settings('caching_artwork_count', value=lang(39310))
settings('fanarttv_lookups', value=lang(39310))
# reset the install run flag
settings('SyncInstallRunDone', value="false")
@ -500,8 +478,8 @@ def reset(ask_user=True):
"""
# Are you sure you want to reset your local Kodi database?
if ask_user and not dialog('yesno',
heading='{plex} %s ' % language(30132),
line1=language(39600)):
heading='{plex} %s ' % lang(30132),
line1=lang(39600)):
return
# first stop any db sync
@ -513,8 +491,8 @@ def reset(ask_user=True):
if count == 0:
# Could not stop the database from running. Please try again later.
dialog('ok',
heading='{plex} %s' % language(30132),
line1=language(39601))
heading='{plex} %s' % lang(30132),
line1=lang(39601))
return
xbmc.sleep(1000)
@ -524,13 +502,11 @@ def reset(ask_user=True):
# Reset all PlexKodiConnect Addon settings? (this is usually NOT
# recommended and unnecessary!)
if ask_user and dialog('yesno',
heading='{plex} %s ' % language(30132),
line1=language(39603)):
heading='{plex} %s ' % lang(30132),
line1=lang(39603)):
# Delete the settings
addon = xbmcaddon.Addon()
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
LOG.info("Deleting: settings.xml")
os.remove("%ssettings.xml" % addondir)
path_ops.remove("%ssettings.xml" % v.ADDON_PROFILE)
reboot_kodi()
@ -592,29 +568,6 @@ def compare_version(current, minimum):
return curr_patch >= min_patch
def normalize_nodes(text):
"""
For video nodes
"""
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
text = text.replace("<", "")
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.replace('(', "")
text = text.replace(')', "")
text = text.strip()
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = try_encode(normalize('NFKD', unicode(text, 'utf-8')))
return text
def normalize_string(text):
"""
For theme media, do not modify unless modified in TV Tunes
@ -636,6 +589,28 @@ def normalize_string(text):
return text
def normalize_nodes(text):
"""
For video nodes. Returns unicode
"""
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
text = text.replace("<", "")
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.replace('(', "")
text = text.replace(')', "")
text = text.strip()
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = normalize('NFKD', unicode(text, 'utf-8'))
return text
def indent(elem, level=0):
"""
Prettifies xml trees. Pass the etree root in
@ -682,9 +657,9 @@ class XmlKodiSetting(object):
top_element=None):
self.filename = filename
if path is None:
self.path = os.path.join(v.KODI_PROFILE, filename)
self.path = path_ops.path.join(v.KODI_PROFILE, filename)
else:
self.path = os.path.join(path, filename)
self.path = path_ops.path.join(path, filename)
self.force_create = force_create
self.top_element = top_element
self.tree = None
@ -708,7 +683,7 @@ class XmlKodiSetting(object):
LOG.error('Error parsing %s', self.path)
# "Kodi cannot parse {0}. PKC will not function correctly. Please
# visit {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
dialog('ok', lang(29999), lang(39716).format(
self.filename,
'http://kodi.wiki'))
self.__exit__(etree.ParseError, None, None)
@ -849,7 +824,7 @@ def passwords_xml():
"""
To add network credentials to Kodi's password xml
"""
path = try_decode(xbmc.translatePath("special://userdata/"))
path = path_ops.translate_path('special://userdata/')
xmlpath = "%spasswords.xml" % path
try:
xmlparse = etree.parse(xmlpath)
@ -861,7 +836,7 @@ def passwords_xml():
LOG.error('Error parsing %s', xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
dialog('ok', lang(29999), lang(39716).format(
'passwords.xml', 'http://forum.kodi.tv/'))
return
else:
@ -970,7 +945,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
"""
Feed with tagname as unicode
"""
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
path = path_ops.translate_path("special://profile/playlists/video/")
if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
@ -979,15 +954,15 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory
if not exists(try_encode(path)):
if not path_ops.exists(path):
LOG.info("Creating directory: %s", path)
os.makedirs(path)
path_ops.makedirs(path)
# Only add the playlist if it doesn't already exists
if exists(try_encode(xsppath)):
if path_ops.exists(xsppath):
LOG.info('Path %s does exist', xsppath)
if delete:
os.remove(xsppath)
path_ops.remove(xsppath)
LOG.info("Successfully removed playlist: %s.", tagname)
return
@ -999,7 +974,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
'show': 'tvshows'
}
LOG.info("Writing playlist file to: %s", xsppath)
with open(xsppath, 'wb') as filer:
with open(path_ops.encode_path(xsppath), 'wb') as filer:
filer.write(try_encode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
@ -1017,21 +992,22 @@ def delete_playlists():
"""
Clean up the playlists
"""
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
for root, _, files in os.walk(path):
path = path_ops.translate_path('special://profile/playlists/video/')
for root, _, files in path_ops.walk(path):
for file in files:
if file.startswith('Plex'):
os.remove(os.path.join(root, file))
path_ops.remove(path_ops.path.join(root, file))
def delete_nodes():
"""
Clean up video nodes
"""
path = try_decode(xbmc.translatePath("special://profile/library/video/"))
for root, dirs, _ in os.walk(path):
path = path_ops.translate_path("special://profile/library/video/")
for root, dirs, _ in path_ops.walk(path):
for directory in dirs:
if directory.startswith('Plex-'):
rmtree(os.path.join(root, directory))
path_ops.rmtree(path_ops.path.join(root, directory))
break
@ -1044,7 +1020,7 @@ def generate_file_md5(path):
"""
m = hashlib.md5()
m.update(path.encode('utf-8'))
with open(path, 'rb') as f:
with open(path_ops.encode_path(path), 'rb') as f:
while True:
piece = f.read(32768)
if not piece:
@ -1192,33 +1168,3 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None):
# Return class to render this a decorator
return cls
class LockFunction(object):
"""
Decorator for class methods and functions to lock them with lock.
Initialize this class first
lockfunction = LockFunction(lock), where lock is a threading.Lock() object
To then lock a function or method:
@lockfunction.lockthis
def some_function(args, kwargs)
"""
def __init__(self, lock):
self.lock = lock
def lockthis(self, func):
"""
Use this method to actually lock a function or method
"""
@wraps(func)
def wrapper(*args, **kwargs):
"""
Wrapper construct
"""
with self.lock:
result = func(*args, **kwargs)
return result
return wrapper

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import os
import sys
import xbmc
from xbmcaddon import Addon
@ -35,7 +35,9 @@ _ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect'
ADDON_ID = 'plugin.video.plexkodiconnect'
ADDON_VERSION = _ADDON.getAddonInfo('version')
ADDON_PATH = try_decode(_ADDON.getAddonInfo('path'))
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
ADDON_PROFILE = try_decode(_ADDON.getAddonInfo('profile'))
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
@ -82,10 +84,6 @@ MIN_DB_VERSION = '2.0.27'
# Database paths
_DB_VIDEO_VERSION = {
13: 78, # Gotham
14: 90, # Helix
15: 93, # Isengard
16: 99, # Jarvis
17: 107, # Krypton
18: 109 # Leia
}
@ -93,10 +91,6 @@ DB_VIDEO_PATH = try_decode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = {
13: 46, # Gotham
14: 48, # Helix
15: 52, # Isengard
16: 56, # Jarvis
17: 60, # Krypton
18: 70 # Leia
}
@ -104,10 +98,6 @@ DB_MUSIC_PATH = try_decode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = {
13: 13, # Gotham
14: 13, # Helix
15: 13, # Isengard
16: 13, # Jarvis
17: 13, # Krypton
18: 13 # Leia
}
@ -123,6 +113,12 @@ EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
# Multiply Plex time by this factor to receive Kodi time
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
# We're "failing" playback with a video of 0 length
NULL_VIDEO = os.path.join(ADDON_FOLDER,
'addons',
ADDON_ID,
'empty_video.mp4')
# Playlist stuff
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
@ -513,3 +509,14 @@ PLEX_STREAM_TYPE_FROM_STREAM_TYPE = {
'audio': '2',
'subtitle': '3'
}
# Encoding to be used for our m3u playlist files
# m3u files do not have encoding specified by definition, unfortunately.
if PLATFORM == 'Windows':
M3U_ENCODING = 'mbcs'
else:
M3U_ENCODING = sys.getfilesystemencoding()
if (not M3U_ENCODING or
M3U_ENCODING == 'ascii' or
M3U_ENCODING == 'ANSI_X3.4-1968'):
M3U_ENCODING = 'utf-8'

View file

@ -1,24 +1,19 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from distutils import dir_util
import xml.etree.ElementTree as etree
from os import makedirs
import xbmc
from xbmcvfs import exists
from utils import window, settings, language as lang, try_encode, indent, \
normalize_nodes, exists_dir, try_decode
import variables as v
import state
from . import utils
from . import path_ops
from . import variables as v
from . import state
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger('PLEX.videonodes')
###############################################################################
# Paths are strings, NOT unicode!
class VideoNodes(object):
@ -30,21 +25,26 @@ class VideoNodes(object):
root = etree.Element('node', attrib={'order': "%s" % order})
elif roottype == 1:
# Filter
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
root = etree.Element('node',
attrib={'order': "%s" % order, 'type': "filter"})
etree.SubElement(root, 'match').text = "all"
# Add tag rule
rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
rule = etree.SubElement(root,
'rule',
attrib={'field': "tag", 'operator': "is"})
etree.SubElement(rule, 'value').text = tagname
else:
# Folder
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
root = etree.Element('node',
attrib={'order': "%s" % order, 'type': "folder"})
etree.SubElement(root, 'label').text = label
etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.plexkodiconnect/icon.png"
return root
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid,
delete=False):
# Plex: reassign mediatype due to Kodi inner workings
# How many items do we get at most?
limit = state.FETCH_PMS_ITEM_NUMBER
@ -63,33 +63,30 @@ class VideoNodes(object):
dirname = viewid
# Returns strings
path = try_decode(xbmc.translatePath(
"special://profile/library/video/"))
nodepath = try_decode(xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname))
path = path_ops.translate_path('special://profile/library/video/')
nodepath = path_ops.translate_path(
'special://profile/library/video/Plex-%s/' % dirname)
if delete:
if exists_dir(nodepath):
from shutil import rmtree
rmtree(nodepath)
log.info("Sucessfully removed videonode: %s." % tagname)
if path_ops.exists(nodepath):
path_ops.rmtree(nodepath)
LOG.info("Sucessfully removed videonode: %s." % tagname)
return
# Verify the video directory
if not exists_dir(path):
dir_util.copy_tree(
src=try_decode(
xbmc.translatePath("special://xbmc/system/library/video")),
dst=try_decode(
xbmc.translatePath("special://profile/library/video")),
if not path_ops.exists(path):
path_ops.copy_tree(
src=path_ops.translate_path(
'special://xbmc/system/library/video'),
dst=path_ops.translate_path('special://profile/library/video'),
preserve_mode=0) # do not copy permission bits!
# Create the node directory
if mediatype != "photos":
if not exists_dir(nodepath):
if not path_ops.exists(nodepath):
# folder does not exist yet
log.debug('Creating folder %s' % nodepath)
makedirs(nodepath)
LOG.debug('Creating folder %s' % nodepath)
path_ops.makedirs(nodepath)
# Create index entry
nodeXML = "%sindex.xml" % nodepath
@ -97,13 +94,13 @@ class VideoNodes(object):
path = "library://video/Plex-%s/" % dirname
for i in range(1, indexnumber):
# Verify to make sure we don't create duplicates
if window('Plex.nodes.%s.index' % i) == path:
if utils.window('Plex.nodes.%s.index' % i) == path:
return
if mediatype == "photos":
path = "plugin://plugin.video.plexkodiconnect?mode=browseplex&key=/library/sections/%s&id=%s" % (viewid, viewid)
window('Plex.nodes.%s.index' % indexnumber, value=path)
utils.window('Plex.nodes.%s.index' % indexnumber, value=path)
# Root
if not mediatype == "photos":
@ -119,7 +116,7 @@ class VideoNodes(object):
tagname=tagname,
roottype=0)
try:
indent(root)
utils.indent(root)
except:
pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
@ -222,14 +219,15 @@ class VideoNodes(object):
# Get label
stringid = nodes[node]
if node != "1":
label = lang(stringid)
label = utils.lang(stringid)
if not label:
label = xbmc.getLocalizedString(stringid)
else:
label = stringid
# Set window properties
if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
if ((mediatype == "homevideos" or mediatype == "photos") and
nodetype == "all"):
# Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s"
% (viewid, mediatype))
@ -278,34 +276,39 @@ class VideoNodes(object):
templabel = label
embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=templabel)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=mediatype)
utils.window('%s.title' % embynode, value=templabel)
utils.window('%s.path' % embynode, value=windowpath)
utils.window('%s.content' % embynode, value=path)
utils.window('%s.type' % embynode, value=mediatype)
else:
embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype)
window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
utils.window('%s.title' % embynode, value=label)
utils.window('%s.path' % embynode, value=windowpath)
utils.window('%s.content' % embynode, value=path)
if mediatype == "photos":
# For photos, we do not create a node in videos but we do want the window props
# to be created.
# To do: add our photos nodes to kodi picture sources somehow
# For photos, we do not create a node in videos but we do want
# the window props to be created. To do: add our photos nodes to
# kodi picture sources somehow
continue
if exists(try_encode(nodeXML)):
if path_ops.exists(nodeXML):
# Don't recreate xml if already exists
continue
# Create the root
if (nodetype in ("nextepisodes", "ondeck", 'recentepisodes', 'browsefiles') or mediatype == "homevideos"):
# Folder type with plugin path
root = self.commonRoot(order=sortorder[node], label=label, tagname=tagname, roottype=2)
root = self.commonRoot(order=sortorder[node],
label=label,
tagname=tagname,
roottype=2)
etree.SubElement(root, 'path').text = path
etree.SubElement(root, 'content').text = "episodes"
else:
root = self.commonRoot(order=sortorder[node], label=label, tagname=tagname)
root = self.commonRoot(order=sortorder[node],
label=label,
tagname=tagname)
if nodetype in ('recentepisodes', 'inprogressepisodes'):
etree.SubElement(root, 'content').text = "episodes"
else:
@ -313,20 +316,24 @@ class VideoNodes(object):
# Elements per nodetype
if nodetype == "all":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
elif nodetype == "recent":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root,
'order',
{'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit
if settings('MovieShowWatched') == 'false':
if utils.settings('MovieShowWatched') == 'false':
rule = etree.SubElement(root,
'rule',
{'field': "playcount",
'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogress":
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
etree.SubElement(root,
'rule',
{'field': "inprogress", 'operator': "true"})
etree.SubElement(root, 'limit').text = limit
etree.SubElement(
root,
@ -335,55 +342,67 @@ class VideoNodes(object):
).text = 'lastplayed'
elif nodetype == "genres":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'group').text = "genres"
elif nodetype == "unwatched":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
rule = etree.SubElement(root,
"rule",
{'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "sets":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'group').text = "tags"
elif nodetype == "random":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "random"
etree.SubElement(root, 'limit').text = limit
elif nodetype == "recommended":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
etree.SubElement(root,
'order',
{'direction': "descending"}).text = "rating"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
rule = etree.SubElement(root,
'rule',
{'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
rule2 = etree.SubElement(root, 'rule',
attrib={'field': "rating", 'operator': "greaterthan"})
rule2 = etree.SubElement(root,
'rule',
attrib={'field': "rating", 'operator': "greaterthan"})
etree.SubElement(rule2, 'value').text = "7"
elif nodetype == "recentepisodes":
# Kodi Isengard, Jarvis
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root,
'order',
{'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
rule = etree.SubElement(root,
'rule',
{'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogressepisodes":
# Kodi Isengard, Jarvis
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule',
attrib={'field': "inprogress", 'operator':"true"})
rule = etree.SubElement(root,
'rule',
attrib={'field': "inprogress", 'operator':"true"})
try:
indent(root)
utils.indent(root)
except:
pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
etree.ElementTree(root).write(path_ops.encode_path(nodeXML),
encoding="UTF-8")
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
tagname = try_encode(tagname)
cleantagname = try_decode(normalize_nodes(tagname))
nodepath = try_decode(xbmc.translatePath(
"special://profile/library/video/"))
cleantagname = utils.normalize_nodes(tagname)
nodepath = path_ops.translate_path('special://profile/library/video/')
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname
if v.KODIVERSION >= 17:
@ -393,13 +412,12 @@ class VideoNodes(object):
windowpath = "ActivateWindow(Video,%s,return)" % path
# Create the video node directory
if not exists_dir(nodepath):
if not path_ops.exists(nodepath):
# We need to copy over the default items
dir_util.copy_tree(
src=try_decode(
xbmc.translatePath("special://xbmc/system/library/video")),
dst=try_decode(
xbmc.translatePath("special://profile/library/video")),
path_ops.copy_tree(
src=path_ops.translate_path(
'special://xbmc/system/library/video'),
dst=path_ops.translate_path('special://profile/library/video'),
preserve_mode=0) # do not copy permission bits!
labels = {
@ -407,14 +425,14 @@ class VideoNodes(object):
'Favorite tvshows': 30181,
'channels': 30173
}
label = lang(labels[tagname])
label = utils.lang(labels[tagname])
embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=itemtype)
utils.window('%s.title' % embynode, value=label)
utils.window('%s.path' % embynode, value=windowpath)
utils.window('%s.content' % embynode, value=path)
utils.window('%s.type' % embynode, value=itemtype)
if exists(try_encode(nodeXML)):
if path_ops.exists(nodeXML):
# Don't recreate xml if already exists
return
@ -423,23 +441,26 @@ class VideoNodes(object):
label=label,
tagname=tagname,
roottype=2)
etree.SubElement(root, 'path').text = "plugin://plugin.video.plexkodiconnect/?id=0&mode=channels"
etree.SubElement(root,
'path').text = "plugin://plugin.video.plexkodiconnect/?id=0&mode=channels"
else:
root = self.commonRoot(order=1, label=label, tagname=tagname)
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
etree.SubElement(root, 'content').text = mediatype
try:
indent(root)
utils.indent(root)
except:
pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
def clearProperties(self):
log.info("Clearing nodes properties.")
plexprops = window('Plex.nodes.total')
LOG.info("Clearing nodes properties.")
plexprops = utils.window('Plex.nodes.total')
propnames = [
"index","path","title","content",
"inprogress.content","inprogress.title",
@ -457,4 +478,5 @@ class VideoNodes(object):
totalnodes = int(plexprops)
for i in range(totalnodes):
for prop in propnames:
window('Plex.nodes.%s.%s' % (str(i), prop), clear=True)
utils.window('Plex.nodes.%s.%s' % (str(i), prop),
clear=True)

View file

@ -88,9 +88,9 @@ Event Handler Classes
import os.path
import logging
import re
from pathtools.patterns import match_any_paths
from watchdog.utils import has_attribute
from watchdog.utils import unicode_paths
from ..pathtools.patterns import match_any_paths
from .utils import has_attribute
from .utils import unicode_paths
EVENT_TYPE_MOVED = 'moved'

View file

@ -55,8 +55,8 @@ Class Platforms Note
"""
import warnings
from watchdog.utils import platform
from watchdog.utils import UnsupportedLibc
from ..utils import platform
from ..utils import UnsupportedLibc
if platform.is_linux():
try:

View file

@ -18,9 +18,9 @@
from __future__ import with_statement
import threading
from watchdog.utils import BaseThread
from watchdog.utils.compat import queue
from watchdog.utils.bricks import SkipRepeatsQueue
from ..utils import BaseThread
from ..utils.compat import queue
from ..utils.bricks import SkipRepeatsQueue
DEFAULT_EMITTER_TIMEOUT = 1 # in seconds.
DEFAULT_OBSERVER_TIMEOUT = 1 # in seconds.

View file

@ -30,7 +30,7 @@ import threading
import unicodedata
import _watchdog_fsevents as _fsevents
from watchdog.events import (
from ..events import (
FileDeletedEvent,
FileModifiedEvent,
FileCreatedEvent,
@ -41,8 +41,8 @@ from watchdog.events import (
DirMovedEvent
)
from watchdog.utils.dirsnapshot import DirectorySnapshot
from watchdog.observers.api import (
from ..utils.dirsnapshot import DirectorySnapshot
from ..observers.api import (
BaseObserver,
EventEmitter,
DEFAULT_EMITTER_TIMEOUT,

View file

@ -24,9 +24,9 @@ import os
import logging
import unicodedata
from threading import Thread
from watchdog.utils.compat import queue
from ..utils.compat import queue
from watchdog.events import (
from ..events import (
FileDeletedEvent,
FileModifiedEvent,
FileCreatedEvent,
@ -36,7 +36,7 @@ from watchdog.events import (
DirCreatedEvent,
DirMovedEvent
)
from watchdog.observers.api import (
from ..observers.api import (
BaseObserver,
EventEmitter,
DEFAULT_EMITTER_TIMEOUT,

View file

@ -73,14 +73,14 @@ import os
import threading
from .inotify_buffer import InotifyBuffer
from watchdog.observers.api import (
from ..observers.api import (
EventEmitter,
BaseObserver,
DEFAULT_EMITTER_TIMEOUT,
DEFAULT_OBSERVER_TIMEOUT
)
from watchdog.events import (
from ..events import (
DirDeletedEvent,
DirModifiedEvent,
DirMovedEvent,
@ -92,7 +92,7 @@ from watchdog.events import (
generate_sub_moved_events,
generate_sub_created_events,
)
from watchdog.utils import unicode_paths
from ..utils import unicode_paths
class InotifyEmitter(EventEmitter):

View file

@ -15,9 +15,9 @@
# limitations under the License.
import logging
from watchdog.utils import BaseThread
from watchdog.utils.delayed_queue import DelayedQueue
from watchdog.observers.inotify_c import Inotify
from ..utils import BaseThread
from ..utils.delayed_queue import DelayedQueue
from ..observers.inotify_c import Inotify
logger = logging.getLogger(__name__)

View file

@ -24,8 +24,8 @@ import ctypes
import ctypes.util
from functools import reduce
from ctypes import c_int, c_char_p, c_uint32
from watchdog.utils import has_attribute
from watchdog.utils import UnsupportedLibc
from ..utils import has_attribute
from ..utils import UnsupportedLibc
def _load_libc():

View file

@ -78,7 +78,7 @@ Collections and Utility Classes
"""
from __future__ import with_statement
from watchdog.utils import platform
from ..utils import platform
import threading
import errno
@ -94,18 +94,18 @@ if sys.version_info < (2, 7, 0):
else:
import select
from pathtools.path import absolute_path
from ...pathtools.path import absolute_path
from watchdog.observers.api import (
from ..observers.api import (
BaseObserver,
EventEmitter,
DEFAULT_OBSERVER_TIMEOUT,
DEFAULT_EMITTER_TIMEOUT
)
from watchdog.utils.dirsnapshot import DirectorySnapshot
from ..utils.dirsnapshot import DirectorySnapshot
from watchdog.events import (
from ..events import (
DirMovedEvent,
DirDeletedEvent,
DirCreatedEvent,

View file

@ -38,16 +38,17 @@ from __future__ import with_statement
import os
import threading
from functools import partial
from watchdog.utils import stat as default_stat
from watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff
from watchdog.observers.api import (
from ..utils import stat as default_stat
from ..utils.dirsnapshot import DirectorySnapshot, \
DirectorySnapshotDiff
from ..observers.api import (
EventEmitter,
BaseObserver,
DEFAULT_OBSERVER_TIMEOUT,
DEFAULT_EMITTER_TIMEOUT
)
from watchdog.events import (
from ..events import (
DirMovedEvent,
DirDeletedEvent,
DirCreatedEvent,

View file

@ -24,7 +24,7 @@ import threading
import os.path
import time
from watchdog.events import (
from ..events import (
DirCreatedEvent,
DirDeletedEvent,
DirMovedEvent,
@ -37,14 +37,14 @@ from watchdog.events import (
generate_sub_created_events,
)
from watchdog.observers.api import (
from ..observers.api import (
EventEmitter,
BaseObserver,
DEFAULT_OBSERVER_TIMEOUT,
DEFAULT_EMITTER_TIMEOUT
)
from watchdog.observers.winapi import (
from ..observers.winapi import (
read_events,
get_directory_handle,
close_directory_handle,

View file

@ -22,8 +22,8 @@ import signal
import subprocess
import time
from watchdog.utils import echo, has_attribute
from watchdog.events import PatternMatchingEventHandler
from ..utils import echo, has_attribute
from ..events import PatternMatchingEventHandler
class Trick(PatternMatchingEventHandler):

View file

@ -33,9 +33,8 @@ Classes
import os
import sys
import threading
import watchdog.utils.platform
from watchdog.utils.compat import Event
from collections import namedtuple
from . import platform
from .compat import Event
if sys.version_info[0] == 2 and platform.is_windows():

View file

@ -24,6 +24,6 @@ except ImportError:
if sys.version_info < (2, 7):
from watchdog.utils.event_backport import Event
from .event_backport import Event
else:
from threading import Event

View file

@ -47,8 +47,7 @@ Classes
import errno
import os
from stat import S_ISDIR
from watchdog.utils import platform
from watchdog.utils import stat as default_stat
from . import stat as default_stat
class DirectorySnapshotDiff(object):

View file

@ -24,7 +24,7 @@
import sys
from watchdog.utils import platform
from . import platform
try:
# Python 2

View file

@ -37,8 +37,8 @@ except ImportError:
from io import StringIO
from argh import arg, aliases, ArghParser, expects_obj
from watchdog.version import VERSION_STRING
from watchdog.utils import load_class
from .version import VERSION_STRING
from .utils import load_class
logging.basicConfig(level=logging.INFO)

View file

@ -48,11 +48,11 @@ import logging
import traceback
import sys
import utils
from . import utils
###############################################################################
LOG = logging.getLogger("PLEX." + __name__)
LOG = logging.getLogger('PLEX.websocket')
###############################################################################

View file

@ -7,17 +7,16 @@ from json import loads
import xml.etree.ElementTree as etree
from threading import Thread
from ssl import CERT_NONE
from xbmc import sleep
from utils import window, settings, thread_methods
from companion import process_command
import state
import variables as v
from . import utils
from . import companion
from . import state
from . import variables as v
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.websocket_client')
###############################################################################
@ -140,14 +139,14 @@ class WebSocket(Thread):
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'BACKGROUND_SYNC_DISABLED'])
@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'BACKGROUND_SYNC_DISABLED'])
class PMS_Websocket(WebSocket):
"""
Websocket connection with the PMS for Plex Companion
"""
def getUri(self):
server = window('pms_server')
server = utils.window('pms_server')
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
@ -158,7 +157,7 @@ class PMS_Websocket(WebSocket):
if state.PLEX_TOKEN:
uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN
sslopt = {}
if settings('sslverify') == "false":
if utils.settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)
@ -206,7 +205,7 @@ class Alexa_Websocket(WebSocket):
"""
Websocket connection to talk to Amazon Alexa.
Can't use thread_methods!
Can't use utils.thread_methods!
"""
thread_stopped = False
thread_suspended = False
@ -244,9 +243,9 @@ class Alexa_Websocket(WebSocket):
LOG.error('%s: Could not parse Alexa message',
self.__class__.__name__)
return
process_command(message.attrib['path'][1:], message.attrib)
companion.process_command(message.attrib['path'][1:], message.attrib)
# Path in thread_methods
# Path in utils.thread_methods
def stop(self):
self.thread_stopped = True

View file

@ -1,297 +1,8 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from os import path as os_path
from sys import path as sys_path, argv
from xbmc import translatePath, Monitor
from xbmcaddon import Addon
###############################################################################
_ADDON = Addon(id='plugin.video.plexkodiconnect')
try:
_ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8')
except TypeError:
_ADDON_PATH = _ADDON.getAddonInfo('path').decode()
try:
_BASE_RESOURCE = translatePath(os_path.join(
_ADDON_PATH,
'resources',
'lib')).decode('utf-8')
except TypeError:
_BASE_RESOURCE = translatePath(os_path.join(
_ADDON_PATH,
'resources',
'lib')).decode()
sys_path.append(_BASE_RESOURCE)
###############################################################################
from utils import settings, window, language as lang, dialog
from userclient import UserClient
import initialsetup
from kodimonitor import KodiMonitor, SpecialMonitor
from librarysync import LibrarySync
from websocket_client import PMS_Websocket, Alexa_Websocket
from PlexFunctions import check_connection
from PlexCompanion import PlexCompanion
from command_pipeline import Monitor_Window
from playback_starter import PlaybackStarter
from playqueue import PlayqueueMonitor
from artwork import Image_Cache_Thread
import variables as v
import state
###############################################################################
import loghandler
loghandler.config()
LOG = getLogger("PLEX.service")
###############################################################################
from __future__ import absolute_import, division, unicode_literals
from resources.lib import service_entry
class Service():
server_online = True
warn_auth = True
user = None
ws = None
library = None
plexCompanion = None
user_running = False
ws_running = False
alexa_running = False
library_running = False
plexCompanion_running = False
kodimonitor_running = False
playback_starter_running = False
image_cache_thread_running = False
def __init__(self):
# Initial logging
LOG.info("======== START %s ========", v.ADDON_NAME)
LOG.info("Platform: %s", v.PLATFORM)
LOG.info("KODI Version: %s", v.KODILONGVERSION)
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == "true")
LOG.info("Number of sync threads: %s", settings('syncThreadNumber'))
LOG.info("Full sys.argv received: %s", argv)
self.monitor = Monitor()
# Load/Reset PKC entirely - important for user/Kodi profile switch
initialsetup.reload_pkc()
def __stop_PKC(self):
"""
Kodi's abortRequested is really unreliable :-(
"""
return self.monitor.abortRequested() or state.STOP_PKC
def ServiceEntryPoint(self):
# Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once.
__stop_PKC = self.__stop_PKC
monitor = self.monitor
kodiProfile = v.KODI_PROFILE
# Server auto-detect
initialsetup.InitialSetup().setup()
# Detect playback start early on
self.command_pipeline = Monitor_Window()
self.command_pipeline.start()
# Initialize important threads, handing over self for callback purposes
self.user = UserClient()
self.ws = PMS_Websocket()
self.alexa = Alexa_Websocket()
self.library = LibrarySync()
self.plexCompanion = PlexCompanion()
self.specialMonitor = SpecialMonitor()
self.playback_starter = PlaybackStarter()
self.playqueue = PlayqueueMonitor()
if settings('enableTextureCache') == "true":
self.image_cache_thread = Image_Cache_Thread()
welcome_msg = True
counter = 0
while not __stop_PKC():
if window('plex_kodiProfile') != kodiProfile:
# Profile change happened, terminate this thread and others
LOG.info("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread.",
kodiProfile, window('plex_kodiProfile'))
break
# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
# 3. User has access to the server
if window('plex_online') == "true":
# Plex server is online
# Verify if user is set and has access to the server
if (self.user.user is not None) and self.user.has_access:
if not self.kodimonitor_running:
# Start up events
self.warn_auth = True
if welcome_msg is True:
# Reset authentication warnings
welcome_msg = False
dialog('notification',
lang(29999),
"%s %s" % (lang(33000),
self.user.user),
icon='{plex}',
time=2000,
sound=False)
# Start monitoring kodi events
self.kodimonitor_running = KodiMonitor()
self.specialMonitor.start()
# Start the Websocket Client
if not self.ws_running:
self.ws_running = True
self.ws.start()
# Start the Alexa thread
if (not self.alexa_running and
settings('enable_alexa') == 'true'):
self.alexa_running = True
self.alexa.start()
# Start the syncing thread
if not self.library_running:
self.library_running = True
self.library.start()
# Start the Plex Companion thread
if not self.plexCompanion_running:
self.plexCompanion_running = True
self.plexCompanion.start()
if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
self.playqueue.start()
if (not self.image_cache_thread_running and
settings('enableTextureCache') == "true"):
self.image_cache_thread_running = True
self.image_cache_thread.start()
else:
if (self.user.user is None) and self.warn_auth:
# Alert user is not authenticated and suppress future
# warning
self.warn_auth = False
LOG.warn("Not authenticated yet.")
# User access is restricted.
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
while self.user.has_access is False:
# Verify access with an API call
self.user.check_access()
if window('plex_online') != "true":
# Server went offline
break
if monitor.waitForAbort(3):
# Abort was requested while waiting. We should exit
break
else:
# Wait until Plex server is online
# or Kodi is shut down.
while not self.__stop_PKC():
server = self.user.get_server()
if server is False:
# No server info set in add-on settings
pass
elif check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online:
self.server_online = False
window('plex_online', value="false")
# Suspend threads
state.SUSPEND_LIBRARY_THREAD = True
LOG.error("Plex Media Server went offline")
if settings('show_pms_offline') == 'true':
dialog('notification',
lang(33001),
"%s %s" % (lang(29999), lang(33002)),
icon='{plex}',
sound=False)
counter += 1
# Periodically check if the IP changed, e.g. per minute
if counter > 20:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.pick_pms()
if tmp is not None:
setup.write_pms_to_settings(tmp)
else:
# Server is online
counter = 0
if not self.server_online:
# Server was offline when Kodi started.
# Wait for server to be fully established.
if monitor.waitForAbort(5):
# Abort was requested while waiting.
break
self.server_online = True
# Alert the user that server is online.
if (welcome_msg is False and
settings('show_pms_offline') == 'true'):
dialog('notification',
lang(29999),
lang(33003),
icon='{plex}',
time=5000,
sound=False)
LOG.info("Server %s is online and ready.", server)
window('plex_online', value="true")
if state.AUTHENTICATED:
# Server got offline when we were authenticated.
# Hence resume threads
state.SUSPEND_LIBRARY_THREAD = False
# Start the userclient thread
if not self.user_running:
self.user_running = True
self.user.start()
break
if monitor.waitForAbort(3):
# Abort was requested while waiting.
break
if monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit
break
# Terminating PlexKodiConnect
# Tell all threads to terminate (e.g. several lib sync threads)
state.STOP_PKC = True
window('plex_service_started', clear=True)
LOG.info("======== STOP %s ========", v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation!
if window('plex_service_started') == 'true':
EXIT = True
else:
window('plex_service_started', value='true')
EXIT = False
# Delay option
DELAY = int(settings('startupDelay'))
LOG.info("Delaying Plex startup by: %s sec...", DELAY)
if EXIT:
LOG.error('PKC service.py already started - exiting this instance')
elif DELAY and Monitor().waitForAbort(DELAY):
# Start the service
LOG.info("Abort requested while waiting. PKC not started.")
else:
Service().ServiceEntryPoint()
if __name__ == "__main__":
service_entry.start()