Merge master
This commit is contained in:
parent
c33565af4c
commit
fa6d95aa61
29 changed files with 816 additions and 652 deletions
|
@ -1,5 +1,5 @@
|
|||
[![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-1.8.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
||||
[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
||||
|
||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
|
@ -105,14 +105,11 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio
|
|||
|
||||
Solutions are unlikely due to the nature of these issues
|
||||
- A Plex Media Server "bug" leads to frequent and slow syncs, see [here for more info](https://github.com/croneter/PlexKodiConnect/issues/135)
|
||||
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issue](https://github.com/croneter/PlexKodiConnect/issues/14) for more details
|
||||
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issues](https://github.com/croneter/PlexKodiConnect/issues/14) for more details. **Workaround**: use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Set-up-Direct-Paths) instead of addon paths.
|
||||
|
||||
*Background Sync:*
|
||||
The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them only on full/delta syncs (standard settings is every 60 minutes)
|
||||
- Toggle the viewstate of an item to (un)watched outside of Kodi
|
||||
- Changing details of an item, e.g. replacing a poster
|
||||
|
||||
However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie.
|
||||
|
||||
|
||||
### Issues being worked on
|
||||
|
|
52
addon.xml
52
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.7" provider-name="croneter">
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.14" provider-name="croneter">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.3.0" />
|
||||
|
@ -59,7 +59,55 @@
|
|||
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
|
||||
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
|
||||
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
|
||||
<news>version 1.8.6:
|
||||
<news>version 1.8.14 (beta only):
|
||||
- Greatly speed up displaying context menu
|
||||
- Fix IndexError e.g. for channels if stream info missing
|
||||
- Sleep a bit before marking item as fully watched
|
||||
- Don't sleep before updating playstate to fully watched (if you watch on another Plex client)
|
||||
- Fix KeyError for TV live channels for getGeople
|
||||
|
||||
version 1.8.13 (beta only):
|
||||
- Background sync now picks up more PMS changes
|
||||
- Detect Plex item deletion more reliably
|
||||
- Fix changed Plex metadata not synced repeatedly
|
||||
- Detect (some, not all) changes to PKC settings and apply them on-the-fly
|
||||
- Fix resuming interrupted sync
|
||||
- PKC logging now uses Kodi log levels
|
||||
- Further code optimizations
|
||||
|
||||
version 1.8.12:
|
||||
- Fix library sync crashing trying to display an error
|
||||
|
||||
version 1.8.11:
|
||||
- version 1.8.10 for everybody
|
||||
|
||||
version 1.8.10 (beta only):
|
||||
- Vastly improve sync speed for music
|
||||
- Never show library sync dialog if media is playing
|
||||
- Improvements to sync dialog
|
||||
- Fix stop synching if path not found
|
||||
- Resume aborted sync on PKC settings change
|
||||
- Don't quit library sync if failed repeatedly
|
||||
- Verify path for every Plex library on install sync
|
||||
- More descriptive downloadable subtitles
|
||||
- More code fixes and optimization
|
||||
|
||||
version 1.8.9
|
||||
- Fix playback not starting in some circumstances
|
||||
- Deactivate some annoying popups on install
|
||||
|
||||
version 1.8.8
|
||||
- Fix playback not starting in some circumstances
|
||||
- Fix first artist "missing" tag (Reset your DB!)
|
||||
- Update Czech translation
|
||||
|
||||
version 1.8.7 (beta only):
|
||||
- Some fixes to playstate reporting, thanks @RickDB
|
||||
- Add Kodi info screen for episodes in context menu
|
||||
- Fix PKC asking for trailers not working
|
||||
- Fix PKC not automatically updating
|
||||
|
||||
version 1.8.6:
|
||||
- Portuguese translation, thanks @goncalo532
|
||||
- Updated other translations
|
||||
|
||||
|
|
|
@ -1,52 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
###############################################################################
|
||||
from os import path as os_path
|
||||
from sys import path as sys_path
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from xbmcaddon import Addon
|
||||
from xbmc import translatePath, sleep, log, LOGERROR
|
||||
from xbmcgui import Window
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
|
||||
###############################################################################
|
||||
|
||||
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
||||
_addon = Addon(id='plugin.video.plexkodiconnect')
|
||||
try:
|
||||
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
||||
except TypeError:
|
||||
_addon_path = _addon.getAddonInfo('path').decode()
|
||||
try:
|
||||
_base_resource = xbmc.translatePath(os.path.join(
|
||||
_base_resource = translatePath(os_path.join(
|
||||
_addon_path,
|
||||
'resources',
|
||||
'lib')).decode('utf-8')
|
||||
except TypeError:
|
||||
_base_resource = xbmc.translatePath(os.path.join(
|
||||
_base_resource = translatePath(os_path.join(
|
||||
_addon_path,
|
||||
'resources',
|
||||
'lib')).decode()
|
||||
sys.path.append(_base_resource)
|
||||
sys_path.append(_base_resource)
|
||||
|
||||
###############################################################################
|
||||
|
||||
import loghandler
|
||||
from context_entry import ContextMenu
|
||||
|
||||
###############################################################################
|
||||
|
||||
loghandler.config()
|
||||
log = logging.getLogger("PLEX.contextmenu")
|
||||
from pickler import unpickle_me, pickl_window
|
||||
|
||||
###############################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
# Start the context menu
|
||||
ContextMenu()
|
||||
except Exception as error:
|
||||
log.exception(error)
|
||||
import traceback
|
||||
log.exception("Traceback:\n%s" % traceback.format_exc())
|
||||
raise
|
||||
win = Window(10000)
|
||||
while win.getProperty('plex_command'):
|
||||
sleep(20)
|
||||
win.setProperty('plex_command', 'CONTEXT_menu')
|
||||
while not pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
result = unpickle_me()
|
||||
if result is None:
|
||||
log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR)
|
||||
|
|
29
default.py
29
default.py
|
@ -32,9 +32,9 @@ sys_path.append(_base_resource)
|
|||
###############################################################################
|
||||
|
||||
import entrypoint
|
||||
from utils import window, pickl_window, reset, passwordsXML, language as lang,\
|
||||
dialog
|
||||
from pickler import unpickle_me
|
||||
from utils import window, reset, passwordsXML, language as lang, dialog, \
|
||||
plex_command
|
||||
from pickler import unpickle_me, pickl_window
|
||||
from PKC_listitem import convert_PKC_to_listitem
|
||||
import variables as v
|
||||
|
||||
|
@ -127,28 +127,29 @@ class Main():
|
|||
log.error('Not connected to a PMS.')
|
||||
else:
|
||||
if mode == 'repair':
|
||||
window('plex_runLibScan', value='repair')
|
||||
log.info('Requesting repair lib sync')
|
||||
plex_command('RUN_LIB_SCAN', 'repair')
|
||||
elif mode == 'manualsync':
|
||||
log.info('Requesting full library scan')
|
||||
window('plex_runLibScan', value='full')
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
|
||||
elif mode == 'texturecache':
|
||||
window('plex_runLibScan', value='del_textures')
|
||||
log.info('Requesting texture caching of all textures')
|
||||
plex_command('RUN_LIB_SCAN', 'textures')
|
||||
|
||||
elif mode == 'chooseServer':
|
||||
entrypoint.chooseServer()
|
||||
|
||||
elif mode == 'refreshplaylist':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
window('plex_runLibScan', value='views')
|
||||
plex_command('RUN_LIB_SCAN', 'views')
|
||||
|
||||
elif mode == 'deviceid':
|
||||
self.deviceid()
|
||||
|
||||
elif mode == 'fanart':
|
||||
log.info('User requested fanarttv refresh')
|
||||
window('plex_runLibScan', value='fanart')
|
||||
plex_command('RUN_LIB_SCAN', 'fanart')
|
||||
|
||||
elif '/extrafanart' in argv[0]:
|
||||
plexpath = argv[2][1:]
|
||||
|
@ -165,15 +166,13 @@ class Main():
|
|||
else:
|
||||
entrypoint.doMainListing(content_type=params.get('content_type'))
|
||||
|
||||
def play(self):
|
||||
@staticmethod
|
||||
def play():
|
||||
"""
|
||||
Start up playback_starter in main Python thread
|
||||
"""
|
||||
# Put the request into the 'queue'
|
||||
while window('plex_command'):
|
||||
sleep(50)
|
||||
window('plex_command',
|
||||
value='play_%s' % argv[2])
|
||||
plex_command('PLAY', argv[2])
|
||||
# Wait for the result
|
||||
while not pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
|
@ -190,7 +189,8 @@ class Main():
|
|||
listitem = convert_PKC_to_listitem(result.listitem)
|
||||
setResolvedUrl(HANDLE, True, listitem)
|
||||
|
||||
def deviceid(self):
|
||||
@staticmethod
|
||||
def deviceid():
|
||||
deviceId_old = window('plex_client_Id')
|
||||
from clientinfo import getDeviceId
|
||||
try:
|
||||
|
@ -205,6 +205,7 @@ class Main():
|
|||
dialog('ok', lang(29999), lang(33033))
|
||||
executebuiltin('RestartApp')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log.info('%s started' % v.ADDON_ID)
|
||||
Main()
|
||||
|
|
|
@ -49,7 +49,7 @@ from xbmcvfs import exists
|
|||
import clientinfo as client
|
||||
from downloadutils import DownloadUtils
|
||||
from utils import window, settings, language as lang, tryDecode, tryEncode, \
|
||||
DateToKodi, exists_dir
|
||||
DateToKodi, exists_dir, slugify
|
||||
from PlexFunctions import PMSHttpsEnabled
|
||||
import plexdb_functions as plexdb
|
||||
import variables as v
|
||||
|
@ -1346,14 +1346,18 @@ class API():
|
|||
cast = []
|
||||
producer = []
|
||||
for child in self.item:
|
||||
if child.tag == 'Director':
|
||||
director.append(child.attrib['tag'])
|
||||
elif child.tag == 'Writer':
|
||||
writer.append(child.attrib['tag'])
|
||||
elif child.tag == 'Role':
|
||||
cast.append(child.attrib['tag'])
|
||||
elif child.tag == 'Producer':
|
||||
producer.append(child.attrib['tag'])
|
||||
try:
|
||||
if child.tag == 'Director':
|
||||
director.append(child.attrib['tag'])
|
||||
elif child.tag == 'Writer':
|
||||
writer.append(child.attrib['tag'])
|
||||
elif child.tag == 'Role':
|
||||
cast.append(child.attrib['tag'])
|
||||
elif child.tag == 'Producer':
|
||||
producer.append(child.attrib['tag'])
|
||||
except KeyError:
|
||||
log.warn('Malformed PMS answer for getPeople: %s: %s'
|
||||
% (child.tag, child.attrib))
|
||||
return {
|
||||
'Director': director,
|
||||
'Writer': writer,
|
||||
|
@ -1750,8 +1754,16 @@ class API():
|
|||
videotracks = []
|
||||
audiotracks = []
|
||||
subtitlelanguages = []
|
||||
# Sometimes, aspectratio is on the "toplevel"
|
||||
aspectratio = self.item[0].attrib.get('aspectRatio', None)
|
||||
try:
|
||||
# Sometimes, aspectratio is on the "toplevel"
|
||||
aspectratio = self.item[0].attrib.get('aspectRatio', None)
|
||||
except IndexError:
|
||||
# There is no stream info at all, returning empty
|
||||
return {
|
||||
'video': videotracks,
|
||||
'audio': audiotracks,
|
||||
'subtitle': subtitlelanguages
|
||||
}
|
||||
# TODO: what if several Media tags exist?!?
|
||||
# Loop over parts
|
||||
for child in self.item[0]:
|
||||
|
@ -2357,11 +2369,11 @@ class API():
|
|||
# ext = stream.attrib.get('format')
|
||||
if key:
|
||||
# We do know the language - temporarily download
|
||||
if stream.attrib.get('languageCode') is not None:
|
||||
if stream.attrib.get('language') is not None:
|
||||
path = self.download_external_subtitles(
|
||||
"{server}%s" % key,
|
||||
"subtitle%02d.%s.%s" % (fileindex,
|
||||
stream.attrib['languageCode'],
|
||||
stream.attrib['language'],
|
||||
stream.attrib['codec']))
|
||||
fileindex += 1
|
||||
# We don't know the language - no need to download
|
||||
|
@ -2395,9 +2407,14 @@ class API():
|
|||
log.error('Could not temporarily download subtitle %s' % url)
|
||||
return
|
||||
else:
|
||||
r.encoding = 'utf-8'
|
||||
with open(path, 'wb') as f:
|
||||
f.write(r.content)
|
||||
log.debug('Writing temp subtitle to %s' % path)
|
||||
try:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(r.content)
|
||||
except UnicodeEncodeError:
|
||||
log.debug('Need to slugify the filename %s' % path)
|
||||
with open(slugify(path), 'wb') as f:
|
||||
f.write(r.content)
|
||||
return path
|
||||
|
||||
def GetKodiPremierDate(self):
|
||||
|
@ -2575,16 +2592,16 @@ class API():
|
|||
if path is None:
|
||||
return None
|
||||
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
|
||||
if window('remapSMB') == 'true':
|
||||
path = path.replace(window('remapSMB%sOrg' % typus),
|
||||
window('remapSMB%sNew' % typus),
|
||||
if state.REMAP_PATH is True:
|
||||
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
|
||||
getattr(state, 'remapSMB%sNew' % typus),
|
||||
1)
|
||||
# There might be backslashes left over:
|
||||
path = path.replace('\\', '/')
|
||||
elif window('replaceSMB') == 'true':
|
||||
elif state.REPLACE_SMB_PATH is True:
|
||||
if path.startswith('\\\\'):
|
||||
path = 'smb:' + path.replace('\\', '/')
|
||||
if ((window('plex_pathverified') == 'true' and forceCheck is False) or
|
||||
if ((state.PATH_VERIFIED and forceCheck is False) or
|
||||
omitCheck is True):
|
||||
return path
|
||||
|
||||
|
@ -2612,12 +2629,12 @@ class API():
|
|||
if self.askToValidate(path):
|
||||
state.STOP_SYNC = True
|
||||
path = None
|
||||
window('plex_pathverified', value='true')
|
||||
state.PATH_VERIFIED = True
|
||||
else:
|
||||
path = None
|
||||
elif forceCheck is False:
|
||||
if window('plex_pathverified') != 'true':
|
||||
window('plex_pathverified', value='true')
|
||||
# Only set the flag if we were not force-checking the path
|
||||
state.PATH_VERIFIED = True
|
||||
return path
|
||||
|
||||
def askToValidate(self, url):
|
||||
|
|
|
@ -15,7 +15,7 @@ from variables import PLEX_TO_KODI_TIMEFACTOR
|
|||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
CONTAINERSIZE = int(settings('limitindex'))
|
||||
|
||||
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -36,9 +36,8 @@ def GetPlexKeyNumber(plexKey):
|
|||
|
||||
Returns ('','') if nothing is found
|
||||
"""
|
||||
regex = re.compile(r'''/(.+)/(\d+)$''')
|
||||
try:
|
||||
result = regex.findall(plexKey)[0]
|
||||
result = REGEX_PLEX_KEY.findall(plexKey)[0]
|
||||
except IndexError:
|
||||
result = ('', '')
|
||||
return result
|
||||
|
|
|
@ -126,8 +126,9 @@ def double_urldecode(text):
|
|||
return unquote(unquote(text))
|
||||
|
||||
|
||||
@thread_methods(add_stops=['STOP_SYNC'],
|
||||
add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'])
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||
'DB_SCAN',
|
||||
'STOP_SYNC'])
|
||||
class Image_Cache_Thread(Thread):
|
||||
xbmc_host = 'localhost'
|
||||
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()
|
||||
|
|
|
@ -68,13 +68,13 @@ def getDeviceId(reset=False):
|
|||
# Because Kodi appears to cache file settings!!
|
||||
if clientId != "" and reset is False:
|
||||
window('plex_client_Id', value=clientId)
|
||||
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
return clientId
|
||||
|
||||
log.warn("Generating a new deviceid.")
|
||||
log.info("Generating a new deviceid.")
|
||||
from uuid import uuid4
|
||||
clientId = str(uuid4())
|
||||
settings('plex_client_Id', value=clientId)
|
||||
window('plex_client_Id', value=clientId)
|
||||
log.warn("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
||||
return clientId
|
||||
|
|
|
@ -21,9 +21,6 @@ class Monitor_Window(Thread):
|
|||
Monitors window('plex_command') for new entries that we need to take care
|
||||
of, e.g. for new plays initiated on the Kodi side with addon paths.
|
||||
|
||||
Possible values of window('plex_command'):
|
||||
'play_....': to start playback using playback_starter
|
||||
|
||||
Adjusts state.py accordingly
|
||||
"""
|
||||
# Borg - multiple instances, shared state
|
||||
|
@ -40,9 +37,8 @@ class Monitor_Window(Thread):
|
|||
if window('plex_command'):
|
||||
value = window('plex_command')
|
||||
window('plex_command', clear=True)
|
||||
if value.startswith('play_'):
|
||||
queue.put(value)
|
||||
|
||||
if value.startswith('PLAY-'):
|
||||
queue.put(value.replace('PLAY-', ''))
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-True':
|
||||
state.SUSPEND_LIBRARY_THREAD = True
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-False':
|
||||
|
@ -64,6 +60,10 @@ class Monitor_Window(Thread):
|
|||
elif value.startswith('PLEX_USERNAME-'):
|
||||
state.PLEX_USERNAME = \
|
||||
value.replace('PLEX_USERNAME-', '') or None
|
||||
elif value.startswith('RUN_LIB_SCAN-'):
|
||||
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
||||
elif value == 'CONTEXT_menu':
|
||||
queue.put('dummy?mode=context_menu')
|
||||
else:
|
||||
raise NotImplementedError('%s not implemented' % value)
|
||||
else:
|
||||
|
|
|
@ -198,12 +198,12 @@ class DownloadUtils():
|
|||
# THE EXCEPTIONS
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Connection error
|
||||
log.debug("Server unreachable at: %s" % url)
|
||||
log.debug(e)
|
||||
log.warn("Server unreachable at: %s" % url)
|
||||
log.warn(e)
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
log.debug("Server timeout at: %s" % url)
|
||||
log.debug(e)
|
||||
log.warn("Server timeout at: %s" % url)
|
||||
log.warn(e)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
log.warn('HTTP Error at %s' % url)
|
||||
|
@ -300,21 +300,21 @@ class DownloadUtils():
|
|||
# update
|
||||
pass
|
||||
else:
|
||||
log.error("Unable to convert the response for: "
|
||||
"%s" % url)
|
||||
log.info("Received headers were: %s" % r.headers)
|
||||
log.info('Received text:')
|
||||
log.info(r.text)
|
||||
log.warn("Unable to convert the response for: "
|
||||
"%s" % url)
|
||||
log.warn("Received headers were: %s" % r.headers)
|
||||
log.warn('Received text:')
|
||||
log.warn(r.text)
|
||||
return True
|
||||
elif r.status_code == 403:
|
||||
# E.g. deleting a PMS item
|
||||
log.error('PMS sent 403: Forbidden error for url %s' % url)
|
||||
log.warn('PMS sent 403: Forbidden error for url %s' % url)
|
||||
return None
|
||||
else:
|
||||
log.error('Unknown answer from PMS %s with status code %s. '
|
||||
'Message:' % (url, r.status_code))
|
||||
log.warn('Unknown answer from PMS %s with status code %s. '
|
||||
'Message:' % (url, r.status_code))
|
||||
r.encoding = 'utf-8'
|
||||
log.info(r.text)
|
||||
log.warn(r.text)
|
||||
return True
|
||||
|
||||
# And now deal with the consequences of the exceptions
|
||||
|
|
|
@ -575,14 +575,6 @@ def getExtraFanArt(plexid, plexPath):
|
|||
xbmcplugin.endOfDirectory(HANDLE)
|
||||
|
||||
|
||||
def RunLibScan(mode):
|
||||
if window('plex_online') != "true":
|
||||
# Server is not online, do not run the sync
|
||||
dialog('ok', lang(29999), lang(39205))
|
||||
else:
|
||||
window('plex_runLibScan', value='full')
|
||||
|
||||
|
||||
def getOnDeck(viewid, mediatype, tagname, limit):
|
||||
"""
|
||||
Retrieves Plex On Deck items, currently only for TV shows
|
||||
|
@ -975,7 +967,7 @@ def __LogIn():
|
|||
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
|
||||
out!
|
||||
"""
|
||||
window('plex_runLibScan', value='full')
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
# Restart user client
|
||||
plex_command('SUSPEND_USER_CLIENT', 'False')
|
||||
|
||||
|
|
|
@ -496,10 +496,10 @@ class InitialSetup():
|
|||
|
||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
|
||||
dialog.ok(heading=lang(29999), line1=lang(39076))
|
||||
# dialog.ok(heading=lang(29999), line1=lang(39076))
|
||||
|
||||
# Need to tell about our image source for collections: themoviedb.org
|
||||
dialog.ok(heading=lang(29999), line1=lang(39717))
|
||||
# dialog.ok(heading=lang(29999), line1=lang(39717))
|
||||
# Make sure that we only ask these questions upon first installation
|
||||
settings('InstallQuestionsAnswered', value='true')
|
||||
|
||||
|
|
|
@ -161,8 +161,9 @@ class Items(object):
|
|||
|
||||
# If offset exceeds duration skip update
|
||||
if item['viewOffset'] > item['duration']:
|
||||
log.error("Error while updating play state, viewOffset exceeded duration")
|
||||
return
|
||||
log.error("Error while updating play state, viewOffset "
|
||||
"exceeded duration")
|
||||
return
|
||||
|
||||
complete = float(item['viewOffset']) / float(item['duration'])
|
||||
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)
|
||||
if complete >= MARK_PLAYED_AT:
|
||||
log.info('Marking as completely watched in Kodi')
|
||||
sleep(500)
|
||||
try:
|
||||
item['viewCount'] += 1
|
||||
except TypeError:
|
||||
|
@ -1729,7 +1729,7 @@ class Music(Items):
|
|||
if album is None or album == 401:
|
||||
log.error('Could not download album, abort')
|
||||
return
|
||||
self.add_updateAlbum(album[0])
|
||||
self.add_updateAlbum(album[0], children=[item])
|
||||
plex_dbalbum = plex_db.getItem_byId(plex_albumId)
|
||||
try:
|
||||
albumid = plex_dbalbum[0]
|
||||
|
|
|
@ -1280,7 +1280,14 @@ class Kodidb_Functions():
|
|||
try:
|
||||
artistid = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
self.cursor.execute("select coalesce(max(idArtist),0) from artist")
|
||||
# Krypton has a dummy first entry idArtist: 1 strArtist:
|
||||
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
||||
if v.KODIVERSION >= 17:
|
||||
self.cursor.execute(
|
||||
"select coalesce(max(idArtist),1) from artist")
|
||||
else:
|
||||
self.cursor.execute(
|
||||
"select coalesce(max(idArtist),0) from artist")
|
||||
artistid = self.cursor.fetchone()[0] + 1
|
||||
query = (
|
||||
'''
|
||||
|
|
|
@ -2,24 +2,48 @@
|
|||
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
from logging import getLogger
|
||||
from json import loads
|
||||
|
||||
from xbmc import Monitor, Player, sleep
|
||||
|
||||
import downloadutils
|
||||
from downloadutils import DownloadUtils
|
||||
import plexdb_functions as plexdb
|
||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
|
||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
|
||||
plex_command
|
||||
from PlexFunctions import scrobble
|
||||
from kodidb_functions import get_kodiid_from_filename
|
||||
from PlexAPI import API
|
||||
from variables import REMAP_TYPE_FROM_PLEXTYPE
|
||||
import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
log = getLogger("PLEX."+__name__)
|
||||
|
||||
# settings: window-variable
|
||||
WINDOW_SETTINGS = {
|
||||
'enableContext': 'plex_context',
|
||||
'plex_restricteduser': 'plex_restricteduser',
|
||||
'force_transcode_pix': 'plex_force_transcode_pix',
|
||||
'fetch_pms_item_number': 'fetch_pms_item_number'
|
||||
}
|
||||
|
||||
# settings: state-variable (state.py)
|
||||
# Need to use getattr and setattr!
|
||||
STATE_SETTINGS = {
|
||||
'dbSyncIndicator': 'SYNC_DIALOG',
|
||||
'remapSMB': 'REMAP_PATH',
|
||||
'remapSMBmovieOrg': 'remapSMBmovieOrg',
|
||||
'remapSMBmovieNew': 'remapSMBmovieNew',
|
||||
'remapSMBtvOrg': 'remapSMBtvOrg',
|
||||
'remapSMBtvNew': 'remapSMBtvNew',
|
||||
'remapSMBmusicOrg': 'remapSMBmusicOrg',
|
||||
'remapSMBmusicNew': 'remapSMBmusicNew',
|
||||
'remapSMBphotoOrg': 'remapSMBphotoOrg',
|
||||
'remapSMBphotoNew': 'remapSMBphotoNew',
|
||||
'enableMusic': 'ENABLE_MUSIC',
|
||||
'enableBackgroundSync': 'BACKGROUND_SYNC'
|
||||
}
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -27,7 +51,7 @@ class KodiMonitor(Monitor):
|
|||
|
||||
def __init__(self, callback):
|
||||
self.mgr = callback
|
||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||
self.doUtils = DownloadUtils().downloadUrl
|
||||
self.xbmcplayer = Player()
|
||||
self.playqueue = self.mgr.playqueue
|
||||
Monitor.__init__(self)
|
||||
|
@ -47,31 +71,42 @@ class KodiMonitor(Monitor):
|
|||
"""
|
||||
Monitor the PKC settings for changes made by the user
|
||||
"""
|
||||
# settings: window-variable
|
||||
items = {
|
||||
'logLevel': 'plex_logLevel',
|
||||
'enableContext': 'plex_context',
|
||||
'plex_restricteduser': 'plex_restricteduser',
|
||||
'dbSyncIndicator': 'dbSyncIndicator',
|
||||
'remapSMB': 'remapSMB',
|
||||
'replaceSMB': 'replaceSMB',
|
||||
'force_transcode_pix': 'plex_force_transcode_pix',
|
||||
'fetch_pms_item_number': 'fetch_pms_item_number'
|
||||
}
|
||||
# Path replacement
|
||||
for typus in REMAP_TYPE_FROM_PLEXTYPE.values():
|
||||
for arg in ('Org', 'New'):
|
||||
key = 'remapSMB%s%s' % (typus, arg)
|
||||
items[key] = key
|
||||
log.debug('PKC settings change detected')
|
||||
changed = False
|
||||
# Reset the window variables from the settings variables
|
||||
for settings_value, window_value in items.iteritems():
|
||||
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
|
||||
if window(window_value) != settings(settings_value):
|
||||
log.debug('PKC settings changed: %s is now %s'
|
||||
changed = True
|
||||
log.debug('PKC window settings changed: %s is now %s'
|
||||
% (settings_value, settings(settings_value)))
|
||||
window(window_value, value=settings(settings_value))
|
||||
if settings_value == 'fetch_pms_item_number':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
window('plex_runLibScan', value="views")
|
||||
plex_command('RUN_LIB_SCAN', 'views')
|
||||
# Reset the state variables in state.py
|
||||
for settings_value, state_name in STATE_SETTINGS.iteritems():
|
||||
new = settings(settings_value)
|
||||
if new == 'true':
|
||||
new = True
|
||||
elif new == 'false':
|
||||
new = False
|
||||
if getattr(state, state_name) != new:
|
||||
changed = True
|
||||
log.debug('PKC state settings %s changed from %s to %s'
|
||||
% (settings_value, getattr(state, state_name), new))
|
||||
setattr(state, state_name, new)
|
||||
# Special cases, overwrite all internal settings
|
||||
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60
|
||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||
settings('backgroundsync_saftyMargin'))
|
||||
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
||||
# Never set through the user
|
||||
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
||||
if changed is True:
|
||||
# Assume that the user changed the settings so that we can now find
|
||||
# the path to all media files
|
||||
state.STOP_SYNC = False
|
||||
state.PATH_VERIFIED = False
|
||||
|
||||
@CatchExceptions(warnuser=False)
|
||||
def onNotification(self, sender, method, data):
|
||||
|
@ -137,7 +172,7 @@ class KodiMonitor(Monitor):
|
|||
elif method == "GUI.OnScreensaverDeactivated":
|
||||
if settings('dbSyncScreensaver') == "true":
|
||||
sleep(5000)
|
||||
window('plex_runLibScan', value="full")
|
||||
plex_command('RUN_LIB_SCAN', 'full')
|
||||
|
||||
elif method == "System.OnQuit":
|
||||
log.info('Kodi OnQuit detected - shutting down')
|
||||
|
|
|
@ -17,8 +17,9 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'],
|
||||
add_stops=['STOP_SYNC'])
|
||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||
'DB_SCAN',
|
||||
'STOP_SYNC'])
|
||||
class Process_Fanart_Thread(Thread):
|
||||
"""
|
||||
Threaded download of additional fanart in the background
|
||||
|
|
|
@ -16,7 +16,7 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Get_Metadata(Thread):
|
||||
"""
|
||||
Threaded download of Plex XML metadata for a certain library item.
|
||||
|
@ -115,17 +115,9 @@ class Threaded_Get_Metadata(Thread):
|
|||
except (TypeError, IndexError, AttributeError):
|
||||
log.error('Could not get children for Plex id %s'
|
||||
% item['itemId'])
|
||||
else:
|
||||
item['children'] = []
|
||||
for child in children_xml:
|
||||
child_xml = GetPlexMetadata(child.attrib['ratingKey'])
|
||||
try:
|
||||
child_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
log.error('Could not get child for Plex id %s'
|
||||
% child.attrib['ratingKey'])
|
||||
else:
|
||||
item['children'].append(child_xml[0])
|
||||
else:
|
||||
item['children'] = children_xml
|
||||
|
||||
# place item into out queue
|
||||
out_queue.put(item)
|
||||
|
|
|
@ -15,7 +15,7 @@ log = getLogger("PLEX."+__name__)
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Process_Metadata(Thread):
|
||||
"""
|
||||
Not yet implemented for more than 1 thread - if ever. Only to be called by
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
from logging import getLogger
|
||||
from threading import Thread, Lock
|
||||
|
||||
from xbmc import sleep
|
||||
from xbmc import sleep, Player
|
||||
from xbmcgui import DialogProgressBG
|
||||
|
||||
from utils import thread_methods, language as lang
|
||||
|
||||
|
@ -18,18 +19,17 @@ LOCK = Lock()
|
|||
###############################################################################
|
||||
|
||||
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
|
||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC'])
|
||||
class Threaded_Show_Sync_Info(Thread):
|
||||
"""
|
||||
Threaded class to show the Kodi statusbar of the metadata download.
|
||||
|
||||
Input:
|
||||
dialog xbmcgui.DialogProgressBG() object to show progress
|
||||
total: Total number of items to get
|
||||
item_type:
|
||||
"""
|
||||
def __init__(self, dialog, total, item_type):
|
||||
def __init__(self, total, item_type):
|
||||
self.total = total
|
||||
self.dialog = dialog
|
||||
self.item_type = item_type
|
||||
Thread.__init__(self)
|
||||
|
||||
|
@ -51,14 +51,15 @@ class Threaded_Show_Sync_Info(Thread):
|
|||
log.debug('Show sync info thread started')
|
||||
# cache local variables because it's faster
|
||||
total = self.total
|
||||
dialog = self.dialog
|
||||
dialog = DialogProgressBG('dialoglogProgressBG')
|
||||
thread_stopped = self.thread_stopped
|
||||
dialog.create("%s %s: %s %s"
|
||||
% (lang(39714), self.item_type, str(total), lang(39715)))
|
||||
player = Player()
|
||||
|
||||
total = 2 * total
|
||||
totalProgress = 0
|
||||
while thread_stopped() is False:
|
||||
while thread_stopped() is False and not player.isPlaying():
|
||||
with LOCK:
|
||||
get_progress = GET_METADATA_COUNT
|
||||
process_progress = PROCESS_METADATA_COUNT
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,74 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
###############################################################################
|
||||
import logging
|
||||
import xbmc
|
||||
###############################################################################
|
||||
LEVELS = {
|
||||
logging.ERROR: xbmc.LOGERROR,
|
||||
logging.WARNING: xbmc.LOGWARNING,
|
||||
logging.INFO: xbmc.LOGNOTICE,
|
||||
logging.DEBUG: xbmc.LOGDEBUG
|
||||
}
|
||||
###############################################################################
|
||||
|
||||
from utils import window, tryEncode
|
||||
|
||||
##################################################################################################
|
||||
def tryEncode(uniString, encoding='utf-8'):
|
||||
"""
|
||||
Will try to encode uniString (in unicode) to encoding. This possibly
|
||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||
string.encode()
|
||||
"""
|
||||
if isinstance(uniString, str):
|
||||
# already encoded
|
||||
return uniString
|
||||
try:
|
||||
uniString = uniString.encode(encoding, "ignore")
|
||||
except TypeError:
|
||||
uniString = uniString.encode()
|
||||
return uniString
|
||||
|
||||
|
||||
def config():
|
||||
|
||||
logger = logging.getLogger('PLEX')
|
||||
logger.addHandler(LogHandler())
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class LogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
self.setFormatter(logging.Formatter(fmt="%(name)s: %(message)s"))
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if self._get_log_level(record.levelno):
|
||||
try:
|
||||
xbmc.log(self.format(record), level=xbmc.LOGNOTICE)
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE)
|
||||
|
||||
@classmethod
|
||||
def _get_log_level(cls, level):
|
||||
|
||||
levels = {
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
}
|
||||
try:
|
||||
log_level = int(window('plex_logLevel'))
|
||||
except ValueError:
|
||||
log_level = 0
|
||||
|
||||
return log_level >= levels[level]
|
||||
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, fmt="%(name)s -> %(message)s"):
|
||||
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
|
||||
# Save the original format configured by the user
|
||||
# when the logger formatter was instantiated
|
||||
format_orig = self._fmt
|
||||
|
||||
# Replace the original format with one customized by logging level
|
||||
if record.levelno in (logging.DEBUG, logging.ERROR):
|
||||
self._fmt = '%(name)s -> %(levelname)s: %(message)s'
|
||||
|
||||
# Call the original formatter class to do the grunt work
|
||||
result = logging.Formatter.format(self, record)
|
||||
|
||||
# Restore the original format configured by the user
|
||||
self._fmt = format_orig
|
||||
|
||||
return result
|
||||
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(tryEncode(self.format(record)),
|
||||
level=LEVELS[record.levelno])
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
import logging
|
||||
import cPickle as Pickle
|
||||
from cPickle import dumps, loads
|
||||
|
||||
from utils import pickl_window
|
||||
from xbmcgui import Window
|
||||
from xbmc import log, LOGDEBUG
|
||||
###############################################################################
|
||||
WINDOW = Window(10000)
|
||||
PREFIX = 'PLEX.%s: ' % __name__
|
||||
###############################################################################
|
||||
log = logging.getLogger("PLEX."+__name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
def pickl_window(property, value=None, clear=False):
|
||||
"""
|
||||
Get or set window property - thread safe! For use with Pickle
|
||||
Property and value must be string
|
||||
"""
|
||||
if clear:
|
||||
WINDOW.clearProperty(property)
|
||||
elif value is not None:
|
||||
WINDOW.setProperty(property, value)
|
||||
else:
|
||||
return WINDOW.getProperty(property)
|
||||
|
||||
|
||||
def pickle_me(obj, window_var='plex_result'):
|
||||
|
@ -19,9 +32,9 @@ def pickle_me(obj, window_var='plex_result'):
|
|||
obj can be pretty much any Python object. However, classes and
|
||||
functions won't work. See the Pickle documentation
|
||||
"""
|
||||
log.debug('Start pickling: %s' % obj)
|
||||
pickl_window(window_var, value=Pickle.dumps(obj))
|
||||
log.debug('Successfully pickled')
|
||||
log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
|
||||
pickl_window(window_var, value=dumps(obj))
|
||||
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
|
||||
|
||||
|
||||
def unpickle_me(window_var='plex_result'):
|
||||
|
@ -31,9 +44,9 @@ def unpickle_me(window_var='plex_result'):
|
|||
"""
|
||||
result = pickl_window(window_var)
|
||||
pickl_window(window_var, clear=True)
|
||||
log.debug('Start unpickling')
|
||||
obj = Pickle.loads(result)
|
||||
log.debug('Successfully unpickled: %s' % obj)
|
||||
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
|
||||
obj = loads(result)
|
||||
log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
|
||||
return obj
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import variables as v
|
|||
from downloadutils import DownloadUtils
|
||||
from PKC_listitem import convert_PKC_to_listitem
|
||||
import plexdb_functions as plexdb
|
||||
from context_entry import ContextMenu
|
||||
import state
|
||||
|
||||
###############################################################################
|
||||
|
@ -142,6 +143,9 @@ class Playback_Starter(Thread):
|
|||
params.get('view_offset'),
|
||||
directplay=True if params.get('play_directly') else False,
|
||||
node=False if params.get('node') == 'false' else True)
|
||||
elif mode == 'context_menu':
|
||||
ContextMenu()
|
||||
result = Playback_Successful()
|
||||
except:
|
||||
log.error('Error encountered for mode %s, params %s'
|
||||
% (mode, params))
|
||||
|
|
|
@ -5,10 +5,8 @@ import logging
|
|||
import json
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from utils import window, settings, language as lang, DateToKodi, \
|
||||
getUnixTimestamp, tryDecode, tryEncode
|
||||
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode
|
||||
import downloadutils
|
||||
import plexdb_functions as plexdb
|
||||
import kodidb_functions as kodidb
|
||||
|
@ -354,6 +352,8 @@ class Player(xbmc.Player):
|
|||
log.info("Percent complete: %s Mark played at: %s"
|
||||
% (percentComplete, markPlayed))
|
||||
if percentComplete >= markPlayed:
|
||||
# Kodi seems to sometimes overwrite our playstate, so wait
|
||||
xbmc.sleep(500)
|
||||
# Tell Kodi that we've finished watching (Plex knows)
|
||||
if (data['fileid'] is not None and
|
||||
data['itemType'] in (v.KODI_TYPE_MOVIE,
|
||||
|
|
|
@ -10,6 +10,8 @@ STOP_PKC = False
|
|||
SUSPEND_LIBRARY_THREAD = False
|
||||
# Set if user decided to cancel sync
|
||||
STOP_SYNC = False
|
||||
# Could we access the paths?
|
||||
PATH_VERIFIED = False
|
||||
# Set if a Plex-Kodi DB sync is being done - along with
|
||||
# window('plex_dbScan') set to 'true'
|
||||
DB_SCAN = False
|
||||
|
@ -24,6 +26,42 @@ RESTRICTED_USER = False
|
|||
DIRECT_PATHS = False
|
||||
# Shall we replace custom user ratings with the number of versions available?
|
||||
INDICATE_MEDIA_VERSIONS = False
|
||||
# Do we need to run a special library scan?
|
||||
RUN_LIB_SCAN = None
|
||||
|
||||
# Stemming from the PKC settings.xml
|
||||
# Shall we show Kodi dialogs when synching?
|
||||
SYNC_DIALOG = True
|
||||
# Have we already checked the Kodi DB on consistency?
|
||||
KODI_DB_CHECKED = False
|
||||
# Is synching of Plex music enabled?
|
||||
ENABLE_MUSIC = True
|
||||
# How often shall we sync?
|
||||
FULL_SYNC_INTERVALL = 0
|
||||
# Background Sync enabled at all?
|
||||
BACKGROUND_SYNC = True
|
||||
# How long shall we wait with synching a new item to make sure Plex got all
|
||||
# metadata?
|
||||
BACKGROUNDSYNC_SAFTYMARGIN = 0
|
||||
# How many threads to download Plex metadata on sync?
|
||||
SYNC_THREAD_NUMBER = 0
|
||||
# What's the time offset between the PMS and Kodi?
|
||||
KODI_PLEX_TIME_OFFSET = 0.0
|
||||
|
||||
# Path remapping mechanism (e.g. smb paths)
|
||||
# Do we replace \\myserver\path to smb://myserver/path?
|
||||
REPLACE_SMB_PATH = False
|
||||
# Do we generally remap?
|
||||
REMAP_PATH = False
|
||||
# Mappings for REMAP_PATH:
|
||||
remapSMBmovieOrg = None
|
||||
remapSMBmovieNew = None
|
||||
remapSMBtvOrg = None
|
||||
remapSMBtvNew = None
|
||||
remapSMBmusicOrg = None
|
||||
remapSMBmusicNew = None
|
||||
remapSMBphotoOrg = None
|
||||
remapSMBphotoNew = None
|
||||
|
||||
# Along with window('plex_authenticated')
|
||||
AUTHENTICATED = False
|
||||
|
|
|
@ -59,24 +59,6 @@ def window(property, value=None, clear=False, windowid=10000):
|
|||
return tryDecode(win.getProperty(property))
|
||||
|
||||
|
||||
def pickl_window(property, value=None, clear=False, windowid=10000):
|
||||
"""
|
||||
Get or set window property - thread safe! For use with Pickle
|
||||
Property and value must be string
|
||||
"""
|
||||
if windowid != 10000:
|
||||
win = xbmcgui.Window(windowid)
|
||||
else:
|
||||
win = WINDOW
|
||||
|
||||
if clear:
|
||||
win.clearProperty(property)
|
||||
elif value is not None:
|
||||
win.setProperty(property, value)
|
||||
else:
|
||||
return win.getProperty(property)
|
||||
|
||||
|
||||
def plex_command(key, value):
|
||||
"""
|
||||
Used to funnel states between different Python instances. NOT really thread
|
||||
|
@ -86,7 +68,7 @@ def plex_command(key, value):
|
|||
value: either 'True' or 'False'
|
||||
"""
|
||||
while window('plex_command'):
|
||||
xbmc.sleep(5)
|
||||
xbmc.sleep(20)
|
||||
window('plex_command', value='%s-%s' % (key, value))
|
||||
|
||||
|
||||
|
@ -140,6 +122,15 @@ def dialog(typus, *args, **kwargs):
|
|||
Displays xbmcgui Dialog. Pass a string as typus:
|
||||
'yesno', 'ok', 'notification', 'input', 'select', 'numeric'
|
||||
|
||||
kwargs:
|
||||
heading='{plex}' title bar (here PlexKodiConnect)
|
||||
message=lang(30128), Actual dialog content. Don't use with OK
|
||||
line1=str(), For 'OK' and 'yesno' dialogs use line1...line3!
|
||||
time=5000,
|
||||
sound=True,
|
||||
nolabel=str(), For 'yesno' dialogs
|
||||
yeslabel=str(), For 'yesno' dialogs
|
||||
|
||||
Icons:
|
||||
icon='{plex}' Display Plex standard icon
|
||||
icon='{info}' xbmcgui.NOTIFICATION_INFO
|
||||
|
@ -221,6 +212,16 @@ def tryDecode(string, encoding='utf-8'):
|
|||
return string
|
||||
|
||||
|
||||
def slugify(text):
|
||||
"""
|
||||
Normalizes text (in unicode or string) to e.g. enable safe filenames.
|
||||
Returns unicode
|
||||
"""
|
||||
if not isinstance(text, unicode):
|
||||
text = unicode(text)
|
||||
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
|
||||
|
||||
|
||||
def escape_html(string):
|
||||
"""
|
||||
Escapes the following:
|
||||
|
@ -248,7 +249,7 @@ def DateToKodi(stamp):
|
|||
None if an error was encountered
|
||||
"""
|
||||
try:
|
||||
stamp = float(stamp) + float(window('kodiplextimeoffset'))
|
||||
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
|
||||
date_time = localtime(stamp)
|
||||
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
|
||||
except:
|
||||
|
|
|
@ -84,7 +84,8 @@ class WebSocket(Thread):
|
|||
# No worries if read timed out
|
||||
pass
|
||||
except websocket.WebSocketConnectionClosedException:
|
||||
log.info("Connection closed, (re)connecting")
|
||||
log.info("%s: connection closed, (re)connecting"
|
||||
% self.__class__.__name__)
|
||||
uri, sslopt = self.getUri()
|
||||
try:
|
||||
# Low timeout - let's us shut this thread down!
|
||||
|
@ -95,7 +96,7 @@ class WebSocket(Thread):
|
|||
enable_multithread=True)
|
||||
except IOError:
|
||||
# Server is probably offline
|
||||
log.info("Error connecting")
|
||||
log.info("%s: Error connecting" % self.__class__.__name__)
|
||||
self.ws = None
|
||||
counter += 1
|
||||
if counter > 3:
|
||||
|
@ -103,33 +104,41 @@ class WebSocket(Thread):
|
|||
self.IOError_response()
|
||||
sleep(1000)
|
||||
except websocket.WebSocketTimeoutException:
|
||||
log.info("timeout while connecting, trying again")
|
||||
log.info("%s: Timeout while connecting, trying again"
|
||||
% self.__class__.__name__)
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
except websocket.WebSocketException as e:
|
||||
log.info('WebSocketException: %s' % e)
|
||||
log.info('%s: WebSocketException: %s'
|
||||
% (self.__class__.__name__, e))
|
||||
if 'Handshake Status 401' in e.args:
|
||||
handshake_counter += 1
|
||||
if handshake_counter >= 5:
|
||||
log.info('Error in handshake detected. Stopping '
|
||||
'%s now' % self.__class__.__name__)
|
||||
log.info('%s: Error in handshake detected. '
|
||||
'Stopping now'
|
||||
% self.__class__.__name__)
|
||||
break
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
except Exception as e:
|
||||
log.error("Unknown exception encountered in connecting: %s"
|
||||
% e)
|
||||
log.error('%s: Unknown exception encountered when '
|
||||
'connecting: %s' % (self.__class__.__name__, e))
|
||||
import traceback
|
||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||
log.error("%s: Traceback:\n%s"
|
||||
% (self.__class__.__name__,
|
||||
traceback.format_exc()))
|
||||
self.ws = None
|
||||
sleep(1000)
|
||||
else:
|
||||
counter = 0
|
||||
handshake_counter = 0
|
||||
except Exception as e:
|
||||
log.error("Unknown exception encountered: %s" % e)
|
||||
log.error("%s: Unknown exception encountered: %s"
|
||||
% (self.__class__.__name__, e))
|
||||
import traceback
|
||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||
log.error("%s: Traceback:\n%s"
|
||||
% (self.__class__.__name__,
|
||||
traceback.format_exc()))
|
||||
try:
|
||||
self.ws.shutdown()
|
||||
except:
|
||||
|
@ -171,37 +180,46 @@ class PMS_Websocket(WebSocket):
|
|||
sslopt = {}
|
||||
if settings('sslverify') == "false":
|
||||
sslopt["cert_reqs"] = CERT_NONE
|
||||
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
||||
log.debug("%s: Uri: %s, sslopt: %s"
|
||||
% (self.__class__.__name__, uri, sslopt))
|
||||
return uri, sslopt
|
||||
|
||||
def process(self, opcode, message):
|
||||
if opcode not in self.opcode_data:
|
||||
return False
|
||||
return
|
||||
|
||||
try:
|
||||
message = loads(message)
|
||||
except Exception as ex:
|
||||
log.error('Error decoding message from websocket: %s' % ex)
|
||||
except ValueError:
|
||||
log.error('%s: Error decoding message from websocket'
|
||||
% self.__class__.__name__)
|
||||
log.error(message)
|
||||
return False
|
||||
return
|
||||
try:
|
||||
message = message['NotificationContainer']
|
||||
except KeyError:
|
||||
log.error('Could not parse PMS message: %s' % message)
|
||||
return False
|
||||
log.error('%s: Could not parse PMS message: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
return
|
||||
# Triage
|
||||
typus = message.get('type')
|
||||
if typus is None:
|
||||
log.error('No message type, dropping message: %s' % message)
|
||||
return False
|
||||
log.debug('Received message from PMS server: %s' % message)
|
||||
log.error('%s: No message type, dropping message: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
return
|
||||
log.debug('%s: Received message from PMS server: %s'
|
||||
% (self.__class__.__name__, message))
|
||||
# Drop everything we're not interested in
|
||||
if typus not in ('playing', 'timeline'):
|
||||
return True
|
||||
|
||||
# Put PMS message on queue and let libsync take care of it
|
||||
self.queue.put(message)
|
||||
return True
|
||||
if typus not in ('playing', 'timeline', 'activity'):
|
||||
return
|
||||
elif typus == 'activity' and state.DB_SCAN is True:
|
||||
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
||||
# possibly causing these reprocessing messages en mass)
|
||||
log.debug('%s: Dropping message as PKC is currently synching'
|
||||
% self.__class__.__name__)
|
||||
else:
|
||||
# Put PMS message on queue and let libsync take care of it
|
||||
self.queue.put(message)
|
||||
|
||||
def IOError_response(self):
|
||||
log.warn("Repeatedly could not connect to PMS, "
|
||||
|
@ -224,32 +242,36 @@ class Alexa_Websocket(WebSocket):
|
|||
% (state.PLEX_USER_ID,
|
||||
self.plex_client_Id, state.PLEX_TOKEN))
|
||||
sslopt = {}
|
||||
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
||||
log.debug("%s: Uri: %s, sslopt: %s"
|
||||
% (self.__class__.__name__, uri, sslopt))
|
||||
return uri, sslopt
|
||||
|
||||
def process(self, opcode, message):
|
||||
if opcode not in self.opcode_data:
|
||||
return False
|
||||
log.debug('Received the following message from Alexa:')
|
||||
log.debug(message)
|
||||
return
|
||||
log.debug('%s: Received the following message from Alexa:'
|
||||
% self.__class__.__name__)
|
||||
log.debug('%s: %s' % (self.__class__.__name__, message))
|
||||
try:
|
||||
message = etree.fromstring(message)
|
||||
except Exception as ex:
|
||||
log.error('Error decoding message from Alexa: %s' % ex)
|
||||
return False
|
||||
log.error('%s: Error decoding message from Alexa: %s'
|
||||
% (self.__class__.__name__, ex))
|
||||
return
|
||||
try:
|
||||
if message.attrib['command'] == 'processRemoteControlCommand':
|
||||
message = message[0]
|
||||
else:
|
||||
log.error('Unknown Alexa message received')
|
||||
return False
|
||||
log.error('%s: Unknown Alexa message received'
|
||||
% self.__class__.__name__)
|
||||
return
|
||||
except:
|
||||
log.error('Could not parse Alexa message')
|
||||
return False
|
||||
log.error('%s: Could not parse Alexa message'
|
||||
% self.__class__.__name__)
|
||||
return
|
||||
process_command(message.attrib['path'][1:],
|
||||
message.attrib,
|
||||
queue=self.mgr.plexCompanion.queue)
|
||||
return True
|
||||
|
||||
def IOError_response(self):
|
||||
pass
|
||||
|
|
|
@ -146,7 +146,6 @@
|
|||
</category>
|
||||
|
||||
<category label="30022"><!-- Advanced -->
|
||||
<setting id="logLevel" type="enum" label="30004" values="Disabled|Info|Debug" default="1" />
|
||||
<setting id="startupDelay" type="number" label="30529" default="0" option="int" />
|
||||
<setting label="39018" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=repair)" option="close" /> <!-- Repair local database (force update all content) -->
|
||||
<setting label="30535" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=deviceid)" /><!-- Reset device id uuid -->
|
||||
|
|
41
service.py
41
service.py
|
@ -30,8 +30,7 @@ sys_path.append(_base_resource)
|
|||
|
||||
###############################################################################
|
||||
|
||||
from utils import settings, window, language as lang, dialog, tryEncode, \
|
||||
tryDecode
|
||||
from utils import settings, window, language as lang, dialog, tryDecode
|
||||
from userclient import UserClient
|
||||
import initialsetup
|
||||
from kodimonitor import KodiMonitor
|
||||
|
@ -82,10 +81,8 @@ class Service():
|
|||
|
||||
def __init__(self):
|
||||
|
||||
logLevel = self.getLogLevel()
|
||||
self.monitor = Monitor()
|
||||
|
||||
window('plex_logLevel', value=str(logLevel))
|
||||
window('plex_kodiProfile',
|
||||
value=tryDecode(translatePath("special://profile")))
|
||||
window('plex_context',
|
||||
|
@ -94,27 +91,26 @@ class Service():
|
|||
value=settings('fetch_pms_item_number'))
|
||||
|
||||
# Initial logging
|
||||
log.warn("======== START %s ========" % v.ADDON_NAME)
|
||||
log.warn("Platform: %s" % v.PLATFORM)
|
||||
log.warn("KODI Version: %s" % v.KODILONGVERSION)
|
||||
log.warn("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
|
||||
log.warn("Using plugin paths: %s"
|
||||
log.info("======== START %s ========" % v.ADDON_NAME)
|
||||
log.info("Platform: %s" % v.PLATFORM)
|
||||
log.info("KODI Version: %s" % v.KODILONGVERSION)
|
||||
log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
|
||||
log.info("Using plugin paths: %s"
|
||||
% (settings('useDirectPaths') != "true"))
|
||||
log.warn("Number of sync threads: %s"
|
||||
log.info("Number of sync threads: %s"
|
||||
% settings('syncThreadNumber'))
|
||||
log.warn("Log Level: %s" % logLevel)
|
||||
log.warn("Full sys.argv received: %s" % argv)
|
||||
log.info("Full sys.argv received: %s" % argv)
|
||||
|
||||
# Reset window props for profile switch
|
||||
properties = [
|
||||
"plex_online", "plex_serverStatus", "plex_onWake",
|
||||
"plex_dbCheck", "plex_kodiScan",
|
||||
"plex_kodiScan",
|
||||
"plex_shouldStop", "plex_dbScan",
|
||||
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
|
||||
"plex_runLibScan", "pms_token", "plex_token",
|
||||
"pms_token", "plex_token",
|
||||
"pms_server", "plex_machineIdentifier", "plex_servername",
|
||||
"plex_authenticated", "PlexUserImage", "useDirectPaths",
|
||||
"kodiplextimeoffset", "countError", "countUnauthorized",
|
||||
"countError", "countUnauthorized",
|
||||
"plex_restricteduser", "plex_allows_mediaDeletion",
|
||||
"plex_command", "plex_result", "plex_force_transcode_pix"
|
||||
]
|
||||
|
@ -127,13 +123,6 @@ class Service():
|
|||
# Set the minimum database version
|
||||
window('plex_minDBVersion', value="1.5.10")
|
||||
|
||||
def getLogLevel(self):
|
||||
try:
|
||||
logLevel = int(settings('logLevel'))
|
||||
except ValueError:
|
||||
logLevel = 0
|
||||
return logLevel
|
||||
|
||||
def __stop_PKC(self):
|
||||
"""
|
||||
Kodi's abortRequested is really unreliable :-(
|
||||
|
@ -173,7 +162,7 @@ class Service():
|
|||
|
||||
if window('plex_kodiProfile') != kodiProfile:
|
||||
# Profile change happened, terminate this thread and others
|
||||
log.warn("Kodi profile was: %s and changed to: %s. "
|
||||
log.info("Kodi profile was: %s and changed to: %s. "
|
||||
"Terminating old PlexKodiConnect thread."
|
||||
% (kodiProfile,
|
||||
window('plex_kodiProfile')))
|
||||
|
@ -332,7 +321,7 @@ class Service():
|
|||
except:
|
||||
pass
|
||||
window('plex_service_started', clear=True)
|
||||
log.warn("======== STOP %s ========" % v.ADDON_NAME)
|
||||
log.info("======== STOP %s ========" % v.ADDON_NAME)
|
||||
|
||||
|
||||
# Safety net - Kody starts PKC twice upon first installation!
|
||||
|
@ -345,11 +334,11 @@ else:
|
|||
# Delay option
|
||||
delay = int(settings('startupDelay'))
|
||||
|
||||
log.warn("Delaying Plex startup by: %s sec..." % delay)
|
||||
log.info("Delaying Plex startup by: %s sec..." % delay)
|
||||
if exit:
|
||||
log.error('PKC service.py already started - exiting this instance')
|
||||
elif delay and Monitor().waitForAbort(delay):
|
||||
# Start the service
|
||||
log.warn("Abort requested while waiting. PKC not started.")
|
||||
log.info("Abort requested while waiting. PKC not started.")
|
||||
else:
|
||||
Service().ServiceEntryPoint()
|
||||
|
|
Loading…
Reference in a new issue