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)
|
[![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
|
||||||
|
|
52
addon.xml
52
addon.xml
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
29
default.py
29
default.py
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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 = (
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
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, \
|
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()
|
||||||
|
|
Loading…
Reference in a new issue