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) [![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.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-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) [![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)
@ -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 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) - 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:* *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) 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 - 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 ### Issues being worked on

View file

@ -1,5 +1,5 @@
<?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="1.8.7" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.14" 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.3.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> <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> <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> <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 - Portuguese translation, thanks @goncalo532
- Updated other translations - Updated other translations

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,13 +68,13 @@ def getDeviceId(reset=False):
# Because Kodi appears to cache file settings!! # Because Kodi appears to cache file settings!!
if clientId != "" and reset is False: if clientId != "" and reset is False:
window('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 return clientId
log.warn("Generating a new deviceid.") log.info("Generating a new deviceid.")
from uuid import uuid4 from uuid import uuid4
clientId = str(uuid4()) clientId = str(uuid4())
settings('plex_client_Id', value=clientId) settings('plex_client_Id', value=clientId)
window('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 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 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. 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 Adjusts state.py accordingly
""" """
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
@ -40,9 +37,8 @@ class Monitor_Window(Thread):
if window('plex_command'): if window('plex_command'):
value = window('plex_command') value = window('plex_command')
window('plex_command', clear=True) window('plex_command', clear=True)
if value.startswith('play_'): if value.startswith('PLAY-'):
queue.put(value) queue.put(value.replace('PLAY-', ''))
elif value == 'SUSPEND_LIBRARY_THREAD-True': elif value == 'SUSPEND_LIBRARY_THREAD-True':
state.SUSPEND_LIBRARY_THREAD = True state.SUSPEND_LIBRARY_THREAD = True
elif value == 'SUSPEND_LIBRARY_THREAD-False': elif value == 'SUSPEND_LIBRARY_THREAD-False':
@ -64,6 +60,10 @@ class Monitor_Window(Thread):
elif value.startswith('PLEX_USERNAME-'): elif value.startswith('PLEX_USERNAME-'):
state.PLEX_USERNAME = \ state.PLEX_USERNAME = \
value.replace('PLEX_USERNAME-', '') or None 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: else:
raise NotImplementedError('%s not implemented' % value) raise NotImplementedError('%s not implemented' % value)
else: else:

View file

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

View file

@ -575,14 +575,6 @@ def getExtraFanArt(plexid, plexPath):
xbmcplugin.endOfDirectory(HANDLE) 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): def getOnDeck(viewid, mediatype, tagname, limit):
""" """
Retrieves Plex On Deck items, currently only for TV shows 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 SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
out! out!
""" """
window('plex_runLibScan', value='full') plex_command('RUN_LIB_SCAN', 'full')
# Restart user client # Restart user client
plex_command('SUSPEND_USER_CLIENT', 'False') 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 # 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=lang(29999), line1=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=lang(29999), line1=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') settings('InstallQuestionsAnswered', value='true')

View file

@ -161,7 +161,8 @@ class Items(object):
# If offset exceeds duration skip update # If offset exceeds duration skip update
if item['viewOffset'] > item['duration']: 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 return
complete = float(item['viewOffset']) / float(item['duration']) complete = float(item['viewOffset']) / float(item['duration'])
@ -170,7 +171,6 @@ class Items(object):
% (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1) % (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1)
if complete >= MARK_PLAYED_AT: if complete >= MARK_PLAYED_AT:
log.info('Marking as completely watched in Kodi') log.info('Marking as completely watched in Kodi')
sleep(500)
try: try:
item['viewCount'] += 1 item['viewCount'] += 1
except TypeError: except TypeError:
@ -1729,7 +1729,7 @@ class Music(Items):
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
self.add_updateAlbum(album[0]) self.add_updateAlbum(album[0], children=[item])
plex_dbalbum = plex_db.getItem_byId(plex_albumId) plex_dbalbum = plex_db.getItem_byId(plex_albumId)
try: try:
albumid = plex_dbalbum[0] albumid = plex_dbalbum[0]

View file

@ -1280,7 +1280,14 @@ class Kodidb_Functions():
try: try:
artistid = self.cursor.fetchone()[0] artistid = self.cursor.fetchone()[0]
except TypeError: 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 artistid = self.cursor.fetchone()[0] + 1
query = ( query = (
''' '''

View file

@ -2,24 +2,48 @@
############################################################################### ###############################################################################
import logging from logging import getLogger
from json import loads from json import loads
from xbmc import Monitor, Player, sleep from xbmc import Monitor, Player, sleep
import downloadutils from downloadutils import DownloadUtils
import plexdb_functions as plexdb 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 PlexFunctions import scrobble
from kodidb_functions import get_kodiid_from_filename from kodidb_functions import get_kodiid_from_filename
from PlexAPI import API from PlexAPI import API
from variables import REMAP_TYPE_FROM_PLEXTYPE
import state 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): def __init__(self, callback):
self.mgr = callback self.mgr = callback
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = DownloadUtils().downloadUrl
self.xbmcplayer = Player() self.xbmcplayer = Player()
self.playqueue = self.mgr.playqueue self.playqueue = self.mgr.playqueue
Monitor.__init__(self) Monitor.__init__(self)
@ -47,31 +71,42 @@ class KodiMonitor(Monitor):
""" """
Monitor the PKC settings for changes made by the user Monitor the PKC settings for changes made by the user
""" """
# settings: window-variable log.debug('PKC settings change detected')
items = { changed = False
'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
# Reset the window variables from the settings variables # 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): 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))) % (settings_value, settings(settings_value)))
window(window_value, value=settings(settings_value)) window(window_value, value=settings(settings_value))
if settings_value == 'fetch_pms_item_number': if settings_value == 'fetch_pms_item_number':
log.info('Requesting playlist/nodes refresh') 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) @CatchExceptions(warnuser=False)
def onNotification(self, sender, method, data): def onNotification(self, sender, method, data):
@ -137,7 +172,7 @@ class KodiMonitor(Monitor):
elif method == "GUI.OnScreensaverDeactivated": elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true": if settings('dbSyncScreensaver') == "true":
sleep(5000) sleep(5000)
window('plex_runLibScan', value="full") 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')

View file

@ -17,8 +17,9 @@ log = getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'], @thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
add_stops=['STOP_SYNC']) 'DB_SCAN',
'STOP_SYNC'])
class Process_Fanart_Thread(Thread): class Process_Fanart_Thread(Thread):
""" """
Threaded download of additional fanart in the background 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): class Threaded_Get_Metadata(Thread):
""" """
Threaded download of Plex XML metadata for a certain library item. Threaded download of Plex XML metadata for a certain library item.
@ -115,17 +115,9 @@ class Threaded_Get_Metadata(Thread):
except (TypeError, IndexError, AttributeError): except (TypeError, IndexError, AttributeError):
log.error('Could not get children for Plex id %s' log.error('Could not get children for Plex id %s'
% item['itemId']) % item['itemId'])
else:
item['children'] = [] 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: else:
item['children'].append(child_xml[0]) item['children'] = children_xml
# place item into out queue # place item into out queue
out_queue.put(item) 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): class Threaded_Process_Metadata(Thread):
""" """
Not yet implemented for more than 1 thread - if ever. Only to be called by Not yet implemented for more than 1 thread - if ever. Only to be called by

View file

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

View file

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

View file

@ -1,74 +1,47 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
###############################################################################
##################################################################################################
import logging import logging
import xbmc 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(): def config():
logger = logging.getLogger('PLEX') logger = logging.getLogger('PLEX')
logger.addHandler(LogHandler()) logger.addHandler(LogHandler())
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
class LogHandler(logging.StreamHandler): class LogHandler(logging.StreamHandler):
def __init__(self): def __init__(self):
logging.StreamHandler.__init__(self) logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter()) self.setFormatter(logging.Formatter(fmt="%(name)s: %(message)s"))
def emit(self, record): def emit(self, record):
if self._get_log_level(record.levelno):
try: try:
xbmc.log(self.format(record), level=xbmc.LOGNOTICE) xbmc.log(self.format(record), level=LEVELS[record.levelno])
except UnicodeEncodeError: except UnicodeEncodeError:
xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE) xbmc.log(tryEncode(self.format(record)),
level=LEVELS[record.levelno])
@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

View file

@ -1,13 +1,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
import logging from cPickle import dumps, loads
import cPickle as Pickle
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'): 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 obj can be pretty much any Python object. However, classes and
functions won't work. See the Pickle documentation functions won't work. See the Pickle documentation
""" """
log.debug('Start pickling: %s' % obj) log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
pickl_window(window_var, value=Pickle.dumps(obj)) pickl_window(window_var, value=dumps(obj))
log.debug('Successfully pickled') log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
def unpickle_me(window_var='plex_result'): def unpickle_me(window_var='plex_result'):
@ -31,9 +44,9 @@ def unpickle_me(window_var='plex_result'):
""" """
result = pickl_window(window_var) result = pickl_window(window_var)
pickl_window(window_var, clear=True) pickl_window(window_var, clear=True)
log.debug('Start unpickling') log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
obj = Pickle.loads(result) obj = loads(result)
log.debug('Successfully unpickled: %s' % obj) log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
return obj return obj

View file

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

View file

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

View file

@ -10,6 +10,8 @@ STOP_PKC = False
SUSPEND_LIBRARY_THREAD = False SUSPEND_LIBRARY_THREAD = False
# Set if user decided to cancel sync # Set if user decided to cancel sync
STOP_SYNC = False STOP_SYNC = False
# Could we access the paths?
PATH_VERIFIED = False
# Set if a Plex-Kodi DB sync is being done - along with # Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true' # window('plex_dbScan') set to 'true'
DB_SCAN = False DB_SCAN = False
@ -24,6 +26,42 @@ RESTRICTED_USER = False
DIRECT_PATHS = False DIRECT_PATHS = False
# Shall we replace custom user ratings with the number of versions available? # Shall we replace custom user ratings with the number of versions available?
INDICATE_MEDIA_VERSIONS = False 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') # Along with window('plex_authenticated')
AUTHENTICATED = False AUTHENTICATED = False

View file

@ -59,24 +59,6 @@ def window(property, value=None, clear=False, windowid=10000):
return tryDecode(win.getProperty(property)) 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): def plex_command(key, value):
""" """
Used to funnel states between different Python instances. NOT really thread 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' value: either 'True' or 'False'
""" """
while window('plex_command'): while window('plex_command'):
xbmc.sleep(5) xbmc.sleep(20)
window('plex_command', value='%s-%s' % (key, value)) 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: Displays xbmcgui Dialog. Pass a string as typus:
'yesno', 'ok', 'notification', 'input', 'select', 'numeric' '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: Icons:
icon='{plex}' Display Plex standard icon icon='{plex}' Display Plex standard icon
icon='{info}' xbmcgui.NOTIFICATION_INFO icon='{info}' xbmcgui.NOTIFICATION_INFO
@ -221,6 +212,16 @@ def tryDecode(string, encoding='utf-8'):
return string 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): def escape_html(string):
""" """
Escapes the following: Escapes the following:
@ -248,7 +249,7 @@ def DateToKodi(stamp):
None if an error was encountered None if an error was encountered
""" """
try: try:
stamp = float(stamp) + float(window('kodiplextimeoffset')) stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
date_time = localtime(stamp) date_time = localtime(stamp)
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time) localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
except: except:

View file

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

View file

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