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,14 +1346,18 @@ class API():
|
||||||
cast = []
|
cast = []
|
||||||
producer = []
|
producer = []
|
||||||
for child in self.item:
|
for child in self.item:
|
||||||
if child.tag == 'Director':
|
try:
|
||||||
director.append(child.attrib['tag'])
|
if child.tag == 'Director':
|
||||||
elif child.tag == 'Writer':
|
director.append(child.attrib['tag'])
|
||||||
writer.append(child.attrib['tag'])
|
elif child.tag == 'Writer':
|
||||||
elif child.tag == 'Role':
|
writer.append(child.attrib['tag'])
|
||||||
cast.append(child.attrib['tag'])
|
elif child.tag == 'Role':
|
||||||
elif child.tag == 'Producer':
|
cast.append(child.attrib['tag'])
|
||||||
producer.append(child.attrib['tag'])
|
elif child.tag == 'Producer':
|
||||||
|
producer.append(child.attrib['tag'])
|
||||||
|
except KeyError:
|
||||||
|
log.warn('Malformed PMS answer for getPeople: %s: %s'
|
||||||
|
% (child.tag, child.attrib))
|
||||||
return {
|
return {
|
||||||
'Director': director,
|
'Director': director,
|
||||||
'Writer': writer,
|
'Writer': writer,
|
||||||
|
@ -1750,8 +1754,16 @@ class API():
|
||||||
videotracks = []
|
videotracks = []
|
||||||
audiotracks = []
|
audiotracks = []
|
||||||
subtitlelanguages = []
|
subtitlelanguages = []
|
||||||
# Sometimes, aspectratio is on the "toplevel"
|
try:
|
||||||
aspectratio = self.item[0].attrib.get('aspectRatio', None)
|
# Sometimes, aspectratio is on the "toplevel"
|
||||||
|
aspectratio = self.item[0].attrib.get('aspectRatio', None)
|
||||||
|
except IndexError:
|
||||||
|
# There is no stream info at all, returning empty
|
||||||
|
return {
|
||||||
|
'video': videotracks,
|
||||||
|
'audio': audiotracks,
|
||||||
|
'subtitle': subtitlelanguages
|
||||||
|
}
|
||||||
# TODO: what if several Media tags exist?!?
|
# 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)
|
||||||
with open(path, 'wb') as f:
|
try:
|
||||||
f.write(r.content)
|
with open(path, 'wb') as f:
|
||||||
|
f.write(r.content)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
log.debug('Need to slugify the filename %s' % path)
|
||||||
|
with open(slugify(path), 'wb') as f:
|
||||||
|
f.write(r.content)
|
||||||
return path
|
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,8 +161,9 @@ 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 "
|
||||||
return
|
"exceeded duration")
|
||||||
|
return
|
||||||
|
|
||||||
complete = float(item['viewOffset']) / float(item['duration'])
|
complete = float(item['viewOffset']) / float(item['duration'])
|
||||||
log.info('Item %s stopped with completion rate %s percent.'
|
log.info('Item %s stopped with completion rate %s percent.'
|
||||||
|
@ -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:
|
else:
|
||||||
child_xml = GetPlexMetadata(child.attrib['ratingKey'])
|
item['children'] = children_xml
|
||||||
try:
|
|
||||||
child_xml[0].attrib
|
|
||||||
except (TypeError, IndexError, AttributeError):
|
|
||||||
log.error('Could not get child for Plex id %s'
|
|
||||||
% child.attrib['ratingKey'])
|
|
||||||
else:
|
|
||||||
item['children'].append(child_xml[0])
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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:
|
|
||||||
xbmc.log(self.format(record), level=xbmc.LOGNOTICE)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_log_level(cls, level):
|
|
||||||
|
|
||||||
levels = {
|
|
||||||
logging.ERROR: 0,
|
|
||||||
logging.WARNING: 0,
|
|
||||||
logging.INFO: 1,
|
|
||||||
logging.DEBUG: 2
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
log_level = int(window('plex_logLevel'))
|
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||||
except ValueError:
|
except UnicodeEncodeError:
|
||||||
log_level = 0
|
xbmc.log(tryEncode(self.format(record)),
|
||||||
|
level=LEVELS[record.levelno])
|
||||||
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:
|
||||||
# Put PMS message on queue and let libsync take care of it
|
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
||||||
self.queue.put(message)
|
# possibly causing these reprocessing messages en mass)
|
||||||
return True
|
log.debug('%s: Dropping message as PKC is currently synching'
|
||||||
|
% self.__class__.__name__)
|
||||||
|
else:
|
||||||
|
# Put PMS message on queue and let libsync take care of it
|
||||||
|
self.queue.put(message)
|
||||||
|
|
||||||
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