Merge branch 'beta-version'
This commit is contained in:
commit
7ee68937bc
68 changed files with 2247 additions and 2068 deletions
|
@ -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)
|
||||||
|
|
18
addon.xml
18
addon.xml
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
89
default.py
89
default.py
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,10 +258,10 @@ 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
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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,9 +59,9 @@ 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)
|
||||||
|
@ -78,25 +72,25 @@ 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)
|
||||||
|
@ -106,10 +100,10 @@ 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': window('plex_token')})
|
headerOptions={'X-Plex-Token': utils.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
|
||||||
|
|
|
@ -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,9 +361,9 @@ 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:
|
||||||
|
@ -367,7 +372,7 @@ class InitialSetup(object):
|
||||||
# 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,7 +476,7 @@ 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
|
||||||
|
@ -493,11 +501,11 @@ 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'])
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
@ -1016,7 +1015,7 @@ class TVShows(Items):
|
||||||
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
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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] + '/'
|
||||||
|
|
|
@ -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,27 +132,42 @@ 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
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self.PlayBackStart(data)
|
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:
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
_playback_cleanup(ended=True)
|
_playback_cleanup(ended=True)
|
||||||
else:
|
else:
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
_playback_cleanup()
|
_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':
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self._playlist_onadd(data)
|
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':
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self._playlist_onclear(data)
|
self._playlist_onclear(data)
|
||||||
elif method == "VideoLibrary.OnUpdate":
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
# Manually marking as watched/unwatched
|
# Manually marking as watched/unwatched
|
||||||
|
@ -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)
|
||||||
|
try:
|
||||||
json_item = js.get_item(playerid)
|
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:
|
||||||
|
|
|
@ -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,7 +18,7 @@ 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'])
|
||||||
|
@ -68,14 +67,14 @@ 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)
|
||||||
|
@ -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 ###===---")
|
||||||
|
|
|
@ -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,7 +15,7 @@ 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):
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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,7 +14,7 @@ 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):
|
||||||
|
|
|
@ -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,7 +18,7 @@ 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):
|
||||||
|
@ -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
|
||||||
|
@ -61,9 +63,9 @@ class ThreadedShowSyncInfo(Thread):
|
||||||
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)
|
||||||
|
|
|
@ -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,13 +84,13 @@ 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}')
|
||||||
|
@ -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,9 +518,9 @@ 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:
|
||||||
|
@ -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):
|
||||||
|
@ -1508,14 +1494,17 @@ class LibrarySync(Thread):
|
||||||
# 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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,7 +34,8 @@ 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(
|
||||||
|
'advancedsettings.xml',
|
||||||
force_create=True,
|
force_create=True,
|
||||||
top_element='advancedsettings') as xml_file:
|
top_element='advancedsettings') as xml_file:
|
||||||
parent = xml_file.set_setting(['audio', 'excludefromscan'])
|
parent = xml_file.set_setting(['audio', 'excludefromscan'])
|
||||||
|
@ -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
192
resources/lib/path_ops.py
Normal 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)
|
|
@ -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: '
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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!
|
|
@ -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,9 +94,16 @@ 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:
|
||||||
|
LOG.debug('Received new plex_id %s, expected %s. Init playback',
|
||||||
|
plex_id, item.plex_id)
|
||||||
|
initiate = True
|
||||||
|
else:
|
||||||
|
initiate = False
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
if initiate:
|
if initiate:
|
||||||
_playback_init(plex_id, plex_type, playqueue, pos)
|
_playback_init(plex_id, plex_type, playqueue, pos)
|
||||||
else:
|
else:
|
||||||
|
@ -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,7 +210,7 @@ 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)
|
||||||
|
@ -206,7 +218,10 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
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')
|
||||||
|
|
|
@ -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,9 +54,9 @@ 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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,19 +91,20 @@ 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),
|
||||||
|
248)],
|
||||||
occurance))
|
occurance))
|
||||||
LOG.debug('Kodi playlist path: %s', path)
|
LOG.debug('Kodi playlist path: %s', path)
|
||||||
playlist.kodi_path = path
|
playlist.kodi_path = path
|
||||||
|
@ -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,15 +260,15 @@ 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
|
||||||
|
with state.LOCK_PLAYLISTS:
|
||||||
playlist = playlist_object_from_db(plex_id=plex_id)
|
playlist = playlist_object_from_db(plex_id=plex_id)
|
||||||
try:
|
try:
|
||||||
if playlist and state == 9:
|
if playlist and status == 9:
|
||||||
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
||||||
delete_kodi_playlist(playlist)
|
delete_kodi_playlist(playlist)
|
||||||
elif playlist and playlist.plex_updatedat == updated_at:
|
elif playlist and playlist.plex_updatedat == updated_at:
|
||||||
|
@ -292,8 +278,9 @@ def process_websocket(plex_id, updated_at, state):
|
||||||
LOG.debug('Change of Plex playlist detected: %s', playlist)
|
LOG.debug('Change of Plex playlist detected: %s', playlist)
|
||||||
delete_kodi_playlist(playlist)
|
delete_kodi_playlist(playlist)
|
||||||
create = True
|
create = True
|
||||||
elif not playlist and not state == 9:
|
elif not playlist and not status == 9:
|
||||||
LOG.debug('Creation of new Plex playlist detected: %s', plex_id)
|
LOG.debug('Creation of new Plex playlist detected: %s',
|
||||||
|
plex_id)
|
||||||
create = True
|
create = True
|
||||||
# To the actual work
|
# To the actual work
|
||||||
if create:
|
if create:
|
||||||
|
@ -302,13 +289,20 @@ def process_websocket(plex_id, updated_at, state):
|
||||||
pass
|
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):
|
||||||
|
|
|
@ -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,7 +60,6 @@ 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
|
||||||
|
@ -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 ##===----")
|
||||||
|
|
|
@ -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,7 +72,7 @@ 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
|
||||||
|
@ -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 = ""
|
||||||
|
@ -270,11 +271,11 @@ class PlayUtils():
|
||||||
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
|
||||||
|
@ -283,16 +284,16 @@ class PlayUtils():
|
||||||
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,12 +336,12 @@ 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:
|
||||||
|
|
|
@ -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,13 +1404,7 @@ 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)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
LOG.debug('Need to slugify the filename %s', path)
|
|
||||||
path = slugify(path)
|
|
||||||
with open(path, 'wb') as filer:
|
|
||||||
filer.write(response.content)
|
filer.write(response.content)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
@ -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 (
|
|
@ -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,13 +213,16 @@ 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':
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self._process_alexa(data)
|
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':
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self._process_playlist(data)
|
self._process_playlist(data)
|
||||||
elif task['action'] == 'refreshPlayQueue':
|
elif task['action'] == 'refreshPlayQueue':
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
self._process_refresh(data)
|
self._process_refresh(data)
|
||||||
elif task['action'] == 'setStreams':
|
elif task['action'] == 'setStreams':
|
||||||
try:
|
try:
|
||||||
|
@ -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:
|
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -824,7 +820,7 @@ def transcode_image_path(key, AuthToken, path, width, height):
|
||||||
path = 'http://127.0.0.1:32400' + key
|
path = 'http://127.0.0.1:32400' + key
|
||||||
else: # internal path, add-on
|
else: # internal path, add-on
|
||||||
path = 'http://127.0.0.1:32400' + path + '/' + key
|
path = 'http://127.0.0.1:32400' + path + '/' + key
|
||||||
path = 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...
|
|
@ -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(
|
||||||
|
'select',
|
||||||
|
'%s%s' % (utils.lang(29999), utils.lang(39306)),
|
||||||
userlist_coded)
|
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,8 +65,8 @@ 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}')
|
||||||
|
@ -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,10 +145,10 @@ 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 \
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from socket import error as socket_error
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
LOG = getLogger("PLEX." + __name__)
|
LOG = getLogger('PLEX.httppersist')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,6 +184,7 @@ class SubscriptionMgr(object):
|
||||||
return answ
|
return answ
|
||||||
|
|
||||||
def _timeline_dict(self, player, ptype):
|
def _timeline_dict(self, player, ptype):
|
||||||
|
with state.LOCK_PLAYQUEUES:
|
||||||
playerid = player['playerid']
|
playerid = player['playerid']
|
||||||
info = state.PLAYER_STATES[playerid]
|
info = state.PLAYER_STATES[playerid]
|
||||||
playqueue = PQ.PLAYQUEUES[playerid]
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
|
@ -203,15 +198,16 @@ class SubscriptionMgr(object):
|
||||||
'type': ptype,
|
'type': ptype,
|
||||||
'state': 'stopped'
|
'state': 'stopped'
|
||||||
}
|
}
|
||||||
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO):
|
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_PHOTO):
|
||||||
self.location = 'fullScreenVideo'
|
self.location = 'fullScreenVideo'
|
||||||
self.stop_sent_to_web = False
|
self.stop_sent_to_web = False
|
||||||
pbmc_server = window('pms_server')
|
pbmc_server = utils.window('pms_server')
|
||||||
if pbmc_server:
|
if pbmc_server:
|
||||||
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
||||||
self.server = self.server.replace('/', '')
|
self.server = self.server.replace('/', '')
|
||||||
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
||||||
duration = kodi_time_to_millis(info['totaltime'])
|
duration = utils.kodi_time_to_millis(info['totaltime'])
|
||||||
shuffle = '1' if info['shuffled'] else '0'
|
shuffle = '1' if info['shuffled'] else '0'
|
||||||
mute = '1' if info['muted'] is True else '0'
|
mute = '1' if info['muted'] is True else '0'
|
||||||
answ = {
|
answ = {
|
||||||
|
@ -219,11 +215,11 @@ class SubscriptionMgr(object):
|
||||||
'protocol': self.protocol,
|
'protocol': self.protocol,
|
||||||
'address': self.server,
|
'address': self.server,
|
||||||
'port': self.port,
|
'port': self.port,
|
||||||
'machineIdentifier': window('plex_machineIdentifier'),
|
'machineIdentifier': utils.window('plex_machineIdentifier'),
|
||||||
'state': status,
|
'state': status,
|
||||||
'type': ptype,
|
'type': ptype,
|
||||||
'itemType': ptype,
|
'itemType': ptype,
|
||||||
'time': kodi_time_to_millis(info['time']),
|
'time': utils.kodi_time_to_millis(info['time']),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'seekRange': '0-%s' % duration,
|
'seekRange': '0-%s' % duration,
|
||||||
'shuffle': shuffle,
|
'shuffle': shuffle,
|
||||||
|
@ -235,8 +231,9 @@ class SubscriptionMgr(object):
|
||||||
'partCount': 1,
|
'partCount': 1,
|
||||||
'providerIdentifier': 'com.plexapp.plugins.library',
|
'providerIdentifier': 'com.plexapp.plugins.library',
|
||||||
}
|
}
|
||||||
# Get the plex id from the PKC playqueue not info, as Kodi jumps to next
|
# Get the plex id from the PKC playqueue not info, as Kodi jumps to
|
||||||
# playqueue element way BEFORE kodi monitor onplayback is called
|
# next playqueue element way BEFORE kodi monitor onplayback is
|
||||||
|
# called
|
||||||
if item.plex_id:
|
if item.plex_id:
|
||||||
answ['key'] = '/library/metadata/%s' % item.plex_id
|
answ['key'] = '/library/metadata/%s' % item.plex_id
|
||||||
answ['ratingKey'] = item.plex_id
|
answ['ratingKey'] = item.plex_id
|
||||||
|
@ -275,7 +272,8 @@ class SubscriptionMgr(object):
|
||||||
# still be {}
|
# still be {}
|
||||||
strm_id = None
|
strm_id = None
|
||||||
if strm_id is not None:
|
if strm_id is not None:
|
||||||
# If None, then the subtitle is only present on Kodi side
|
# If None, then the subtitle is only present on Kodi
|
||||||
|
# side
|
||||||
answ['subtitleStreamID'] = strm_id
|
answ['subtitleStreamID'] = strm_id
|
||||||
return answ
|
return answ
|
||||||
|
|
||||||
|
@ -302,12 +300,12 @@ 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
|
||||||
"""
|
"""
|
||||||
|
with state.LOCK_SUBSCRIBER:
|
||||||
if command_id and self.subscribers.get(uuid):
|
if command_id and self.subscribers.get(uuid):
|
||||||
self.subscribers[uuid].command_id = int(command_id)
|
self.subscribers[uuid].command_id = int(command_id)
|
||||||
|
|
||||||
|
@ -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,12 +330,12 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
with state.LOCK_SUBSCRIBER:
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
# Get all the active/playing Kodi players (video, audio, pictures)
|
# Get all the active/playing Kodi players (video, audio, pictures)
|
||||||
players = js.get_players()
|
players = js.get_players()
|
||||||
|
@ -349,7 +345,7 @@ class SubscriptionMgr(object):
|
||||||
# Check whether we can use the CURRENT info or whether PKC is still
|
# Check whether we can use the CURRENT info or whether PKC is still
|
||||||
# initializing
|
# initializing
|
||||||
if self._playqueue_init_done(players) is False:
|
if self._playqueue_init_done(players) is False:
|
||||||
LOG.debug('PKC playqueue is still initializing - skipping update')
|
LOG.debug('PKC playqueue is still initializing - skip update')
|
||||||
return
|
return
|
||||||
self._notify_server(players)
|
self._notify_server(players)
|
||||||
if self.subscribers:
|
if self.subscribers:
|
||||||
|
@ -360,8 +356,6 @@ class SubscriptionMgr(object):
|
||||||
|
|
||||||
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,16 +424,17 @@ class SubscriptionMgr(object):
|
||||||
command_id,
|
command_id,
|
||||||
self,
|
self,
|
||||||
self.request_mgr)
|
self.request_mgr)
|
||||||
|
with state.LOCK_SUBSCRIBER:
|
||||||
self.subscribers[subscriber.uuid] = 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)
|
||||||
"""
|
"""
|
||||||
|
with state.LOCK_SUBSCRIBER:
|
||||||
for subscriber in self.subscribers.values():
|
for subscriber in self.subscribers.values():
|
||||||
if subscriber.uuid == uuid or subscriber.host == uuid:
|
if subscriber.uuid == uuid or subscriber.host == uuid:
|
||||||
subscriber.cleanup()
|
subscriber.cleanup()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
275
resources/lib/service_entry.py
Normal file
275
resources/lib/service_entry.py
Normal 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()
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
|
'rule',
|
||||||
attrib={'field': "rating", 'operator': "greaterthan"})
|
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,
|
||||||
|
'rule',
|
||||||
attrib={'field': "inprogress", 'operator':"true"})
|
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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
|
@ -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):
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from watchdog.utils import platform
|
from . import platform
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 2
|
# Python 2
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
297
service.py
297
service.py
|
@ -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()
|
|
||||||
|
|
Loading…
Reference in a new issue