Merge master
This commit is contained in:
parent
c33565af4c
commit
fa6d95aa61
29 changed files with 816 additions and 652 deletions
|
@ -1,5 +1,5 @@
|
|||
[![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-1.8.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
||||
[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
||||
|
||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
|
@ -105,14 +105,11 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio
|
|||
|
||||
Solutions are unlikely due to the nature of these issues
|
||||
- A Plex Media Server "bug" leads to frequent and slow syncs, see [here for more info](https://github.com/croneter/PlexKodiConnect/issues/135)
|
||||
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issue](https://github.com/croneter/PlexKodiConnect/issues/14) for more details
|
||||
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issues](https://github.com/croneter/PlexKodiConnect/issues/14) for more details. **Workaround**: use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Set-up-Direct-Paths) instead of addon paths.
|
||||
|
||||
*Background Sync:*
|
||||
The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them only on full/delta syncs (standard settings is every 60 minutes)
|
||||
- Toggle the viewstate of an item to (un)watched outside of Kodi
|
||||
- Changing details of an item, e.g. replacing a poster
|
||||
|
||||
However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie.
|
||||
|
||||
|
||||
### Issues being worked on
|
||||
|
|
52
addon.xml
52
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.7" provider-name="croneter">
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.14" provider-name="croneter">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.3.0" />
|
||||
|
@ -59,7 +59,55 @@
|
|||
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
|
||||
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
|
||||
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
|
||||
<news>version 1.8.6:
|
||||
<news>version 1.8.14 (beta only):
|
||||
- Greatly speed up displaying context menu
|
||||
- Fix IndexError e.g. for channels if stream info missing
|
||||
- Sleep a bit before marking item as fully watched
|
||||
- Don't sleep before updating playstate to fully watched (if you watch on another Plex client)
|
||||
- Fix KeyError for TV live channels for getGeople
|
||||
|
||||
version 1.8.13 (beta only):
|
||||
- Background sync now picks up more PMS changes
|
||||
- Detect Plex item deletion more reliably
|
||||
- Fix changed Plex metadata not synced repeatedly
|
||||
- Detect (some, not all) changes to PKC settings and apply them on-the-fly
|
||||
- Fix resuming interrupted sync
|
||||
- PKC logging now uses Kodi log levels
|
||||
- Further code optimizations
|
||||
|
||||
version 1.8.12:
|
||||
- Fix library sync crashing trying to display an error
|
||||
|
||||
version 1.8.11:
|
||||
- version 1.8.10 for everybody
|
||||
|
||||
version 1.8.10 (beta only):
|
||||
- Vastly improve sync speed for music
|
||||
- Never show library sync dialog if media is playing
|
||||
- Improvements to sync dialog
|
||||
- Fix stop synching if path not found
|
||||
- Resume aborted sync on PKC settings change
|
||||
- Don't quit library sync if failed repeatedly
|
||||
- Verify path for every Plex library on install sync
|
||||
- More descriptive downloadable subtitles
|
||||
- More code fixes and optimization
|
||||
|
||||
version 1.8.9
|
||||
- Fix playback not starting in some circumstances
|
||||
- Deactivate some annoying popups on install
|
||||
|
||||
version 1.8.8
|
||||
- Fix playback not starting in some circumstances
|
||||
- Fix first artist "missing" tag (Reset your DB!)
|
||||
- Update Czech translation
|
||||
|
||||
version 1.8.7 (beta only):
|
||||
- Some fixes to playstate reporting, thanks @RickDB
|
||||
- Add Kodi info screen for episodes in context menu
|
||||
- Fix PKC asking for trailers not working
|
||||
- Fix PKC not automatically updating
|
||||
|
||||
version 1.8.6:
|
||||
- Portuguese translation, thanks @goncalo532
|
||||
- Updated other translations
|
||||
|
||||
|
|
|
@ -1,52 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
###############################################################################
|
||||
from os import path as os_path
|
||||
from sys import path as sys_path
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from xbmcaddon import Addon
|
||||
from xbmc import translatePath, sleep, log, LOGERROR
|
||||
from xbmcgui import Window
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
|
||||
###############################################################################
|
||||
|
||||
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
||||
_addon = Addon(id='plugin.video.plexkodiconnect')
|
||||
try:
|
||||
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
||||
except TypeError:
|
||||
_addon_path = _addon.getAddonInfo('path').decode()
|
||||
try:
|
||||
_base_resource = xbmc.translatePath(os.path.join(
|
||||
_base_resource = translatePath(os_path.join(
|
||||
_addon_path,
|
||||
'resources',
|
||||
'lib')).decode('utf-8')
|
||||
except TypeError:
|
||||
_base_resource = xbmc.translatePath(os.path.join(
|
||||
_base_resource = translatePath(os_path.join(
|
||||
_addon_path,
|
||||
'resources',
|
||||
'lib')).decode()
|
||||
sys.path.append(_base_resource)
|
||||
sys_path.append(_base_resource)
|
||||
|
||||
###############################################################################
|
||||
|
||||
import loghandler
|
||||
from context_entry import ContextMenu
|
||||
|
||||
###############################################################################
|
||||
|
||||
loghandler.config()
|
||||
log = logging.getLogger("PLEX.contextmenu")
|
||||
from pickler import unpickle_me, pickl_window
|
||||
|
||||
###############################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
# Start the context menu
|
||||
ContextMenu()
|
||||
except Exception as error:
|
||||
log.exception(error)
|
||||
import traceback
|
||||
log.exception("Traceback:\n%s" % traceback.format_exc())
|
||||
raise
|
||||
win = Window(10000)
|
||||
while win.getProperty('plex_command'):
|
||||
sleep(20)
|
||||
win.setProperty('plex_command', 'CONTEXT_menu')
|
||||
while not pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
result = unpickle_me()
|
||||
if result is None:
|
||||
log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR)
|
||||
|
|
29
default.py
29
default.py
|
@ -32,9 +32,9 @@ sys_path.append(_base_resource)
|
|||
###############################################################################
|
||||
|
||||
import entrypoint
|
||||
from utils import window, pickl_window, reset, passwordsXML, language as lang,\
|
||||
dialog
|
||||
from pickler import unpickle_me
|
||||
from utils import window, reset, passwordsXML, language as lang, dialog, \
|
||||
plex_command
|
||||
from pickler import unpickle_me, pickl_window
|
||||
from PKC_listitem import convert_PKC_to_listitem
|
||||
import variables as v
|
||||
|
||||
|
@ -127,28 +127,29 @@ class Main():
|
|||
log.error('Not connected to a PMS.')
|
||||
else:
|
||||
if mode == 'repair':
|
||||
window('plex_runLibScan', value='repair')
|
||||
log.info('Requesting repair lib sync')
|
||||
plex_command('RUN_LIB_SCAN', 'repair')
|
||||
elif mode == 'manualsync':
|
||||
log.info('Requesting full library scan')
|
||||
window('plex_runLibScan', value='full')
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
|
||||
elif mode == 'texturecache':
|
||||
window('plex_runLibScan', value='del_textures')
|
||||
log.info('Requesting texture caching of all textures')
|
||||
plex_command('RUN_LIB_SCAN', 'textures')
|
||||
|
||||
elif mode == 'chooseServer':
|
||||
entrypoint.chooseServer()
|
||||
|
||||
elif mode == 'refreshplaylist':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
window('plex_runLibScan', value='views')
|
||||
plex_command('RUN_LIB_SCAN', 'views')
|
||||
|
||||
elif mode == 'deviceid':
|
||||
self.deviceid()
|
||||
|
||||
elif mode == 'fanart':
|
||||
log.info('User requested fanarttv refresh')
|
||||
window('plex_runLibScan', value='fanart')
|
||||
plex_command('RUN_LIB_SCAN', 'fanart')
|
||||
|
||||
elif '/extrafanart' in argv[0]:
|
||||
plexpath = argv[2][1:]
|
||||
|
@ -165,15 +166,13 @@ class Main():
|
|||
else:
|
||||
entrypoint.doMainListing(content_type=params.get('content_type'))
|
||||
|
||||
def play(self):
|
||||
@staticmethod
|
||||
def play():
|
||||
"""
|
||||
Start up playback_starter in main Python thread
|
||||
"""
|
||||
# Put the request into the 'queue'
|
||||
while window('plex_command'):
|
||||
sleep(50)
|
||||
window('plex_command',
|
||||
value='play_%s' % argv[2])
|
||||
plex_command('PLAY', argv[2])
|
||||
# Wait for the result
|
||||
while not pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
|
@ -190,7 +189,8 @@ class Main():
|
|||
listitem = convert_PKC_to_listitem(result.listitem)
|
||||
setResolvedUrl(HANDLE, True, listitem)
|
||||
|
||||
def deviceid(self):
|
||||
@staticmethod
|
||||
def deviceid():
|
||||
deviceId_old = window('plex_client_Id')
|
||||
from clientinfo import getDeviceId
|
||||
try:
|
||||
|
@ -205,6 +205,7 @@ class Main():
|
|||
dialog('ok', lang(29999), lang(33033))
|
||||
executebuiltin('RestartApp')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log.info('%s started' % v.ADDON_ID)
|
||||
Main()
|
||||
|
|
|
@ -49,7 +49,7 @@ from xbmcvfs import exists
|
|||
import clientinfo as client
|
||||
from downloadutils import DownloadUtils
|
||||
from utils import window, settings, language as lang, tryDecode, tryEncode, \
|
||||
DateToKodi, exists_dir
|
||||
DateToKodi, exists_dir, slugify
|
||||
from PlexFunctions import PMSHttpsEnabled
|
||||
import plexdb_functions as plexdb
|
||||
import variables as v
|
||||
|
@ -1346,6 +1346,7 @@ class API():
|
|||
cast = []
|
||||
producer = []
|
||||
for child in self.item:
|
||||
try:
|
||||
if child.tag == 'Director':
|
||||
director.append(child.attrib['tag'])
|
||||
elif child.tag == 'Writer':
|
||||
|
@ -1354,6 +1355,9 @@ class API():
|
|||
cast.append(child.attrib['tag'])
|
||||
elif child.tag == 'Producer':
|
||||
producer.append(child.attrib['tag'])
|
||||
except KeyError:
|
||||
log.warn('Malformed PMS answer for getPeople: %s: %s'
|
||||
% (child.tag, child.attrib))
|
||||
return {
|
||||
'Director': director,
|
||||
'Writer': writer,
|
||||
|
@ -1750,8 +1754,16 @@ class API():
|
|||
videotracks = []
|
||||
audiotracks = []
|
||||
subtitlelanguages = []
|
||||
try:
|
||||
# Sometimes, aspectratio is on the "toplevel"
|
||||
aspectratio = self.item[0].attrib.get('aspectRatio', None)
|
||||
except IndexError:
|
||||
# There is no stream info at all, returning empty
|
||||
return {
|
||||
'video': videotracks,
|
||||
'audio': audiotracks,
|
||||
'subtitle': subtitlelanguages
|
||||
}
|
||||
# TODO: what if several Media tags exist?!?
|
||||
# Loop over parts
|
||||
for child in self.item[0]:
|
||||
|
@ -2357,11 +2369,11 @@ class API():
|
|||
# ext = stream.attrib.get('format')
|
||||
if key:
|
||||
# We do know the language - temporarily download
|
||||
if stream.attrib.get('languageCode') is not None:
|
||||
if stream.attrib.get('language') is not None:
|
||||
path = self.download_external_subtitles(
|
||||
"{server}%s" % key,
|
||||
"subtitle%02d.%s.%s" % (fileindex,
|
||||
stream.attrib['languageCode'],
|
||||
stream.attrib['language'],
|
||||
stream.attrib['codec']))
|
||||
fileindex += 1
|
||||
# We don't know the language - no need to download
|
||||
|
@ -2395,9 +2407,14 @@ class API():
|
|||
log.error('Could not temporarily download subtitle %s' % url)
|
||||
return
|
||||
else:
|
||||
r.encoding = 'utf-8'
|
||||
log.debug('Writing temp subtitle to %s' % path)
|
||||
try:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(r.content)
|
||||
except UnicodeEncodeError:
|
||||
log.debug('Need to slugify the filename %s' % path)
|
||||
with open(slugify(path), 'wb') as f:
|
||||
f.write(r.content)
|
||||
return path
|
||||
|
||||
def GetKodiPremierDate(self):
|
||||
|
@ -2575,16 +2592,16 @@ class API():
|
|||
if path is None:
|
||||
return None
|
||||
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
|
||||
if window('remapSMB') == 'true':
|
||||
path = path.replace(window('remapSMB%sOrg' % typus),
|
||||
window('remapSMB%sNew' % typus),
|
||||
if state.REMAP_PATH is True:
|
||||
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
|
||||
getattr(state, 'remapSMB%sNew' % typus),
|
||||
1)
|
||||
# There might be backslashes left over:
|
||||
path = path.replace('\\', '/')
|
||||
elif window('replaceSMB') == 'true':
|
||||
elif state.REPLACE_SMB_PATH is True:
|
||||
if path.startswith('\\\\'):
|
||||
path = 'smb:' + path.replace('\\', '/')
|
||||
if ((window('plex_pathverified') == 'true' and forceCheck is False) or
|
||||
if ((state.PATH_VERIFIED and forceCheck is False) or
|
||||
omitCheck is True):
|
||||
return path
|
||||
|
||||
|
@ -2612,12 +2629,12 @@ class API():
|
|||
if self.askToValidate(path):
|
||||
state.STOP_SYNC = True
|
||||
path = None
|
||||
window('plex_pathverified', value='true')
|
||||
state.PATH_VERIFIED = True
|
||||
else:
|
||||
path = None
|
||||
elif forceCheck is False:
|
||||
if window('plex_pathverified') != 'true':
|
||||
window('plex_pathverified', value='true')
|
||||
# Only set the flag if we were not force-checking the path
|
||||
state.PATH_VERIFIED = True
|
||||
return path
|
||||
|
||||
def askToValidate(self, url):
|
||||
|
|
|
@ -15,7 +15,7 @@ from variables import PLEX_TO_KODI_TIMEFACTOR
|
|||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
CONTAINERSIZE = int(settings('limitindex'))
|
||||
|
||||
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -36,9 +36,8 @@ def GetPlexKeyNumber(plexKey):
|
|||
|
||||
Returns ('','') if nothing is found
|
||||
"""
|
||||
regex = re.compile(r'''/(.+)/(\d+)$''')
|
||||
try:
|
||||
result = regex.findall(plexKey)[0]
|
||||
result = REGEX_PLEX_KEY.findall(plexKey)[0]
|
||||
except IndexError:
|
||||
result = ('', '')
|
||||
return result
|
||||
|
|
|
@ -126,8 +126,9 @@ def double_urldecode(text):
|
|||
return unquote(unquote(text))
|
||||
|
||||
|
||||
@thread_methods(add_stops=['STOP_SYNC'],
|
||||
add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'])
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||
'DB_SCAN',
|
||||
'STOP_SYNC'])
|
||||
class Image_Cache_Thread(Thread):
|
||||
xbmc_host = 'localhost'
|
||||
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()
|
||||
|
|
|
@ -68,13 +68,13 @@ def getDeviceId(reset=False):
|
|||
# Because Kodi appears to cache file settings!!
|
||||
if clientId != "" and reset is False:
|
||||
window('plex_client_Id', value=clientId)
|
||||
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
return clientId
|
||||
|
||||
log.warn("Generating a new deviceid.")
|
||||
log.info("Generating a new deviceid.")
|
||||
from uuid import uuid4
|
||||
clientId = str(uuid4())
|
||||
settings('plex_client_Id', value=clientId)
|
||||
window('plex_client_Id', value=clientId)
|
||||
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
return clientId
|
||||
|
|
|
@ -21,9 +21,6 @@ class Monitor_Window(Thread):
|
|||
Monitors window('plex_command') for new entries that we need to take care
|
||||
of, e.g. for new plays initiated on the Kodi side with addon paths.
|
||||
|
||||
Possible values of window('plex_command'):
|
||||
'play_....': to start playback using playback_starter
|
||||
|
||||
Adjusts state.py accordingly
|
||||
"""
|
||||
# Borg - multiple instances, shared state
|
||||
|
@ -40,9 +37,8 @@ class Monitor_Window(Thread):
|
|||
if window('plex_command'):
|
||||
value = window('plex_command')
|
||||
window('plex_command', clear=True)
|
||||
if value.startswith('play_'):
|
||||
queue.put(value)
|
||||
|
||||
if value.startswith('PLAY-'):
|
||||
queue.put(value.replace('PLAY-', ''))
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-True':
|
||||
state.SUSPEND_LIBRARY_THREAD = True
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-False':
|
||||
|
@ -64,6 +60,10 @@ class Monitor_Window(Thread):
|
|||
elif value.startswith('PLEX_USERNAME-'):
|
||||
state.PLEX_USERNAME = \
|
||||
value.replace('PLEX_USERNAME-', '') or None
|
||||
elif value.startswith('RUN_LIB_SCAN-'):
|
||||
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
||||
elif value == 'CONTEXT_menu':
|
||||
queue.put('dummy?mode=context_menu')
|
||||
else:
|
||||
raise NotImplementedError('%s not implemented' % value)
|
||||
else:
|
||||
|
|
|
@ -198,12 +198,12 @@ class DownloadUtils():
|
|||
# THE EXCEPTIONS
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Connection error
|
||||
log.debug("Server unreachable at: %s" % url)
|
||||
log.debug(e)
|
||||
log.warn("Server unreachable at: %s" % url)
|
||||
log.warn(e)
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
log.debug("Server timeout at: %s" % url)
|
||||
log.debug(e)
|
||||
log.warn("Server timeout at: %s" % url)
|
||||
log.warn(e)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
log.warn('HTTP Error at %s' % url)
|
||||
|
@ -300,21 +300,21 @@ class DownloadUtils():
|
|||
# update
|
||||
pass
|
||||
else:
|
||||
log.error("Unable to convert the response for: "
|
||||
log.warn("Unable to convert the response for: "
|
||||
"%s" % url)
|
||||
log.info("Received headers were: %s" % r.headers)
|
||||
log.info('Received text:')
|
||||
log.info(r.text)
|
||||
log.warn("Received headers were: %s" % r.headers)
|
||||
log.warn('Received text:')
|
||||
log.warn(r.text)
|
||||
return True
|
||||
elif r.status_code == 403:
|
||||
# E.g. deleting a PMS item
|
||||
log.error('PMS sent 403: Forbidden error for url %s' % url)
|
||||
log.warn('PMS sent 403: Forbidden error for url %s' % url)
|
||||
return None
|
||||
else:
|
||||
log.error('Unknown answer from PMS %s with status code %s. '
|
||||
log.warn('Unknown answer from PMS %s with status code %s. '
|
||||
'Message:' % (url, r.status_code))
|
||||
r.encoding = 'utf-8'
|
||||
log.info(r.text)
|
||||
log.warn(r.text)
|
||||
return True
|
||||
|
||||
# And now deal with the consequences of the exceptions
|
||||
|
|
|
@ -575,14 +575,6 @@ def getExtraFanArt(plexid, plexPath):
|
|||
xbmcplugin.endOfDirectory(HANDLE)
|
||||
|
||||
|
||||
def RunLibScan(mode):
|
||||
if window('plex_online') != "true":
|
||||
# Server is not online, do not run the sync
|
||||
dialog('ok', lang(29999), lang(39205))
|
||||
else:
|
||||
window('plex_runLibScan', value='full')
|
||||
|
||||
|
||||
def getOnDeck(viewid, mediatype, tagname, limit):
|
||||
"""
|
||||
Retrieves Plex On Deck items, currently only for TV shows
|
||||
|
@ -975,7 +967,7 @@ def __LogIn():
|
|||
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
|
||||
out!
|
||||
"""
|
||||
window('plex_runLibScan', value='full')
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
# Restart user client
|
||||
plex_command('SUSPEND_USER_CLIENT', 'False')
|
||||
|
||||
|
|
|
@ -496,10 +496,10 @@ class InitialSetup():
|
|||
|
||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
|
||||
dialog.ok(heading=lang(29999), line1=lang(39076))
|
||||
# dialog.ok(heading=lang(29999), line1=lang(39076))
|
||||
|
||||
# Need to tell about our image source for collections: themoviedb.org
|
||||
dialog.ok(heading=lang(29999), line1=lang(39717))
|
||||
# dialog.ok(heading=lang(29999), line1=lang(39717))
|
||||
# Make sure that we only ask these questions upon first installation
|
||||
settings('InstallQuestionsAnswered', value='true')
|
||||
|
||||
|
|
|
@ -161,7 +161,8 @@ class Items(object):
|
|||
|
||||
# If offset exceeds duration skip update
|
||||
if item['viewOffset'] > item['duration']:
|
||||
log.error("Error while updating play state, viewOffset exceeded duration")
|
||||
log.error("Error while updating play state, viewOffset "
|
||||
"exceeded duration")
|
||||
return
|
||||
|
||||
complete = float(item['viewOffset']) / float(item['duration'])
|
||||
|
@ -170,7 +171,6 @@ class Items(object):
|
|||
% (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1)
|
||||
if complete >= MARK_PLAYED_AT:
|
||||
log.info('Marking as completely watched in Kodi')
|
||||
sleep(500)
|
||||
try:
|
||||
item['viewCount'] += 1
|
||||
except TypeError:
|
||||
|
@ -1729,7 +1729,7 @@ class Music(Items):
|
|||
if album is None or album == 401:
|
||||
log.error('Could not download album, abort')
|
||||
return
|
||||
self.add_updateAlbum(album[0])
|
||||
self.add_updateAlbum(album[0], children=[item])
|
||||
plex_dbalbum = plex_db.getItem_byId(plex_albumId)
|
||||
try:
|
||||
albumid = plex_dbalbum[0]
|
||||
|
|
|
@ -1280,7 +1280,14 @@ class Kodidb_Functions():
|
|||
try:
|
||||
artistid = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
self.cursor.execute("select coalesce(max(idArtist),0) from artist")
|
||||
# Krypton has a dummy first entry idArtist: 1 strArtist:
|
||||
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
||||
if v.KODIVERSION >= 17:
|
||||
self.cursor.execute(
|
||||
"select coalesce(max(idArtist),1) from artist")
|
||||
else:
|
||||
self.cursor.execute(
|
||||
"select coalesce(max(idArtist),0) from artist")
|
||||
artistid = self.cursor.fetchone()[0] + 1
|
||||
query = (
|
||||
'''
|
||||
|
|
|
@ -2,24 +2,48 @@
|
|||
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
from logging import getLogger
|
||||
from json import loads
|
||||
|
||||
from xbmc import Monitor, Player, sleep
|
||||
|
||||
import downloadutils
|
||||
from downloadutils import DownloadUtils
|
||||
import plexdb_functions as plexdb
|
||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
|
||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
|
||||
plex_command
|
||||
from PlexFunctions import scrobble
|
||||
from kodidb_functions import get_kodiid_from_filename
|
||||
from PlexAPI import API
|
||||
from variables import REMAP_TYPE_FROM_PLEXTYPE
|
||||
import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
# settings: window-variable
|
||||
WINDOW_SETTINGS = {
|
||||
'enableContext': 'plex_context',
|
||||
'plex_restricteduser': 'plex_restricteduser',
|
||||
'force_transcode_pix': 'plex_force_transcode_pix',
|
||||
'fetch_pms_item_number': 'fetch_pms_item_number'
|
||||
}
|
||||
|
||||
# settings: state-variable (state.py)
|
||||
# Need to use getattr and setattr!
|
||||
STATE_SETTINGS = {
|
||||
'dbSyncIndicator': 'SYNC_DIALOG',
|
||||
'remapSMB': 'REMAP_PATH',
|
||||
'remapSMBmovieOrg': 'remapSMBmovieOrg',
|
||||
'remapSMBmovieNew': 'remapSMBmovieNew',
|
||||
'remapSMBtvOrg': 'remapSMBtvOrg',
|
||||
'remapSMBtvNew': 'remapSMBtvNew',
|
||||
'remapSMBmusicOrg': 'remapSMBmusicOrg',
|
||||
'remapSMBmusicNew': 'remapSMBmusicNew',
|
||||
'remapSMBphotoOrg': 'remapSMBphotoOrg',
|
||||
'remapSMBphotoNew': 'remapSMBphotoNew',
|
||||
'enableMusic': 'ENABLE_MUSIC',
|
||||
'enableBackgroundSync': 'BACKGROUND_SYNC'
|
||||
}
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -27,7 +51,7 @@ class KodiMonitor(Monitor):
|
|||
|
||||
def __init__(self, callback):
|
||||
self.mgr = callback
|
||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||
self.doUtils = DownloadUtils().downloadUrl
|
||||
self.xbmcplayer = Player()
|
||||
self.playqueue = self.mgr.playqueue
|
||||
Monitor.__init__(self)
|
||||
|
@ -47,31 +71,42 @@ class KodiMonitor(Monitor):
|
|||
"""
|
||||
Monitor the PKC settings for changes made by the user
|
||||
"""
|
||||
# settings: window-variable
|
||||
items = {
|
||||
'logLevel': 'plex_logLevel',
|
||||
'enableContext': 'plex_context',
|
||||
'plex_restricteduser': 'plex_restricteduser',
|
||||
'dbSyncIndicator': 'dbSyncIndicator',
|
||||
'remapSMB': 'remapSMB',
|
||||
'replaceSMB': 'replaceSMB',
|
||||
'force_transcode_pix': 'plex_force_transcode_pix',
|
||||
'fetch_pms_item_number': 'fetch_pms_item_number'
|
||||
}
|
||||
# Path replacement
|
||||
for typus in REMAP_TYPE_FROM_PLEXTYPE.values():
|
||||
for arg in ('Org', 'New'):
|
||||
key = 'remapSMB%s%s' % (typus, arg)
|
||||
items[key] = key
|
||||
log.debug('PKC settings change detected')
|
||||
changed = False
|
||||
# Reset the window variables from the settings variables
|
||||
for settings_value, window_value in items.iteritems():
|
||||
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
|
||||
if window(window_value) != settings(settings_value):
|
||||
log.debug('PKC settings changed: %s is now %s'
|
||||
changed = True
|
||||
log.debug('PKC window settings changed: %s is now %s'
|
||||
% (settings_value, settings(settings_value)))
|
||||
window(window_value, value=settings(settings_value))
|
||||
if settings_value == 'fetch_pms_item_number':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
window('plex_runLibScan', value="views")
|
||||
plex_command('RUN_LIB_SCAN', 'views')
|
||||
# Reset the state variables in state.py
|
||||
for settings_value, state_name in STATE_SETTINGS.iteritems():
|
||||
new = settings(settings_value)
|
||||
if new == 'true':
|
||||
new = True
|
||||
elif new == 'false':
|
||||
new = False
|
||||
if getattr(state, state_name) != new:
|
||||
changed = True
|
||||
log.debug('PKC state settings %s changed from %s to %s'
|
||||
% (settings_value, getattr(state, state_name), new))
|
||||
setattr(state, state_name, new)
|
||||
# Special cases, overwrite all internal settings
|
||||
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60
|
||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||
settings('backgroundsync_saftyMargin'))
|
||||
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
||||
# Never set through the user
|
||||
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
||||
if changed is True:
|
||||
# Assume that the user changed the settings so that we can now find
|
||||
# the path to all media files
|
||||
state.STOP_SYNC = False
|
||||
state.PATH_VERIFIED = False
|
||||
|
||||
@CatchExceptions(warnuser=False)
|
||||
def onNotification(self, sender, method, data):
|
||||
|
@ -137,7 +172,7 @@ class KodiMonitor(Monitor):
|
|||
elif method == "GUI.OnScreensaverDeactivated":
|
||||
if settings('dbSyncScreensaver') == "true":
|
||||
sleep(5000)
|
||||
window('plex_runLibScan', value="full")
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
|
||||
elif method == "System.OnQuit":
|
||||
log.info('Kodi OnQuit detected - shutting down')
|
||||
|
|
|
@ -17,8 +17,9 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'],
|
||||
add_stops=['STOP_SYNC'])
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||
'DB_SCAN',
|
||||
'STOP_SYNC'])
|
||||
class Process_Fanart_Thread(Thread):
|
||||
"""
|
||||
Threaded download of additional fanart in the background
|
||||
|
|
|
@ -16,7 +16,7 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Get_Metadata(Thread):
|
||||
"""
|
||||
Threaded download of Plex XML metadata for a certain library item.
|
||||
|
@ -115,17 +115,9 @@ class Threaded_Get_Metadata(Thread):
|
|||
except (TypeError, IndexError, AttributeError):
|
||||
log.error('Could not get children for Plex id %s'
|
||||
% item['itemId'])
|
||||
else:
|
||||
item['children'] = []
|
||||
for child in children_xml:
|
||||
child_xml = GetPlexMetadata(child.attrib['ratingKey'])
|
||||
try:
|
||||
child_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
log.error('Could not get child for Plex id %s'
|
||||
% child.attrib['ratingKey'])
|
||||
else:
|
||||
item['children'].append(child_xml[0])
|
||||
item['children'] = children_xml
|
||||
|
||||
# place item into out queue
|
||||
out_queue.put(item)
|
||||
|
|
|
@ -15,7 +15,7 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Process_Metadata(Thread):
|
||||
"""
|
||||
Not yet implemented for more than 1 thread - if ever. Only to be called by
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
from logging import getLogger
|
||||
from threading import Thread, Lock
|
||||
|
||||
from xbmc import sleep
|
||||
from xbmc import sleep, Player
|
||||
from xbmcgui import DialogProgressBG
|
||||
|
||||
from utils import thread_methods, language as lang
|
||||
|
||||
|
@ -18,18 +19,17 @@ LOCK = Lock()
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Show_Sync_Info(Thread):
|
||||
"""
|
||||
Threaded class to show the Kodi statusbar of the metadata download.
|
||||
|
||||
Input:
|
||||
dialog xbmcgui.DialogProgressBG() object to show progress
|
||||
total: Total number of items to get
|
||||
item_type:
|
||||
"""
|
||||
def __init__(self, dialog, total, item_type):
|
||||
def __init__(self, total, item_type):
|
||||
self.total = total
|
||||
self.dialog = dialog
|
||||
self.item_type = item_type
|
||||
Thread.__init__(self)
|
||||
|
||||
|
@ -51,14 +51,15 @@ class Threaded_Show_Sync_Info(Thread):
|
|||
log.debug('Show sync info thread started')
|
||||
# cache local variables because it's faster
|
||||
total = self.total
|
||||
dialog = self.dialog
|
||||
dialog = DialogProgressBG('dialoglogProgressBG')
|
||||
thread_stopped = self.thread_stopped
|
||||
dialog.create("%s %s: %s %s"
|
||||
% (lang(39714), self.item_type, str(total), lang(39715)))
|
||||
player = Player()
|
||||
|
||||
total = 2 * total
|
||||
totalProgress = 0
|
||||
while thread_stopped() is False:
|
||||
while thread_stopped() is False and not player.isPlaying():
|
||||
with LOCK:
|
||||
get_progress = GET_METADATA_COUNT
|
||||
process_progress = PROCESS_METADATA_COUNT
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
import logging
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
import Queue
|
||||
from random import shuffle
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
from xbmcvfs import exists
|
||||
|
||||
from utils import window, settings, getUnixTimestamp, sourcesXML,\
|
||||
|
@ -22,7 +21,8 @@ import videonodes
|
|||
import variables as v
|
||||
|
||||
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
|
||||
GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus, get_plex_sections
|
||||
GetPlexSectionResults, GetPlexKeyNumber, GetPMSStatus, get_plex_sections, \
|
||||
GetAllPlexChildren
|
||||
import PlexAPI
|
||||
from library_sync.get_metadata import Threaded_Get_Metadata
|
||||
from library_sync.process_metadata import Threaded_Process_Metadata
|
||||
|
@ -33,84 +33,77 @@ import state
|
|||
|
||||
###############################################################################
|
||||
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['STOP_SYNC'],
|
||||
add_suspends=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class LibrarySync(Thread):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, callback=None):
|
||||
self.mgr = callback
|
||||
|
||||
# Dict of items we just processed in order to prevent a reprocessing
|
||||
# caused by websocket
|
||||
self.just_processed = {}
|
||||
# How long do we wait until we start re-processing? (in seconds)
|
||||
self.ignore_just_processed = 10*60
|
||||
self.itemsToProcess = []
|
||||
self.sessionKeys = []
|
||||
self.fanartqueue = Queue.Queue()
|
||||
if settings('FanartTV') == 'true':
|
||||
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
|
||||
# How long should we wait at least to process new/changed PMS items?
|
||||
self.saftyMargin = int(settings('backgroundsync_saftyMargin'))
|
||||
|
||||
self.fullSyncInterval = int(settings('fullSyncInterval')) * 60
|
||||
|
||||
self.user = userclient.UserClient()
|
||||
self.vnodes = videonodes.VideoNodes()
|
||||
self.dialog = xbmcgui.Dialog()
|
||||
self.xbmcplayer = xbmc.Player()
|
||||
|
||||
self.syncThreadNumber = int(settings('syncThreadNumber'))
|
||||
self.installSyncDone = settings('SyncInstallRunDone') == 'true'
|
||||
window('dbSyncIndicator', value=settings('dbSyncIndicator'))
|
||||
self.enableMusic = settings('enableMusic') == "true"
|
||||
self.enableBackgroundSync = settings(
|
||||
'enableBackgroundSync') == "true"
|
||||
|
||||
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
|
||||
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
||||
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
|
||||
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
|
||||
state.BACKGROUND_SYNC = settings(
|
||||
'enableBackgroundSync') == 'true'
|
||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||
settings('backgroundsync_saftyMargin'))
|
||||
|
||||
# Show sync dialog even if user deactivated?
|
||||
self.force_dialog = True
|
||||
# Init for replacing paths
|
||||
window('remapSMB', value=settings('remapSMB'))
|
||||
window('replaceSMB', value=settings('replaceSMB'))
|
||||
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
|
||||
state.REMAP_PATH = settings('remapSMB') == 'true'
|
||||
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
|
||||
for arg in ('Org', 'New'):
|
||||
key = 'remapSMB%s%s' % (typus, arg)
|
||||
window(key, value=settings(key))
|
||||
setattr(state, key, settings(key))
|
||||
# Just in case a time sync goes wrong
|
||||
self.timeoffset = int(settings('kodiplextimeoffset'))
|
||||
window('kodiplextimeoffset', value=str(self.timeoffset))
|
||||
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
||||
Thread.__init__(self)
|
||||
|
||||
def showKodiNote(self, message, forced=False, icon="plex"):
|
||||
def showKodiNote(self, message, icon="plex"):
|
||||
"""
|
||||
Shows a Kodi popup, if user selected to do so. Pass message in unicode
|
||||
or string
|
||||
|
||||
icon: "plex": shows Plex icon
|
||||
"error": shows Kodi error icon
|
||||
|
||||
forced: always show popup, even if user setting to off
|
||||
"""
|
||||
if settings('dbSyncIndicator') != 'true':
|
||||
if not forced:
|
||||
if self.xbmcplayer.isPlaying():
|
||||
# Don't show any dialog if media is playing
|
||||
return
|
||||
if state.SYNC_DIALOG is not True and self.force_dialog is not True:
|
||||
return
|
||||
if icon == "plex":
|
||||
self.dialog.notification(
|
||||
lang(29999),
|
||||
message,
|
||||
"special://home/addons/plugin.video.plexkodiconnect/icon.png",
|
||||
5000,
|
||||
False)
|
||||
dialog('notification',
|
||||
heading='{plex}',
|
||||
message=message,
|
||||
icon='{plex}',
|
||||
sound=False)
|
||||
elif icon == "error":
|
||||
self.dialog.notification(
|
||||
lang(29999),
|
||||
message,
|
||||
xbmcgui.NOTIFICATION_ERROR,
|
||||
7000,
|
||||
True)
|
||||
dialog('notification',
|
||||
heading='{plex}',
|
||||
message=message,
|
||||
icon='{error}')
|
||||
|
||||
def syncPMStime(self):
|
||||
"""
|
||||
|
@ -208,11 +201,10 @@ class LibrarySync(Thread):
|
|||
return False
|
||||
|
||||
# Calculate time offset Kodi-PMS
|
||||
self.timeoffset = int(koditime) - int(plextime)
|
||||
window('kodiplextimeoffset', value=str(self.timeoffset))
|
||||
settings('kodiplextimeoffset', value=str(self.timeoffset))
|
||||
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
|
||||
settings('kodiplextimeoffset', value=str(state.KODI_PLEX_TIME_OFFSET))
|
||||
log.info("Time offset Koditime - Plextime in seconds: %s"
|
||||
% str(self.timeoffset))
|
||||
% str(state.KODI_PLEX_TIME_OFFSET))
|
||||
return True
|
||||
|
||||
def initializeDBs(self):
|
||||
|
@ -257,9 +249,6 @@ class LibrarySync(Thread):
|
|||
# True: we're syncing only the delta, e.g. different checksum
|
||||
self.compare = not repair
|
||||
|
||||
# Empty our list of item's we've just processed in the past
|
||||
self.just_processed = {}
|
||||
|
||||
self.new_items_only = True
|
||||
# This will also update playstates and userratings!
|
||||
log.info('Running fullsync for NEW PMS items with repair=%s' % repair)
|
||||
|
@ -293,23 +282,21 @@ class LibrarySync(Thread):
|
|||
'movies': self.PlexMovies,
|
||||
'tvshows': self.PlexTVShows,
|
||||
}
|
||||
if self.enableMusic:
|
||||
if state.ENABLE_MUSIC:
|
||||
process['music'] = self.PlexMusic
|
||||
|
||||
# Do the processing
|
||||
for itemtype in process:
|
||||
if self.thread_stopped():
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||
setScreensaver(value=screensaver)
|
||||
return False
|
||||
if not process[itemtype]():
|
||||
if (self.thread_stopped() or
|
||||
self.thread_suspended() or
|
||||
not process[itemtype]()):
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||
setScreensaver(value=screensaver)
|
||||
return False
|
||||
|
||||
# Let kodi update the views in any case, since we're doing a full sync
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
if self.enableMusic:
|
||||
if state.ENABLE_MUSIC:
|
||||
xbmc.executebuiltin('UpdateLibrary(music)')
|
||||
|
||||
window('plex_initialScan', clear=True)
|
||||
|
@ -317,13 +304,13 @@ class LibrarySync(Thread):
|
|||
setScreensaver(value=screensaver)
|
||||
if window('plex_scancrashed') == 'true':
|
||||
# Show warning if itemtypes.py crashed at some point
|
||||
self.dialog.ok(lang(29999), lang(39408))
|
||||
dialog('ok', heading='{plex}', line1=lang(39408))
|
||||
window('plex_scancrashed', clear=True)
|
||||
elif window('plex_scancrashed') == '401':
|
||||
window('plex_scancrashed', clear=True)
|
||||
if state.PMS_STATUS not in ('401', 'Auth'):
|
||||
# Plex server had too much and returned ERROR
|
||||
self.dialog.ok(lang(29999), lang(39409))
|
||||
dialog('ok', heading='{plex}', line1=lang(39409))
|
||||
|
||||
# Path hack, so Kodis Information screen works
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
|
@ -472,12 +459,12 @@ class LibrarySync(Thread):
|
|||
"""
|
||||
Compare the views to Plex
|
||||
"""
|
||||
if state.DIRECT_PATHS is True and self.enableMusic is True:
|
||||
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
|
||||
if music.set_excludefromscan_music_folders() is True:
|
||||
log.info('Detected new Music library - restarting now')
|
||||
# 'New Plex music library detected. Sorry, but we need to
|
||||
# restart Kodi now due to the changes made.'
|
||||
dialog('ok', lang(29999), lang(39711))
|
||||
dialog('ok', heading='{plex}', line1=lang(39711))
|
||||
from xbmc import executebuiltin
|
||||
executebuiltin('RestartApp')
|
||||
return False
|
||||
|
@ -625,7 +612,6 @@ class LibrarySync(Thread):
|
|||
self.allPlexElementsId APPENDED(!!) dict
|
||||
= {itemid: checksum}
|
||||
"""
|
||||
now = getUnixTimestamp()
|
||||
if self.new_items_only is True:
|
||||
# Only process Plex items that Kodi does not already have in lib
|
||||
for item in xml:
|
||||
|
@ -633,8 +619,8 @@ class LibrarySync(Thread):
|
|||
if not itemId:
|
||||
# Skipping items 'title=All episodes' without a 'ratingKey'
|
||||
continue
|
||||
self.allPlexElementsId[itemId] = ("K%s%s" %
|
||||
(itemId, item.attrib.get('updatedAt', '')))
|
||||
self.allPlexElementsId[itemId] = "K%s%s" % \
|
||||
(itemId, item.attrib.get('updatedAt', ''))
|
||||
if itemId not in self.allKodiElementsId:
|
||||
self.updatelist.append({
|
||||
'itemId': itemId,
|
||||
|
@ -646,10 +632,8 @@ class LibrarySync(Thread):
|
|||
'mediaType': item.attrib.get('type'),
|
||||
'get_children': get_children
|
||||
})
|
||||
self.just_processed[itemId] = now
|
||||
return
|
||||
|
||||
if self.compare:
|
||||
elif self.compare:
|
||||
# Only process the delta - new or changed items
|
||||
for item in xml:
|
||||
itemId = item.attrib.get('ratingKey')
|
||||
|
@ -673,7 +657,6 @@ class LibrarySync(Thread):
|
|||
'mediaType': item.attrib.get('type'),
|
||||
'get_children': get_children
|
||||
})
|
||||
self.just_processed[itemId] = now
|
||||
else:
|
||||
# Initial or repair sync: get all Plex movies
|
||||
for item in xml:
|
||||
|
@ -681,8 +664,8 @@ class LibrarySync(Thread):
|
|||
if not itemId:
|
||||
# Skipping items 'title=All episodes' without a 'ratingKey'
|
||||
continue
|
||||
self.allPlexElementsId[itemId] = ("K%s%s"
|
||||
% (itemId, item.attrib.get('updatedAt', '')))
|
||||
self.allPlexElementsId[itemId] = "K%s%s" \
|
||||
% (itemId, item.attrib.get('updatedAt', ''))
|
||||
self.updatelist.append({
|
||||
'itemId': itemId,
|
||||
'itemType': itemType,
|
||||
|
@ -693,7 +676,6 @@ class LibrarySync(Thread):
|
|||
'mediaType': item.attrib.get('type'),
|
||||
'get_children': get_children
|
||||
})
|
||||
self.just_processed[itemId] = now
|
||||
|
||||
def GetAndProcessXMLs(self, itemType):
|
||||
"""
|
||||
|
@ -725,7 +707,7 @@ class LibrarySync(Thread):
|
|||
getMetadataQueue.put(updateItem)
|
||||
# Spawn GetMetadata threads for downloading
|
||||
threads = []
|
||||
for i in range(min(self.syncThreadNumber, itemNumber)):
|
||||
for i in range(min(state.SYNC_THREAD_NUMBER, itemNumber)):
|
||||
thread = Threaded_Get_Metadata(getMetadataQueue,
|
||||
processMetadataQueue)
|
||||
thread.setDaemon(True)
|
||||
|
@ -739,12 +721,9 @@ class LibrarySync(Thread):
|
|||
thread.start()
|
||||
threads.append(thread)
|
||||
# Start one thread to show sync progress ONLY for new PMS items
|
||||
if self.new_items_only is True and window('dbSyncIndicator') == 'true':
|
||||
dialog = xbmcgui.DialogProgressBG()
|
||||
thread = sync_info.Threaded_Show_Sync_Info(
|
||||
dialog,
|
||||
itemNumber,
|
||||
itemType)
|
||||
if self.new_items_only is True and (state.SYNC_DIALOG is True or
|
||||
self.force_dialog is True):
|
||||
thread = sync_info.Threaded_Show_Sync_Info(itemNumber, itemType)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
@ -803,7 +782,9 @@ class LibrarySync(Thread):
|
|||
# PROCESS MOVIES #####
|
||||
self.updatelist = []
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.installSyncDone is not True:
|
||||
state.PATH_VERIFIED = False
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
# Get items per view
|
||||
viewId = view['id']
|
||||
|
@ -821,10 +802,9 @@ class LibrarySync(Thread):
|
|||
viewName,
|
||||
viewId)
|
||||
self.GetAndProcessXMLs(itemType)
|
||||
log.info("Processed view")
|
||||
# Update viewstate for EVERY item
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
self.PlexUpdateWatched(view['id'], itemType)
|
||||
|
||||
|
@ -896,7 +876,9 @@ class LibrarySync(Thread):
|
|||
# PROCESS TV Shows #####
|
||||
self.updatelist = []
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.installSyncDone is not True:
|
||||
state.PATH_VERIFIED = False
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
# Get items per view
|
||||
viewId = view['id']
|
||||
|
@ -925,7 +907,7 @@ class LibrarySync(Thread):
|
|||
# PROCESS TV Seasons #####
|
||||
# Cycle through tv shows
|
||||
for tvShowId in allPlexTvShowsId:
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
# Grab all seasons to tvshow from PMS
|
||||
seasons = GetAllPlexChildren(tvShowId)
|
||||
|
@ -950,7 +932,7 @@ class LibrarySync(Thread):
|
|||
# PROCESS TV Episodes #####
|
||||
# Cycle through tv shows
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
# Grab all episodes to tvshow from PMS
|
||||
episodes = GetAllPlexLeaves(view['id'])
|
||||
|
@ -985,7 +967,7 @@ class LibrarySync(Thread):
|
|||
|
||||
# Update viewstate:
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
self.PlexUpdateWatched(view['id'], itemType)
|
||||
|
||||
|
@ -1022,7 +1004,7 @@ class LibrarySync(Thread):
|
|||
for kind in (v.PLEX_TYPE_ARTIST,
|
||||
v.PLEX_TYPE_ALBUM,
|
||||
v.PLEX_TYPE_SONG):
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
log.debug("Start processing music %s" % kind)
|
||||
self.allKodiElementsId = {}
|
||||
|
@ -1039,7 +1021,7 @@ class LibrarySync(Thread):
|
|||
|
||||
# Update viewstate for EVERY item
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
self.PlexUpdateWatched(view['id'], itemType)
|
||||
|
||||
|
@ -1064,7 +1046,9 @@ class LibrarySync(Thread):
|
|||
except ValueError:
|
||||
pass
|
||||
for view in views:
|
||||
if self.thread_stopped():
|
||||
if self.installSyncDone is not True:
|
||||
state.PATH_VERIFIED = False
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
return False
|
||||
# Get items per view
|
||||
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
|
||||
|
@ -1092,11 +1076,24 @@ class LibrarySync(Thread):
|
|||
processes json.loads() messages from websocket. Triage what we need to
|
||||
do with "process_" methods
|
||||
"""
|
||||
typus = message.get('type')
|
||||
if typus == 'playing':
|
||||
if message['type'] == 'playing':
|
||||
try:
|
||||
self.process_playing(message['PlaySessionStateNotification'])
|
||||
elif typus == 'timeline':
|
||||
except KeyError:
|
||||
log.error('Received invalid PMS message for playstate: %s'
|
||||
% message)
|
||||
elif message['type'] == 'timeline':
|
||||
try:
|
||||
self.process_timeline(message['TimelineEntry'])
|
||||
except (KeyError, ValueError):
|
||||
log.error('Received invalid PMS message for timeline: %s'
|
||||
% message)
|
||||
elif message['type'] == 'activity':
|
||||
try:
|
||||
self.process_activity(message['ActivityNotification'])
|
||||
except KeyError:
|
||||
log.error('Received invalid PMS message for activity: %s'
|
||||
% message)
|
||||
|
||||
def multi_delete(self, liste, deleteListe):
|
||||
"""
|
||||
|
@ -1139,25 +1136,22 @@ class LibrarySync(Thread):
|
|||
now = getUnixTimestamp()
|
||||
deleteListe = []
|
||||
for i, item in enumerate(self.itemsToProcess):
|
||||
if self.thread_stopped():
|
||||
if self.thread_stopped() or self.thread_suspended():
|
||||
# Chances are that Kodi gets shut down
|
||||
break
|
||||
if item['state'] == 9:
|
||||
successful = self.process_deleteditems(item)
|
||||
elif now - item['timestamp'] < self.saftyMargin:
|
||||
elif now - item['timestamp'] < state.BACKGROUNDSYNC_SAFTYMARGIN:
|
||||
# We haven't waited long enough for the PMS to finish
|
||||
# processing the item. Do it later (excepting deletions)
|
||||
continue
|
||||
else:
|
||||
successful = self.process_newitems(item)
|
||||
if successful:
|
||||
self.just_processed[str(item['ratingKey'])] = now
|
||||
if successful and settings('FanartTV') == 'true':
|
||||
plex_type = v.PLEX_TYPE_FROM_WEBSOCKET[item['type']]
|
||||
if plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW):
|
||||
if item['type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW):
|
||||
self.fanartqueue.put({
|
||||
'plex_id': item['ratingKey'],
|
||||
'plex_type': plex_type,
|
||||
'plex_type': item['type'],
|
||||
'refresh': False
|
||||
})
|
||||
if successful is True:
|
||||
|
@ -1213,22 +1207,25 @@ class LibrarySync(Thread):
|
|||
return True
|
||||
|
||||
def process_deleteditems(self, item):
|
||||
if item.get('type') == 1:
|
||||
log.debug("Removing movie %s" % item.get('ratingKey'))
|
||||
if item['type'] == v.PLEX_TYPE_MOVIE:
|
||||
log.debug("Removing movie %s" % item['ratingKey'])
|
||||
self.videoLibUpdate = True
|
||||
with itemtypes.Movies() as movie:
|
||||
movie.remove(item.get('ratingKey'))
|
||||
elif item.get('type') in (2, 3, 4):
|
||||
log.debug("Removing episode/season/tv show %s"
|
||||
% item.get('ratingKey'))
|
||||
movie.remove(item['ratingKey'])
|
||||
elif item['type'] in (v.PLEX_TYPE_SHOW,
|
||||
v.PLEX_TYPE_SEASON,
|
||||
v.PLEX_TYPE_EPISODE):
|
||||
log.debug("Removing episode/season/tv show %s" % item['ratingKey'])
|
||||
self.videoLibUpdate = True
|
||||
with itemtypes.TVShows() as show:
|
||||
show.remove(item.get('ratingKey'))
|
||||
elif item.get('type') in (8, 9, 10):
|
||||
log.debug("Removing song/album/artist %s" % item.get('ratingKey'))
|
||||
show.remove(item['ratingKey'])
|
||||
elif item['type'] in (v.PLEX_TYPE_ARTIST,
|
||||
v.PLEX_TYPE_ALBUM,
|
||||
v.PLEX_TYPE_SONG):
|
||||
log.debug("Removing song/album/artist %s" % item['ratingKey'])
|
||||
self.musicLibUpdate = True
|
||||
with itemtypes.Music() as music:
|
||||
music.remove(item.get('ratingKey'))
|
||||
music.remove(item['ratingKey'])
|
||||
return True
|
||||
|
||||
def process_timeline(self, data):
|
||||
|
@ -1236,30 +1233,32 @@ class LibrarySync(Thread):
|
|||
PMS is messing with the library items, e.g. new or changed. Put in our
|
||||
"processing queue" for later
|
||||
"""
|
||||
now = getUnixTimestamp()
|
||||
for item in data:
|
||||
if 'tv.plex' in item.get('identifier', ''):
|
||||
# Ommit Plex DVR messages - the Plex IDs are not corresponding
|
||||
# (DVR ratingKeys are not unique and might correspond to a
|
||||
# movie or episode)
|
||||
continue
|
||||
typus = int(item.get('type', 0))
|
||||
status = int(item.get('state', 0))
|
||||
if status == 9 or (typus in (1, 4, 10) and status == 5):
|
||||
# Only process deleted items OR movies, episodes, tracks/songs
|
||||
plex_id = str(item.get('itemID', '0'))
|
||||
if plex_id == '0':
|
||||
log.error('Received malformed PMS message: %s' % item)
|
||||
typus = v.PLEX_TYPE_FROM_WEBSOCKET[int(item['type'])]
|
||||
if typus == v.PLEX_TYPE_CLIP:
|
||||
# No need to process extras or trailers
|
||||
continue
|
||||
try:
|
||||
if (now - self.just_processed[plex_id] <
|
||||
self.ignore_just_processed and status != 9):
|
||||
log.debug('We just processed %s: ignoring' % plex_id)
|
||||
continue
|
||||
except KeyError:
|
||||
# Item has NOT just been processed
|
||||
pass
|
||||
# Have we already added this element?
|
||||
status = int(item['state'])
|
||||
if status == 9:
|
||||
# Immediately and always process deletions (as the PMS will
|
||||
# send additional message with other codes)
|
||||
self.itemsToProcess.append({
|
||||
'state': status,
|
||||
'type': typus,
|
||||
'ratingKey': str(item['itemID']),
|
||||
'timestamp': getUnixTimestamp(),
|
||||
'attempt': 0
|
||||
})
|
||||
elif typus in (v.PLEX_TYPE_MOVIE,
|
||||
v.PLEX_TYPE_EPISODE,
|
||||
v.PLEX_TYPE_SONG) and status == 5:
|
||||
plex_id = str(item['itemID'])
|
||||
# Have we already added this element for processing?
|
||||
for existingItem in self.itemsToProcess:
|
||||
if existingItem['ratingKey'] == plex_id:
|
||||
break
|
||||
|
@ -1273,24 +1272,63 @@ class LibrarySync(Thread):
|
|||
'attempt': 0
|
||||
})
|
||||
|
||||
def process_activity(self, data):
|
||||
"""
|
||||
PMS is re-scanning an item, e.g. after having changed a movie poster.
|
||||
WATCH OUT for this if it's triggered by our PKC library scan!
|
||||
"""
|
||||
for item in data:
|
||||
if item['event'] != 'ended':
|
||||
# Scan still going on, so skip for now
|
||||
continue
|
||||
elif item['Activity'].get('Context') is None:
|
||||
# Not related to any Plex element, but entire library
|
||||
continue
|
||||
elif item['Activity']['type'] != 'library.refresh.items':
|
||||
# Not the type of message relevant for us
|
||||
continue
|
||||
plex_id = GetPlexKeyNumber(item['Activity']['Context']['key'])[1]
|
||||
if plex_id == '':
|
||||
# Likely a Plex id like /library/metadata/3/children
|
||||
continue
|
||||
# We're only looking at existing elements - have we synced yet?
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
kodi_info = plex_db.getItem_byId(plex_id)
|
||||
if kodi_info is None:
|
||||
log.debug('Plex id %s not synced yet - skipping' % plex_id)
|
||||
continue
|
||||
# Have we already added this element?
|
||||
for existingItem in self.itemsToProcess:
|
||||
if existingItem['ratingKey'] == plex_id:
|
||||
break
|
||||
else:
|
||||
# Haven't added this element to the queue yet
|
||||
self.itemsToProcess.append({
|
||||
'state': None, # Don't need a state here
|
||||
'type': kodi_info[5],
|
||||
'ratingKey': plex_id,
|
||||
'timestamp': getUnixTimestamp(),
|
||||
'attempt': 0
|
||||
})
|
||||
|
||||
def process_playing(self, data):
|
||||
"""
|
||||
Someone (not necessarily the user signed in) is playing something some-
|
||||
where
|
||||
"""
|
||||
items = []
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
for item in data:
|
||||
# Drop buffering messages immediately
|
||||
status = item.get('state')
|
||||
status = item['state']
|
||||
if status == 'buffering':
|
||||
continue
|
||||
ratingKey = item.get('ratingKey')
|
||||
kodiInfo = plex_db.getItem_byId(ratingKey)
|
||||
if kodiInfo is None:
|
||||
ratingKey = str(item['ratingKey'])
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
kodi_info = plex_db.getItem_byId(ratingKey)
|
||||
if kodi_info is None:
|
||||
# Item not (yet) in Kodi library
|
||||
continue
|
||||
sessionKey = item.get('sessionKey')
|
||||
sessionKey = item['sessionKey']
|
||||
# Do we already have a sessionKey stored?
|
||||
if sessionKey not in self.sessionKeys:
|
||||
if settings('plex_serverowned') == 'false':
|
||||
|
@ -1355,9 +1393,9 @@ class LibrarySync(Thread):
|
|||
# Append to list that we need to process
|
||||
items.append({
|
||||
'ratingKey': ratingKey,
|
||||
'kodi_id': kodiInfo[0],
|
||||
'file_id': kodiInfo[1],
|
||||
'kodi_type': kodiInfo[4],
|
||||
'kodi_id': kodi_info[0],
|
||||
'file_id': kodi_info[1],
|
||||
'kodi_type': kodi_info[4],
|
||||
'viewOffset': resume,
|
||||
'state': status,
|
||||
'duration': currSess['duration'],
|
||||
|
@ -1394,6 +1432,68 @@ class LibrarySync(Thread):
|
|||
'refresh': refresh
|
||||
})
|
||||
|
||||
def triage_lib_scans(self):
|
||||
"""
|
||||
Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually
|
||||
triggered full or repair syncs
|
||||
"""
|
||||
if state.RUN_LIB_SCAN in ("full", "repair"):
|
||||
log.info('Full library scan requested, starting')
|
||||
window('plex_dbScan', value="true")
|
||||
state.DB_SCAN = True
|
||||
if state.RUN_LIB_SCAN == "full":
|
||||
self.fullSync()
|
||||
else:
|
||||
self.fullSync(repair=True)
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
# Full library sync finished
|
||||
self.showKodiNote(lang(39407))
|
||||
# Reset views was requested from somewhere else
|
||||
elif state.RUN_LIB_SCAN == "views":
|
||||
log.info('Refresh playlist and nodes requested, starting')
|
||||
window('plex_dbScan', value="true")
|
||||
state.DB_SCAN = True
|
||||
# First remove playlists
|
||||
deletePlaylists()
|
||||
# Remove video nodes
|
||||
deleteNodes()
|
||||
# Kick off refresh
|
||||
if self.maintainViews() is True:
|
||||
# Ran successfully
|
||||
log.info("Refresh playlists/nodes completed")
|
||||
# "Plex playlists/nodes refreshed"
|
||||
self.showKodiNote(lang(39405))
|
||||
else:
|
||||
# Failed
|
||||
log.error("Refresh playlists/nodes failed")
|
||||
# "Plex playlists/nodes refresh failed"
|
||||
self.showKodiNote(lang(39406),
|
||||
icon="error")
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
elif state.RUN_LIB_SCAN == 'fanart':
|
||||
# Only look for missing fanart (No)
|
||||
# or refresh all fanart (Yes)
|
||||
self.fanartSync(refresh=dialog(
|
||||
'yesno',
|
||||
heading='{plex}',
|
||||
line1=lang(39223),
|
||||
nolabel=lang(39224),
|
||||
yeslabel=lang(39225)))
|
||||
elif state.RUN_LIB_SCAN == 'textures':
|
||||
state.DB_SCAN = True
|
||||
window('plex_dbScan', value="true")
|
||||
import artwork
|
||||
artwork.Artwork().fullTextureCacheSync()
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
else:
|
||||
raise NotImplementedError('Library scan not defined: %s'
|
||||
% state.RUN_LIB_SCAN)
|
||||
# Reset
|
||||
state.RUN_LIB_SCAN = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.run_internal()
|
||||
|
@ -1404,7 +1504,7 @@ class LibrarySync(Thread):
|
|||
import traceback
|
||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||
# Library sync thread has crashed
|
||||
self.dialog.ok(lang(29999), lang(39400))
|
||||
dialog('ok', heading='{plex}', line1=lang(39400))
|
||||
raise
|
||||
|
||||
def run_internal(self):
|
||||
|
@ -1412,24 +1512,21 @@ class LibrarySync(Thread):
|
|||
thread_stopped = self.thread_stopped
|
||||
thread_suspended = self.thread_suspended
|
||||
installSyncDone = self.installSyncDone
|
||||
enableBackgroundSync = self.enableBackgroundSync
|
||||
background_sync = state.BACKGROUND_SYNC
|
||||
fullSync = self.fullSync
|
||||
processMessage = self.processMessage
|
||||
processItems = self.processItems
|
||||
fullSyncInterval = self.fullSyncInterval
|
||||
FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL
|
||||
lastSync = 0
|
||||
lastTimeSync = 0
|
||||
lastProcessing = 0
|
||||
oneDay = 60*60*24
|
||||
|
||||
xbmcplayer = xbmc.Player()
|
||||
|
||||
# Link to Websocket queue
|
||||
queue = self.mgr.ws.queue
|
||||
|
||||
startupComplete = False
|
||||
self.views = []
|
||||
errorcount = 0
|
||||
|
||||
log.info("---===### Starting LibrarySync ###===---")
|
||||
|
||||
|
@ -1450,7 +1547,9 @@ class LibrarySync(Thread):
|
|||
return
|
||||
xbmc.sleep(1000)
|
||||
|
||||
if (window('plex_dbCheck') != "true" and installSyncDone):
|
||||
if state.KODI_DB_CHECKED is False and installSyncDone:
|
||||
# Install sync was already done, don't force-show dialogs
|
||||
self.force_dialog = False
|
||||
# Verify the validity of the database
|
||||
currentVersion = settings('dbCreatedWithVersion')
|
||||
minVersion = window('plex_minDBVersion')
|
||||
|
@ -1459,18 +1558,19 @@ class LibrarySync(Thread):
|
|||
log.warn("Db version out of date: %s minimum version "
|
||||
"required: %s" % (currentVersion, minVersion))
|
||||
# DB out of date. Proceed to recreate?
|
||||
resp = self.dialog.yesno(heading=lang(29999),
|
||||
resp = dialog('yesno',
|
||||
heading=lang(29999),
|
||||
line1=lang(39401))
|
||||
if not resp:
|
||||
log.warn("Db version out of date! USER IGNORED!")
|
||||
# PKC may not work correctly until reset
|
||||
self.dialog.ok(heading=lang(29999),
|
||||
line1=(lang(29999) + lang(39402)))
|
||||
dialog('ok',
|
||||
heading='{plex}',
|
||||
line1=lang(29999) + lang(39402))
|
||||
else:
|
||||
reset()
|
||||
break
|
||||
|
||||
window('plex_dbCheck', value="true")
|
||||
state.KODI_DB_CHECKED = True
|
||||
|
||||
if not startupComplete:
|
||||
# Also runs when first installed
|
||||
|
@ -1483,7 +1583,7 @@ class LibrarySync(Thread):
|
|||
log.error('Current Kodi version: %s' % tryDecode(
|
||||
xbmc.getInfoLabel('System.BuildVersion')))
|
||||
# "Current Kodi version is unsupported, cancel lib sync"
|
||||
self.dialog.ok(heading=lang(29999), line1=lang(39403))
|
||||
dialog('ok', heading='{plex}', line1=lang(39403))
|
||||
break
|
||||
# Run start up sync
|
||||
state.DB_SCAN = True
|
||||
|
@ -1518,93 +1618,38 @@ class LibrarySync(Thread):
|
|||
settings('SyncInstallRunDone', value="true")
|
||||
settings("dbCreatedWithVersion", v.ADDON_VERSION)
|
||||
installSyncDone = True
|
||||
self.force_dialog = False
|
||||
else:
|
||||
log.error("Initial start-up full sync unsuccessful")
|
||||
errorcount += 1
|
||||
if errorcount > 2:
|
||||
log.error("Startup full sync failed. Stopping sync")
|
||||
# "Startup syncing process failed repeatedly"
|
||||
# "Please restart"
|
||||
self.dialog.ok(heading=lang(29999),
|
||||
line1=lang(39404))
|
||||
break
|
||||
|
||||
# Currently no db scan, so we can start a new scan
|
||||
elif state.DB_SCAN is False:
|
||||
# Full scan was requested from somewhere else, e.g. userclient
|
||||
if window('plex_runLibScan') in ("full", "repair"):
|
||||
log.info('Full library scan requested, starting')
|
||||
window('plex_dbScan', value="true")
|
||||
state.DB_SCAN = True
|
||||
if window('plex_runLibScan') == "full":
|
||||
fullSync()
|
||||
elif window('plex_runLibScan') == "repair":
|
||||
fullSync(repair=True)
|
||||
window('plex_runLibScan', clear=True)
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
# Full library sync finished
|
||||
self.showKodiNote(lang(39407), forced=False)
|
||||
# Reset views was requested from somewhere else
|
||||
elif window('plex_runLibScan') == "views":
|
||||
log.info('Refresh playlist and nodes requested, starting')
|
||||
window('plex_dbScan', value="true")
|
||||
state.DB_SCAN = True
|
||||
window('plex_runLibScan', clear=True)
|
||||
|
||||
# First remove playlists
|
||||
deletePlaylists()
|
||||
# Remove video nodes
|
||||
deleteNodes()
|
||||
# Kick off refresh
|
||||
if self.maintainViews() is True:
|
||||
# Ran successfully
|
||||
log.info("Refresh playlists/nodes completed")
|
||||
# "Plex playlists/nodes refreshed"
|
||||
self.showKodiNote(lang(39405), forced=True)
|
||||
else:
|
||||
# Failed
|
||||
log.error("Refresh playlists/nodes failed")
|
||||
# "Plex playlists/nodes refresh failed"
|
||||
self.showKodiNote(lang(39406),
|
||||
forced=True,
|
||||
icon="error")
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
elif window('plex_runLibScan') == 'fanart':
|
||||
window('plex_runLibScan', clear=True)
|
||||
# Only look for missing fanart (No)
|
||||
# or refresh all fanart (Yes)
|
||||
self.fanartSync(refresh=self.dialog.yesno(
|
||||
heading=lang(29999),
|
||||
line1=lang(39223),
|
||||
nolabel=lang(39224),
|
||||
yeslabel=lang(39225)))
|
||||
elif window('plex_runLibScan') == 'del_textures':
|
||||
window('plex_runLibScan', clear=True)
|
||||
state.DB_SCAN = True
|
||||
window('plex_dbScan', value="true")
|
||||
import artwork
|
||||
artwork.Artwork().fullTextureCacheSync()
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
else:
|
||||
if state.RUN_LIB_SCAN is not None:
|
||||
# Force-show dialogs since they are user-initiated
|
||||
self.force_dialog = True
|
||||
self.triage_lib_scans()
|
||||
self.force_dialog = False
|
||||
continue
|
||||
now = getUnixTimestamp()
|
||||
if (now - lastSync > fullSyncInterval and
|
||||
not xbmcplayer.isPlaying()):
|
||||
# Standard syncs - don't force-show dialogs
|
||||
self.force_dialog = False
|
||||
if (now - lastSync > FULL_SYNC_INTERVALL and
|
||||
not self.xbmcplayer.isPlaying()):
|
||||
lastSync = now
|
||||
log.info('Doing scheduled full library scan')
|
||||
state.DB_SCAN = True
|
||||
window('plex_dbScan', value="true")
|
||||
if fullSync() is False and not thread_stopped():
|
||||
log.error('Could not finish scheduled full sync')
|
||||
self.force_dialog = True
|
||||
self.showKodiNote(lang(39410),
|
||||
forced=True,
|
||||
icon='error')
|
||||
self.force_dialog = False
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
# Full library sync finished
|
||||
self.showKodiNote(lang(39407), forced=False)
|
||||
self.showKodiNote(lang(39407))
|
||||
elif now - lastTimeSync > oneDay:
|
||||
lastTimeSync = now
|
||||
log.info('Starting daily time sync')
|
||||
|
@ -1613,7 +1658,7 @@ class LibrarySync(Thread):
|
|||
self.syncPMStime()
|
||||
window('plex_dbScan', clear=True)
|
||||
state.DB_SCAN = False
|
||||
elif enableBackgroundSync:
|
||||
elif background_sync:
|
||||
# Check back whether we should process something
|
||||
# Only do this once every while (otherwise, potentially
|
||||
# many screen refreshes lead to flickering)
|
||||
|
|
|
@ -1,74 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
###############################################################################
|
||||
import logging
|
||||
import xbmc
|
||||
###############################################################################
|
||||
LEVELS = {
|
||||
logging.ERROR: xbmc.LOGERROR,
|
||||
logging.WARNING: xbmc.LOGWARNING,
|
||||
logging.INFO: xbmc.LOGNOTICE,
|
||||
logging.DEBUG: xbmc.LOGDEBUG
|
||||
}
|
||||
###############################################################################
|
||||
|
||||
from utils import window, tryEncode
|
||||
|
||||
##################################################################################################
|
||||
def tryEncode(uniString, encoding='utf-8'):
|
||||
"""
|
||||
Will try to encode uniString (in unicode) to encoding. This possibly
|
||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||
string.encode()
|
||||
"""
|
||||
if isinstance(uniString, str):
|
||||
# already encoded
|
||||
return uniString
|
||||
try:
|
||||
uniString = uniString.encode(encoding, "ignore")
|
||||
except TypeError:
|
||||
uniString = uniString.encode()
|
||||
return uniString
|
||||
|
||||
|
||||
def config():
|
||||
|
||||
logger = logging.getLogger('PLEX')
|
||||
logger.addHandler(LogHandler())
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class LogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
self.setFormatter(logging.Formatter(fmt="%(name)s: %(message)s"))
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if self._get_log_level(record.levelno):
|
||||
try:
|
||||
xbmc.log(self.format(record), level=xbmc.LOGNOTICE)
|
||||
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE)
|
||||
|
||||
@classmethod
|
||||
def _get_log_level(cls, level):
|
||||
|
||||
levels = {
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
}
|
||||
try:
|
||||
log_level = int(window('plex_logLevel'))
|
||||
except ValueError:
|
||||
log_level = 0
|
||||
|
||||
return log_level >= levels[level]
|
||||
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, fmt="%(name)s -> %(message)s"):
|
||||
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
|
||||
# Save the original format configured by the user
|
||||
# when the logger formatter was instantiated
|
||||
format_orig = self._fmt
|
||||
|
||||
# Replace the original format with one customized by logging level
|
||||
if record.levelno in (logging.DEBUG, logging.ERROR):
|
||||
self._fmt = '%(name)s -> %(levelname)s: %(message)s'
|
||||
|
||||
# Call the original formatter class to do the grunt work
|
||||
result = logging.Formatter.format(self, record)
|
||||
|
||||
# Restore the original format configured by the user
|
||||
self._fmt = format_orig
|
||||
|
||||
return result
|
||||
xbmc.log(tryEncode(self.format(record)),
|
||||
level=LEVELS[record.levelno])
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
import logging
|
||||
import cPickle as Pickle
|
||||
from cPickle import dumps, loads
|
||||
|
||||
from utils import pickl_window
|
||||
from xbmcgui import Window
|
||||
from xbmc import log, LOGDEBUG
|
||||
###############################################################################
|
||||
WINDOW = Window(10000)
|
||||
PREFIX = 'PLEX.%s: ' % __name__
|
||||
###############################################################################
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
def pickl_window(property, value=None, clear=False):
|
||||
"""
|
||||
Get or set window property - thread safe! For use with Pickle
|
||||
Property and value must be string
|
||||
"""
|
||||
if clear:
|
||||
WINDOW.clearProperty(property)
|
||||
elif value is not None:
|
||||
WINDOW.setProperty(property, value)
|
||||
else:
|
||||
return WINDOW.getProperty(property)
|
||||
|
||||
|
||||
def pickle_me(obj, window_var='plex_result'):
|
||||
|
@ -19,9 +32,9 @@ def pickle_me(obj, window_var='plex_result'):
|
|||
obj can be pretty much any Python object. However, classes and
|
||||
functions won't work. See the Pickle documentation
|
||||
"""
|
||||
log.debug('Start pickling: %s' % obj)
|
||||
pickl_window(window_var, value=Pickle.dumps(obj))
|
||||
log.debug('Successfully pickled')
|
||||
log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
|
||||
pickl_window(window_var, value=dumps(obj))
|
||||
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
|
||||
|
||||
|
||||
def unpickle_me(window_var='plex_result'):
|
||||
|
@ -31,9 +44,9 @@ def unpickle_me(window_var='plex_result'):
|
|||
"""
|
||||
result = pickl_window(window_var)
|
||||
pickl_window(window_var, clear=True)
|
||||
log.debug('Start unpickling')
|
||||
obj = Pickle.loads(result)
|
||||
log.debug('Successfully unpickled: %s' % obj)
|
||||
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
|
||||
obj = loads(result)
|
||||
log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
|
||||
return obj
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import variables as v
|
|||
from downloadutils import DownloadUtils
|
||||
from PKC_listitem import convert_PKC_to_listitem
|
||||
import plexdb_functions as plexdb
|
||||
from context_entry import ContextMenu
|
||||
import state
|
||||
|
||||
###############################################################################
|
||||
|
@ -142,6 +143,9 @@ class Playback_Starter(Thread):
|
|||
params.get('view_offset'),
|
||||
directplay=True if params.get('play_directly') else False,
|
||||
node=False if params.get('node') == 'false' else True)
|
||||
elif mode == 'context_menu':
|
||||
ContextMenu()
|
||||
result = Playback_Successful()
|
||||
except:
|
||||
log.error('Error encountered for mode %s, params %s'
|
||||
% (mode, params))
|
||||
|
|
|
@ -5,10 +5,8 @@ import logging
|
|||
import json
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from utils import window, settings, language as lang, DateToKodi, \
|
||||
getUnixTimestamp, tryDecode, tryEncode
|
||||
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode
|
||||
import downloadutils
|
||||
import plexdb_functions as plexdb
|
||||
import kodidb_functions as kodidb
|
||||
|
@ -354,6 +352,8 @@ class Player(xbmc.Player):
|
|||
log.info("Percent complete: %s Mark played at: %s"
|
||||
% (percentComplete, markPlayed))
|
||||
if percentComplete >= markPlayed:
|
||||
# Kodi seems to sometimes overwrite our playstate, so wait
|
||||
xbmc.sleep(500)
|
||||
# Tell Kodi that we've finished watching (Plex knows)
|
||||
if (data['fileid'] is not None and
|
||||
data['itemType'] in (v.KODI_TYPE_MOVIE,
|
||||
|
|
|
@ -10,6 +10,8 @@ STOP_PKC = False
|
|||
SUSPEND_LIBRARY_THREAD = False
|
||||
# Set if user decided to cancel sync
|
||||
STOP_SYNC = False
|
||||
# Could we access the paths?
|
||||
PATH_VERIFIED = False
|
||||
# Set if a Plex-Kodi DB sync is being done - along with
|
||||
# window('plex_dbScan') set to 'true'
|
||||
DB_SCAN = False
|
||||
|
@ -24,6 +26,42 @@ RESTRICTED_USER = False
|
|||
DIRECT_PATHS = False
|
||||
# Shall we replace custom user ratings with the number of versions available?
|
||||
INDICATE_MEDIA_VERSIONS = False
|
||||
# Do we need to run a special library scan?
|
||||
RUN_LIB_SCAN = None
|
||||
|
||||
# Stemming from the PKC settings.xml
|
||||
# Shall we show Kodi dialogs when synching?
|
||||
SYNC_DIALOG = True
|
||||
# Have we already checked the Kodi DB on consistency?
|
||||
KODI_DB_CHECKED = False
|
||||
# Is synching of Plex music enabled?
|
||||
ENABLE_MUSIC = True
|
||||
# How often shall we sync?
|
||||
FULL_SYNC_INTERVALL = 0
|
||||
# Background Sync enabled at all?
|
||||
BACKGROUND_SYNC = True
|
||||
# How long shall we wait with synching a new item to make sure Plex got all
|
||||
# metadata?
|
||||
BACKGROUNDSYNC_SAFTYMARGIN = 0
|
||||
# How many threads to download Plex metadata on sync?
|
||||
SYNC_THREAD_NUMBER = 0
|
||||
# What's the time offset between the PMS and Kodi?
|
||||
KODI_PLEX_TIME_OFFSET = 0.0
|
||||
|
||||
# Path remapping mechanism (e.g. smb paths)
|
||||
# Do we replace \\myserver\path to smb://myserver/path?
|
||||
REPLACE_SMB_PATH = False
|
||||
# Do we generally remap?
|
||||
REMAP_PATH = False
|
||||
# Mappings for REMAP_PATH:
|
||||
remapSMBmovieOrg = None
|
||||
remapSMBmovieNew = None
|
||||
remapSMBtvOrg = None
|
||||
remapSMBtvNew = None
|
||||
remapSMBmusicOrg = None
|
||||
remapSMBmusicNew = None
|
||||
remapSMBphotoOrg = None
|
||||
remapSMBphotoNew = None
|
||||
|
||||
# Along with window('plex_authenticated')
|
||||
AUTHENTICATED = False
|
||||
|
|
|
@ -59,24 +59,6 @@ def window(property, value=None, clear=False, windowid=10000):
|
|||
return tryDecode(win.getProperty(property))
|
||||
|
||||
|
||||
def pickl_window(property, value=None, clear=False, windowid=10000):
|
||||
"""
|
||||
Get or set window property - thread safe! For use with Pickle
|
||||
Property and value must be string
|
||||
"""
|
||||
if windowid != 10000:
|
||||
win = xbmcgui.Window(windowid)
|
||||
else:
|
||||
win = WINDOW
|
||||
|
||||
if clear:
|
||||
win.clearProperty(property)
|
||||
elif value is not None:
|
||||
win.setProperty(property, value)
|
||||
else:
|
||||
return win.getProperty(property)
|
||||
|
||||
|
||||
def plex_command(key, value):
|
||||
"""
|
||||
Used to funnel states between different Python instances. NOT really thread
|
||||
|
@ -86,7 +68,7 @@ def plex_command(key, value):
|
|||
value: either 'True' or 'False'
|
||||
"""
|
||||
while window('plex_command'):
|
||||
xbmc.sleep(5)
|
||||
xbmc.sleep(20)
|
||||
window('plex_command', value='%s-%s' % (key, value))
|
||||
|
||||
|
||||
|
@ -140,6 +122,15 @@ def dialog(typus, *args, **kwargs):
|
|||
Displays xbmcgui Dialog. Pass a string as typus:
|
||||
'yesno', 'ok', 'notification', 'input', 'select', 'numeric'
|
||||
|
||||
kwargs:
|
||||
heading='{plex}' title bar (here PlexKodiConnect)
|
||||
message=lang(30128), Actual dialog content. Don't use with OK
|
||||
line1=str(), For 'OK' and 'yesno' dialogs use line1...line3!
|
||||
time=5000,
|
||||
sound=True,
|
||||
nolabel=str(), For 'yesno' dialogs
|
||||
yeslabel=str(), For 'yesno' dialogs
|
||||
|
||||
Icons:
|
||||
icon='{plex}' Display Plex standard icon
|
||||
icon='{info}' xbmcgui.NOTIFICATION_INFO
|
||||
|
@ -221,6 +212,16 @@ def tryDecode(string, encoding='utf-8'):
|
|||
return string
|
||||
|
||||
|
||||
def slugify(text):
|
||||
"""
|
||||
Normalizes text (in unicode or string) to e.g. enable safe filenames.
|
||||
Returns unicode
|
||||
"""
|
||||
if not isinstance(text, unicode):
|
||||
text = unicode(text)
|
||||
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
|
||||
|
||||
|
||||
def escape_html(string):
|
||||
"""
|
||||
Escapes the following:
|
||||
|
@ -248,7 +249,7 @@ def DateToKodi(stamp):
|
|||
None if an error was encountered
|
||||
"""
|
||||
try:
|
||||
stamp = float(stamp) + float(window('kodiplextimeoffset'))
|
||||
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
|
||||
date_time = localtime(stamp)
|
||||
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
|
||||
except:
|
||||
|
|
|
@ -84,7 +84,8 @@ class WebSocket(Thread):
|
|||
# No worries if read timed out
|
||||
pass
|
||||
except websocket.WebSocketConnectionClosedException:
|
||||
log.info("Connection closed, (re)connecting")
|
||||
log.info("%s: connection closed, (re)connecting"
|
||||
% self.__class__.__name__)
|
||||
uri, sslopt = self.getUri()
|
||||
try:
|
||||
# Low timeout - let's us shut this thread down!
|
||||
|
@ -95,7 +96,7 @@ class WebSocket(Thread):
|
|||
enable_multithread=True)
|
||||
except IOError:
|
||||
# Server is probably offline
|
||||
log.info("Error connecting")
|
||||
log.info("%s: Error connecting" % self.__class__.__name__)
|
||||
self.ws = None
|
||||
counter += 1
|
||||
if counter > 3:
|
||||
|
@ -103,33 +104,41 @@ class WebSocket(Thread):
|
|||
self.IOError_response()
|
||||
sleep(1000)
|
||||
except websocket.WebSocketTimeoutException:
|
||||
log.info("timeout while connecting, trying again")
|
||||
log.info("%s: Timeout while connecting, trying again"
|
||||
% self.__class__.__name__)
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
except websocket.WebSocketException as e:
|
||||
log.info('WebSocketException: %s' % e)
|
||||
log.info('%s: WebSocketException: %s'
|
||||
% (self.__class__.__name__, e))
|
||||
if 'Handshake Status 401' in e.args:
|
||||
handshake_counter += 1
|
||||
if handshake_counter >= 5:
|
||||
log.info('Error in handshake detected. Stopping '
|
||||
'%s now' % self.__class__.__name__)
|
||||
log.info('%s: Error in handshake detected. '
|
||||
'Stopping now'
|
||||
% self.__class__.__name__)
|
||||
break
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
except Exception as e:
|
||||
log.error("Unknown exception encountered in connecting: %s"
|
||||
% e)
|
||||
log.error('%s: Unknown exception encountered when '
|
||||
'connecting: %s' % (self.__class__.__name__, e))
|
||||
import traceback
|
||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||
log.error("%s: Traceback:\n%s"
|
||||
% (self.__class__.__name__,
|
||||
traceback.format_exc()))
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
else:
|
||||
counter = 0
|
||||
handshake_counter = 0
|
||||
except Exception as e:
|
||||
log.error("Unknown exception encountered: %s" % e)
|
||||
log.error("%s: Unknown exception encountered: %s"
|
||||
% (self.__class__.__name__, e))
|
||||
import traceback
|
||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||
log.error("%s: Traceback:\n%s"
|
||||
% (self.__class__.__name__,
|
||||
traceback.format_exc()))
|
||||
try:
|
||||
self.ws.shutdown()
|
||||
except:
|
||||
|
@ -171,37 +180,46 @@ class PMS_Websocket(WebSocket):
|
|||
sslopt = {}
|
||||
if settings('sslverify') == "false":
|
||||
sslopt["cert_reqs"] = CERT_NONE
|
||||
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
||||
log.debug("%s: Uri: %s, sslopt: %s"
|
||||
% (self.__class__.__name__, uri, sslopt))
|
||||
return uri, sslopt
|
||||
|
||||
def process(self, opcode, message):
|
||||
if opcode not in self.opcode_data:
|
||||
return False
|
||||
return
|
||||
|
||||
try:
|
||||
message = loads(message)
|
||||
except Exception as ex:
|
||||
log.error('Error decoding message from websocket: %s' % ex)
|
||||
except ValueError:
|
||||
log.error('%s: Error decoding message from websocket'
|
||||
% self.__class__.__name__)
|
||||
log.error(message)
|
||||
return False
|
||||
return
|
||||
try:
|
||||
message = message['NotificationContainer']
|
||||
except KeyError:
|
||||
log.error('Could not parse PMS message: %s' % message)
|
||||
return False
|
||||
log.error('%s: Could not parse PMS message: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
return
|
||||
# Triage
|
||||
typus = message.get('type')
|
||||
if typus is None:
|
||||
log.error('No message type, dropping message: %s' % message)
|
||||
return False
|
||||
log.debug('Received message from PMS server: %s' % message)
|
||||
log.error('%s: No message type, dropping message: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
return
|
||||
log.debug('%s: Received message from PMS server: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
# Drop everything we're not interested in
|
||||
if typus not in ('playing', 'timeline'):
|
||||
return True
|
||||
|
||||
if typus not in ('playing', 'timeline', 'activity'):
|
||||
return
|
||||
elif typus == 'activity' and state.DB_SCAN is True:
|
||||
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
||||
# possibly causing these reprocessing messages en mass)
|
||||
log.debug('%s: Dropping message as PKC is currently synching'
|
||||
% self.__class__.__name__)
|
||||
else:
|
||||
# Put PMS message on queue and let libsync take care of it
|
||||
self.queue.put(message)
|
||||
return True
|
||||
|
||||
def IOError_response(self):
|
||||
log.warn("Repeatedly could not connect to PMS, "
|
||||
|
@ -224,32 +242,36 @@ class Alexa_Websocket(WebSocket):
|
|||
% (state.PLEX_USER_ID,
|
||||
self.plex_client_Id, state.PLEX_TOKEN))
|
||||
sslopt = {}
|
||||
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
||||
log.debug("%s: Uri: %s, sslopt: %s"
|
||||
% (self.__class__.__name__, uri, sslopt))
|
||||
return uri, sslopt
|
||||
|
||||
def process(self, opcode, message):
|
||||
if opcode not in self.opcode_data:
|
||||
return False
|
||||
log.debug('Received the following message from Alexa:')
|
||||
log.debug(message)
|
||||
return
|
||||
log.debug('%s: Received the following message from Alexa:'
|
||||
% self.__class__.__name__)
|
||||
log.debug('%s: %s' % (self.__class__.__name__, message))
|
||||
try:
|
||||
message = etree.fromstring(message)
|
||||
except Exception as ex:
|
||||
log.error('Error decoding message from Alexa: %s' % ex)
|
||||
return False
|
||||
log.error('%s: Error decoding message from Alexa: %s'
|
||||
% (self.__class__.__name__, ex))
|
||||
return
|
||||
try:
|
||||
if message.attrib['command'] == 'processRemoteControlCommand':
|
||||
message = message[0]
|
||||
else:
|
||||
log.error('Unknown Alexa message received')
|
||||
return False
|
||||
log.error('%s: Unknown Alexa message received'
|
||||
% self.__class__.__name__)
|
||||
return
|
||||
except:
|
||||
log.error('Could not parse Alexa message')
|
||||
return False
|
||||
log.error('%s: Could not parse Alexa message'
|
||||
% self.__class__.__name__)
|
||||
return
|
||||
process_command(message.attrib['path'][1:],
|
||||
message.attrib,
|
||||
queue=self.mgr.plexCompanion.queue)
|
||||
return True
|
||||
|
||||
def IOError_response(self):
|
||||
pass
|
||||
|
|
|
@ -146,7 +146,6 @@
|
|||
</category>
|
||||
|
||||
<category label="30022"><!-- Advanced -->
|
||||
<setting id="logLevel" type="enum" label="30004" values="Disabled|Info|Debug" default="1" />
|
||||
<setting id="startupDelay" type="number" label="30529" default="0" option="int" />
|
||||
<setting label="39018" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=repair)" option="close" /> <!-- Repair local database (force update all content) -->
|
||||
<setting label="30535" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=deviceid)" /><!-- Reset device id uuid -->
|
||||
|
|
41
service.py
41
service.py
|
@ -30,8 +30,7 @@ sys_path.append(_base_resource)
|
|||
|
||||
###############################################################################
|
||||
|
||||
from utils import settings, window, language as lang, dialog, tryEncode, \
|
||||
tryDecode
|
||||
from utils import settings, window, language as lang, dialog, tryDecode
|
||||
from userclient import UserClient
|
||||
import initialsetup
|
||||
from kodimonitor import KodiMonitor
|
||||
|
@ -82,10 +81,8 @@ class Service():
|
|||
|
||||
def __init__(self):
|
||||
|
||||
logLevel = self.getLogLevel()
|
||||
self.monitor = Monitor()
|
||||
|
||||
window('plex_logLevel', value=str(logLevel))
|
||||
window('plex_kodiProfile',
|
||||
value=tryDecode(translatePath("special://profile")))
|
||||
window('plex_context',
|
||||
|
@ -94,27 +91,26 @@ class Service():
|
|||
value=settings('fetch_pms_item_number'))
|
||||
|
||||
# Initial logging
|
||||
log.warn("======== START %s ========" % v.ADDON_NAME)
|
||||
log.warn("Platform: %s" % v.PLATFORM)
|
||||
log.warn("KODI Version: %s" % v.KODILONGVERSION)
|
||||
log.warn("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
|
||||
log.warn("Using plugin paths: %s"
|
||||
log.info("======== START %s ========" % v.ADDON_NAME)
|
||||
log.info("Platform: %s" % v.PLATFORM)
|
||||
log.info("KODI Version: %s" % v.KODILONGVERSION)
|
||||
log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
|
||||
log.info("Using plugin paths: %s"
|
||||
% (settings('useDirectPaths') != "true"))
|
||||
log.warn("Number of sync threads: %s"
|
||||
log.info("Number of sync threads: %s"
|
||||
% settings('syncThreadNumber'))
|
||||
log.warn("Log Level: %s" % logLevel)
|
||||
log.warn("Full sys.argv received: %s" % argv)
|
||||
log.info("Full sys.argv received: %s" % argv)
|
||||
|
||||
# Reset window props for profile switch
|
||||
properties = [
|
||||
"plex_online", "plex_serverStatus", "plex_onWake",
|
||||
"plex_dbCheck", "plex_kodiScan",
|
||||
"plex_kodiScan",
|
||||
"plex_shouldStop", "plex_dbScan",
|
||||
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
|
||||
"plex_runLibScan", "pms_token", "plex_token",
|
||||
"pms_token", "plex_token",
|
||||
"pms_server", "plex_machineIdentifier", "plex_servername",
|
||||
"plex_authenticated", "PlexUserImage", "useDirectPaths",
|
||||
"kodiplextimeoffset", "countError", "countUnauthorized",
|
||||
"countError", "countUnauthorized",
|
||||
"plex_restricteduser", "plex_allows_mediaDeletion",
|
||||
"plex_command", "plex_result", "plex_force_transcode_pix"
|
||||
]
|
||||
|
@ -127,13 +123,6 @@ class Service():
|
|||
# Set the minimum database version
|
||||
window('plex_minDBVersion', value="1.5.10")
|
||||
|
||||
def getLogLevel(self):
|
||||
try:
|
||||
logLevel = int(settings('logLevel'))
|
||||
except ValueError:
|
||||
logLevel = 0
|
||||
return logLevel
|
||||
|
||||
def __stop_PKC(self):
|
||||
"""
|
||||
Kodi's abortRequested is really unreliable :-(
|
||||
|
@ -173,7 +162,7 @@ class Service():
|
|||
|
||||
if window('plex_kodiProfile') != kodiProfile:
|
||||
# Profile change happened, terminate this thread and others
|
||||
log.warn("Kodi profile was: %s and changed to: %s. "
|
||||
log.info("Kodi profile was: %s and changed to: %s. "
|
||||
"Terminating old PlexKodiConnect thread."
|
||||
% (kodiProfile,
|
||||
window('plex_kodiProfile')))
|
||||
|
@ -332,7 +321,7 @@ class Service():
|
|||
except:
|
||||
pass
|
||||
window('plex_service_started', clear=True)
|
||||
log.warn("======== STOP %s ========" % v.ADDON_NAME)
|
||||
log.info("======== STOP %s ========" % v.ADDON_NAME)
|
||||
|
||||
|
||||
# Safety net - Kody starts PKC twice upon first installation!
|
||||
|
@ -345,11 +334,11 @@ else:
|
|||
# Delay option
|
||||
delay = int(settings('startupDelay'))
|
||||
|
||||
log.warn("Delaying Plex startup by: %s sec..." % delay)
|
||||
log.info("Delaying Plex startup by: %s sec..." % delay)
|
||||
if exit:
|
||||
log.error('PKC service.py already started - exiting this instance')
|
||||
elif delay and Monitor().waitForAbort(delay):
|
||||
# Start the service
|
||||
log.warn("Abort requested while waiting. PKC not started.")
|
||||
log.info("Abort requested while waiting. PKC not started.")
|
||||
else:
|
||||
Service().ServiceEntryPoint()
|
||||
|
|
Loading…
Reference in a new issue