Merge master

This commit is contained in:
croneter 2017-09-17 13:36:59 +02:00
parent c33565af4c
commit fa6d95aa61
29 changed files with 816 additions and 652 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
[![beta version](https://img.shields.io/badge/beta_version-1.8.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
@ -105,14 +105,11 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio
Solutions are unlikely due to the nature of these issues
- A Plex Media Server "bug" leads to frequent and slow syncs, see [here for more info](https://github.com/croneter/PlexKodiConnect/issues/135)
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issue](https://github.com/croneter/PlexKodiConnect/issues/14) for more details
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issues](https://github.com/croneter/PlexKodiConnect/issues/14) for more details. **Workaround**: use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Set-up-Direct-Paths) instead of addon paths.
*Background Sync:*
The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them only on full/delta syncs (standard settings is every 60 minutes)
- Toggle the viewstate of an item to (un)watched outside of Kodi
- Changing details of an item, e.g. replacing a poster
However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie.
### Issues being worked on

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.7" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.14" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.3.0" />
@ -59,7 +59,55 @@
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
<news>version 1.8.6:
<news>version 1.8.14 (beta only):
- Greatly speed up displaying context menu
- Fix IndexError e.g. for channels if stream info missing
- Sleep a bit before marking item as fully watched
- Don't sleep before updating playstate to fully watched (if you watch on another Plex client)
- Fix KeyError for TV live channels for getGeople
version 1.8.13 (beta only):
- Background sync now picks up more PMS changes
- Detect Plex item deletion more reliably
- Fix changed Plex metadata not synced repeatedly
- Detect (some, not all) changes to PKC settings and apply them on-the-fly
- Fix resuming interrupted sync
- PKC logging now uses Kodi log levels
- Further code optimizations
version 1.8.12:
- Fix library sync crashing trying to display an error
version 1.8.11:
- version 1.8.10 for everybody
version 1.8.10 (beta only):
- Vastly improve sync speed for music
- Never show library sync dialog if media is playing
- Improvements to sync dialog
- Fix stop synching if path not found
- Resume aborted sync on PKC settings change
- Don't quit library sync if failed repeatedly
- Verify path for every Plex library on install sync
- More descriptive downloadable subtitles
- More code fixes and optimization
version 1.8.9
- Fix playback not starting in some circumstances
- Deactivate some annoying popups on install
version 1.8.8
- Fix playback not starting in some circumstances
- Fix first artist "missing" tag (Reset your DB!)
- Update Czech translation
version 1.8.7 (beta only):
- Some fixes to playstate reporting, thanks @RickDB
- Add Kodi info screen for episodes in context menu
- Fix PKC asking for trailers not working
- Fix PKC not automatically updating
version 1.8.6:
- Portuguese translation, thanks @goncalo532
- Updated other translations

View file

@ -1,52 +1,41 @@
# -*- coding: utf-8 -*-
###############################################################################
from os import path as os_path
from sys import path as sys_path
import logging
import os
import sys
from xbmcaddon import Addon
from xbmc import translatePath, sleep, log, LOGERROR
from xbmcgui import Window
import xbmc
import xbmcaddon
###############################################################################
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
_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 = xbmc.translatePath(os.path.join(
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
_base_resource = xbmc.translatePath(os.path.join(
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode()
sys.path.append(_base_resource)
sys_path.append(_base_resource)
###############################################################################
import loghandler
from context_entry import ContextMenu
###############################################################################
loghandler.config()
log = logging.getLogger("PLEX.contextmenu")
from pickler import unpickle_me, pickl_window
###############################################################################
if __name__ == "__main__":
try:
# Start the context menu
ContextMenu()
except Exception as error:
log.exception(error)
import traceback
log.exception("Traceback:\n%s" % traceback.format_exc())
raise
win = Window(10000)
while win.getProperty('plex_command'):
sleep(20)
win.setProperty('plex_command', 'CONTEXT_menu')
while not pickl_window('plex_result'):
sleep(50)
result = unpickle_me()
if result is None:
log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR)

View file

@ -32,9 +32,9 @@ sys_path.append(_base_resource)
###############################################################################
import entrypoint
from utils import window, pickl_window, reset, passwordsXML, language as lang,\
dialog
from pickler import unpickle_me
from utils import window, reset, passwordsXML, 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
@ -127,28 +127,29 @@ class Main():
log.error('Not connected to a PMS.')
else:
if mode == 'repair':
window('plex_runLibScan', value='repair')
log.info('Requesting repair lib sync')
plex_command('RUN_LIB_SCAN', 'repair')
elif mode == 'manualsync':
log.info('Requesting full library scan')
window('plex_runLibScan', value='full')
plex_command('RUN_LIB_SCAN', 'full')
elif mode == 'texturecache':
window('plex_runLibScan', value='del_textures')
log.info('Requesting texture caching of all textures')
plex_command('RUN_LIB_SCAN', 'textures')
elif mode == 'chooseServer':
entrypoint.chooseServer()
elif mode == 'refreshplaylist':
log.info('Requesting playlist/nodes refresh')
window('plex_runLibScan', value='views')
plex_command('RUN_LIB_SCAN', 'views')
elif mode == 'deviceid':
self.deviceid()
elif mode == 'fanart':
log.info('User requested fanarttv refresh')
window('plex_runLibScan', value='fanart')
plex_command('RUN_LIB_SCAN', 'fanart')
elif '/extrafanart' in argv[0]:
plexpath = argv[2][1:]
@ -165,15 +166,13 @@ class Main():
else:
entrypoint.doMainListing(content_type=params.get('content_type'))
def play(self):
@staticmethod
def play():
"""
Start up playback_starter in main Python thread
"""
# Put the request into the 'queue'
while window('plex_command'):
sleep(50)
window('plex_command',
value='play_%s' % argv[2])
plex_command('PLAY', argv[2])
# Wait for the result
while not pickl_window('plex_result'):
sleep(50)
@ -190,7 +189,8 @@ class Main():
listitem = convert_PKC_to_listitem(result.listitem)
setResolvedUrl(HANDLE, True, listitem)
def deviceid(self):
@staticmethod
def deviceid():
deviceId_old = window('plex_client_Id')
from clientinfo import getDeviceId
try:
@ -205,6 +205,7 @@ class Main():
dialog('ok', lang(29999), lang(33033))
executebuiltin('RestartApp')
if __name__ == '__main__':
log.info('%s started' % v.ADDON_ID)
Main()

View file

@ -49,7 +49,7 @@ from xbmcvfs import exists
import clientinfo as client
from downloadutils import DownloadUtils
from utils import window, settings, language as lang, tryDecode, tryEncode, \
DateToKodi, exists_dir
DateToKodi, exists_dir, slugify
from PlexFunctions import PMSHttpsEnabled
import plexdb_functions as plexdb
import variables as v
@ -1346,6 +1346,7 @@ class API():
cast = []
producer = []
for child in self.item:
try:
if child.tag == 'Director':
director.append(child.attrib['tag'])
elif child.tag == 'Writer':
@ -1354,6 +1355,9 @@ class API():
cast.append(child.attrib['tag'])
elif child.tag == 'Producer':
producer.append(child.attrib['tag'])
except KeyError:
log.warn('Malformed PMS answer for getPeople: %s: %s'
% (child.tag, child.attrib))
return {
'Director': director,
'Writer': writer,
@ -1750,8 +1754,16 @@ class API():
videotracks = []
audiotracks = []
subtitlelanguages = []
try:
# Sometimes, aspectratio is on the "toplevel"
aspectratio = self.item[0].attrib.get('aspectRatio', None)
except IndexError:
# There is no stream info at all, returning empty
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
# TODO: what if several Media tags exist?!?
# Loop over parts
for child in self.item[0]:
@ -2357,11 +2369,11 @@ class API():
# ext = stream.attrib.get('format')
if key:
# We do know the language - temporarily download
if stream.attrib.get('languageCode') is not None:
if stream.attrib.get('language') is not None:
path = self.download_external_subtitles(
"{server}%s" % key,
"subtitle%02d.%s.%s" % (fileindex,
stream.attrib['languageCode'],
stream.attrib['language'],
stream.attrib['codec']))
fileindex += 1
# We don't know the language - no need to download
@ -2395,9 +2407,14 @@ class API():
log.error('Could not temporarily download subtitle %s' % url)
return
else:
r.encoding = 'utf-8'
log.debug('Writing temp subtitle to %s' % path)
try:
with open(path, 'wb') as f:
f.write(r.content)
except UnicodeEncodeError:
log.debug('Need to slugify the filename %s' % path)
with open(slugify(path), 'wb') as f:
f.write(r.content)
return path
def GetKodiPremierDate(self):
@ -2575,16 +2592,16 @@ class API():
if path is None:
return None
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
if window('remapSMB') == 'true':
path = path.replace(window('remapSMB%sOrg' % typus),
window('remapSMB%sNew' % typus),
if state.REMAP_PATH is True:
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
getattr(state, 'remapSMB%sNew' % typus),
1)
# There might be backslashes left over:
path = path.replace('\\', '/')
elif window('replaceSMB') == 'true':
elif state.REPLACE_SMB_PATH is True:
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
if ((window('plex_pathverified') == 'true' and forceCheck is False) or
if ((state.PATH_VERIFIED and forceCheck is False) or
omitCheck is True):
return path
@ -2612,12 +2629,12 @@ class API():
if self.askToValidate(path):
state.STOP_SYNC = True
path = None
window('plex_pathverified', value='true')
state.PATH_VERIFIED = True
else:
path = None
elif forceCheck is False:
if window('plex_pathverified') != 'true':
window('plex_pathverified', value='true')
# Only set the flag if we were not force-checking the path
state.PATH_VERIFIED = True
return path
def askToValidate(self, url):

View file

@ -15,7 +15,7 @@ from variables import PLEX_TO_KODI_TIMEFACTOR
log = getLogger("PLEX."+__name__)
CONTAINERSIZE = int(settings('limitindex'))
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
###############################################################################
@ -36,9 +36,8 @@ def GetPlexKeyNumber(plexKey):
Returns ('','') if nothing is found
"""
regex = re.compile(r'''/(.+)/(\d+)$''')
try:
result = regex.findall(plexKey)[0]
result = REGEX_PLEX_KEY.findall(plexKey)[0]
except IndexError:
result = ('', '')
return result

View file

@ -126,8 +126,9 @@ def double_urldecode(text):
return unquote(unquote(text))
@thread_methods(add_stops=['STOP_SYNC'],
add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'])
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'DB_SCAN',
'STOP_SYNC'])
class Image_Cache_Thread(Thread):
xbmc_host = 'localhost'
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()

View file

@ -68,13 +68,13 @@ def getDeviceId(reset=False):
# Because Kodi appears to cache file settings!!
if clientId != "" and reset is False:
window('plex_client_Id', value=clientId)
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
return clientId
log.warn("Generating a new deviceid.")
log.info("Generating a new deviceid.")
from uuid import uuid4
clientId = str(uuid4())
settings('plex_client_Id', value=clientId)
window('plex_client_Id', value=clientId)
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
return clientId

View file

@ -21,9 +21,6 @@ class Monitor_Window(Thread):
Monitors window('plex_command') for new entries that we need to take care
of, e.g. for new plays initiated on the Kodi side with addon paths.
Possible values of window('plex_command'):
'play_....': to start playback using playback_starter
Adjusts state.py accordingly
"""
# Borg - multiple instances, shared state
@ -40,9 +37,8 @@ class Monitor_Window(Thread):
if window('plex_command'):
value = window('plex_command')
window('plex_command', clear=True)
if value.startswith('play_'):
queue.put(value)
if value.startswith('PLAY-'):
queue.put(value.replace('PLAY-', ''))
elif value == 'SUSPEND_LIBRARY_THREAD-True':
state.SUSPEND_LIBRARY_THREAD = True
elif value == 'SUSPEND_LIBRARY_THREAD-False':
@ -64,6 +60,10 @@ class Monitor_Window(Thread):
elif value.startswith('PLEX_USERNAME-'):
state.PLEX_USERNAME = \
value.replace('PLEX_USERNAME-', '') or None
elif value.startswith('RUN_LIB_SCAN-'):
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
elif value == 'CONTEXT_menu':
queue.put('dummy?mode=context_menu')
else:
raise NotImplementedError('%s not implemented' % value)
else:

View file

@ -198,12 +198,12 @@ class DownloadUtils():
# THE EXCEPTIONS
except requests.exceptions.ConnectionError as e:
# Connection error
log.debug("Server unreachable at: %s" % url)
log.debug(e)
log.warn("Server unreachable at: %s" % url)
log.warn(e)
except requests.exceptions.Timeout as e:
log.debug("Server timeout at: %s" % url)
log.debug(e)
log.warn("Server timeout at: %s" % url)
log.warn(e)
except requests.exceptions.HTTPError as e:
log.warn('HTTP Error at %s' % url)
@ -300,21 +300,21 @@ class DownloadUtils():
# update
pass
else:
log.error("Unable to convert the response for: "
log.warn("Unable to convert the response for: "
"%s" % url)
log.info("Received headers were: %s" % r.headers)
log.info('Received text:')
log.info(r.text)
log.warn("Received headers were: %s" % r.headers)
log.warn('Received text:')
log.warn(r.text)
return True
elif r.status_code == 403:
# E.g. deleting a PMS item
log.error('PMS sent 403: Forbidden error for url %s' % url)
log.warn('PMS sent 403: Forbidden error for url %s' % url)
return None
else:
log.error('Unknown answer from PMS %s with status code %s. '
log.warn('Unknown answer from PMS %s with status code %s. '
'Message:' % (url, r.status_code))
r.encoding = 'utf-8'
log.info(r.text)
log.warn(r.text)
return True
# And now deal with the consequences of the exceptions

View file

@ -575,14 +575,6 @@ def getExtraFanArt(plexid, plexPath):
xbmcplugin.endOfDirectory(HANDLE)
def RunLibScan(mode):
if window('plex_online') != "true":
# Server is not online, do not run the sync
dialog('ok', lang(29999), lang(39205))
else:
window('plex_runLibScan', value='full')
def getOnDeck(viewid, mediatype, tagname, limit):
"""
Retrieves Plex On Deck items, currently only for TV shows
@ -975,7 +967,7 @@ def __LogIn():
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
out!
"""
window('plex_runLibScan', value='full')
plex_command('RUN_LIB_SCAN', 'full')
# Restart user client
plex_command('SUSPEND_USER_CLIENT', 'False')

View file

@ -496,10 +496,10 @@ class InitialSetup():
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
dialog.ok(heading=lang(29999), line1=lang(39076))
# dialog.ok(heading=lang(29999), line1=lang(39076))
# Need to tell about our image source for collections: themoviedb.org
dialog.ok(heading=lang(29999), line1=lang(39717))
# dialog.ok(heading=lang(29999), line1=lang(39717))
# Make sure that we only ask these questions upon first installation
settings('InstallQuestionsAnswered', value='true')

View file

@ -161,7 +161,8 @@ class Items(object):
# If offset exceeds duration skip update
if item['viewOffset'] > item['duration']:
log.error("Error while updating play state, viewOffset exceeded duration")
log.error("Error while updating play state, viewOffset "
"exceeded duration")
return
complete = float(item['viewOffset']) / float(item['duration'])
@ -170,7 +171,6 @@ class Items(object):
% (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1)
if complete >= MARK_PLAYED_AT:
log.info('Marking as completely watched in Kodi')
sleep(500)
try:
item['viewCount'] += 1
except TypeError:
@ -1729,7 +1729,7 @@ class Music(Items):
if album is None or album == 401:
log.error('Could not download album, abort')
return
self.add_updateAlbum(album[0])
self.add_updateAlbum(album[0], children=[item])
plex_dbalbum = plex_db.getItem_byId(plex_albumId)
try:
albumid = plex_dbalbum[0]

View file

@ -1280,7 +1280,14 @@ class Kodidb_Functions():
try:
artistid = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute("select coalesce(max(idArtist),0) from artist")
# Krypton has a dummy first entry idArtist: 1 strArtist:
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
if v.KODIVERSION >= 17:
self.cursor.execute(
"select coalesce(max(idArtist),1) from artist")
else:
self.cursor.execute(
"select coalesce(max(idArtist),0) from artist")
artistid = self.cursor.fetchone()[0] + 1
query = (
'''

View file

@ -2,24 +2,48 @@
###############################################################################
import logging
from logging import getLogger
from json import loads
from xbmc import Monitor, Player, sleep
import downloadutils
from downloadutils import DownloadUtils
import plexdb_functions as plexdb
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
plex_command
from PlexFunctions import scrobble
from kodidb_functions import get_kodiid_from_filename
from PlexAPI import API
from variables import REMAP_TYPE_FROM_PLEXTYPE
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
log = getLogger("PLEX."+__name__)
# settings: window-variable
WINDOW_SETTINGS = {
'enableContext': 'plex_context',
'plex_restricteduser': 'plex_restricteduser',
'force_transcode_pix': 'plex_force_transcode_pix',
'fetch_pms_item_number': 'fetch_pms_item_number'
}
# settings: state-variable (state.py)
# Need to use getattr and setattr!
STATE_SETTINGS = {
'dbSyncIndicator': 'SYNC_DIALOG',
'remapSMB': 'REMAP_PATH',
'remapSMBmovieOrg': 'remapSMBmovieOrg',
'remapSMBmovieNew': 'remapSMBmovieNew',
'remapSMBtvOrg': 'remapSMBtvOrg',
'remapSMBtvNew': 'remapSMBtvNew',
'remapSMBmusicOrg': 'remapSMBmusicOrg',
'remapSMBmusicNew': 'remapSMBmusicNew',
'remapSMBphotoOrg': 'remapSMBphotoOrg',
'remapSMBphotoNew': 'remapSMBphotoNew',
'enableMusic': 'ENABLE_MUSIC',
'enableBackgroundSync': 'BACKGROUND_SYNC'
}
###############################################################################
@ -27,7 +51,7 @@ class KodiMonitor(Monitor):
def __init__(self, callback):
self.mgr = callback
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.doUtils = DownloadUtils().downloadUrl
self.xbmcplayer = Player()
self.playqueue = self.mgr.playqueue
Monitor.__init__(self)
@ -47,31 +71,42 @@ class KodiMonitor(Monitor):
"""
Monitor the PKC settings for changes made by the user
"""
# settings: window-variable
items = {
'logLevel': 'plex_logLevel',
'enableContext': 'plex_context',
'plex_restricteduser': 'plex_restricteduser',
'dbSyncIndicator': 'dbSyncIndicator',
'remapSMB': 'remapSMB',
'replaceSMB': 'replaceSMB',
'force_transcode_pix': 'plex_force_transcode_pix',
'fetch_pms_item_number': 'fetch_pms_item_number'
}
# Path replacement
for typus in REMAP_TYPE_FROM_PLEXTYPE.values():
for arg in ('Org', 'New'):
key = 'remapSMB%s%s' % (typus, arg)
items[key] = key
log.debug('PKC settings change detected')
changed = False
# Reset the window variables from the settings variables
for settings_value, window_value in items.iteritems():
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
if window(window_value) != settings(settings_value):
log.debug('PKC settings changed: %s is now %s'
changed = True
log.debug('PKC window settings changed: %s is now %s'
% (settings_value, settings(settings_value)))
window(window_value, value=settings(settings_value))
if settings_value == 'fetch_pms_item_number':
log.info('Requesting playlist/nodes refresh')
window('plex_runLibScan', value="views")
plex_command('RUN_LIB_SCAN', 'views')
# Reset the state variables in state.py
for settings_value, state_name in STATE_SETTINGS.iteritems():
new = settings(settings_value)
if new == 'true':
new = True
elif new == 'false':
new = False
if getattr(state, state_name) != new:
changed = True
log.debug('PKC state settings %s changed from %s to %s'
% (settings_value, getattr(state, state_name), new))
setattr(state, state_name, new)
# Special cases, overwrite all internal settings
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
settings('backgroundsync_saftyMargin'))
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
# Never set through the user
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
if changed is True:
# Assume that the user changed the settings so that we can now find
# the path to all media files
state.STOP_SYNC = False
state.PATH_VERIFIED = False
@CatchExceptions(warnuser=False)
def onNotification(self, sender, method, data):
@ -137,7 +172,7 @@ class KodiMonitor(Monitor):
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
sleep(5000)
window('plex_runLibScan', value="full")
plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit":
log.info('Kodi OnQuit detected - shutting down')

View file

@ -17,8 +17,9 @@ log = getLogger("PLEX."+__name__)
###############################################################################
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'],
add_stops=['STOP_SYNC'])
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'DB_SCAN',
'STOP_SYNC'])
class Process_Fanart_Thread(Thread):
"""
Threaded download of additional fanart in the background

View file

@ -16,7 +16,7 @@ log = getLogger("PLEX."+__name__)
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
class Threaded_Get_Metadata(Thread):
"""
Threaded download of Plex XML metadata for a certain library item.
@ -115,17 +115,9 @@ class Threaded_Get_Metadata(Thread):
except (TypeError, IndexError, AttributeError):
log.error('Could not get children for Plex id %s'
% item['itemId'])
else:
item['children'] = []
for child in children_xml:
child_xml = GetPlexMetadata(child.attrib['ratingKey'])
try:
child_xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not get child for Plex id %s'
% child.attrib['ratingKey'])
else:
item['children'].append(child_xml[0])
item['children'] = children_xml
# place item into out queue
out_queue.put(item)

View file

@ -15,7 +15,7 @@ log = getLogger("PLEX."+__name__)
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
class Threaded_Process_Metadata(Thread):
"""
Not yet implemented for more than 1 thread - if ever. Only to be called by

View file

@ -2,7 +2,8 @@
from logging import getLogger
from threading import Thread, Lock
from xbmc import sleep
from xbmc import sleep, Player
from xbmcgui import DialogProgressBG
from utils import thread_methods, language as lang
@ -18,18 +19,17 @@ LOCK = Lock()
###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
class Threaded_Show_Sync_Info(Thread):
"""
Threaded class to show the Kodi statusbar of the metadata download.
Input:
dialog xbmcgui.DialogProgressBG() object to show progress
total: Total number of items to get
item_type:
"""
def __init__(self, dialog, total, item_type):
def __init__(self, total, item_type):
self.total = total
self.dialog = dialog
self.item_type = item_type
Thread.__init__(self)
@ -51,14 +51,15 @@ class Threaded_Show_Sync_Info(Thread):
log.debug('Show sync info thread started')
# cache local variables because it's faster
total = self.total
dialog = self.dialog
dialog = DialogProgressBG('dialoglogProgressBG')
thread_stopped = self.thread_stopped
dialog.create("%s %s: %s %s"
% (lang(39714), self.item_type, str(total), lang(39715)))
player = Player()
total = 2 * total
totalProgress = 0
while thread_stopped() is False:
while thread_stopped() is False and not player.isPlaying():
with LOCK:
get_progress = GET_METADATA_COUNT
process_progress = PROCESS_METADATA_COUNT

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from logging import getLogger
from threading import Thread
import Queue
from random import shuffle
import xbmc
import xbmcgui
from xbmcvfs import exists
from utils import window, settings, getUnixTimestamp, sourcesXML,\
@ -22,7 +21,8 @@ import videonodes
import variables as v
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus, get_plex_sections
GetPlexSectionResults, GetPlexKeyNumber, GetPMSStatus, get_plex_sections, \
GetAllPlexChildren
import PlexAPI
from library_sync.get_metadata import Threaded_Get_Metadata
from library_sync.process_metadata import Threaded_Process_Metadata
@ -33,84 +33,77 @@ import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
log = getLogger("PLEX."+__name__)
###############################################################################
@thread_methods(add_stops=['STOP_SYNC'],
add_suspends=['SUSPEND_LIBRARY_THREAD'])
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
class LibrarySync(Thread):
"""
"""
def __init__(self, callback=None):
self.mgr = callback
# Dict of items we just processed in order to prevent a reprocessing
# caused by websocket
self.just_processed = {}
# How long do we wait until we start re-processing? (in seconds)
self.ignore_just_processed = 10*60
self.itemsToProcess = []
self.sessionKeys = []
self.fanartqueue = Queue.Queue()
if settings('FanartTV') == 'true':
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
# How long should we wait at least to process new/changed PMS items?
self.saftyMargin = int(settings('backgroundsync_saftyMargin'))
self.fullSyncInterval = int(settings('fullSyncInterval')) * 60
self.user = userclient.UserClient()
self.vnodes = videonodes.VideoNodes()
self.dialog = xbmcgui.Dialog()
self.xbmcplayer = xbmc.Player()
self.syncThreadNumber = int(settings('syncThreadNumber'))
self.installSyncDone = settings('SyncInstallRunDone') == 'true'
window('dbSyncIndicator', value=settings('dbSyncIndicator'))
self.enableMusic = settings('enableMusic') == "true"
self.enableBackgroundSync = settings(
'enableBackgroundSync') == "true"
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
state.BACKGROUND_SYNC = settings(
'enableBackgroundSync') == 'true'
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
settings('backgroundsync_saftyMargin'))
# Show sync dialog even if user deactivated?
self.force_dialog = True
# Init for replacing paths
window('remapSMB', value=settings('remapSMB'))
window('replaceSMB', value=settings('replaceSMB'))
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
state.REMAP_PATH = settings('remapSMB') == 'true'
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
for arg in ('Org', 'New'):
key = 'remapSMB%s%s' % (typus, arg)
window(key, value=settings(key))
setattr(state, key, settings(key))
# Just in case a time sync goes wrong
self.timeoffset = int(settings('kodiplextimeoffset'))
window('kodiplextimeoffset', value=str(self.timeoffset))
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
Thread.__init__(self)
def showKodiNote(self, message, forced=False, icon="plex"):
def showKodiNote(self, message, icon="plex"):
"""
Shows a Kodi popup, if user selected to do so. Pass message in unicode
or string
icon: "plex": shows Plex icon
"error": shows Kodi error icon
forced: always show popup, even if user setting to off
"""
if settings('dbSyncIndicator') != 'true':
if not forced:
if self.xbmcplayer.isPlaying():
# Don't show any dialog if media is playing
return
if state.SYNC_DIALOG is not True and self.force_dialog is not True:
return
if icon == "plex":
self.dialog.notification(
lang(29999),
message,
"special://home/addons/plugin.video.plexkodiconnect/icon.png",
5000,
False)
dialog('notification',
heading='{plex}',
message=message,
icon='{plex}',
sound=False)
elif icon == "error":
self.dialog.notification(
lang(29999),
message,
xbmcgui.NOTIFICATION_ERROR,
7000,
True)
dialog('notification',
heading='{plex}',
message=message,
icon='{error}')
def syncPMStime(self):
"""
@ -208,11 +201,10 @@ class LibrarySync(Thread):
return False
# Calculate time offset Kodi-PMS
self.timeoffset = int(koditime) - int(plextime)
window('kodiplextimeoffset', value=str(self.timeoffset))
settings('kodiplextimeoffset', value=str(self.timeoffset))
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
settings('kodiplextimeoffset', value=str(state.KODI_PLEX_TIME_OFFSET))
log.info("Time offset Koditime - Plextime in seconds: %s"
% str(self.timeoffset))
% str(state.KODI_PLEX_TIME_OFFSET))
return True
def initializeDBs(self):
@ -257,9 +249,6 @@ class LibrarySync(Thread):
# True: we're syncing only the delta, e.g. different checksum
self.compare = not repair
# Empty our list of item's we've just processed in the past
self.just_processed = {}
self.new_items_only = True
# This will also update playstates and userratings!
log.info('Running fullsync for NEW PMS items with repair=%s' % repair)
@ -293,23 +282,21 @@ class LibrarySync(Thread):
'movies': self.PlexMovies,
'tvshows': self.PlexTVShows,
}
if self.enableMusic:
if state.ENABLE_MUSIC:
process['music'] = self.PlexMusic
# Do the processing
for itemtype in process:
if self.thread_stopped():
xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver)
return False
if not process[itemtype]():
if (self.thread_stopped() or
self.thread_suspended() or
not process[itemtype]()):
xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver)
return False
# Let kodi update the views in any case, since we're doing a full sync
xbmc.executebuiltin('UpdateLibrary(video)')
if self.enableMusic:
if state.ENABLE_MUSIC:
xbmc.executebuiltin('UpdateLibrary(music)')
window('plex_initialScan', clear=True)
@ -317,13 +304,13 @@ class LibrarySync(Thread):
setScreensaver(value=screensaver)
if window('plex_scancrashed') == 'true':
# Show warning if itemtypes.py crashed at some point
self.dialog.ok(lang(29999), lang(39408))
dialog('ok', heading='{plex}', line1=lang(39408))
window('plex_scancrashed', clear=True)
elif window('plex_scancrashed') == '401':
window('plex_scancrashed', clear=True)
if state.PMS_STATUS not in ('401', 'Auth'):
# Plex server had too much and returned ERROR
self.dialog.ok(lang(29999), lang(39409))
dialog('ok', heading='{plex}', line1=lang(39409))
# Path hack, so Kodis Information screen works
with kodidb.GetKodiDB('video') as kodi_db:
@ -472,12 +459,12 @@ class LibrarySync(Thread):
"""
Compare the views to Plex
"""
if state.DIRECT_PATHS is True and self.enableMusic is True:
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
if music.set_excludefromscan_music_folders() is True:
log.info('Detected new Music library - restarting now')
# 'New Plex music library detected. Sorry, but we need to
# restart Kodi now due to the changes made.'
dialog('ok', lang(29999), lang(39711))
dialog('ok', heading='{plex}', line1=lang(39711))
from xbmc import executebuiltin
executebuiltin('RestartApp')
return False
@ -625,7 +612,6 @@ class LibrarySync(Thread):
self.allPlexElementsId APPENDED(!!) dict
= {itemid: checksum}
"""
now = getUnixTimestamp()
if self.new_items_only is True:
# Only process Plex items that Kodi does not already have in lib
for item in xml:
@ -633,8 +619,8 @@ class LibrarySync(Thread):
if not itemId:
# Skipping items 'title=All episodes' without a 'ratingKey'
continue
self.allPlexElementsId[itemId] = ("K%s%s" %
(itemId, item.attrib.get('updatedAt', '')))
self.allPlexElementsId[itemId] = "K%s%s" % \
(itemId, item.attrib.get('updatedAt', ''))
if itemId not in self.allKodiElementsId:
self.updatelist.append({
'itemId': itemId,
@ -646,10 +632,8 @@ class LibrarySync(Thread):
'mediaType': item.attrib.get('type'),
'get_children': get_children
})
self.just_processed[itemId] = now
return
if self.compare:
elif self.compare:
# Only process the delta - new or changed items
for item in xml:
itemId = item.attrib.get('ratingKey')
@ -673,7 +657,6 @@ class LibrarySync(Thread):
'mediaType': item.attrib.get('type'),
'get_children': get_children
})
self.just_processed[itemId] = now
else:
# Initial or repair sync: get all Plex movies
for item in xml:
@ -681,8 +664,8 @@ class LibrarySync(Thread):
if not itemId:
# Skipping items 'title=All episodes' without a 'ratingKey'
continue
self.allPlexElementsId[itemId] = ("K%s%s"
% (itemId, item.attrib.get('updatedAt', '')))
self.allPlexElementsId[itemId] = "K%s%s" \
% (itemId, item.attrib.get('updatedAt', ''))
self.updatelist.append({
'itemId': itemId,
'itemType': itemType,
@ -693,7 +676,6 @@ class LibrarySync(Thread):
'mediaType': item.attrib.get('type'),
'get_children': get_children
})
self.just_processed[itemId] = now
def GetAndProcessXMLs(self, itemType):
"""
@ -725,7 +707,7 @@ class LibrarySync(Thread):
getMetadataQueue.put(updateItem)
# Spawn GetMetadata threads for downloading
threads = []
for i in range(min(self.syncThreadNumber, itemNumber)):
for i in range(min(state.SYNC_THREAD_NUMBER, itemNumber)):
thread = Threaded_Get_Metadata(getMetadataQueue,
processMetadataQueue)
thread.setDaemon(True)
@ -739,12 +721,9 @@ class LibrarySync(Thread):
thread.start()
threads.append(thread)
# Start one thread to show sync progress ONLY for new PMS items
if self.new_items_only is True and window('dbSyncIndicator') == 'true':
dialog = xbmcgui.DialogProgressBG()
thread = sync_info.Threaded_Show_Sync_Info(
dialog,
itemNumber,
itemType)
if self.new_items_only is True and (state.SYNC_DIALOG is True or
self.force_dialog is True):
thread = sync_info.Threaded_Show_Sync_Info(itemNumber, itemType)
thread.setDaemon(True)
thread.start()
threads.append(thread)
@ -803,7 +782,9 @@ class LibrarySync(Thread):
# PROCESS MOVIES #####
self.updatelist = []
for view in views:
if self.thread_stopped():
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
return False
# Get items per view
viewId = view['id']
@ -821,10 +802,9 @@ class LibrarySync(Thread):
viewName,
viewId)
self.GetAndProcessXMLs(itemType)
log.info("Processed view")
# Update viewstate for EVERY item
for view in views:
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -896,7 +876,9 @@ class LibrarySync(Thread):
# PROCESS TV Shows #####
self.updatelist = []
for view in views:
if self.thread_stopped():
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
return False
# Get items per view
viewId = view['id']
@ -925,7 +907,7 @@ class LibrarySync(Thread):
# PROCESS TV Seasons #####
# Cycle through tv shows
for tvShowId in allPlexTvShowsId:
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
# Grab all seasons to tvshow from PMS
seasons = GetAllPlexChildren(tvShowId)
@ -950,7 +932,7 @@ class LibrarySync(Thread):
# PROCESS TV Episodes #####
# Cycle through tv shows
for view in views:
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
# Grab all episodes to tvshow from PMS
episodes = GetAllPlexLeaves(view['id'])
@ -985,7 +967,7 @@ class LibrarySync(Thread):
# Update viewstate:
for view in views:
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -1022,7 +1004,7 @@ class LibrarySync(Thread):
for kind in (v.PLEX_TYPE_ARTIST,
v.PLEX_TYPE_ALBUM,
v.PLEX_TYPE_SONG):
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
log.debug("Start processing music %s" % kind)
self.allKodiElementsId = {}
@ -1039,7 +1021,7 @@ class LibrarySync(Thread):
# Update viewstate for EVERY item
for view in views:
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -1064,7 +1046,9 @@ class LibrarySync(Thread):
except ValueError:
pass
for view in views:
if self.thread_stopped():
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
return False
# Get items per view
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
@ -1092,11 +1076,24 @@ class LibrarySync(Thread):
processes json.loads() messages from websocket. Triage what we need to
do with "process_" methods
"""
typus = message.get('type')
if typus == 'playing':
if message['type'] == 'playing':
try:
self.process_playing(message['PlaySessionStateNotification'])
elif typus == 'timeline':
except KeyError:
log.error('Received invalid PMS message for playstate: %s'
% message)
elif message['type'] == 'timeline':
try:
self.process_timeline(message['TimelineEntry'])
except (KeyError, ValueError):
log.error('Received invalid PMS message for timeline: %s'
% message)
elif message['type'] == 'activity':
try:
self.process_activity(message['ActivityNotification'])
except KeyError:
log.error('Received invalid PMS message for activity: %s'
% message)
def multi_delete(self, liste, deleteListe):
"""
@ -1139,25 +1136,22 @@ class LibrarySync(Thread):
now = getUnixTimestamp()
deleteListe = []
for i, item in enumerate(self.itemsToProcess):
if self.thread_stopped():
if self.thread_stopped() or self.thread_suspended():
# Chances are that Kodi gets shut down
break
if item['state'] == 9:
successful = self.process_deleteditems(item)
elif now - item['timestamp'] < self.saftyMargin:
elif now - item['timestamp'] < state.BACKGROUNDSYNC_SAFTYMARGIN:
# We haven't waited long enough for the PMS to finish
# processing the item. Do it later (excepting deletions)
continue
else:
successful = self.process_newitems(item)
if successful:
self.just_processed[str(item['ratingKey'])] = now
if successful and settings('FanartTV') == 'true':
plex_type = v.PLEX_TYPE_FROM_WEBSOCKET[item['type']]
if plex_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({
'plex_id': item['ratingKey'],
'plex_type': plex_type,
'plex_type': item['type'],
'refresh': False
})
if successful is True:
@ -1213,22 +1207,25 @@ class LibrarySync(Thread):
return True
def process_deleteditems(self, item):
if item.get('type') == 1:
log.debug("Removing movie %s" % item.get('ratingKey'))
if item['type'] == v.PLEX_TYPE_MOVIE:
log.debug("Removing movie %s" % item['ratingKey'])
self.videoLibUpdate = True
with itemtypes.Movies() as movie:
movie.remove(item.get('ratingKey'))
elif item.get('type') in (2, 3, 4):
log.debug("Removing episode/season/tv show %s"
% item.get('ratingKey'))
movie.remove(item['ratingKey'])
elif item['type'] in (v.PLEX_TYPE_SHOW,
v.PLEX_TYPE_SEASON,
v.PLEX_TYPE_EPISODE):
log.debug("Removing episode/season/tv show %s" % item['ratingKey'])
self.videoLibUpdate = True
with itemtypes.TVShows() as show:
show.remove(item.get('ratingKey'))
elif item.get('type') in (8, 9, 10):
log.debug("Removing song/album/artist %s" % item.get('ratingKey'))
show.remove(item['ratingKey'])
elif item['type'] in (v.PLEX_TYPE_ARTIST,
v.PLEX_TYPE_ALBUM,
v.PLEX_TYPE_SONG):
log.debug("Removing song/album/artist %s" % item['ratingKey'])
self.musicLibUpdate = True
with itemtypes.Music() as music:
music.remove(item.get('ratingKey'))
music.remove(item['ratingKey'])
return True
def process_timeline(self, data):
@ -1236,30 +1233,32 @@ class LibrarySync(Thread):
PMS is messing with the library items, e.g. new or changed. Put in our
"processing queue" for later
"""
now = getUnixTimestamp()
for item in data:
if 'tv.plex' in item.get('identifier', ''):
# Ommit Plex DVR messages - the Plex IDs are not corresponding
# (DVR ratingKeys are not unique and might correspond to a
# movie or episode)
continue
typus = int(item.get('type', 0))
status = int(item.get('state', 0))
if status == 9 or (typus in (1, 4, 10) and status == 5):
# Only process deleted items OR movies, episodes, tracks/songs
plex_id = str(item.get('itemID', '0'))
if plex_id == '0':
log.error('Received malformed PMS message: %s' % item)
typus = v.PLEX_TYPE_FROM_WEBSOCKET[int(item['type'])]
if typus == v.PLEX_TYPE_CLIP:
# No need to process extras or trailers
continue
try:
if (now - self.just_processed[plex_id] <
self.ignore_just_processed and status != 9):
log.debug('We just processed %s: ignoring' % plex_id)
continue
except KeyError:
# Item has NOT just been processed
pass
# Have we already added this element?
status = int(item['state'])
if status == 9:
# Immediately and always process deletions (as the PMS will
# send additional message with other codes)
self.itemsToProcess.append({
'state': status,
'type': typus,
'ratingKey': str(item['itemID']),
'timestamp': getUnixTimestamp(),
'attempt': 0
})
elif typus in (v.PLEX_TYPE_MOVIE,
v.PLEX_TYPE_EPISODE,
v.PLEX_TYPE_SONG) and status == 5:
plex_id = str(item['itemID'])
# Have we already added this element for processing?
for existingItem in self.itemsToProcess:
if existingItem['ratingKey'] == plex_id:
break
@ -1273,24 +1272,63 @@ class LibrarySync(Thread):
'attempt': 0
})
def process_activity(self, data):
"""
PMS is re-scanning an item, e.g. after having changed a movie poster.
WATCH OUT for this if it's triggered by our PKC library scan!
"""
for item in data:
if item['event'] != 'ended':
# Scan still going on, so skip for now
continue
elif item['Activity'].get('Context') is None:
# Not related to any Plex element, but entire library
continue
elif item['Activity']['type'] != 'library.refresh.items':
# Not the type of message relevant for us
continue
plex_id = GetPlexKeyNumber(item['Activity']['Context']['key'])[1]
if plex_id == '':
# Likely a Plex id like /library/metadata/3/children
continue
# We're only looking at existing elements - have we synced yet?
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(plex_id)
if kodi_info is None:
log.debug('Plex id %s not synced yet - skipping' % plex_id)
continue
# Have we already added this element?
for existingItem in self.itemsToProcess:
if existingItem['ratingKey'] == plex_id:
break
else:
# Haven't added this element to the queue yet
self.itemsToProcess.append({
'state': None, # Don't need a state here
'type': kodi_info[5],
'ratingKey': plex_id,
'timestamp': getUnixTimestamp(),
'attempt': 0
})
def process_playing(self, data):
"""
Someone (not necessarily the user signed in) is playing something some-
where
"""
items = []
with plexdb.Get_Plex_DB() as plex_db:
for item in data:
# Drop buffering messages immediately
status = item.get('state')
status = item['state']
if status == 'buffering':
continue
ratingKey = item.get('ratingKey')
kodiInfo = plex_db.getItem_byId(ratingKey)
if kodiInfo is None:
ratingKey = str(item['ratingKey'])
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(ratingKey)
if kodi_info is None:
# Item not (yet) in Kodi library
continue
sessionKey = item.get('sessionKey')
sessionKey = item['sessionKey']
# Do we already have a sessionKey stored?
if sessionKey not in self.sessionKeys:
if settings('plex_serverowned') == 'false':
@ -1355,9 +1393,9 @@ class LibrarySync(Thread):
# Append to list that we need to process
items.append({
'ratingKey': ratingKey,
'kodi_id': kodiInfo[0],
'file_id': kodiInfo[1],
'kodi_type': kodiInfo[4],
'kodi_id': kodi_info[0],
'file_id': kodi_info[1],
'kodi_type': kodi_info[4],
'viewOffset': resume,
'state': status,
'duration': currSess['duration'],
@ -1394,6 +1432,68 @@ class LibrarySync(Thread):
'refresh': refresh
})
def triage_lib_scans(self):
"""
Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually
triggered full or repair syncs
"""
if state.RUN_LIB_SCAN in ("full", "repair"):
log.info('Full library scan requested, starting')
window('plex_dbScan', value="true")
state.DB_SCAN = True
if state.RUN_LIB_SCAN == "full":
self.fullSync()
else:
self.fullSync(repair=True)
window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Full library sync finished
self.showKodiNote(lang(39407))
# Reset views was requested from somewhere else
elif state.RUN_LIB_SCAN == "views":
log.info('Refresh playlist and nodes requested, starting')
window('plex_dbScan', value="true")
state.DB_SCAN = True
# First remove playlists
deletePlaylists()
# Remove video nodes
deleteNodes()
# Kick off refresh
if self.maintainViews() is True:
# Ran successfully
log.info("Refresh playlists/nodes completed")
# "Plex playlists/nodes refreshed"
self.showKodiNote(lang(39405))
else:
# Failed
log.error("Refresh playlists/nodes failed")
# "Plex playlists/nodes refresh failed"
self.showKodiNote(lang(39406),
icon="error")
window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif state.RUN_LIB_SCAN == 'fanart':
# Only look for missing fanart (No)
# or refresh all fanart (Yes)
self.fanartSync(refresh=dialog(
'yesno',
heading='{plex}',
line1=lang(39223),
nolabel=lang(39224),
yeslabel=lang(39225)))
elif state.RUN_LIB_SCAN == 'textures':
state.DB_SCAN = True
window('plex_dbScan', value="true")
import artwork
artwork.Artwork().fullTextureCacheSync()
window('plex_dbScan', clear=True)
state.DB_SCAN = False
else:
raise NotImplementedError('Library scan not defined: %s'
% state.RUN_LIB_SCAN)
# Reset
state.RUN_LIB_SCAN = None
def run(self):
try:
self.run_internal()
@ -1404,7 +1504,7 @@ class LibrarySync(Thread):
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
# Library sync thread has crashed
self.dialog.ok(lang(29999), lang(39400))
dialog('ok', heading='{plex}', line1=lang(39400))
raise
def run_internal(self):
@ -1412,24 +1512,21 @@ class LibrarySync(Thread):
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
installSyncDone = self.installSyncDone
enableBackgroundSync = self.enableBackgroundSync
background_sync = state.BACKGROUND_SYNC
fullSync = self.fullSync
processMessage = self.processMessage
processItems = self.processItems
fullSyncInterval = self.fullSyncInterval
FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL
lastSync = 0
lastTimeSync = 0
lastProcessing = 0
oneDay = 60*60*24
xbmcplayer = xbmc.Player()
# Link to Websocket queue
queue = self.mgr.ws.queue
startupComplete = False
self.views = []
errorcount = 0
log.info("---===### Starting LibrarySync ###===---")
@ -1450,7 +1547,9 @@ class LibrarySync(Thread):
return
xbmc.sleep(1000)
if (window('plex_dbCheck') != "true" and installSyncDone):
if state.KODI_DB_CHECKED is False and installSyncDone:
# Install sync was already done, don't force-show dialogs
self.force_dialog = False
# Verify the validity of the database
currentVersion = settings('dbCreatedWithVersion')
minVersion = window('plex_minDBVersion')
@ -1459,18 +1558,19 @@ class LibrarySync(Thread):
log.warn("Db version out of date: %s minimum version "
"required: %s" % (currentVersion, minVersion))
# DB out of date. Proceed to recreate?
resp = self.dialog.yesno(heading=lang(29999),
resp = dialog('yesno',
heading=lang(29999),
line1=lang(39401))
if not resp:
log.warn("Db version out of date! USER IGNORED!")
# PKC may not work correctly until reset
self.dialog.ok(heading=lang(29999),
line1=(lang(29999) + lang(39402)))
dialog('ok',
heading='{plex}',
line1=lang(29999) + lang(39402))
else:
reset()
break
window('plex_dbCheck', value="true")
state.KODI_DB_CHECKED = True
if not startupComplete:
# Also runs when first installed
@ -1483,7 +1583,7 @@ class LibrarySync(Thread):
log.error('Current Kodi version: %s' % tryDecode(
xbmc.getInfoLabel('System.BuildVersion')))
# "Current Kodi version is unsupported, cancel lib sync"
self.dialog.ok(heading=lang(29999), line1=lang(39403))
dialog('ok', heading='{plex}', line1=lang(39403))
break
# Run start up sync
state.DB_SCAN = True
@ -1518,93 +1618,38 @@ class LibrarySync(Thread):
settings('SyncInstallRunDone', value="true")
settings("dbCreatedWithVersion", v.ADDON_VERSION)
installSyncDone = True
self.force_dialog = False
else:
log.error("Initial start-up full sync unsuccessful")
errorcount += 1
if errorcount > 2:
log.error("Startup full sync failed. Stopping sync")
# "Startup syncing process failed repeatedly"
# "Please restart"
self.dialog.ok(heading=lang(29999),
line1=lang(39404))
break
# Currently no db scan, so we can start a new scan
elif state.DB_SCAN is False:
# Full scan was requested from somewhere else, e.g. userclient
if window('plex_runLibScan') in ("full", "repair"):
log.info('Full library scan requested, starting')
window('plex_dbScan', value="true")
state.DB_SCAN = True
if window('plex_runLibScan') == "full":
fullSync()
elif window('plex_runLibScan') == "repair":
fullSync(repair=True)
window('plex_runLibScan', clear=True)
window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Full library sync finished
self.showKodiNote(lang(39407), forced=False)
# Reset views was requested from somewhere else
elif window('plex_runLibScan') == "views":
log.info('Refresh playlist and nodes requested, starting')
window('plex_dbScan', value="true")
state.DB_SCAN = True
window('plex_runLibScan', clear=True)
# First remove playlists
deletePlaylists()
# Remove video nodes
deleteNodes()
# Kick off refresh
if self.maintainViews() is True:
# Ran successfully
log.info("Refresh playlists/nodes completed")
# "Plex playlists/nodes refreshed"
self.showKodiNote(lang(39405), forced=True)
else:
# Failed
log.error("Refresh playlists/nodes failed")
# "Plex playlists/nodes refresh failed"
self.showKodiNote(lang(39406),
forced=True,
icon="error")
window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif window('plex_runLibScan') == 'fanart':
window('plex_runLibScan', clear=True)
# Only look for missing fanart (No)
# or refresh all fanart (Yes)
self.fanartSync(refresh=self.dialog.yesno(
heading=lang(29999),
line1=lang(39223),
nolabel=lang(39224),
yeslabel=lang(39225)))
elif window('plex_runLibScan') == 'del_textures':
window('plex_runLibScan', clear=True)
state.DB_SCAN = True
window('plex_dbScan', value="true")
import artwork
artwork.Artwork().fullTextureCacheSync()
window('plex_dbScan', clear=True)
state.DB_SCAN = False
else:
if state.RUN_LIB_SCAN is not None:
# Force-show dialogs since they are user-initiated
self.force_dialog = True
self.triage_lib_scans()
self.force_dialog = False
continue
now = getUnixTimestamp()
if (now - lastSync > fullSyncInterval and
not xbmcplayer.isPlaying()):
# Standard syncs - don't force-show dialogs
self.force_dialog = False
if (now - lastSync > FULL_SYNC_INTERVALL and
not self.xbmcplayer.isPlaying()):
lastSync = now
log.info('Doing scheduled full library scan')
state.DB_SCAN = True
window('plex_dbScan', value="true")
if fullSync() is False and not thread_stopped():
log.error('Could not finish scheduled full sync')
self.force_dialog = True
self.showKodiNote(lang(39410),
forced=True,
icon='error')
self.force_dialog = False
window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Full library sync finished
self.showKodiNote(lang(39407), forced=False)
self.showKodiNote(lang(39407))
elif now - lastTimeSync > oneDay:
lastTimeSync = now
log.info('Starting daily time sync')
@ -1613,7 +1658,7 @@ class LibrarySync(Thread):
self.syncPMStime()
window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif enableBackgroundSync:
elif background_sync:
# Check back whether we should process something
# Only do this once every while (otherwise, potentially
# many screen refreshes lead to flickering)

View file

@ -1,74 +1,47 @@
# -*- coding: utf-8 -*-
##################################################################################################
###############################################################################
import logging
import xbmc
###############################################################################
LEVELS = {
logging.ERROR: xbmc.LOGERROR,
logging.WARNING: xbmc.LOGWARNING,
logging.INFO: xbmc.LOGNOTICE,
logging.DEBUG: xbmc.LOGDEBUG
}
###############################################################################
from utils import window, tryEncode
##################################################################################################
def tryEncode(uniString, encoding='utf-8'):
"""
Will try to encode uniString (in unicode) to encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
"""
if isinstance(uniString, str):
# already encoded
return uniString
try:
uniString = uniString.encode(encoding, "ignore")
except TypeError:
uniString = uniString.encode()
return uniString
def config():
logger = logging.getLogger('PLEX')
logger.addHandler(LogHandler())
logger.setLevel(logging.DEBUG)
class LogHandler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter())
self.setFormatter(logging.Formatter(fmt="%(name)s: %(message)s"))
def emit(self, record):
if self._get_log_level(record.levelno):
try:
xbmc.log(self.format(record), level=xbmc.LOGNOTICE)
xbmc.log(self.format(record), level=LEVELS[record.levelno])
except UnicodeEncodeError:
xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE)
@classmethod
def _get_log_level(cls, level):
levels = {
logging.ERROR: 0,
logging.WARNING: 0,
logging.INFO: 1,
logging.DEBUG: 2
}
try:
log_level = int(window('plex_logLevel'))
except ValueError:
log_level = 0
return log_level >= levels[level]
class MyFormatter(logging.Formatter):
def __init__(self, fmt="%(name)s -> %(message)s"):
logging.Formatter.__init__(self, fmt)
def format(self, record):
# Save the original format configured by the user
# when the logger formatter was instantiated
format_orig = self._fmt
# Replace the original format with one customized by logging level
if record.levelno in (logging.DEBUG, logging.ERROR):
self._fmt = '%(name)s -> %(levelname)s: %(message)s'
# Call the original formatter class to do the grunt work
result = logging.Formatter.format(self, record)
# Restore the original format configured by the user
self._fmt = format_orig
return result
xbmc.log(tryEncode(self.format(record)),
level=LEVELS[record.levelno])

View file

@ -1,13 +1,26 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import cPickle as Pickle
from cPickle import dumps, loads
from utils import pickl_window
from xbmcgui import Window
from xbmc import log, LOGDEBUG
###############################################################################
WINDOW = Window(10000)
PREFIX = 'PLEX.%s: ' % __name__
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
def pickl_window(property, value=None, clear=False):
"""
Get or set window property - thread safe! For use with Pickle
Property and value must be string
"""
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
def pickle_me(obj, window_var='plex_result'):
@ -19,9 +32,9 @@ def pickle_me(obj, window_var='plex_result'):
obj can be pretty much any Python object. However, classes and
functions won't work. See the Pickle documentation
"""
log.debug('Start pickling: %s' % obj)
pickl_window(window_var, value=Pickle.dumps(obj))
log.debug('Successfully pickled')
log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
pickl_window(window_var, value=dumps(obj))
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
def unpickle_me(window_var='plex_result'):
@ -31,9 +44,9 @@ def unpickle_me(window_var='plex_result'):
"""
result = pickl_window(window_var)
pickl_window(window_var, clear=True)
log.debug('Start unpickling')
obj = Pickle.loads(result)
log.debug('Successfully unpickled: %s' % obj)
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
obj = loads(result)
log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
return obj

View file

@ -17,6 +17,7 @@ import variables as v
from downloadutils import DownloadUtils
from PKC_listitem import convert_PKC_to_listitem
import plexdb_functions as plexdb
from context_entry import ContextMenu
import state
###############################################################################
@ -142,6 +143,9 @@ class Playback_Starter(Thread):
params.get('view_offset'),
directplay=True if params.get('play_directly') else False,
node=False if params.get('node') == 'false' else True)
elif mode == 'context_menu':
ContextMenu()
result = Playback_Successful()
except:
log.error('Error encountered for mode %s, params %s'
% (mode, params))

View file

@ -5,10 +5,8 @@ import logging
import json
import xbmc
import xbmcgui
from utils import window, settings, language as lang, DateToKodi, \
getUnixTimestamp, tryDecode, tryEncode
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode
import downloadutils
import plexdb_functions as plexdb
import kodidb_functions as kodidb
@ -354,6 +352,8 @@ class Player(xbmc.Player):
log.info("Percent complete: %s Mark played at: %s"
% (percentComplete, markPlayed))
if percentComplete >= markPlayed:
# Kodi seems to sometimes overwrite our playstate, so wait
xbmc.sleep(500)
# Tell Kodi that we've finished watching (Plex knows)
if (data['fileid'] is not None and
data['itemType'] in (v.KODI_TYPE_MOVIE,

View file

@ -10,6 +10,8 @@ STOP_PKC = False
SUSPEND_LIBRARY_THREAD = False
# Set if user decided to cancel sync
STOP_SYNC = False
# Could we access the paths?
PATH_VERIFIED = False
# Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true'
DB_SCAN = False
@ -24,6 +26,42 @@ RESTRICTED_USER = False
DIRECT_PATHS = False
# Shall we replace custom user ratings with the number of versions available?
INDICATE_MEDIA_VERSIONS = False
# Do we need to run a special library scan?
RUN_LIB_SCAN = None
# Stemming from the PKC settings.xml
# Shall we show Kodi dialogs when synching?
SYNC_DIALOG = True
# Have we already checked the Kodi DB on consistency?
KODI_DB_CHECKED = False
# Is synching of Plex music enabled?
ENABLE_MUSIC = True
# How often shall we sync?
FULL_SYNC_INTERVALL = 0
# Background Sync enabled at all?
BACKGROUND_SYNC = True
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
BACKGROUNDSYNC_SAFTYMARGIN = 0
# How many threads to download Plex metadata on sync?
SYNC_THREAD_NUMBER = 0
# What's the time offset between the PMS and Kodi?
KODI_PLEX_TIME_OFFSET = 0.0
# Path remapping mechanism (e.g. smb paths)
# Do we replace \\myserver\path to smb://myserver/path?
REPLACE_SMB_PATH = False
# Do we generally remap?
REMAP_PATH = False
# Mappings for REMAP_PATH:
remapSMBmovieOrg = None
remapSMBmovieNew = None
remapSMBtvOrg = None
remapSMBtvNew = None
remapSMBmusicOrg = None
remapSMBmusicNew = None
remapSMBphotoOrg = None
remapSMBphotoNew = None
# Along with window('plex_authenticated')
AUTHENTICATED = False

View file

@ -59,24 +59,6 @@ def window(property, value=None, clear=False, windowid=10000):
return tryDecode(win.getProperty(property))
def pickl_window(property, value=None, clear=False, windowid=10000):
"""
Get or set window property - thread safe! For use with Pickle
Property and value must be string
"""
if windowid != 10000:
win = xbmcgui.Window(windowid)
else:
win = WINDOW
if clear:
win.clearProperty(property)
elif value is not None:
win.setProperty(property, value)
else:
return win.getProperty(property)
def plex_command(key, value):
"""
Used to funnel states between different Python instances. NOT really thread
@ -86,7 +68,7 @@ def plex_command(key, value):
value: either 'True' or 'False'
"""
while window('plex_command'):
xbmc.sleep(5)
xbmc.sleep(20)
window('plex_command', value='%s-%s' % (key, value))
@ -140,6 +122,15 @@ def dialog(typus, *args, **kwargs):
Displays xbmcgui Dialog. Pass a string as typus:
'yesno', 'ok', 'notification', 'input', 'select', 'numeric'
kwargs:
heading='{plex}' title bar (here PlexKodiConnect)
message=lang(30128), Actual dialog content. Don't use with OK
line1=str(), For 'OK' and 'yesno' dialogs use line1...line3!
time=5000,
sound=True,
nolabel=str(), For 'yesno' dialogs
yeslabel=str(), For 'yesno' dialogs
Icons:
icon='{plex}' Display Plex standard icon
icon='{info}' xbmcgui.NOTIFICATION_INFO
@ -221,6 +212,16 @@ def tryDecode(string, encoding='utf-8'):
return string
def slugify(text):
"""
Normalizes text (in unicode or string) to e.g. enable safe filenames.
Returns unicode
"""
if not isinstance(text, unicode):
text = unicode(text)
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
def escape_html(string):
"""
Escapes the following:
@ -248,7 +249,7 @@ def DateToKodi(stamp):
None if an error was encountered
"""
try:
stamp = float(stamp) + float(window('kodiplextimeoffset'))
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
date_time = localtime(stamp)
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
except:

View file

@ -84,7 +84,8 @@ class WebSocket(Thread):
# No worries if read timed out
pass
except websocket.WebSocketConnectionClosedException:
log.info("Connection closed, (re)connecting")
log.info("%s: connection closed, (re)connecting"
% self.__class__.__name__)
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
@ -95,7 +96,7 @@ class WebSocket(Thread):
enable_multithread=True)
except IOError:
# Server is probably offline
log.info("Error connecting")
log.info("%s: Error connecting" % self.__class__.__name__)
self.ws = None
counter += 1
if counter > 3:
@ -103,33 +104,41 @@ class WebSocket(Thread):
self.IOError_response()
sleep(1000)
except websocket.WebSocketTimeoutException:
log.info("timeout while connecting, trying again")
log.info("%s: Timeout while connecting, trying again"
% self.__class__.__name__)
self.ws = None
sleep(1000)
except websocket.WebSocketException as e:
log.info('WebSocketException: %s' % e)
log.info('%s: WebSocketException: %s'
% (self.__class__.__name__, e))
if 'Handshake Status 401' in e.args:
handshake_counter += 1
if handshake_counter >= 5:
log.info('Error in handshake detected. Stopping '
'%s now' % self.__class__.__name__)
log.info('%s: Error in handshake detected. '
'Stopping now'
% self.__class__.__name__)
break
self.ws = None
sleep(1000)
except Exception as e:
log.error("Unknown exception encountered in connecting: %s"
% e)
log.error('%s: Unknown exception encountered when '
'connecting: %s' % (self.__class__.__name__, e))
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
log.error("%s: Traceback:\n%s"
% (self.__class__.__name__,
traceback.format_exc()))
self.ws = None
sleep(1000)
else:
counter = 0
handshake_counter = 0
except Exception as e:
log.error("Unknown exception encountered: %s" % e)
log.error("%s: Unknown exception encountered: %s"
% (self.__class__.__name__, e))
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
log.error("%s: Traceback:\n%s"
% (self.__class__.__name__,
traceback.format_exc()))
try:
self.ws.shutdown()
except:
@ -171,37 +180,46 @@ class PMS_Websocket(WebSocket):
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
log.debug("%s: Uri: %s, sslopt: %s"
% (self.__class__.__name__, uri, sslopt))
return uri, sslopt
def process(self, opcode, message):
if opcode not in self.opcode_data:
return False
return
try:
message = loads(message)
except Exception as ex:
log.error('Error decoding message from websocket: %s' % ex)
except ValueError:
log.error('%s: Error decoding message from websocket'
% self.__class__.__name__)
log.error(message)
return False
return
try:
message = message['NotificationContainer']
except KeyError:
log.error('Could not parse PMS message: %s' % message)
return False
log.error('%s: Could not parse PMS message: %s'
% (self.__class__.__name__, message))
return
# Triage
typus = message.get('type')
if typus is None:
log.error('No message type, dropping message: %s' % message)
return False
log.debug('Received message from PMS server: %s' % message)
log.error('%s: No message type, dropping message: %s'
% (self.__class__.__name__, message))
return
log.debug('%s: Received message from PMS server: %s'
% (self.__class__.__name__, message))
# Drop everything we're not interested in
if typus not in ('playing', 'timeline'):
return True
if typus not in ('playing', 'timeline', 'activity'):
return
elif typus == 'activity' and state.DB_SCAN is True:
# Only add to processing if PKC is NOT doing a lib scan (and thus
# possibly causing these reprocessing messages en mass)
log.debug('%s: Dropping message as PKC is currently synching'
% self.__class__.__name__)
else:
# Put PMS message on queue and let libsync take care of it
self.queue.put(message)
return True
def IOError_response(self):
log.warn("Repeatedly could not connect to PMS, "
@ -224,32 +242,36 @@ class Alexa_Websocket(WebSocket):
% (state.PLEX_USER_ID,
self.plex_client_Id, state.PLEX_TOKEN))
sslopt = {}
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
log.debug("%s: Uri: %s, sslopt: %s"
% (self.__class__.__name__, uri, sslopt))
return uri, sslopt
def process(self, opcode, message):
if opcode not in self.opcode_data:
return False
log.debug('Received the following message from Alexa:')
log.debug(message)
return
log.debug('%s: Received the following message from Alexa:'
% self.__class__.__name__)
log.debug('%s: %s' % (self.__class__.__name__, message))
try:
message = etree.fromstring(message)
except Exception as ex:
log.error('Error decoding message from Alexa: %s' % ex)
return False
log.error('%s: Error decoding message from Alexa: %s'
% (self.__class__.__name__, ex))
return
try:
if message.attrib['command'] == 'processRemoteControlCommand':
message = message[0]
else:
log.error('Unknown Alexa message received')
return False
log.error('%s: Unknown Alexa message received'
% self.__class__.__name__)
return
except:
log.error('Could not parse Alexa message')
return False
log.error('%s: Could not parse Alexa message'
% self.__class__.__name__)
return
process_command(message.attrib['path'][1:],
message.attrib,
queue=self.mgr.plexCompanion.queue)
return True
def IOError_response(self):
pass

View file

@ -146,7 +146,6 @@
</category>
<category label="30022"><!-- Advanced -->
<setting id="logLevel" type="enum" label="30004" values="Disabled|Info|Debug" default="1" />
<setting id="startupDelay" type="number" label="30529" default="0" option="int" />
<setting label="39018" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=repair)" option="close" /> <!-- Repair local database (force update all content) -->
<setting label="30535" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=deviceid)" /><!-- Reset device id uuid -->

View file

@ -30,8 +30,7 @@ sys_path.append(_base_resource)
###############################################################################
from utils import settings, window, language as lang, dialog, tryEncode, \
tryDecode
from utils import settings, window, language as lang, dialog, tryDecode
from userclient import UserClient
import initialsetup
from kodimonitor import KodiMonitor
@ -82,10 +81,8 @@ class Service():
def __init__(self):
logLevel = self.getLogLevel()
self.monitor = Monitor()
window('plex_logLevel', value=str(logLevel))
window('plex_kodiProfile',
value=tryDecode(translatePath("special://profile")))
window('plex_context',
@ -94,27 +91,26 @@ class Service():
value=settings('fetch_pms_item_number'))
# Initial logging
log.warn("======== START %s ========" % v.ADDON_NAME)
log.warn("Platform: %s" % v.PLATFORM)
log.warn("KODI Version: %s" % v.KODILONGVERSION)
log.warn("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
log.warn("Using plugin paths: %s"
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("Using plugin paths: %s"
% (settings('useDirectPaths') != "true"))
log.warn("Number of sync threads: %s"
log.info("Number of sync threads: %s"
% settings('syncThreadNumber'))
log.warn("Log Level: %s" % logLevel)
log.warn("Full sys.argv received: %s" % argv)
log.info("Full sys.argv received: %s" % argv)
# Reset window props for profile switch
properties = [
"plex_online", "plex_serverStatus", "plex_onWake",
"plex_dbCheck", "plex_kodiScan",
"plex_kodiScan",
"plex_shouldStop", "plex_dbScan",
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
"plex_runLibScan", "pms_token", "plex_token",
"pms_token", "plex_token",
"pms_server", "plex_machineIdentifier", "plex_servername",
"plex_authenticated", "PlexUserImage", "useDirectPaths",
"kodiplextimeoffset", "countError", "countUnauthorized",
"countError", "countUnauthorized",
"plex_restricteduser", "plex_allows_mediaDeletion",
"plex_command", "plex_result", "plex_force_transcode_pix"
]
@ -127,13 +123,6 @@ class Service():
# Set the minimum database version
window('plex_minDBVersion', value="1.5.10")
def getLogLevel(self):
try:
logLevel = int(settings('logLevel'))
except ValueError:
logLevel = 0
return logLevel
def __stop_PKC(self):
"""
Kodi's abortRequested is really unreliable :-(
@ -173,7 +162,7 @@ class Service():
if window('plex_kodiProfile') != kodiProfile:
# Profile change happened, terminate this thread and others
log.warn("Kodi profile was: %s and changed to: %s. "
log.info("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread."
% (kodiProfile,
window('plex_kodiProfile')))
@ -332,7 +321,7 @@ class Service():
except:
pass
window('plex_service_started', clear=True)
log.warn("======== STOP %s ========" % v.ADDON_NAME)
log.info("======== STOP %s ========" % v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation!
@ -345,11 +334,11 @@ else:
# Delay option
delay = int(settings('startupDelay'))
log.warn("Delaying Plex startup by: %s sec..." % delay)
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.warn("Abort requested while waiting. PKC not started.")
log.info("Abort requested while waiting. PKC not started.")
else:
Service().ServiceEntryPoint()