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) [![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) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.1" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.2" provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.4" /> <import addon="plugin.video.plexkodiconnect.movies" version="2.0.5" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.4" /> <import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.5" />
</requires> </requires>
<extension point="xbmc.python.pluginsource" library="default.py"> <extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides> <provides>video audio image</provides>
@ -67,7 +67,17 @@
<summary lang="ru_RU">Нативная интеграция сервера Plex в Kodi</summary> <summary lang="ru_RU">Нативная интеграция сервера Plex в Kodi</summary>
<description lang="ru_RU">Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы данных музыки и видео в Kodi (так как плагин напрямую их изменяет). Используйте на свой страх и риск</description> <description lang="ru_RU">Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы данных музыки и видео в Kodi (так как плагин напрямую их изменяет). Используйте на свой страх и риск</description>
<disclaimer lang="ru_RU">Используйте на свой страх и риск</disclaimer> <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 library sync crash due to PMS sending string, not unicode
- Fix playback from playlists for add-on paths - 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 - 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): version 2.2.1 (beta only):
- Fix library sync crash due to PMS sending string, not unicode - Fix library sync crash due to PMS sending string, not unicode
- Fix playback from playlists for add-on paths - Fix playback from playlists for add-on paths

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +1,31 @@
from logging import getLogger 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(): def check_migration():
log.info('Checking whether we need to migrate something') LOG.info('Checking whether we need to migrate something')
last_migration = settings('last_migrated_PKC_version') last_migration = utils.settings('last_migrated_PKC_version')
if last_migration == v.ADDON_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! # 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 return
if not compare_version(last_migration, '1.8.2'): if not utils.compare_version(last_migration, '1.8.2'):
log.info('Migrating to version 1.8.1') LOG.info('Migrating to version 1.8.1')
# Set the new PKC theMovieDB key # Set the new PKC theMovieDB key
settings('themoviedbAPIKey', value='19c90103adb9e98f2172c6a6a3d85dc4') utils.settings('themoviedbAPIKey',
value='19c90103adb9e98f2172c6a6a3d85dc4')
if not compare_version(last_migration, '2.0.25'): if not utils.compare_version(last_migration, '2.0.25'):
log.info('Migrating to version 2.0.24') LOG.info('Migrating to version 2.0.24')
# Need to re-connect with PMS to pick up on plex.direct URIs # Need to re-connect with PMS to pick up on plex.direct URIs
settings('ipaddress', value='') utils.settings('ipaddress', value='')
settings('port', 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 -*- # -*- coding: utf-8 -*-
from logging import getLogger from logging import getLogger
from re import compile as re_compile
from xml.etree.ElementTree import ParseError from xml.etree.ElementTree import ParseError
from utils import XmlKodiSetting, reboot_kodi, language as lang from . import utils
from PlexAPI import API from .plex_api import API
import variables as v from . import variables as v
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.music')
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
############################################################################### ###############################################################################
@ -37,9 +34,10 @@ def excludefromscan_music_folders(xml):
omit_check=True) omit_check=True)
paths.append(__turn_to_regex(path)) paths.append(__turn_to_regex(path))
try: try:
with XmlKodiSetting('advancedsettings.xml', with utils.XmlKodiSetting(
force_create=True, 'advancedsettings.xml',
top_element='advancedsettings') as xml_file: force_create=True,
top_element='advancedsettings') as xml_file:
parent = xml_file.set_setting(['audio', 'excludefromscan']) parent = xml_file.set_setting(['audio', 'excludefromscan'])
for path in paths: for path in paths:
for element in parent: for element in parent:
@ -71,7 +69,7 @@ def excludefromscan_music_folders(xml):
if reboot is True: if reboot is True:
# 'New Plex music library detected. Sorry, but we need to # 'New Plex music library detected. Sorry, but we need to
# restart Kodi now due to the changes made.' # restart Kodi now due to the changes made.'
reboot_kodi(lang(39711)) utils.reboot_kodi(utils.lang(39711))
def __turn_to_regex(path): 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 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from cPickle import dumps, loads from cPickle import dumps, loads
from xbmcgui import Window from xbmcgui import Window
from xbmc import log, LOGDEBUG from xbmc import log, LOGDEBUG
############################################################################### ###############################################################################
WINDOW = Window(10000) WINDOW = Window(10000)
PREFIX = 'PLEX.%s: ' % __name__ PREFIX = 'PLEX.pickler: '
############################################################################### ###############################################################################

View file

@ -3,11 +3,11 @@
from xbmcgui import ListItem 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'), listitem = ListItem(label=data.get('label'),
label2=data.get('label2'), label2=data.get('label2'),
path=data.get('path')) path=data.get('path'))
@ -26,7 +26,7 @@ def convert_PKC_to_listitem(PKC_listitem):
return listitem return listitem
class PKC_ListItem(object): class PKCListItem(object):
""" """
Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data
when pickling! when pickling!

View file

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

View file

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

View file

@ -3,25 +3,23 @@
Collection of functions associated with Kodi and Plex playlists and playqueues Collection of functions associated with Kodi and Plex playlists and playqueues
""" """
from logging import getLogger from logging import getLogger
import os
import urllib import urllib
from urlparse import parse_qsl, urlsplit from urlparse import parse_qsl, urlsplit
from re import compile as re_compile
import plexdb_functions as plexdb from .plex_api import API
from downloadutils import DownloadUtils as DU from . import plex_functions as PF
from utils import try_decode, try_encode from . import plexdb_functions as plexdb
from PlexAPI import API from . import kodidb_functions as kodidb
from PlexFunctions import GetPlexMetadata from .downloadutils import DownloadUtils as DU
from kodidb_functions import kodiid_from_filename from . import utils
import json_rpc as js from . import path_ops
import variables as v 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): 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 some reason, can't use dir directly
for key in self.__dict__: for key in self.__dict__:
if key in ('id', 'kodi_pl'): if key in ('id', 'kodi_pl'):
continue continue
if isinstance(getattr(self, key), str): if isinstance(getattr(self, key), str):
answ += '\'%s\': \'%s\', ' % (key, answ += '\'%s\': \'%s\', ' % (key,
try_decode(getattr(self, key))) utils.try_decode(getattr(self,
key)))
elif isinstance(getattr(self, key), unicode): elif isinstance(getattr(self, key), unicode):
answ += '\'%s\': \'%s\', ' % (key, getattr(self, key)) answ += '\'%s\': \'%s\', ' % (key, getattr(self, key))
else: else:
# e.g. int # e.g. int
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
return try_encode(answ + '}}') return answ + '}}'
class Playlist_Object(PlaylistObjectBaseclase): class Playlist_Object(PlaylistObjectBaseclase):
@ -81,7 +80,9 @@ class Playlist_Object(PlaylistObjectBaseclase):
@kodi_path.setter @kodi_path.setter
def kodi_path(self, path): 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: try:
self.kodi_filename, self.kodi_extension = file.split('.', 1) self.kodi_filename, self.kodi_extension = file.split('.', 1)
except ValueError: except ValueError:
@ -219,16 +220,17 @@ class Playlist_Item(object):
def __repr__(self): 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)) % (self.__class__.__name__, self.id, self.plex_id))
for key in self.__dict__: for key in self.__dict__:
if key in ('id', 'plex_id', 'xml'): if key in ('id', 'plex_id', 'xml'):
continue continue
if isinstance(getattr(self, key), str): if isinstance(getattr(self, key), str):
answ += '\'%s\': \'%s\', ' % (key, answ += '\'%s\': \'%s\', ' % (key,
try_decode(getattr(self, key))) utils.try_decode(getattr(self,
key)))
elif isinstance(getattr(self, key), unicode): elif isinstance(getattr(self, key), unicode):
answ += '\'%s\': \'%s\', ' % (key, getattr(self, key)) answ += '\'%s\': \'%s\', ' % (key, getattr(self, key))
else: else:
@ -238,7 +240,7 @@ class Playlist_Item(object):
answ += '\'xml\': None}}' answ += '\'xml\': None}}'
else: else:
answ += '\'xml\': \'%s\'}}' % self.xml.tag answ += '\'xml\': \'%s\'}}' % self.xml.tag
return try_encode(answ) return answ
def plex_stream_index(self, kodi_stream_index, stream_type): 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') item.plex_type = query.get('itemType')
if item.plex_id is None and item.file is not None: if item.plex_id is None and item.file is not None:
item.uri = ('library://whatever/item/%s' item.uri = ('library://whatever/item/%s'
% urllib.quote(try_encode(item.file), safe='')) % urllib.quote(utils.try_encode(item.file), safe=''))
else: else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
@ -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. # Need more info since we don't have kodi_id nor type. Use file path.
if (kodi_item['file'].startswith('plugin') or if (kodi_item['file'].startswith('plugin') or
kodi_item['file'].startswith('http')): 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', LOG.debug('Starting research for Kodi id since we didnt get one: %s',
kodi_item) 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 kodi_item['type'] = v.KODI_TYPE_MOVIE
if kodi_id is None: if kodi_id is None:
kodi_id, _ = kodiid_from_filename(kodi_item['file'], kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_EPISODE) v.KODI_TYPE_EPISODE)
kodi_item['type'] = v.KODI_TYPE_EPISODE kodi_item['type'] = v.KODI_TYPE_EPISODE
if kodi_id is None: 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['type'] = v.KODI_TYPE_SONG
kodi_item['id'] = kodi_id kodi_item['id'] = kodi_id
kodi_item['type'] = None if kodi_id is None else kodi_item['type'] 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 # Need to get the details for the playlist item
item = playlist_item_from_xml(xml[0]) item = playlist_item_from_xml(xml[0])
except (KeyError, IndexError, TypeError): except (KeyError, IndexError, TypeError):
raise PlaylistError('Could not init Plex playlist with plex_id %s and ' LOG.error('Could not init Plex playlist: plex_id %s, kodi_item %s',
'kodi_item %s' % (plex_id, kodi_item)) plex_id, kodi_item)
raise PlaylistError
playlist.items.append(item) playlist.items.append(item)
LOG.debug('Initialized the playqueue on the Plex side: %s', playlist) LOG.debug('Initialized the playqueue on the Plex side: %s', playlist)
return item 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( item = playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file}) {'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None: if item.plex_id is not None:
xml = GetPlexMetadata(item.plex_id) xml = PF.GetPlexMetadata(item.plex_id)
item.xml = xml[-1] item.xml = xml[-1]
playlist.items.insert(pos, item) playlist.items.insert(pos, item)
return item return item
@ -859,11 +865,12 @@ def get_plextype_from_xml(xml):
returns None if unsuccessful returns None if unsuccessful
""" """
try: try:
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0] plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
xml.attrib['playQueueSourceURI'])[0]
except IndexError: except IndexError:
LOG.error('Could not get plex_id from xml: %s', xml.attrib) LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return return
new_xml = GetPlexMetadata(plex_id) new_xml = PF.GetPlexMetadata(plex_id)
try: try:
new_xml[0].attrib new_xml[0].attrib
except (TypeError, IndexError, AttributeError): except (TypeError, IndexError, AttributeError):

View file

@ -1,29 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from logging import getLogger 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
from watchdog.events import FileSystemEventHandler from . import playlist_func as PL
from watchdog.observers import Observer from .plex_api import API
import playlist_func as PL from . import kodidb_functions as kodidb
from PlexAPI import API from . import plexdb_functions as plexdb
import kodidb_functions as kodidb from . import utils
import plexdb_functions as plexdb from . import path_ops
import utils from . import variables as v
import variables as v from . import state
import state
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.playlists')
# Necessary to temporarily hold back librarysync/websocket listener when doing
# a full sync
LOCK = Lock()
LOCKER = utils.LockFunction(LOCK)
# Which playlist formates are supported by PKC? # Which playlist formates are supported by PKC?
SUPPORTED_FILETYPES = ( SUPPORTED_FILETYPES = (
@ -39,12 +30,6 @@ EVENT_TYPE_DELETED = 'deleted'
EVENT_TYPE_CREATED = 'created' EVENT_TYPE_CREATED = 'created'
EVENT_TYPE_MODIFIED = 'modified' 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): def create_plex_playlist(playlist):
""" """
@ -106,20 +91,21 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
playlist.plex_updatedat = updated_at playlist.plex_updatedat = updated_at
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist) LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
name = utils.valid_filename(playlist.plex_name) name = utils.valid_filename(playlist.plex_name)
path = os.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name) path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
while exists(path) or playlist_object_from_db(path=path): while path_ops.exists(path) or playlist_object_from_db(path=path):
# In case the Plex playlist names are not unique # In case the Plex playlist names are not unique
occurance = utils.REGEX_FILE_NUMBERING.search(path) occurance = utils.REGEX_FILE_NUMBERING.search(path)
if not occurance: if not occurance:
path = os.path.join(v.PLAYLIST_PATH, path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type, playlist.type,
'%s_01.m3u' % name[:min(len(name), 248)]) '%s_01.m3u' % name[:min(len(name), 248)])
else: else:
occurance = int(occurance.group(1)) + 1 occurance = int(occurance.group(1)) + 1
path = os.path.join(v.PLAYLIST_PATH, path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type, playlist.type,
'%s_%02d.m3u' % (name[:min(len(name), 248)], '%s_%02d.m3u' % (name[:min(len(name),
occurance)) 248)],
occurance))
LOG.debug('Kodi playlist path: %s', path) LOG.debug('Kodi playlist path: %s', path)
playlist.kodi_path = path playlist.kodi_path = path
# Derive filename close to Plex playlist name # Derive filename close to Plex playlist name
@ -137,7 +123,7 @@ def delete_kodi_playlist(playlist):
Returns None or raises PL.PlaylistError Returns None or raises PL.PlaylistError
""" """
try: try:
os.remove(playlist.kodi_path) path_ops.remove(playlist.kodi_path)
except (OSError, IOError) as err: except (OSError, IOError) as err:
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s', LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
playlist, err.errno, err.strerror) 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! Adapter to process *.m3u playlist files. Encoding is not uniform!
""" """
plex_ids = list() 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() text = f.read()
try: try:
text = text.decode(ENCODING) text = text.decode(v.M3U_ENCODING)
except UnicodeDecodeError: except UnicodeDecodeError:
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist) LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
text = text.decode('ISO-8859-1') 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 Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
Returns None or raises PL.PlaylistError Returns None or raises PL.PlaylistError
""" """
text = u'#EXTCPlayListM3U::M3U\n' text = '#EXTCPlayListM3U::M3U\n'
for element in xml: for element in xml:
api = API(element) api = API(element)
text += (u'#EXTINF:%s,%s\n%s\n' text += ('#EXTINF:%s,%s\n%s\n'
% (api.runtime(), api.title(), api.path())) % (api.runtime(), api.title(), api.path()))
text += '\n' text += '\n'
text = text.encode(ENCODING, 'ignore') text = text.encode(v.M3U_ENCODING, 'strict')
try: try:
with open(playlist.kodi_path, 'wb') as f: with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
f.write(text) f.write(text)
except (OSError, IOError) as err: except (OSError, IOError) as err:
LOG.error('Could not write Kodi playlist file: %s', playlist) LOG.error('Could not write Kodi playlist file: %s', playlist)
@ -274,41 +260,49 @@ def _kodi_playlist_identical(xml_element):
pass pass
@LOCKER.lockthis def process_websocket(plex_id, updated_at, status):
def process_websocket(plex_id, updated_at, state):
""" """
Hit by librarysync to process websocket messages concerning playlists Hit by librarysync to process websocket messages concerning playlists
""" """
create = False create = False
playlist = playlist_object_from_db(plex_id=plex_id) with state.LOCK_PLAYLISTS:
try: playlist = playlist_object_from_db(plex_id=plex_id)
if playlist and state == 9: try:
LOG.debug('Plex deletion of playlist detected: %s', playlist) if playlist and status == 9:
delete_kodi_playlist(playlist) LOG.debug('Plex deletion of playlist detected: %s', playlist)
elif playlist and playlist.plex_updatedat == updated_at: delete_kodi_playlist(playlist)
LOG.debug('Playlist with id %s already synced: %s', elif playlist and playlist.plex_updatedat == updated_at:
plex_id, playlist) LOG.debug('Playlist with id %s already synced: %s',
elif playlist: plex_id, playlist)
LOG.debug('Change of Plex playlist detected: %s', playlist) elif playlist:
delete_kodi_playlist(playlist) LOG.debug('Change of Plex playlist detected: %s', playlist)
create = True delete_kodi_playlist(playlist)
elif not playlist and not state == 9: create = True
LOG.debug('Creation of new Plex playlist detected: %s', plex_id) elif not playlist and not status == 9:
create = True LOG.debug('Creation of new Plex playlist detected: %s',
# To the actual work plex_id)
if create: create = True
create_kodi_playlist(plex_id=plex_id, updated_at=updated_at) # To the actual work
except PL.PlaylistError: if create:
pass create_kodi_playlist(plex_id=plex_id, updated_at=updated_at)
except PL.PlaylistError:
pass
@LOCKER.lockthis
def full_sync(): def full_sync():
""" """
Full sync of playlists between Kodi and Plex. Returns True is successful, Full sync of playlists between Kodi and Plex. Returns True is successful,
False otherwise False otherwise
""" """
LOG.info('Starting playlist full sync') 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 # Get all Plex playlists
xml = PL.get_all_playlists() xml = PL.get_all_playlists()
if xml is None: if xml is None:
@ -332,7 +326,7 @@ def full_sync():
elif playlist.plex_updatedat != api.updated_at(): elif playlist.plex_updatedat != api.updated_at():
LOG.debug('Detected changed Plex playlist %s: %s', LOG.debug('Detected changed Plex playlist %s: %s',
api.plex_id(), api.title()) api.plex_id(), api.title())
if exists(playlist.kodi_path): if path_ops.exists(playlist.kodi_path):
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
else: else:
update_plex_table(playlist, delete=True) update_plex_table(playlist, delete=True)
@ -345,7 +339,7 @@ def full_sync():
pass pass
# Get rid of old Plex playlists that were deleted on the Plex side # Get rid of old Plex playlists that were deleted on the Plex side
for plex_id in old_plex_ids: 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: if playlist:
LOG.debug('Removing outdated Plex playlist %s from %s', LOG.debug('Removing outdated Plex playlist %s from %s',
playlist.plex_name, playlist.kodi_path) playlist.plex_name, playlist.kodi_path)
@ -360,7 +354,7 @@ def full_sync():
if state.ENABLE_MUSIC: if state.ENABLE_MUSIC:
master_paths.append(v.PLAYLIST_PATH_MUSIC) master_paths.append(v.PLAYLIST_PATH_MUSIC)
for master_path in master_paths: 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: for file in files:
try: try:
extension = file.rsplit('.', 1)[1] extension = file.rsplit('.', 1)[1]
@ -368,7 +362,7 @@ def full_sync():
continue continue
if extension not in SUPPORTED_FILETYPES: if extension not in SUPPORTED_FILETYPES:
continue continue
path = os.path.join(root, file) path = path_ops.path.join(root, file)
kodi_hash = utils.generate_file_md5(path) kodi_hash = utils.generate_file_md5(path)
playlist = playlist_object_from_db(kodi_hash=kodi_hash) playlist = playlist_object_from_db(kodi_hash=kodi_hash)
playlist_2 = playlist_object_from_db(path=path) playlist_2 = playlist_object_from_db(path=path)
@ -438,7 +432,7 @@ class PlaylistEventhandler(FileSystemEventHandler):
EVENT_TYPE_DELETED: self.on_deleted, EVENT_TYPE_DELETED: self.on_deleted,
} }
event_type = event.event_type event_type = event.event_type
with LOCK: with state.LOCK_PLAYLISTS:
_method_map[event_type](event) _method_map[event_type](event)
def on_created(self, 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 logging import getLogger
from threading import Thread from threading import Thread
from re import compile as re_compile import xbmc
from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep from . import utils
from . import playlist_func as PL
from utils import thread_methods from . import plex_functions as PF
import playlist_func as PL from .plex_api import API
from PlexFunctions import GetAllPlexChildren from . import json_rpc as js
from PlexAPI import API from . import variables as v
from plexbmchelper.subscribers import LOCK from . import state
from playback import play_xml
import json_rpc as js
import variables as v
import state
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.playqueue')
PLUGIN = 'plugin://%s' % v.ADDON_ID PLUGIN = 'plugin://%s' % v.ADDON_ID
REGEX = re_compile(r'''plex_id=(\d+)''')
# Our PKC playqueues (3 instances of Playqueue_Object()) # Our PKC playqueues (3 instances of Playqueue_Object())
PLAYQUEUES = [] PLAYQUEUES = []
@ -37,7 +32,7 @@ def init_playqueues():
LOG.debug('Playqueues have already been initialized') LOG.debug('Playqueues have already been initialized')
return return
# Initialize Kodi playqueues # Initialize Kodi playqueues
with LOCK: with state.LOCK_PLAYQUEUES:
for i in (0, 1, 2): for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly # Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists(): for queue in js.get_playlists():
@ -48,12 +43,12 @@ def init_playqueues():
playqueue.type = queue['type'] playqueue.type = queue['type']
# Initialize each Kodi playlist # Initialize each Kodi playlist
if playqueue.type == v.KODI_TYPE_AUDIO: 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: elif playqueue.type == v.KODI_TYPE_VIDEO:
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
else: else:
# Currently, only video or audio playqueues available # 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' # Overwrite 'picture' with 'photo'
playqueue.type = v.KODI_TYPE_PHOTO playqueue.type = v.KODI_TYPE_PHOTO
PLAYQUEUES.append(playqueue) 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', Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in 'audio', 'picture') passed in
""" """
with LOCK: for playqueue in PLAYQUEUES:
for playqueue in PLAYQUEUES: if playqueue.type == kodi_playlist_type:
if playqueue.type == kodi_playlist_type: break
break else:
else: raise ValueError('Wrong playlist type passed in: %s',
raise ValueError('Wrong playlist type passed in: %s', kodi_playlist_type)
kodi_playlist_type) return playqueue
return playqueue
def init_playqueue_from_plex_children(plex_id, transient_token=None): 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 Returns the Playlist_Object
""" """
xml = GetAllPlexChildren(plex_id) xml = PF.GetAllPlexChildren(plex_id)
try: try:
xml[0].attrib xml[0].attrib
except (TypeError, IndexError, AttributeError): 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()) PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
playqueue.plex_transient_token = transient_token playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player') 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 return playqueue
def update_playqueue_from_PMS(playqueue, @utils.thread_methods(add_suspends=['PMS_STATUS'])
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'])
class PlayqueueMonitor(Thread): class PlayqueueMonitor(Thread):
""" """
Unfortunately, Kodi does not tell if items within a Kodi playqueue 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']) old_item.kodi_type == new_item['type'])
else: else:
try: try:
plex_id = REGEX.findall(new_item['file'])[0] plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0]
except IndexError: except IndexError:
LOG.debug('Comparing paths directly as a fallback') LOG.debug('Comparing paths directly as a fallback')
identical = old_item.file == new_item['file'] identical = old_item.file == new_item['file']
@ -222,8 +186,8 @@ class PlayqueueMonitor(Thread):
while suspended(): while suspended():
if stopped(): if stopped():
break break
sleep(1000) xbmc.sleep(1000)
with LOCK: with state.LOCK_PLAYQUEUES:
for playqueue in PLAYQUEUES: for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid) kodi_pl = js.playlist_get_items(playqueue.playlistid)
if playqueue.old_kodi_pl != kodi_pl: if playqueue.old_kodi_pl != kodi_pl:
@ -236,5 +200,5 @@ class PlayqueueMonitor(Thread):
# compare old and new playqueue # compare old and new playqueue
self._compare_playqueues(playqueue, kodi_pl) self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl) playqueue.old_kodi_pl = list(kodi_pl)
sleep(200) xbmc.sleep(200)
LOG.info("----===## PlayqueueMonitor stopped ##===----") LOG.info("----===## PlayqueueMonitor stopped ##===----")

View file

@ -2,13 +2,13 @@
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from downloadutils import DownloadUtils as DU
from utils import window, settings, language as lang, dialog, try_encode from .downloadutils import DownloadUtils as DU
import variables as v 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(), 'maxVideoBitrate': self.get_bitrate(),
'videoResolution': self.get_resolution(), 'videoResolution': self.get_resolution(),
'videoQuality': '100', 'videoQuality': '100',
'mediaBufferSize': int(settings('kodi_video_cache'))/1024, 'mediaBufferSize': int(
utils.settings('kodi_video_cache')) / 1024,
}) })
self.item.playmethod = 'Transcode' self.item.playmethod = 'Transcode'
LOG.info("The playurl is: %s", playurl) LOG.info("The playurl is: %s", playurl)
@ -71,15 +72,15 @@ class PlayUtils():
return playurl return playurl
# set to either 'Direct Stream=1' or 'Transcode=2' # set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0' # and NOT to 'Direct Play=0'
if settings('playType') != "0": if utils.settings('playType') != "0":
# User forcing to play via HTTP # User forcing to play via HTTP
LOG.info("User chose to not direct play") LOG.info("User chose to not direct play")
return return
if self.mustTranscode(): if self.mustTranscode():
return return
return self.api.validate_playurl(path, return self.api.validate_playurl(path,
self.api.plex_type(), self.api.plex_type(),
force_check=True) force_check=True)
def mustTranscode(self): def mustTranscode(self):
""" """
@ -106,7 +107,7 @@ class PlayUtils():
# e.g. trailers. Avoids TypeError with "'h265' in codec" # e.g. trailers. Avoids TypeError with "'h265' in codec"
LOG.info('No codec from PMS, not transcoding.') LOG.info('No codec from PMS, not transcoding.')
return False return False
if ((settings('transcodeHi10P') == 'true' and if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and videoCodec['bitDepth'] == '10') and
('h264' in codec)): ('h264' in codec)):
LOG.info('Option to transcode 10bit h264 video content enabled.') LOG.info('Option to transcode 10bit h264 video content enabled.')
@ -139,7 +140,7 @@ class PlayUtils():
if self.api.plex_type() == 'track': if self.api.plex_type() == 'track':
return True return True
# set to 'Transcode=2' # set to 'Transcode=2'
if settings('playType') == "2": if utils.settings('playType') == "2":
# User forcing to play via HTTP # User forcing to play via HTTP
LOG.info("User chose to transcode") LOG.info("User chose to transcode")
return False return False
@ -149,7 +150,7 @@ class PlayUtils():
def get_max_bitrate(self): def get_max_bitrate(self):
# get the addon video quality # get the addon video quality
videoQuality = settings('maxVideoQualities') videoQuality = utils.settings('maxVideoQualities')
bitrate = { bitrate = {
'0': 320, '0': 320,
'1': 720, '1': 720,
@ -180,13 +181,13 @@ class PlayUtils():
'2': 720, '2': 720,
'3': 1080 '3': 1080
} }
return H265[settings('transcodeH265')] return H265[utils.settings('transcodeH265')]
def get_bitrate(self): def get_bitrate(self):
""" """
Get the desired transcoding bitrate from the settings Get the desired transcoding bitrate from the settings
""" """
videoQuality = settings('transcoderVideoQualities') videoQuality = utils.settings('transcoderVideoQualities')
bitrate = { bitrate = {
'0': 320, '0': 320,
'1': 720, '1': 720,
@ -207,7 +208,7 @@ class PlayUtils():
""" """
Get the desired transcoding resolutions from the settings Get the desired transcoding resolutions from the settings
""" """
chosen = settings('transcoderVideoQualities') chosen = utils.settings('transcoderVideoQualities')
res = { res = {
'0': '420x420', '0': '420x420',
'1': '576x320', '1': '576x320',
@ -244,7 +245,7 @@ class PlayUtils():
audio_streams = [] audio_streams = []
subtitle_streams_list = [] subtitle_streams_list = []
# No subtitles as an option # No subtitles as an option
subtitle_streams = [lang(39706)] subtitle_streams = [utils.lang(39706)]
downloadable_streams = [] downloadable_streams = []
download_subs = [] download_subs = []
# selectAudioIndex = "" # selectAudioIndex = ""
@ -264,35 +265,35 @@ class PlayUtils():
codec = stream.attrib.get('codec') codec = stream.attrib.get('codec')
channellayout = stream.attrib.get('audioChannelLayout', "") channellayout = stream.attrib.get('audioChannelLayout', "")
try: try:
track = "%s %s - %s %s" % (audio_numb+1, track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'], stream.attrib['language'],
codec, codec,
channellayout) channellayout)
except KeyError: except KeyError:
track = "%s %s - %s %s" % (audio_numb+1, track = "%s %s - %s %s" % (audio_numb + 1,
lang(39707), # unknown utils.lang(39707), # unknown
codec, codec,
channellayout) channellayout)
audio_streams_list.append(index) audio_streams_list.append(index)
audio_streams.append(try_encode(track)) audio_streams.append(utils.try_encode(track))
audio_numb += 1 audio_numb += 1
# Subtitles # Subtitles
elif typus == "3": elif typus == "3":
try: try:
track = "%s %s" % (sub_num+1, stream.attrib['language']) track = "%s %s" % (sub_num + 1, stream.attrib['language'])
except KeyError: except KeyError:
track = "%s %s (%s)" % (sub_num+1, track = "%s %s (%s)" % (sub_num + 1,
lang(39707), # unknown utils.lang(39707), # unknown
stream.attrib.get('codec')) stream.attrib.get('codec'))
default = stream.attrib.get('default') default = stream.attrib.get('default')
forced = stream.attrib.get('forced') forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key') downloadable = stream.attrib.get('key')
if default: if default:
track = "%s - %s" % (track, lang(39708)) # Default track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced: if forced:
track = "%s - %s" % (track, lang(39709)) # Forced track = "%s - %s" % (track, utils.lang(39709)) # Forced
if downloadable: if downloadable:
# We do know the language - temporarily download # We do know the language - temporarily download
if 'language' in stream.attrib: if 'language' in stream.attrib:
@ -303,23 +304,23 @@ class PlayUtils():
# We don't know the language - no need to download # We don't know the language - no need to download
else: else:
path = self.api.attach_plex_token_to_url( path = self.api.attach_plex_token_to_url(
"%s%s" % (window('pms_server'), "%s%s" % (utils.window('pms_server'),
stream.attrib['key'])) stream.attrib['key']))
downloadable_streams.append(index) downloadable_streams.append(index)
download_subs.append(try_encode(path)) download_subs.append(utils.try_encode(path))
else: 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: if stream.attrib.get('selected') == '1' and downloadable:
# Only show subs without asking user if they can be # Only show subs without asking user if they can be
# turned off # turned off
default_sub = index default_sub = index
subtitle_streams_list.append(index) subtitle_streams_list.append(index)
subtitle_streams.append(try_encode(track)) subtitle_streams.append(utils.try_encode(track))
sub_num += 1 sub_num += 1
if audio_numb > 1: if audio_numb > 1:
resp = dialog('select', lang(33013), audio_streams) resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp > -1: if resp > -1:
# User selected some audio track # User selected some audio track
args = { args = {
@ -335,14 +336,14 @@ class PlayUtils():
return return
select_subs_index = None select_subs_index = None
if (settings('pickPlexSubtitles') == 'true' and if (utils.settings('pickPlexSubtitles') == 'true' and
default_sub is not None): default_sub is not None):
LOG.info('Using default Plex subtitle: %s', default_sub) LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub select_subs_index = default_sub
else: else:
resp = dialog('select', lang(33014), subtitle_streams) resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp > 0: if resp > 0:
select_subs_index = subtitle_streams_list[resp-1] select_subs_index = subtitle_streams_list[resp - 1]
else: else:
# User selected no subtitles or backed out of dialog # User selected no subtitles or backed out of dialog
select_subs_index = '' 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...) (and others...)
""" """
from logging import getLogger from logging import getLogger
from re import compile as re_compile, sub from re import sub
from urllib import urlencode, unquote from urllib import urlencode, unquote
from os.path import basename, join
from os import makedirs
from xbmcgui import ListItem from xbmcgui import ListItem
from xbmcvfs import exists from xbmcvfs import exists
import clientinfo as client from .downloadutils import DownloadUtils as DU
from downloadutils import DownloadUtils as DU from . import clientinfo
from utils import window, settings, language as lang, try_decode, try_encode, \ from . import utils
unix_date_to_kodi, exists_dir, slugify, dialog, escape_html from . import path_ops
import PlexFunctions as PF from . import plex_functions as PF
import plexdb_functions as plexdb from . import plexdb_functions as plexdb
import kodidb_functions as kodidb from . import kodidb_functions as kodidb
import variables as v from . import variables as v
import state from . import state
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.plex_api')
REGEX_IMDB = re_compile(r'''/(tt\d+)''')
REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''')
############################################################################### ###############################################################################
@ -70,7 +64,7 @@ class API(object):
# which media part in the XML response shall we look at? # which media part in the XML response shall we look at?
self.part = 0 self.part = 0
self.mediastream = None self.mediastream = None
self.server = window('pms_server') self.server = utils.window('pms_server')
def set_part_number(self, number=None): def set_part_number(self, number=None):
""" """
@ -203,7 +197,7 @@ class API(object):
ans = None ans = None
if ans is not None: if ans is not None:
try: try:
ans = try_decode(unquote(ans)) ans = utils.try_decode(unquote(ans))
except UnicodeDecodeError: except UnicodeDecodeError:
# Sometimes, Plex seems to have encoded in latin1 # Sometimes, Plex seems to have encoded in latin1
ans = unquote(ans).decode('latin1') ans = unquote(ans).decode('latin1')
@ -215,23 +209,23 @@ class API(object):
Will always use addon paths, never direct paths Will always use addon paths, never direct paths
""" """
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower() 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): extension not in v.KODI_SUPPORTED_IMAGES):
# Let Plex transcode # Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080 # max width/height supported by plex image transcoder is 1920x1080
path = self.server + PF.transcode_image_path( path = self.server + PF.transcode_image_path(
self.item[0][0].get('key'), self.item[0][0].get('key'),
window('pms_token'), utils.window('pms_token'),
"%s%s" % (self.server, self.item[0][0].get('key')), "%s%s" % (self.server, self.item[0][0].get('key')),
1920, 1920,
1080) 1080)
else: else:
path = self.attach_plex_token_to_url( path = self.attach_plex_token_to_url(
'%s%s' % (window('pms_server'), '%s%s' % (utils.window('pms_server'),
self.item[0][0].attrib['key'])) self.item[0][0].attrib['key']))
# Attach Plex id to url to let it be picked up by our playqueue agent # Attach Plex id to url to let it be picked up by our playqueue agent
# later # later
return 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): def tv_show_path(self):
""" """
@ -258,7 +252,7 @@ class API(object):
""" """
res = self.item.get('addedAt') res = self.item.get('addedAt')
if res is not None: if res is not None:
res = unix_date_to_kodi(res) res = utils.unix_date_to_kodi(res)
else: else:
res = '2000-01-01 10:00:00' res = '2000-01-01 10:00:00'
return res return res
@ -295,7 +289,7 @@ class API(object):
played = True if playcount else False played = True if playcount else False
try: try:
last_played = unix_date_to_kodi(int(item['lastViewedAt'])) last_played = utils.unix_date_to_kodi(int(item['lastViewedAt']))
except (KeyError, ValueError): except (KeyError, ValueError):
last_played = None last_played = None
@ -423,7 +417,7 @@ class API(object):
""" """
answ = self.item.get('guid') answ = self.item.get('guid')
if answ is not None: if answ is not None:
answ = escape_html(answ) answ = utils.escape_html(answ)
return answ return answ
def provider(self, providername=None): def provider(self, providername=None):
@ -438,10 +432,10 @@ class API(object):
return None return None
if providername == 'imdb': if providername == 'imdb':
regex = REGEX_IMDB regex = utils.REGEX_IMDB
elif providername == 'tvdb': elif providername == 'tvdb':
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en # originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
regex = REGEX_TVDB regex = utils.REGEX_TVDB
else: else:
return None return None
@ -456,7 +450,7 @@ class API(object):
""" """
Returns the title of the element as unicode or 'Missing Title Name' 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): def titles(self):
""" """
@ -657,12 +651,12 @@ class API(object):
url may or may not already contain a '?' url may or may not already contain a '?'
""" """
if window('pms_token') == '': if utils.window('pms_token') == '':
return url return url
if '?' not in 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: else:
url = "%s&X-Plex-Token=%s" % (url, window('pms_token')) url = "%s&X-Plex-Token=%s" % (url, utils.window('pms_token'))
return url return url
def item_id(self): def item_id(self):
@ -808,12 +802,12 @@ class API(object):
track['channels'] = stream.get('channels') track['channels'] = stream.get('channels')
# 'unknown' if we cannot get language # 'unknown' if we cannot get language
track['language'] = stream.get( track['language'] = stream.get(
'languageCode', lang(39310)).lower() 'languageCode', utils.lang(39310)).lower()
audiotracks.append(track) audiotracks.append(track)
elif media_type == 3: # Subtitle streams elif media_type == 3: # Subtitle streams
# 'unknown' if we cannot get language # 'unknown' if we cannot get language
subtitlelanguages.append( subtitlelanguages.append(
stream.get('languageCode', lang(39310)).lower()) stream.get('languageCode', utils.lang(39310)).lower())
return { return {
'video': videotracks, 'video': videotracks,
'audio': audiotracks, 'audio': audiotracks,
@ -966,7 +960,7 @@ class API(object):
LOG.info('Start movie set/collection lookup on themoviedb with %s', LOG.info('Start movie set/collection lookup on themoviedb with %s',
item.get('title', '')) item.get('title', ''))
api_key = settings('themoviedbAPIKey') api_key = utils.settings('themoviedbAPIKey')
if media_type == v.PLEX_TYPE_SHOW: if media_type == v.PLEX_TYPE_SHOW:
media_type = 'tv' media_type = 'tv'
title = item.get('title', '') title = item.get('title', '')
@ -977,7 +971,7 @@ class API(object):
parameters = { parameters = {
'api_key': api_key, 'api_key': api_key,
'language': v.KODILANGUAGE, 'language': v.KODILANGUAGE,
'query': try_encode(title) 'query': utils.try_encode(title)
} }
data = DU().downloadUrl(url, data = DU().downloadUrl(url,
authenticate=False, authenticate=False,
@ -1103,8 +1097,8 @@ class API(object):
try: try:
data.get('poster_path') data.get('poster_path')
except AttributeError: except AttributeError:
LOG.debug('Could not find TheMovieDB poster paths for %s in ' LOG.debug('Could not find TheMovieDB poster paths for %s'
'the language %s', title, language) ' in the language %s', title, language)
continue continue
if not poster and data.get('poster_path'): if not poster and data.get('poster_path'):
poster = ('https://image.tmdb.org/t/p/original%s' % 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 media_id: IMDB id for movies, tvdb id for TV shows
""" """
api_key = settings('FanArtTVAPIKey') api_key = utils.settings('FanArtTVAPIKey')
typus = self.plex_type() typus = self.plex_type()
if typus == v.PLEX_TYPE_SHOW: if typus == v.PLEX_TYPE_SHOW:
typus = 'tv' typus = 'tv'
@ -1236,17 +1230,17 @@ class API(object):
count += 1 count += 1
if (count > 1 and ( if (count > 1 and (
(self.plex_type() != 'clip' and (self.plex_type() != 'clip' and
settings('bestQuality') == 'false') utils.settings('bestQuality') == 'false')
or or
(self.plex_type() == 'clip' and (self.plex_type() == 'clip' and
settings('bestTrailer') == 'false'))): utils.settings('bestTrailer') == 'false'))):
# Several streams/files available. # Several streams/files available.
dialoglist = [] dialoglist = []
for entry in self.item.iterfind('./Media'): for entry in self.item.iterfind('./Media'):
# Get additional info (filename / languages) # Get additional info (filename / languages)
filename = None filename = None
if 'file' in entry[0].attrib: 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 of audio streams
languages = [] languages = []
for stream in entry[0]: for stream in entry[0]:
@ -1255,12 +1249,13 @@ class API(object):
languages.append(stream.attrib['language']) languages.append(stream.attrib['language'])
languages = ', '.join(languages) languages = ', '.join(languages)
if filename: if filename:
option = try_encode(filename) option = utils.try_encode(filename)
if languages: if languages:
if option: if option:
option = '%s (%s): ' % (option, try_encode(languages)) option = '%s (%s): ' % (option,
utils.try_encode(languages))
else: else:
option = '%s: ' % try_encode(languages) option = '%s: ' % utils.try_encode(languages)
if 'videoResolution' in entry.attrib: if 'videoResolution' in entry.attrib:
option = '%s%sp ' % (option, option = '%s%sp ' % (option,
entry.get('videoResolution')) entry.get('videoResolution'))
@ -1275,7 +1270,7 @@ class API(object):
option = '%s%s ' % (option, option = '%s%s ' % (option,
entry.get('audioCodec')) entry.get('audioCodec'))
dialoglist.append(option) dialoglist.append(option)
media = dialog('select', 'Select stream', dialoglist) media = utils.dialog('select', 'Select stream', dialoglist)
else: else:
media = 0 media = 0
self.mediastream = media self.mediastream = media
@ -1306,7 +1301,7 @@ class API(object):
self.mediastream_number() self.mediastream_number()
if quality is None: if quality is None:
quality = {} quality = {}
xargs = client.getXArgsDeviceInfo() xargs = clientinfo.getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed # For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls # trailers are 'clip' with PMS xmls
if action == "DirectStream": if action == "DirectStream":
@ -1331,19 +1326,19 @@ class API(object):
transcode_path = self.server + \ transcode_path = self.server + \
'/video/:/transcode/universal/start.m3u8?' '/video/:/transcode/universal/start.m3u8?'
args = { args = {
'audioBoost': settings('audioBoost'), 'audioBoost': utils.settings('audioBoost'),
'autoAdjustQuality': 0, 'autoAdjustQuality': 0,
'directPlay': 0, 'directPlay': 0,
'directStream': 1, 'directStream': 1,
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': window('plex_client_Id'), 'session': utils.window('plex_client_Id'),
'fastSeek': 1, 'fastSeek': 1,
'path': path, 'path': path,
'mediaIndex': self.mediastream, 'mediaIndex': self.mediastream,
'partIndex': self.part, 'partIndex': self.part,
'hasMDE': 1, 'hasMDE': 1,
'location': 'lan', 'location': 'lan',
'subtitleSize': settings('subtitleSize') 'subtitleSize': utils.settings('subtitleSize')
} }
# Look like Android to let the PMS use the transcoding profile # Look like Android to let the PMS use the transcoding profile
xargs.update(headers) xargs.update(headers)
@ -1400,9 +1395,7 @@ class API(object):
Returns the path to the downloaded subtitle or None Returns the path to the downloaded subtitle or None
""" """
if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH): path = path_ops.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
response = DU().downloadUrl(url, return_response=True) response = DU().downloadUrl(url, return_response=True)
try: try:
response.status_code response.status_code
@ -1411,14 +1404,8 @@ class API(object):
return return
else: else:
LOG.debug('Writing temp subtitle to %s', path) LOG.debug('Writing temp subtitle to %s', path)
try: with open(path_ops.encode_path(path), 'wb') as filer:
with open(path, 'wb') as filer: filer.write(response.content)
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)
return path return path
def kodi_premiere_date(self): def kodi_premiere_date(self):
@ -1560,7 +1547,8 @@ class API(object):
listitem.setInfo('video', infoLabels=metadata) listitem.setInfo('video', infoLabels=metadata)
try: try:
# Add context menu entry for information screen # Add context menu entry for information screen
listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) listitem.addContextMenuItems([(utils.lang(30032),
'XBMC.Action(Info)',)])
except TypeError: except TypeError:
# Kodi fuck-up # Kodi fuck-up
pass pass
@ -1606,20 +1594,21 @@ class API(object):
# exist() needs a / or \ at the end to work for directories # exist() needs a / or \ at the end to work for directories
if folder is False: if folder is False:
# files # files
check = exists(try_encode(path)) check = exists(utils.try_encode(path))
else: else:
# directories # directories
if "\\" in path: checkpath = utils.try_encode(path)
if not path.endswith('\\'): if b"\\" in checkpath:
if not checkpath.endswith('\\'):
# Add the missing backslash # Add the missing backslash
check = exists_dir(path + "\\") check = utils.exists_dir(checkpath + "\\")
else: else:
check = exists_dir(path) check = utils.exists_dir(checkpath)
else: else:
if not path.endswith('/'): if not checkpath.endswith('/'):
check = exists_dir(path + "/") check = utils.exists_dir(checkpath + "/")
else: else:
check = exists_dir(path) check = utils.exists_dir(checkpath)
if not check: if not check:
if force_check is False: if force_check is False:
@ -1648,25 +1637,11 @@ class API(object):
LOG.warn('Cannot access file: %s', url) LOG.warn('Cannot access file: %s', url)
# Kodi cannot locate the file #s. Please verify your PKC settings. Stop # Kodi cannot locate the file #s. Please verify your PKC settings. Stop
# syncing? # syncing?
resp = dialog('yesno', heading='{plex}', line1=lang(39031) % url) resp = utils.dialog('yesno',
heading='{plex}',
line1=utils.lang(39031) % url)
return resp 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 @staticmethod
def _set_listitem_artprop(listitem, arttype, path): def _set_listitem_artprop(listitem, arttype, path):
if arttype in ( if arttype in (

View file

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

View file

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

View file

@ -1,15 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from logging import getLogger from logging import getLogger
from xbmc import sleep, executebuiltin from xbmc import sleep, executebuiltin
from downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from utils import dialog, language as lang, settings, try_encode from . import utils
import variables as v from . import variables as v
import state from . import state
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.plex_tx')
############################################################################### ###############################################################################
@ -39,7 +38,7 @@ def choose_home_user(token):
username = user['title'] username = user['title']
userlist.append(username) userlist.append(username)
# To take care of non-ASCII usernames # To take care of non-ASCII usernames
userlist_coded.append(try_encode(username)) userlist_coded.append(utils.try_encode(username))
usernumber = len(userlist) usernumber = len(userlist)
username = '' username = ''
usertoken = '' usertoken = ''
@ -47,12 +46,14 @@ def choose_home_user(token):
while trials < 3: while trials < 3:
if usernumber > 1: if usernumber > 1:
# Select user # Select user
user_select = dialog('select', lang(29999) + lang(39306), user_select = utils.dialog(
userlist_coded) 'select',
'%s%s' % (utils.lang(29999), utils.lang(39306)),
userlist_coded)
if user_select == -1: if user_select == -1:
LOG.info("No user selected.") LOG.info("No user selected.")
settings('username', value='') utils.settings('username', value='')
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID)
return False return False
# Only 1 user received, choose that one # Only 1 user received, choose that one
else: else:
@ -64,11 +65,11 @@ def choose_home_user(token):
pin = None pin = None
if user['protected'] == '1': if user['protected'] == '1':
LOG.debug('Asking for users PIN') LOG.debug('Asking for users PIN')
pin = dialog('input', pin = utils.dialog('input',
lang(39307) + selected_user, '%s%s' % (utils.lang(39307), selected_user),
'', '',
type='{numeric}', type='{numeric}',
option='{hide}') option='{hide}')
# User chose to cancel # User chose to cancel
# Plex bug: don't call url for protected user with empty PIN # Plex bug: don't call url for protected user with empty PIN
if not pin: if not pin:
@ -78,7 +79,7 @@ def choose_home_user(token):
result = switch_home_user(user['id'], result = switch_home_user(user['id'],
pin, pin,
token, token,
settings('plex_machineIdentifier')) utils.settings('plex_machineIdentifier'))
if result: if result:
# Successfully retrieved username: break out of while loop # Successfully retrieved username: break out of while loop
username = result['username'] username = result['username']
@ -88,15 +89,16 @@ def choose_home_user(token):
else: else:
trials += 1 trials += 1
# Could not login user, please try again # Could not login user, please try again
if not dialog('yesno', if not utils.dialog('yesno',
heading='{plex}', heading='{plex}',
line1=lang(39308) + selected_user, line1='%s%s' % (utils.lang(39308),
line2=lang(39309)): selected_user),
line2=utils.lang(39309)):
# User chose to cancel # User chose to cancel
break break
if not username: if not username:
LOG.error('Failed signing in a user to plex.tv') 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 False
return { return {
'username': username, 'username': username,
@ -123,7 +125,7 @@ def switch_home_user(userid, pin, token, machineIdentifier):
for the machineIdentifier that was chosen 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) LOG.info('Switching to user %s', userid)
url = 'https://plex.tv/api/home/users/' + userid + '/switch' 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', '') token = answer.attrib.get('authenticationToken', '')
# Write to settings file # Write to settings file
settings('username', username) utils.settings('username', username)
settings('accessToken', token) utils.settings('accessToken', token)
settings('userid', answer.attrib.get('id', '')) utils.settings('userid', answer.attrib.get('id', ''))
settings('plex_restricteduser', utils.settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1' 'true' if answer.attrib.get('restricted', '0') == '1'
else 'false') else 'false')
state.RESTRICTED_USER = True if \ state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False answer.attrib.get('restricted', '0') == '1' else False
@ -239,15 +241,15 @@ def sign_in_with_pin():
code, identifier = get_pin() code, identifier = get_pin()
if not code: if not code:
# Problems trying to contact plex.tv. Try again later # 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 return False
# Go to https://plex.tv/pin and enter the code: # Go to https://plex.tv/pin and enter the code:
# Or press No to cancel the sign in. # Or press No to cancel the sign in.
answer = dialog('yesno', answer = utils.dialog('yesno',
heading='{plex}', heading='{plex}',
line1=lang(39304) + "\n\n", line1='%s%s' % (utils.lang(39304), "\n\n"),
line2=code + "\n\n", line2='%s%s' % (code, "\n\n"),
line3=lang(39311)) line3=utils.lang(39311))
if not answer: if not answer:
return False return False
count = 0 count = 0
@ -261,7 +263,7 @@ def sign_in_with_pin():
count += 1 count += 1
if xml is False: if xml is False:
# Could not sign in to plex.tv Try again later # 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 return False
# Parse xml # Parse xml
userid = xml.attrib.get('id') userid = xml.attrib.get('id')
@ -282,15 +284,15 @@ def sign_in_with_pin():
'plexid': userid, 'plexid': userid,
'homesize': home_size 'homesize': home_size
} }
settings('plexLogin', username) utils.settings('plexLogin', username)
settings('plexToken', token) utils.settings('plexToken', token)
settings('plexhome', home) utils.settings('plexhome', home)
settings('plexid', userid) utils.settings('plexid', userid)
settings('plexAvatar', avatar) utils.settings('plexAvatar', avatar)
settings('plexHomeSize', home_size) utils.settings('plexHomeSize', home_size)
# Let Kodi log into plex.tv on startup from now on # Let Kodi log into plex.tv on startup from now on
settings('myplexlogin', 'true') utils.settings('myplexlogin', 'true')
settings('plex_status', value=lang(39227)) utils.settings('plex_status', value=utils.lang(39227))
return result 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 SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs from urlparse import urlparse, parse_qs
import xbmc
from xbmc import sleep, Player, Monitor from .. import companion
from .. import json_rpc as js
from companion import process_command from .. import clientinfo
import json_rpc as js from .. import variables as v
from clientinfo import getXArgsDeviceInfo
import variables as v
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.listener')
PLAYER = Player() PLAYER = xbmc.Player()
MONITOR = Monitor() MONITOR = xbmc.Monitor()
# Hack we need in order to keep track of the open connections from Plex Web # Hack we need in order to keep track of the open connections from Plex Web
CLIENT_DICT = {} CLIENT_DICT = {}
@ -122,7 +121,7 @@ class MyHandler(BaseHTTPRequestHandler):
RESOURCES_XML.format( RESOURCES_XML.format(
title=v.DEVICENAME, title=v.DEVICENAME,
machineIdentifier=v.PKC_MACHINE_IDENTIFIER), machineIdentifier=v.PKC_MACHINE_IDENTIFIER),
getXArgsDeviceInfo(include_token=False)) clientinfo.getXArgsDeviceInfo(include_token=False))
elif request_path == 'player/timeline/poll': elif request_path == 'player/timeline/poll':
# Plex web does polling if connected to PKC via Companion # Plex web does polling if connected to PKC via Companion
# Only reply if there is indeed something playing # Only reply if there is indeed something playing
@ -188,7 +187,7 @@ class MyHandler(BaseHTTPRequestHandler):
code=500) code=500)
elif "/subscribe" in request_path: elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE, self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False)) clientinfo.getXArgsDeviceInfo(include_token=False))
protocol = params.get('protocol') protocol = params.get('protocol')
host = self.client_address[0] host = self.client_address[0]
port = params.get('port') port = params.get('port')
@ -201,14 +200,14 @@ class MyHandler(BaseHTTPRequestHandler):
command_id) command_id)
elif "/unsubscribe" in request_path: elif "/unsubscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE, self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False)) clientinfo.getXArgsDeviceInfo(include_token=False))
uuid = self.headers.get('X-Plex-Client-Identifier') \ uuid = self.headers.get('X-Plex-Client-Identifier') \
or self.client_address[0] or self.client_address[0]
sub_mgr.remove_subscriber(uuid) sub_mgr.remove_subscriber(uuid)
else: else:
# Throw it to companion.py # Throw it to companion.py
process_command(request_path, params) companion.process_command(request_path, params)
self.response('', getXArgsDeviceInfo(include_token=False)) self.response('', clientinfo.getXArgsDeviceInfo(include_token=False))
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):

View file

@ -25,16 +25,15 @@ import logging
import socket import socket
import threading import threading
import time import time
from xbmc import sleep from xbmc import sleep
import downloadutils from ..downloadutils import DownloadUtils as DU
from utils import window, settings, dialog, language from .. import utils
import variables as v 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._multicast_address = '239.0.0.250'
self.discover_group = (self._multicast_address, 32414) self.discover_group = (self._multicast_address, 32414)
self.client_register_group = (self._multicast_address, 32413) 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.server_list = []
self.discovery_interval = 120 self.discovery_interval = 120
@ -58,7 +57,7 @@ class plexgdm:
self._registration_is_running = False self._registration_is_running = False
self.client_registered = False self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl self.download = DU().downloadUrl
def clientDetails(self): def clientDetails(self):
self.client_data = ( self.client_data = (
@ -120,14 +119,15 @@ class plexgdm:
log.error("Unable to bind to port [%s] - Plex Companion will not " log.error("Unable to bind to port [%s] - Plex Companion will not "
"be registered. Change the Plex Companion update port!" "be registered. Change the Plex Companion update port!"
% self.client_update_port) % self.client_update_port)
if settings('companion_show_gdm_port_warning') == 'true': if utils.settings('companion_show_gdm_port_warning') == 'true':
if dialog('yesno', if utils.dialog('yesno',
language(29999), utils.lang(29999),
'Port %s' % self.client_update_port, 'Port %s' % self.client_update_port,
language(39079), utils.lang(39079),
yeslabel=language(30013), # Never show again yeslabel=utils.lang(30013), # Never show again
nolabel=language(30012)): # OK nolabel=utils.lang(30012)): # OK
settings('companion_show_gdm_port_warning', value='false') utils.settings('companion_show_gdm_port_warning',
value='false')
from xbmc import executebuiltin from xbmc import executebuiltin
executebuiltin( executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)') 'Addon.OpenSettings(plugin.video.plexkodiconnect)')
@ -189,7 +189,7 @@ class plexgdm:
log.info("Server list is empty. Unable to check") log.info("Server list is empty. Unable to check")
return False return False
for server in self.server_list: for server in self.server_list:
if server['uuid'] == window('plex_machineIdentifier'): if server['uuid'] == utils.window('plex_machineIdentifier'):
media_server = server['server'] media_server = server['server']
media_port = server['port'] media_port = server['port']
scheme = server['protocol'] scheme = server['protocol']
@ -223,7 +223,7 @@ class plexgdm:
return self.server_list return self.server_list
def discover(self): def discover(self):
currServer = window('pms_server') currServer = utils.window('pms_server')
if not currServer: if not currServer:
return return
currServerProt, currServerIP, currServerPort = \ currServerProt, currServerIP, currServerPort = \
@ -240,9 +240,9 @@ class plexgdm:
'owned': '1', 'owned': '1',
'role': 'master', 'role': 'master',
'server': currServerIP, 'server': currServerIP,
'serverName': window('plex_servername'), 'serverName': utils.window('plex_servername'),
'updated': int(time.time()), 'updated': int(time.time()),
'uuid': window('plex_machineIdentifier'), 'uuid': utils.window('plex_machineIdentifier'),
'version': 'irrelevant' 'version': 'irrelevant'
}] }]
@ -305,5 +305,5 @@ class plexgdm:
def start_all(self, daemon=False): def start_all(self, daemon=False):
self.start_discovery(daemon) self.start_discovery(daemon)
if settings('plexCompanion') == 'true': if utils.settings('plexCompanion') == 'true':
self.start_registration(daemon) 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. subscribed Plex Companion clients.
""" """
from logging import getLogger from logging import getLogger
from threading import Thread, RLock from threading import Thread
from downloadutils import DownloadUtils as DU from ..downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis, LockFunction from .. import utils
import state from .. import state
import variables as v from .. import variables as v
import json_rpc as js from .. import json_rpc as js
import playqueue as PQ from .. import playqueue as PQ
############################################################################### ###############################################################################
LOG = getLogger('PLEX.subscribers')
LOG = getLogger("PLEX." + __name__)
# Need to lock all methods and functions messing with subscribers or state
LOCK = RLock()
LOCKER = LockFunction(LOCK)
############################################################################### ###############################################################################
# What is Companion controllable? # What is Companion controllable?
@ -150,7 +145,6 @@ class SubscriptionMgr(object):
position = info['position'] position = info['position']
return position return position
@LOCKER.lockthis
def msg(self, players): def msg(self, players):
""" """
Returns a timeline xml as str Returns a timeline xml as str
@ -190,94 +184,98 @@ class SubscriptionMgr(object):
return answ return answ
def _timeline_dict(self, player, ptype): def _timeline_dict(self, player, ptype):
playerid = player['playerid'] with state.LOCK_PLAYQUEUES:
info = state.PLAYER_STATES[playerid] playerid = player['playerid']
playqueue = PQ.PLAYQUEUES[playerid] info = state.PLAYER_STATES[playerid]
position = self._get_correct_position(info, playqueue) playqueue = PQ.PLAYQUEUES[playerid]
try: position = self._get_correct_position(info, playqueue)
item = playqueue.items[position] try:
except IndexError: item = playqueue.items[position]
# E.g. for direct path playback for single item except IndexError:
return { # 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], 'controllable': CONTROLLABLE[ptype],
'protocol': self.protocol,
'address': self.server,
'port': self.port,
'machineIdentifier': utils.window('plex_machineIdentifier'),
'state': status,
'type': ptype, '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): # Get the plex id from the PKC playqueue not info, as Kodi jumps to
self.location = 'fullScreenVideo' # next playqueue element way BEFORE kodi monitor onplayback is
self.stop_sent_to_web = False # called
pbmc_server = window('pms_server') if item.plex_id:
if pbmc_server: answ['key'] = '/library/metadata/%s' % item.plex_id
(self.protocol, self.server, self.port) = pbmc_server.split(':') answ['ratingKey'] = item.plex_id
self.server = self.server.replace('/', '') # PlayQueue stuff
status = 'paused' if int(info['speed']) == 0 else 'playing' if info['container_key']:
duration = kodi_time_to_millis(info['totaltime']) answ['containerKey'] = info['container_key']
shuffle = '1' if info['shuffled'] else '0' if (info['container_key'] is not None and
mute = '1' if info['muted'] is True else '0' info['container_key'].startswith('/playQueues')):
answ = { answ['playQueueID'] = playqueue.id
'controllable': CONTROLLABLE[ptype], answ['playQueueVersion'] = playqueue.version
'protocol': self.protocol, answ['playQueueItemID'] = item.id
'address': self.server, if playqueue.items[position].guid:
'port': self.port, answ['guid'] = item.guid
'machineIdentifier': window('plex_machineIdentifier'), # Temp. token set?
'state': status, if state.PLEX_TRANSIENT_TOKEN:
'type': ptype, answ['token'] = state.PLEX_TRANSIENT_TOKEN
'itemType': ptype, elif playqueue.plex_transient_token:
'time': kodi_time_to_millis(info['time']), answ['token'] = playqueue.plex_transient_token
'duration': duration, # Process audio and subtitle streams
'seekRange': '0-%s' % duration, if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
'shuffle': shuffle, strm_id = self._plex_stream_index(playerid, 'audio')
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']], if strm_id:
'volume': info['volume'], answ['audioStreamID'] = strm_id
'mute': mute, else:
'mediaIndex': 0, # Still to implement from here LOG.error('We could not select a Plex audiostream')
'partIndex':0, strm_id = self._plex_stream_index(playerid, 'video')
'partCount': 1, if strm_id:
'providerIdentifier': 'com.plexapp.plugins.library', answ['videoStreamID'] = strm_id
} else:
# Get the plex id from the PKC playqueue not info, as Kodi jumps to next LOG.error('We could not select a Plex videostream')
# playqueue element way BEFORE kodi monitor onplayback is called if info['subtitleenabled']:
if item.plex_id: try:
answ['key'] = '/library/metadata/%s' % item.plex_id strm_id = self._plex_stream_index(playerid, 'subtitle')
answ['ratingKey'] = item.plex_id except KeyError:
# PlayQueue stuff # subtitleenabled can be True while currentsubtitle can
if info['container_key']: # still be {}
answ['containerKey'] = info['container_key'] strm_id = None
if (info['container_key'] is not None and if strm_id is not None:
info['container_key'].startswith('/playQueues')): # If None, then the subtitle is only present on Kodi
answ['playQueueID'] = playqueue.id # side
answ['playQueueVersion'] = playqueue.version answ['subtitleStreamID'] = strm_id
answ['playQueueItemID'] = item.id return answ
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): def signal_stop(self):
""" """
@ -302,14 +300,14 @@ class SubscriptionMgr(object):
return playqueue.items[position].plex_stream_index( return playqueue.items[position].plex_stream_index(
info[STREAM_DETAILS[stream_type]]['index'], stream_type) info[STREAM_DETAILS[stream_type]]['index'], stream_type)
@LOCKER.lockthis
def update_command_id(self, uuid, command_id): def update_command_id(self, uuid, command_id):
""" """
Updates the Plex Companien client with the machine identifier uuid with Updates the Plex Companien client with the machine identifier uuid with
command_id command_id
""" """
if command_id and self.subscribers.get(uuid): with state.LOCK_SUBSCRIBER:
self.subscribers[uuid].command_id = int(command_id) if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
def _playqueue_init_done(self, players): def _playqueue_init_done(self, players):
""" """
@ -320,8 +318,6 @@ class SubscriptionMgr(object):
for player in players.values(): for player in players.values():
info = state.PLAYER_STATES[player['playerid']] info = state.PLAYER_STATES[player['playerid']]
playqueue = PQ.PLAYQUEUES[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) position = self._get_correct_position(info, playqueue)
try: try:
item = playqueue.items[position] item = playqueue.items[position]
@ -334,34 +330,32 @@ class SubscriptionMgr(object):
return False return False
return True return True
@LOCKER.lockthis
def notify(self): def notify(self):
""" """
Causes PKC to tell the PMS and Plex Companion players to receive a Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played. notification what's being played.
""" """
self._cleanup() with state.LOCK_SUBSCRIBER:
# Get all the active/playing Kodi players (video, audio, pictures) self._cleanup()
players = js.get_players() # Get all the active/playing Kodi players (video, audio, pictures)
# Update the PKC info with what's playing on the Kodi side players = js.get_players()
for player in players.values(): # Update the PKC info with what's playing on the Kodi side
update_player_info(player['playerid']) for player in players.values():
# Check whether we can use the CURRENT info or whether PKC is still update_player_info(player['playerid'])
# initializing # Check whether we can use the CURRENT info or whether PKC is still
if self._playqueue_init_done(players) is False: # initializing
LOG.debug('PKC playqueue is still initializing - skipping update') if self._playqueue_init_done(players) is False:
return LOG.debug('PKC playqueue is still initializing - skip update')
self._notify_server(players) return
if self.subscribers: self._notify_server(players)
msg = self.msg(players) if self.subscribers:
for subscriber in self.subscribers.values(): msg = self.msg(players)
subscriber.send_update(msg) for subscriber in self.subscribers.values():
self.lastplayers = players subscriber.send_update(msg)
self.lastplayers = players
def _notify_server(self, players): def _notify_server(self, players):
for typus, player in players.iteritems(): for typus, player in players.iteritems():
LOG.debug('player is %s', player)
LOG.debug('typus is %s', typus)
self._send_pms_notification( self._send_pms_notification(
player['playerid'], self._get_pms_params(player['playerid'])) player['playerid'], self._get_pms_params(player['playerid']))
try: try:
@ -386,8 +380,8 @@ class SubscriptionMgr(object):
'state': status, 'state': status,
'ratingKey': item.plex_id, 'ratingKey': item.plex_id,
'key': '/library/metadata/%s' % item.plex_id, 'key': '/library/metadata/%s' % item.plex_id,
'time': kodi_time_to_millis(info['time']), 'time': utils.kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime']) 'duration': utils.kodi_time_to_millis(info['totaltime'])
} }
if info['container_key'] is not None: if info['container_key'] is not None:
# params['containerKey'] = info['container_key'] # params['containerKey'] = info['container_key']
@ -419,7 +413,6 @@ class SubscriptionMgr(object):
LOG.debug("Sent server notification with parameters: %s to %s", LOG.debug("Sent server notification with parameters: %s to %s",
xargs, url) xargs, url)
@LOCKER.lockthis
def add_subscriber(self, protocol, host, port, uuid, command_id): def add_subscriber(self, protocol, host, port, uuid, command_id):
""" """
Adds a new Plex Companion subscriber to PKC. Adds a new Plex Companion subscriber to PKC.
@ -431,20 +424,21 @@ class SubscriptionMgr(object):
command_id, command_id,
self, self,
self.request_mgr) self.request_mgr)
self.subscribers[subscriber.uuid] = subscriber with state.LOCK_SUBSCRIBER:
self.subscribers[subscriber.uuid] = subscriber
return subscriber return subscriber
@LOCKER.lockthis
def remove_subscriber(self, uuid): def remove_subscriber(self, uuid):
""" """
Removes a connected Plex Companion subscriber with machine identifier Removes a connected Plex Companion subscriber with machine identifier
uuid from PKC notifications. uuid from PKC notifications.
(Calls the cleanup() method of the subscriber) (Calls the cleanup() method of the subscriber)
""" """
for subscriber in self.subscribers.values(): with state.LOCK_SUBSCRIBER:
if subscriber.uuid == uuid or subscriber.host == uuid: for subscriber in self.subscribers.values():
subscriber.cleanup() if subscriber.uuid == uuid or subscriber.host == uuid:
del self.subscribers[subscriber.uuid] subscriber.cleanup()
del self.subscribers[subscriber.uuid]
def _cleanup(self): def _cleanup(self):
for subscriber in self.subscribers.values(): for subscriber in self.subscribers.values():

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from . import utils
from utils import kodi_sql from . import variables as v
import variables as v
############################################################################### ###############################################################################
@ -17,7 +15,7 @@ class Get_Plex_DB():
and the db gets closed and the db gets closed
""" """
def __enter__(self): def __enter__(self):
self.plexconn = kodi_sql('plex') self.plexconn = utils.kodi_sql('plex')
return Plex_DB_Functions(self.plexconn.cursor()) return Plex_DB_Functions(self.plexconn.cursor())
def __exit__(self, type, value, traceback): 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 -*- # -*- coding: utf-8 -*-
# THREAD SAFE # 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 # Quit PKC
STOP_PKC = False STOP_PKC = False

View file

@ -3,24 +3,24 @@
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
from xbmc import sleep, executebuiltin, translatePath from xbmc import sleep, executebuiltin
import xbmcaddon
from xbmcvfs import exists
from utils import window, settings, language as lang, thread_methods, dialog from .downloadutils import DownloadUtils as DU
from downloadutils import DownloadUtils as DU from . import utils
import plex_tv from . import path_ops
import PlexFunctions as PF from . import plex_tv
import state 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): class UserClient(Thread):
""" """
Manage Plex users Manage Plex users
@ -44,7 +44,6 @@ class UserClient(Thread):
self.ssl = None self.ssl = None
self.sslcert = None self.sslcert = None
self.addon = xbmcaddon.Addon()
self.do_utils = None self.do_utils = None
Thread.__init__(self) Thread.__init__(self)
@ -54,11 +53,11 @@ class UserClient(Thread):
Get the current PMS' URL Get the current PMS' URL
""" """
# Original host # Original host
self.server_name = settings('plex_servername') self.server_name = utils.settings('plex_servername')
https = settings('https') == "true" https = utils.settings('https') == "true"
host = settings('ipaddress') host = utils.settings('ipaddress')
port = settings('port') port = utils.settings('port')
self.machine_identifier = settings('plex_machineIdentifier') self.machine_identifier = utils.settings('plex_machineIdentifier')
if not host: if not host:
LOG.debug("No server information saved.") LOG.debug("No server information saved.")
return False return False
@ -74,7 +73,8 @@ class UserClient(Thread):
self.machine_identifier = PF.GetMachineIdentifier(server) self.machine_identifier = PF.GetMachineIdentifier(server)
if not self.machine_identifier: if not self.machine_identifier:
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) LOG.debug('Returning active server: %s', server)
return server return server
@ -84,15 +84,15 @@ class UserClient(Thread):
Do we need to verify the SSL certificate? Return None if that is the Do we need to verify the SSL certificate? Return None if that is the
case, else False case, else False
""" """
return None if settings('sslverify') == 'true' else False return None if utils.settings('sslverify') == 'true' else False
@staticmethod @staticmethod
def get_ssl_certificate(): def get_ssl_certificate():
""" """
Client side certificate Client side certificate
""" """
return None if settings('sslcert') == 'None' \ return None if utils.settings('sslcert') == 'None' \
else settings('sslcert') else utils.settings('sslcert')
def set_user_prefs(self): def set_user_prefs(self):
""" """
@ -103,7 +103,7 @@ class UserClient(Thread):
if self.token: if self.token:
url = PF.GetUserArtworkURL(self.user) url = PF.GetUserArtworkURL(self.user)
if url: if url:
window('PlexUserImage', value=url) utils.window('PlexUserImage', value=url)
@staticmethod @staticmethod
def check_access(): def check_access():
@ -141,29 +141,32 @@ class UserClient(Thread):
state.PLEX_USER_ID = user_id or None state.PLEX_USER_ID = user_id or None
state.PLEX_USERNAME = username state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '') # 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 state.PMS_TOKEN = usertoken
# This is the token for plex.tv for the current user # This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv # Is only '' if user is not signed in to plex.tv
window('plex_token', value=settings('plexToken')) utils.window('plex_token', value=utils.settings('plexToken'))
state.PLEX_TOKEN = settings('plexToken') or None state.PLEX_TOKEN = utils.settings('plexToken') or None
window('plex_restricteduser', value=settings('plex_restricteduser')) utils.window('plex_restricteduser',
value=utils.settings('plex_restricteduser'))
state.RESTRICTED_USER = True \ state.RESTRICTED_USER = True \
if settings('plex_restricteduser') == 'true' else False if utils.settings('plex_restricteduser') == 'true' else False
window('pms_server', value=self.server) utils.window('pms_server', value=self.server)
window('plex_machineIdentifier', value=self.machine_identifier) utils.window('plex_machineIdentifier', value=self.machine_identifier)
window('plex_servername', value=self.server_name) utils.window('plex_servername', value=self.server_name)
window('plex_authenticated', value='true') utils.window('plex_authenticated', value='true')
state.AUTHENTICATED = True state.AUTHENTICATED = True
window('useDirectPaths', value='true' utils.window('useDirectPaths',
if settings('useDirectPaths') == "1" else 'false') value='true' if utils.settings('useDirectPaths') == "1"
state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \ else 'false')
state.DIRECT_PATHS = True if utils.settings('useDirectPaths') == "1" \
else False else False
state.INDICATE_MEDIA_VERSIONS = True \ state.INDICATE_MEDIA_VERSIONS = True \
if settings('indicate_media_versions') == "true" else False if utils.settings('indicate_media_versions') == "true" else False
window('plex_force_transcode_pix', value='true' utils.window('plex_force_transcode_pix',
if settings('force_transcode_pix') == "1" else 'false') value='true' if utils.settings('force_transcode_pix') == "1"
else 'false')
# Start DownloadUtils session # Start DownloadUtils session
self.do_utils = DU() self.do_utils = DU()
@ -173,9 +176,9 @@ class UserClient(Thread):
self.set_user_prefs() self.set_user_prefs()
# Writing values to settings file # Writing values to settings file
settings('username', value=username) utils.settings('username', value=username)
settings('userid', value=user_id) utils.settings('userid', value=user_id)
settings('accessToken', value=usertoken) utils.settings('accessToken', value=usertoken)
return True return True
def authenticate(self): def authenticate(self):
@ -188,16 +191,13 @@ class UserClient(Thread):
if self.retry >= 2: if self.retry >= 2:
LOG.error("Too many retries to login.") LOG.error("Too many retries to login.")
state.PMS_STATUS = 'Stop' state.PMS_STATUS = 'Stop'
dialog('ok', lang(33001), lang(39023)) utils.dialog('ok', utils.lang(33001), utils.lang(39023))
executebuiltin( executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)') 'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
return False return False
# Get /profile/addon_data
addondir = translatePath(self.addon.getAddonInfo('profile'))
# If there's no settings.xml # 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.") LOG.error("Error, no settings.xml found.")
self.auth = False self.auth = False
return False return False
@ -209,10 +209,10 @@ class UserClient(Thread):
return False return False
# If there is a username in the settings, try authenticating # If there is a username in the settings, try authenticating
username = settings('username') username = utils.settings('username')
userId = settings('userid') userId = utils.settings('userid')
usertoken = settings('accessToken') usertoken = utils.settings('accessToken')
enforceLogin = settings('enforceUserLogin') enforceLogin = utils.settings('enforceUserLogin')
# Found a user in the settings, try to authenticate # Found a user in the settings, try to authenticate
if username and enforceLogin == 'false': if username and enforceLogin == 'false':
LOG.debug('Trying to authenticate with old settings') LOG.debug('Trying to authenticate with old settings')
@ -225,15 +225,15 @@ class UserClient(Thread):
return True return True
elif answ == 401: elif answ == 401:
LOG.error("User token no longer valid. Sign user out") LOG.error("User token no longer valid. Sign user out")
settings('username', value='') utils.settings('username', value='')
settings('userid', value='') utils.settings('userid', value='')
settings('accessToken', value='') utils.settings('accessToken', value='')
else: else:
LOG.debug("Could not yet authenticate user") LOG.debug("Could not yet authenticate user")
return False return False
# Could not use settings - try to get Plex user list from plex.tv # Could not use settings - try to get Plex user list from plex.tv
plextoken = settings('plexToken') plextoken = utils.settings('plexToken')
if plextoken: if plextoken:
LOG.info("Trying to connect to plex.tv to get a user list") LOG.info("Trying to connect to plex.tv to get a user list")
userInfo = plex_tv.choose_home_user(plextoken) userInfo = plex_tv.choose_home_user(plextoken)
@ -268,24 +268,24 @@ class UserClient(Thread):
self.do_utils.stopSession() self.do_utils.stopSession()
except AttributeError: except AttributeError:
pass pass
window('plex_authenticated', clear=True) utils.window('plex_authenticated', clear=True)
state.AUTHENTICATED = False state.AUTHENTICATED = False
window('pms_token', clear=True) utils.window('pms_token', clear=True)
state.PLEX_TOKEN = None state.PLEX_TOKEN = None
state.PLEX_TRANSIENT_TOKEN = None state.PLEX_TRANSIENT_TOKEN = None
state.PMS_TOKEN = None state.PMS_TOKEN = None
window('plex_token', clear=True) utils.window('plex_token', clear=True)
window('pms_server', clear=True) utils.window('pms_server', clear=True)
window('plex_machineIdentifier', clear=True) utils.window('plex_machineIdentifier', clear=True)
window('plex_servername', clear=True) utils.window('plex_servername', clear=True)
state.PLEX_USER_ID = None state.PLEX_USER_ID = None
state.PLEX_USERNAME = None state.PLEX_USERNAME = None
window('plex_restricteduser', clear=True) utils.window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False state.RESTRICTED_USER = False
settings('username', value='') utils.settings('username', value='')
settings('userid', value='') utils.settings('userid', value='')
settings('accessToken', value='') utils.settings('accessToken', value='')
self.token = None self.token = None
self.auth = True self.auth = True
@ -313,7 +313,7 @@ class UserClient(Thread):
elif state.PMS_STATUS == "401": elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token # Unauthorized access, revoke token
state.PMS_STATUS = 'Auth' state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth') utils.window('plex_serverStatus', value='Auth')
self.reset_client() self.reset_client()
sleep(3000) sleep(3000)
@ -330,7 +330,7 @@ class UserClient(Thread):
LOG.info("Current userId: %s", state.PLEX_USER_ID) LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0 self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True) utils.window('plex_serverStatus', clear=True)
state.PMS_STATUS = False state.PMS_STATUS = False
if not self.auth and (self.user is None): 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 from logging import getLogger
import os
from cProfile import Profile from cProfile import Profile
from pstats import Stats from pstats import Stats
from sqlite3 import connect, OperationalError from sqlite3 import connect, OperationalError
@ -14,31 +13,38 @@ from time import localtime, strftime
from unicodedata import normalize from unicodedata import normalize
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from functools import wraps, partial from functools import wraps, partial
from shutil import rmtree
from urllib import quote_plus from urllib import quote_plus
import hashlib import hashlib
import re import re
import unicodedata
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
from xbmcvfs import exists, delete
import variables as v from . import path_ops
import state from . import variables as v
from . import state
############################################################################### ###############################################################################
LOG = getLogger("PLEX." + __name__) LOG = getLogger('PLEX.utils')
WINDOW = xbmcgui.Window(10000) WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
EPOCH = datetime.utcfromtimestamp(0) EPOCH = datetime.utcfromtimestamp(0)
# Grab Plex id from '...plex_id=XXXX....'
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''') 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+$''') 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 # Main methods
@ -51,7 +57,7 @@ def reboot_kodi(message=None):
Set optional custom message Set optional custom message
""" """
message = message or language(33033) message = message or lang(33033)
dialog('ok', heading='{plex}', line1=message) dialog('ok', heading='{plex}', line1=message)
xbmc.executebuiltin('RestartApp') xbmc.executebuiltin('RestartApp')
@ -106,31 +112,7 @@ def settings(setting, value=None):
return try_decode(addon.getSetting(setting)) return try_decode(addon.getSetting(setting))
def exists_dir(path): def lang(stringid):
"""
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):
""" """
Central string retrieval from strings.po Central string retrieval from strings.po
""" """
@ -194,7 +176,7 @@ def dialog(typus, *args, **kwargs):
kwargs['option'] = types[kwargs['option']] kwargs['option'] = types[kwargs['option']]
if 'heading' in kwargs: if 'heading' in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}", kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999)) lang(29999))
dia = xbmcgui.Dialog() dia = xbmcgui.Dialog()
types = { types = {
'yesno': dia.yesno, 'yesno': dia.yesno,
@ -313,10 +295,6 @@ def valid_filename(text):
else: else:
# Linux # Linux
text = re.sub(r'/', '', text) 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 # Ensure that filename length is at most 255 chars (including 3 chars for
# filename extension and 1 dot to separate the extension) # filename extension and 1 dot to separate the extension)
text = text[:min(len(text), 251)] text = text[:min(len(text), 251)]
@ -465,15 +443,15 @@ def wipe_database():
# Delete all synced playlists # Delete all synced playlists
for path in playlist_paths: for path in playlist_paths:
try: try:
os.remove(path) path_ops.remove(path)
except (OSError, IOError): except (OSError, IOError):
pass pass
LOG.info("Resetting all cached artwork.") LOG.info("Resetting all cached artwork.")
# Remove all existing textures first # Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/") path = path_ops.translate_path("special://thumbnails/")
if exists(path): if path_ops.exists(path):
rmtree(try_decode(path), ignore_errors=True) path_ops.rmtree(path, ignore_errors=True)
# remove all existing data from texture DB # remove all existing data from texture DB
connection = kodi_sql('texture') connection = kodi_sql('texture')
cursor = connection.cursor() cursor = connection.cursor()
@ -487,8 +465,8 @@ def wipe_database():
connection.commit() connection.commit()
cursor.close() cursor.close()
# Reset the artwork sync status in the PKC settings # Reset the artwork sync status in the PKC settings
settings('caching_artwork_count', value=language(39310)) settings('caching_artwork_count', value=lang(39310))
settings('fanarttv_lookups', value=language(39310)) settings('fanarttv_lookups', value=lang(39310))
# reset the install run flag # reset the install run flag
settings('SyncInstallRunDone', value="false") settings('SyncInstallRunDone', value="false")
@ -500,8 +478,8 @@ def reset(ask_user=True):
""" """
# Are you sure you want to reset your local Kodi database? # Are you sure you want to reset your local Kodi database?
if ask_user and not dialog('yesno', if ask_user and not dialog('yesno',
heading='{plex} %s ' % language(30132), heading='{plex} %s ' % lang(30132),
line1=language(39600)): line1=lang(39600)):
return return
# first stop any db sync # first stop any db sync
@ -513,8 +491,8 @@ def reset(ask_user=True):
if count == 0: if count == 0:
# Could not stop the database from running. Please try again later. # Could not stop the database from running. Please try again later.
dialog('ok', dialog('ok',
heading='{plex} %s' % language(30132), heading='{plex} %s' % lang(30132),
line1=language(39601)) line1=lang(39601))
return return
xbmc.sleep(1000) xbmc.sleep(1000)
@ -524,13 +502,11 @@ def reset(ask_user=True):
# Reset all PlexKodiConnect Addon settings? (this is usually NOT # Reset all PlexKodiConnect Addon settings? (this is usually NOT
# recommended and unnecessary!) # recommended and unnecessary!)
if ask_user and dialog('yesno', if ask_user and dialog('yesno',
heading='{plex} %s ' % language(30132), heading='{plex} %s ' % lang(30132),
line1=language(39603)): line1=lang(39603)):
# Delete the settings # Delete the settings
addon = xbmcaddon.Addon()
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
LOG.info("Deleting: settings.xml") LOG.info("Deleting: settings.xml")
os.remove("%ssettings.xml" % addondir) path_ops.remove("%ssettings.xml" % v.ADDON_PROFILE)
reboot_kodi() reboot_kodi()
@ -592,29 +568,6 @@ def compare_version(current, minimum):
return curr_patch >= min_patch 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): def normalize_string(text):
""" """
For theme media, do not modify unless modified in TV Tunes For theme media, do not modify unless modified in TV Tunes
@ -636,6 +589,28 @@ def normalize_string(text):
return 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): def indent(elem, level=0):
""" """
Prettifies xml trees. Pass the etree root in Prettifies xml trees. Pass the etree root in
@ -682,9 +657,9 @@ class XmlKodiSetting(object):
top_element=None): top_element=None):
self.filename = filename self.filename = filename
if path is None: if path is None:
self.path = os.path.join(v.KODI_PROFILE, filename) self.path = path_ops.path.join(v.KODI_PROFILE, filename)
else: else:
self.path = os.path.join(path, filename) self.path = path_ops.path.join(path, filename)
self.force_create = force_create self.force_create = force_create
self.top_element = top_element self.top_element = top_element
self.tree = None self.tree = None
@ -708,7 +683,7 @@ class XmlKodiSetting(object):
LOG.error('Error parsing %s', self.path) LOG.error('Error parsing %s', self.path)
# "Kodi cannot parse {0}. PKC will not function correctly. Please # "Kodi cannot parse {0}. PKC will not function correctly. Please
# visit {1} and correct your file!" # visit {1} and correct your file!"
dialog('ok', language(29999), language(39716).format( dialog('ok', lang(29999), lang(39716).format(
self.filename, self.filename,
'http://kodi.wiki')) 'http://kodi.wiki'))
self.__exit__(etree.ParseError, None, None) self.__exit__(etree.ParseError, None, None)
@ -849,7 +824,7 @@ def passwords_xml():
""" """
To add network credentials to Kodi's password 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 xmlpath = "%spasswords.xml" % path
try: try:
xmlparse = etree.parse(xmlpath) xmlparse = etree.parse(xmlpath)
@ -861,7 +836,7 @@ def passwords_xml():
LOG.error('Error parsing %s', xmlpath) LOG.error('Error parsing %s', xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit # "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!" # {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/')) 'passwords.xml', 'http://forum.kodi.tv/'))
return return
else: else:
@ -970,7 +945,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
""" """
Feed with tagname as unicode 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": if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype) plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, 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) xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory # Create the playlist directory
if not exists(try_encode(path)): if not path_ops.exists(path):
LOG.info("Creating directory: %s", path) LOG.info("Creating directory: %s", path)
os.makedirs(path) path_ops.makedirs(path)
# Only add the playlist if it doesn't already exists # 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) LOG.info('Path %s does exist', xsppath)
if delete: if delete:
os.remove(xsppath) path_ops.remove(xsppath)
LOG.info("Successfully removed playlist: %s.", tagname) LOG.info("Successfully removed playlist: %s.", tagname)
return return
@ -999,7 +974,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
'show': 'tvshows' 'show': 'tvshows'
} }
LOG.info("Writing playlist file to: %s", xsppath) 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( filer.write(try_encode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t' '<smartplaylist type="%s">\n\t'
@ -1017,21 +992,22 @@ def delete_playlists():
""" """
Clean up the playlists Clean up the playlists
""" """
path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) path = path_ops.translate_path('special://profile/playlists/video/')
for root, _, files in os.walk(path): for root, _, files in path_ops.walk(path):
for file in files: for file in files:
if file.startswith('Plex'): if file.startswith('Plex'):
os.remove(os.path.join(root, file)) path_ops.remove(path_ops.path.join(root, file))
def delete_nodes(): def delete_nodes():
""" """
Clean up video nodes Clean up video nodes
""" """
path = try_decode(xbmc.translatePath("special://profile/library/video/")) path = path_ops.translate_path("special://profile/library/video/")
for root, dirs, _ in os.walk(path): for root, dirs, _ in path_ops.walk(path):
for directory in dirs: for directory in dirs:
if directory.startswith('Plex-'): if directory.startswith('Plex-'):
rmtree(os.path.join(root, directory)) path_ops.rmtree(path_ops.path.join(root, directory))
break break
@ -1044,7 +1020,7 @@ def generate_file_md5(path):
""" """
m = hashlib.md5() m = hashlib.md5()
m.update(path.encode('utf-8')) m.update(path.encode('utf-8'))
with open(path, 'rb') as f: with open(path_ops.encode_path(path), 'rb') as f:
while True: while True:
piece = f.read(32768) piece = f.read(32768)
if not piece: 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 class to render this a decorator
return cls 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 -*- # -*- coding: utf-8 -*-
import os import os
import sys
import xbmc import xbmc
from xbmcaddon import Addon from xbmcaddon import Addon
@ -35,7 +35,9 @@ _ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect' ADDON_NAME = 'PlexKodiConnect'
ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_ID = 'plugin.video.plexkodiconnect'
ADDON_VERSION = _ADDON.getAddonInfo('version') ADDON_VERSION = _ADDON.getAddonInfo('version')
ADDON_PATH = try_decode(_ADDON.getAddonInfo('path'))
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home')) ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
ADDON_PROFILE = try_decode(_ADDON.getAddonInfo('profile'))
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
@ -82,10 +84,6 @@ MIN_DB_VERSION = '2.0.27'
# Database paths # Database paths
_DB_VIDEO_VERSION = { _DB_VIDEO_VERSION = {
13: 78, # Gotham
14: 90, # Helix
15: 93, # Isengard
16: 99, # Jarvis
17: 107, # Krypton 17: 107, # Krypton
18: 109 # Leia 18: 109 # Leia
} }
@ -93,10 +91,6 @@ DB_VIDEO_PATH = try_decode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])) "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = { _DB_MUSIC_VERSION = {
13: 46, # Gotham
14: 48, # Helix
15: 52, # Isengard
16: 56, # Jarvis
17: 60, # Krypton 17: 60, # Krypton
18: 70 # Leia 18: 70 # Leia
} }
@ -104,10 +98,6 @@ DB_MUSIC_PATH = try_decode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])) "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = { _DB_TEXTURE_VERSION = {
13: 13, # Gotham
14: 13, # Helix
15: 13, # Isengard
16: 13, # Jarvis
17: 13, # Krypton 17: 13, # Krypton
18: 13 # Leia 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 # Multiply Plex time by this factor to receive Kodi time
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0 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 stuff
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists') PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed') PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
@ -513,3 +509,14 @@ PLEX_STREAM_TYPE_FROM_STREAM_TYPE = {
'audio': '2', 'audio': '2',
'subtitle': '3' '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 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from distutils import dir_util
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from os import makedirs
import xbmc import xbmc
from xbmcvfs import exists
from utils import window, settings, language as lang, try_encode, indent, \ from . import utils
normalize_nodes, exists_dir, try_decode from . import path_ops
import variables as v from . import variables as v
import state from . import state
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger('PLEX.videonodes')
############################################################################### ###############################################################################
# Paths are strings, NOT unicode!
class VideoNodes(object): class VideoNodes(object):
@ -30,21 +25,26 @@ class VideoNodes(object):
root = etree.Element('node', attrib={'order': "%s" % order}) root = etree.Element('node', attrib={'order': "%s" % order})
elif roottype == 1: elif roottype == 1:
# Filter # 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" etree.SubElement(root, 'match').text = "all"
# Add tag rule # 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 etree.SubElement(rule, 'value').text = tagname
else: else:
# Folder # 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, 'label').text = label
etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.plexkodiconnect/icon.png" etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.plexkodiconnect/icon.png"
return root 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 # Plex: reassign mediatype due to Kodi inner workings
# How many items do we get at most? # How many items do we get at most?
limit = state.FETCH_PMS_ITEM_NUMBER limit = state.FETCH_PMS_ITEM_NUMBER
@ -63,33 +63,30 @@ class VideoNodes(object):
dirname = viewid dirname = viewid
# Returns strings # Returns strings
path = try_decode(xbmc.translatePath( path = path_ops.translate_path('special://profile/library/video/')
"special://profile/library/video/")) nodepath = path_ops.translate_path(
nodepath = try_decode(xbmc.translatePath( 'special://profile/library/video/Plex-%s/' % dirname)
"special://profile/library/video/Plex-%s/" % dirname))
if delete: if delete:
if exists_dir(nodepath): if path_ops.exists(nodepath):
from shutil import rmtree path_ops.rmtree(nodepath)
rmtree(nodepath) LOG.info("Sucessfully removed videonode: %s." % tagname)
log.info("Sucessfully removed videonode: %s." % tagname)
return return
# Verify the video directory # Verify the video directory
if not exists_dir(path): if not path_ops.exists(path):
dir_util.copy_tree( path_ops.copy_tree(
src=try_decode( src=path_ops.translate_path(
xbmc.translatePath("special://xbmc/system/library/video")), 'special://xbmc/system/library/video'),
dst=try_decode( dst=path_ops.translate_path('special://profile/library/video'),
xbmc.translatePath("special://profile/library/video")),
preserve_mode=0) # do not copy permission bits! preserve_mode=0) # do not copy permission bits!
# Create the node directory # Create the node directory
if mediatype != "photos": if mediatype != "photos":
if not exists_dir(nodepath): if not path_ops.exists(nodepath):
# folder does not exist yet # folder does not exist yet
log.debug('Creating folder %s' % nodepath) LOG.debug('Creating folder %s' % nodepath)
makedirs(nodepath) path_ops.makedirs(nodepath)
# Create index entry # Create index entry
nodeXML = "%sindex.xml" % nodepath nodeXML = "%sindex.xml" % nodepath
@ -97,13 +94,13 @@ class VideoNodes(object):
path = "library://video/Plex-%s/" % dirname path = "library://video/Plex-%s/" % dirname
for i in range(1, indexnumber): for i in range(1, indexnumber):
# Verify to make sure we don't create duplicates # 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 return
if mediatype == "photos": if mediatype == "photos":
path = "plugin://plugin.video.plexkodiconnect?mode=browseplex&key=/library/sections/%s&id=%s" % (viewid, viewid) 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 # Root
if not mediatype == "photos": if not mediatype == "photos":
@ -119,7 +116,7 @@ class VideoNodes(object):
tagname=tagname, tagname=tagname,
roottype=0) roottype=0)
try: try:
indent(root) utils.indent(root)
except: except:
pass pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8") etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
@ -222,14 +219,15 @@ class VideoNodes(object):
# Get label # Get label
stringid = nodes[node] stringid = nodes[node]
if node != "1": if node != "1":
label = lang(stringid) label = utils.lang(stringid)
if not label: if not label:
label = xbmc.getLocalizedString(stringid) label = xbmc.getLocalizedString(stringid)
else: else:
label = stringid label = stringid
# Set window properties # Set window properties
if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all": if ((mediatype == "homevideos" or mediatype == "photos") and
nodetype == "all"):
# Custom query # Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s" path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s"
% (viewid, mediatype)) % (viewid, mediatype))
@ -278,34 +276,39 @@ class VideoNodes(object):
templabel = label templabel = label
embynode = "Plex.nodes.%s" % indexnumber embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=templabel) utils.window('%s.title' % embynode, value=templabel)
window('%s.path' % embynode, value=windowpath) utils.window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) utils.window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=mediatype) utils.window('%s.type' % embynode, value=mediatype)
else: else:
embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype) embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype)
window('%s.title' % embynode, value=label) utils.window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath) utils.window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) utils.window('%s.content' % embynode, value=path)
if mediatype == "photos": if mediatype == "photos":
# For photos, we do not create a node in videos but we do want the window props # For photos, we do not create a node in videos but we do want
# to be created. # the window props to be created. To do: add our photos nodes to
# To do: add our photos nodes to kodi picture sources somehow # kodi picture sources somehow
continue continue
if exists(try_encode(nodeXML)): if path_ops.exists(nodeXML):
# Don't recreate xml if already exists # Don't recreate xml if already exists
continue continue
# Create the root # Create the root
if (nodetype in ("nextepisodes", "ondeck", 'recentepisodes', 'browsefiles') or mediatype == "homevideos"): if (nodetype in ("nextepisodes", "ondeck", 'recentepisodes', 'browsefiles') or mediatype == "homevideos"):
# Folder type with plugin path # 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, 'path').text = path
etree.SubElement(root, 'content').text = "episodes" etree.SubElement(root, 'content').text = "episodes"
else: 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'): if nodetype in ('recentepisodes', 'inprogressepisodes'):
etree.SubElement(root, 'content').text = "episodes" etree.SubElement(root, 'content').text = "episodes"
else: else:
@ -313,20 +316,24 @@ class VideoNodes(object):
# Elements per nodetype # Elements per nodetype
if nodetype == "all": if nodetype == "all":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" etree.SubElement(root,
'order',
{'direction': "ascending"}).text = "sorttitle"
elif nodetype == "recent": 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 etree.SubElement(root, 'limit').text = limit
if settings('MovieShowWatched') == 'false': if utils.settings('MovieShowWatched') == 'false':
rule = etree.SubElement(root, rule = etree.SubElement(root,
'rule', 'rule',
{'field': "playcount", {'field': "playcount",
'operator': "is"}) 'operator': "is"})
etree.SubElement(rule, 'value').text = "0" etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogress": 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, 'limit').text = limit
etree.SubElement( etree.SubElement(
root, root,
@ -335,55 +342,67 @@ class VideoNodes(object):
).text = 'lastplayed' ).text = 'lastplayed'
elif nodetype == "genres": 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" etree.SubElement(root, 'group').text = "genres"
elif nodetype == "unwatched": elif nodetype == "unwatched":
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" etree.SubElement(root,
rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) 'order',
{'direction': "ascending"}).text = "sorttitle"
rule = etree.SubElement(root,
"rule",
{'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0" etree.SubElement(rule, 'value').text = "0"
elif nodetype == "sets": 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" etree.SubElement(root, 'group').text = "tags"
elif nodetype == "random": 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 etree.SubElement(root, 'limit').text = limit
elif nodetype == "recommended": 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 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" etree.SubElement(rule, 'value').text = "0"
rule2 = etree.SubElement(root, 'rule', rule2 = etree.SubElement(root,
attrib={'field': "rating", 'operator': "greaterthan"}) 'rule',
attrib={'field': "rating", 'operator': "greaterthan"})
etree.SubElement(rule2, 'value').text = "7" etree.SubElement(rule2, 'value').text = "7"
elif nodetype == "recentepisodes": elif nodetype == "recentepisodes":
# Kodi Isengard, Jarvis # 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 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" etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogressepisodes": elif nodetype == "inprogressepisodes":
# Kodi Isengard, Jarvis # Kodi Isengard, Jarvis
etree.SubElement(root, 'limit').text = limit etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', rule = etree.SubElement(root,
attrib={'field': "inprogress", 'operator':"true"}) 'rule',
attrib={'field': "inprogress", 'operator':"true"})
try: try:
indent(root) utils.indent(root)
except: except:
pass 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): def singleNode(self, indexnumber, tagname, mediatype, itemtype):
tagname = try_encode(tagname) cleantagname = utils.normalize_nodes(tagname)
cleantagname = try_decode(normalize_nodes(tagname)) nodepath = path_ops.translate_path('special://profile/library/video/')
nodepath = try_decode(xbmc.translatePath(
"special://profile/library/video/"))
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname path = "library://video/plex_%s.xml" % cleantagname
if v.KODIVERSION >= 17: if v.KODIVERSION >= 17:
@ -393,13 +412,12 @@ class VideoNodes(object):
windowpath = "ActivateWindow(Video,%s,return)" % path windowpath = "ActivateWindow(Video,%s,return)" % path
# Create the video node directory # Create the video node directory
if not exists_dir(nodepath): if not path_ops.exists(nodepath):
# We need to copy over the default items # We need to copy over the default items
dir_util.copy_tree( path_ops.copy_tree(
src=try_decode( src=path_ops.translate_path(
xbmc.translatePath("special://xbmc/system/library/video")), 'special://xbmc/system/library/video'),
dst=try_decode( dst=path_ops.translate_path('special://profile/library/video'),
xbmc.translatePath("special://profile/library/video")),
preserve_mode=0) # do not copy permission bits! preserve_mode=0) # do not copy permission bits!
labels = { labels = {
@ -407,14 +425,14 @@ class VideoNodes(object):
'Favorite tvshows': 30181, 'Favorite tvshows': 30181,
'channels': 30173 'channels': 30173
} }
label = lang(labels[tagname]) label = utils.lang(labels[tagname])
embynode = "Plex.nodes.%s" % indexnumber embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=label) utils.window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath) utils.window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) utils.window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=itemtype) utils.window('%s.type' % embynode, value=itemtype)
if exists(try_encode(nodeXML)): if path_ops.exists(nodeXML):
# Don't recreate xml if already exists # Don't recreate xml if already exists
return return
@ -423,23 +441,26 @@ class VideoNodes(object):
label=label, label=label,
tagname=tagname, tagname=tagname,
roottype=2) 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: else:
root = self.commonRoot(order=1, label=label, tagname=tagname) 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 etree.SubElement(root, 'content').text = mediatype
try: try:
indent(root) utils.indent(root)
except: except:
pass pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8") etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
def clearProperties(self): def clearProperties(self):
log.info("Clearing nodes properties.") LOG.info("Clearing nodes properties.")
plexprops = window('Plex.nodes.total') plexprops = utils.window('Plex.nodes.total')
propnames = [ propnames = [
"index","path","title","content", "index","path","title","content",
"inprogress.content","inprogress.title", "inprogress.content","inprogress.title",
@ -457,4 +478,5 @@ class VideoNodes(object):
totalnodes = int(plexprops) totalnodes = int(plexprops)
for i in range(totalnodes): for i in range(totalnodes):
for prop in propnames: 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 os.path
import logging import logging
import re import re
from pathtools.patterns import match_any_paths from ..pathtools.patterns import match_any_paths
from watchdog.utils import has_attribute from .utils import has_attribute
from watchdog.utils import unicode_paths from .utils import unicode_paths
EVENT_TYPE_MOVED = 'moved' EVENT_TYPE_MOVED = 'moved'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,297 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger from __future__ import absolute_import, division, unicode_literals
from os import path as os_path from resources.lib import service_entry
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")
###############################################################################
class Service(): if __name__ == "__main__":
service_entry.start()
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()