Merge branch 'develop' into translations

This commit is contained in:
tomkat83 2017-05-22 21:42:28 +02:00
commit 9fdab58cdb
36 changed files with 674 additions and 1093 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-1.7.7-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.7.7-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.7.17-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.7.21-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)
@ -63,6 +63,7 @@ PKC synchronizes your media from your Plex server to the native Kodi database. H
+ Danish, thanks @FIGHT + Danish, thanks @FIGHT
+ Italian, thanks @nikkux, @chicco83 + Italian, thanks @nikkux, @chicco83
+ Dutch, thanks @mvanbaak + Dutch, thanks @mvanbaak
+ French, thanks @lflforce, @ahivert, @Nox71, @CotzaDev, @vinch100, @Polymorph31, @jbnitro
+ Chinese Traditional, thanks @old2tan + Chinese Traditional, thanks @old2tan
+ Chinese Simplified, thanks @everdream + Chinese Simplified, thanks @everdream
+ [Please help translating](https://www.transifex.com/croneter/pkc) + [Please help translating](https://www.transifex.com/croneter/pkc)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.7.17" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.7.21" 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" />
@ -44,7 +44,33 @@
<disclaimer lang="nl_NL">Gebruik op eigen risico</disclaimer> <disclaimer lang="nl_NL">Gebruik op eigen risico</disclaimer>
<disclaimer lang="zh_TW">使用風險由您自己承擔</disclaimer> <disclaimer lang="zh_TW">使用風險由您自己承擔</disclaimer>
<disclaimer lang="es_ES">Usar a su propio riesgo</disclaimer> <disclaimer lang="es_ES">Usar a su propio riesgo</disclaimer>
<news>version 1.7.17 (beta only) <news>version 1.7.21 (beta only)
- Fix Playback and watched status not syncing
- Fix PKC syncing progress to wrong account
- Warn user if a xml cannot be parsed
version 1.7.20 (beta only)
- Fix for Windows usernames with non-ASCII chars
- Companion: Fix TypeError
- Use SSL settings when checking server connection
- Fix TypeError when PMS connection lost
- Increase timeout
version 1.7.19 (beta only)
- Big code refactoring
- Many Plex Companion fixes
- Fix WindowsError or alike when deleting video nodes
- Remove restart on first setup
- Only set advancedsettings tweaks if Music enabled
version 1.7.18 (beta only)
- Fix OperationalError when resetting PKC
- Fix possible OperationalErrors
- Companion: ensure sockets get closed
- Fix TypeError for Plex Companion
- Update Czech
version 1.7.17 (beta only)
- Don't add media by other add-ons to queue - Don't add media by other add-ons to queue
- Fix KeyError for Plex Companion - Fix KeyError for Plex Companion
- Repace Kodi mkdirs with os.makedirs - Repace Kodi mkdirs with os.makedirs

View file

@ -1,3 +1,29 @@
version 1.7.21 (beta only)
- Fix Playback and watched status not syncing
- Fix PKC syncing progress to wrong account
- Warn user if a xml cannot be parsed
version 1.7.20 (beta only)
- Fix for Windows usernames with non-ASCII chars
- Companion: Fix TypeError
- Use SSL settings when checking server connection
- Fix TypeError when PMS connection lost
- Increase timeout
version 1.7.19 (beta only)
- Big code refactoring
- Many Plex Companion fixes
- Fix WindowsError or alike when deleting video nodes
- Remove restart on first setup
- Only set advancedsettings tweaks if Music enabled
version 1.7.18 (beta only)
- Fix OperationalError when resetting PKC
- Fix possible OperationalErrors
- Companion: ensure sockets get closed
- Fix TypeError for Plex Companion
- Update Czech
version 1.7.17 (beta only) version 1.7.17 (beta only)
- Don't add media by other add-ons to queue - Don't add media by other add-ons to queue
- Fix KeyError for Plex Companion - Fix KeyError for Plex Companion

View file

@ -170,10 +170,10 @@ class Main():
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_play_new_item'): while window('plex_command'):
sleep(50) sleep(50)
window('plex_play_new_item', window('plex_command',
value='%s%s' % ('play', argv[2])) 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)

View file

@ -1919,3 +1919,8 @@ msgstr ""
msgctxt "#39715" msgctxt "#39715"
msgid "items" msgid "items"
msgstr "" msgstr ""
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is screwed up; formated the wrong way). Do NOT replace {0} and {1}!
msgctxt "#39716"
msgid "Kodi cannot parse {0}. PKC will not function correctly. Please visit {1} and correct your file!"
msgstr ""

View file

@ -53,6 +53,7 @@ from utils import window, settings, language as lang, tryDecode, tryEncode, \
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
import state
############################################################################### ###############################################################################
@ -628,7 +629,7 @@ class PlexAPI():
authenticate=False, authenticate=False,
headerOptions={'X-Plex-Token': PMS['token']}, headerOptions={'X-Plex-Token': PMS['token']},
verifySSL=False, verifySSL=False,
timeout=3) timeout=10)
try: try:
xml.attrib['machineIdentifier'] xml.attrib['machineIdentifier']
except (AttributeError, KeyError): except (AttributeError, KeyError):
@ -879,6 +880,8 @@ class PlexAPI():
settings('plex_restricteduser', settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1' 'true' if answer.attrib.get('restricted', '0') == '1'
else 'false') else 'false')
state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False
# Get final token to the PMS we've chosen # Get final token to the PMS we've chosen
url = 'https://plex.tv/api/resources?includeHttps=1' url = 'https://plex.tv/api/resources?includeHttps=1'
@ -2550,20 +2553,20 @@ class API():
if "\\" in path: if "\\" in path:
if not path.endswith('\\'): if not path.endswith('\\'):
# Add the missing backslash # Add the missing backslash
check = exists_dir(tryEncode(path + "\\")) check = exists_dir(path + "\\")
else: else:
check = exists_dir(tryEncode(path)) check = exists_dir(path)
else: else:
if not path.endswith('/'): if not path.endswith('/'):
check = exists_dir(tryEncode(path + "/")) check = exists_dir(path + "/")
else: else:
check = exists_dir(tryEncode(path)) check = exists_dir(path)
if not check: if not check:
if forceCheck is False: if forceCheck is False:
# Validate the path is correct with user intervention # Validate the path is correct with user intervention
if self.askToValidate(path): if self.askToValidate(path):
window('plex_shouldStop', value="true") state.STOP_SYNC = True
path = None path = None
window('plex_pathverified', value='true') window('plex_pathverified', value='true')
else: else:

View file

@ -7,13 +7,14 @@ from urllib import urlencode
from xbmc import sleep, executebuiltin from xbmc import sleep, executebuiltin
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, functions, \ from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API from PlexAPI import API
import player import player
import variables as v import variables as v
import state
############################################################################### ###############################################################################
@ -22,8 +23,7 @@ log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('plex_serverStatus') @thread_methods(add_suspends=['PMS_STATUS'])
@ThreadMethods
class PlexCompanion(Thread): class PlexCompanion(Thread):
""" """
""" """
@ -77,6 +77,8 @@ class PlexCompanion(Thread):
log.debug('Processing: %s' % task) log.debug('Processing: %s' % task)
data = task['data'] data = task['data']
# Get the token of the user flinging media (might be different one)
state.PLEX_TRANSIENT_TOKEN = data.get('token')
if task['action'] == 'alexa': if task['action'] == 'alexa':
# e.g. Alexa # e.g. Alexa
xml = GetPlexMetadata(data['key']) xml = GetPlexMetadata(data['key'])
@ -144,11 +146,28 @@ class PlexCompanion(Thread):
offset=data.get('offset')) offset=data.get('offset'))
def run(self): def run(self):
httpd = False # Ensure that sockets will be closed no matter what
try:
self.__run()
finally:
try:
self.httpd.socket.shutdown(SHUT_RDWR)
except AttributeError:
pass
finally:
try:
self.httpd.socket.close()
except AttributeError:
pass
log.info("----===## Plex Companion stopped ##===----")
def __run(self):
self.httpd = False
httpd = self.httpd
# Cache for quicker while loops # Cache for quicker while loops
client = self.client client = self.client
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
# Start up instances # Start up instances
requestMgr = httppersist.RequestMgr() requestMgr = httppersist.RequestMgr()
@ -196,12 +215,12 @@ class PlexCompanion(Thread):
if httpd: if httpd:
t = Thread(target=httpd.handle_request) t = Thread(target=httpd.handle_request)
while not threadStopped(): while not thread_stopped():
# If we are not authorized, sleep # If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a # Otherwise, we trigger a download which leads to a
# re-authorizations # re-authorizations
while threadSuspended(): while thread_suspended():
if threadStopped(): if thread_stopped():
break break
sleep(1000) sleep(1000)
try: try:
@ -245,11 +264,3 @@ class PlexCompanion(Thread):
sleep(50) sleep(50)
client.stop_all() client.stop_all()
if httpd:
try:
httpd.socket.shutdown(SHUT_RDWR)
except:
pass
finally:
httpd.socket.close()
log.info("----===## Plex Companion stopped ##===----")

View file

@ -13,7 +13,7 @@ from xbmc import executeJSONRPC, sleep, translatePath
from xbmcvfs import exists from xbmcvfs import exists
from utils import window, settings, language as lang, kodiSQL, tryEncode, \ from utils import window, settings, language as lang, kodiSQL, tryEncode, \
ThreadMethods, ThreadMethodsAdditionalStop, dialog, exists_dir thread_methods, dialog, exists_dir, tryDecode
# Disable annoying requests warnings # Disable annoying requests warnings
import requests.packages.urllib3 import requests.packages.urllib3
@ -126,8 +126,8 @@ def double_urldecode(text):
return unquote(unquote(text)) return unquote(unquote(text))
@ThreadMethodsAdditionalStop('plex_shouldStop') @thread_methods(add_stops=['STOP_SYNC'],
@ThreadMethods add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'])
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()
@ -140,22 +140,16 @@ class Image_Cache_Thread(Thread):
self.queue = ARTWORK_QUEUE self.queue = ARTWORK_QUEUE
Thread.__init__(self) Thread.__init__(self)
def threadSuspended(self):
# Overwrite method to add TWO additional suspends
return (self._threadSuspended or
window('suspend_LibraryThread') or
window('plex_dbScan'))
def run(self): def run(self):
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
queue = self.queue queue = self.queue
sleep_between = self.sleep_between sleep_between = self.sleep_between
while not threadStopped(): while not thread_stopped():
# In the event the server goes offline # In the event the server goes offline
while threadSuspended(): while thread_suspended():
# Set in service.py # Set in service.py
if threadStopped(): if thread_stopped():
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("---===### Stopped Image_Cache_Thread ###===---") log.info("---===### Stopped Image_Cache_Thread ###===---")
return return
@ -178,7 +172,7 @@ class Image_Cache_Thread(Thread):
# download. All is well # download. All is well
break break
except requests.ConnectionError: except requests.ConnectionError:
if threadStopped(): if thread_stopped():
# Kodi terminated # Kodi terminated
break break
# Server thinks its a DOS attack, ('error 10053') # Server thinks its a DOS attack, ('error 10053')
@ -228,7 +222,7 @@ class Artwork():
if dialog('yesno', "Image Texture Cache", lang(39251)): if dialog('yesno', "Image Texture Cache", lang(39251)):
log.info("Resetting all cache data first") log.info("Resetting all cache data first")
# Remove all existing textures first # Remove all existing textures first
path = translatePath("special://thumbnails/") path = tryDecode(translatePath("special://thumbnails/"))
if exists_dir(path): if exists_dir(path):
rmtree(path, ignore_errors=True) rmtree(path, ignore_errors=True)
@ -241,8 +235,7 @@ class Artwork():
for row in rows: for row in rows:
tableName = row[0] tableName = row[0]
if tableName != "version": if tableName != "version":
query = "DELETE FROM ?" cursor.execute("DELETE FROM %s" % tableName)
cursor.execute(query, (tableName,))
connection.commit() connection.commit()
connection.close() connection.close()
@ -430,7 +423,7 @@ class Artwork():
path = translatePath("special://thumbnails/%s" % cachedurl) path = translatePath("special://thumbnails/%s" % cachedurl)
log.debug("Deleting cached thumbnail: %s" % path) log.debug("Deleting cached thumbnail: %s" % path)
if exists(path): if exists(path):
rmtree(path, ignore_errors=True) rmtree(tryDecode(path), ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit() connection.commit()
finally: finally:

View file

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import Thread
from Queue import Queue
from xbmc import sleep
from utils import window, thread_methods
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
@thread_methods
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
def __init__(self, callback=None):
self.mgr = callback
self.playback_queue = Queue()
Thread.__init__(self)
def run(self):
thread_stopped = self.thread_stopped
queue = self.playback_queue
log.info("----===## Starting Kodi_Play_Client ##===----")
while not thread_stopped():
if window('plex_command'):
value = window('plex_command')
window('plex_command', clear=True)
if value.startswith('play_'):
queue.put(value)
elif value == 'SUSPEND_LIBRARY_THREAD-True':
state.SUSPEND_LIBRARY_THREAD = True
elif value == 'SUSPEND_LIBRARY_THREAD-False':
state.SUSPEND_LIBRARY_THREAD = False
elif value == 'STOP_SYNC-True':
state.STOP_SYNC = True
elif value == 'STOP_SYNC-False':
state.STOP_SYNC = False
elif value == 'PMS_STATUS-Auth':
state.PMS_STATUS = 'Auth'
elif value == 'PMS_STATUS-401':
state.PMS_STATUS = '401'
elif value == 'SUSPEND_USER_CLIENT-True':
state.SUSPEND_USER_CLIENT = True
elif value == 'SUSPEND_USER_CLIENT-False':
state.SUSPEND_USER_CLIENT = False
elif value.startswith('PLEX_TOKEN-'):
state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None
elif value.startswith('PLEX_USERNAME-'):
state.PLEX_USERNAME = \
value.replace('PLEX_USERNAME-', '') or None
else:
raise NotImplementedError('%s not implemented' % value)
else:
sleep(50)
# Put one last item into the queue to let playback_starter end
queue.put(None)
log.info("----===## Kodi_Play_Client stopped ##===----")

View file

@ -9,6 +9,8 @@ import xml.etree.ElementTree as etree
from utils import settings, window, language as lang, dialog from utils import settings, window, language as lang, dialog
import clientinfo as client import clientinfo as client
import state
############################################################################### ###############################################################################
# Disable annoying requests warnings # Disable annoying requests warnings
@ -40,20 +42,6 @@ class DownloadUtils():
def __init__(self): def __init__(self):
self.__dict__ = self._shared_state self.__dict__ = self._shared_state
def setUsername(self, username):
"""
Reserved for userclient only
"""
self.username = username
log.debug("Set username: %s" % username)
def setUserId(self, userId):
"""
Reserved for userclient only
"""
self.userId = userId
log.debug("Set userId: %s" % userId)
def setServer(self, server): def setServer(self, server):
""" """
Reserved for userclient only Reserved for userclient only
@ -108,8 +96,6 @@ class DownloadUtils():
# Set other stuff # Set other stuff
self.setServer(window('pms_server')) self.setServer(window('pms_server'))
self.setToken(window('pms_token')) self.setToken(window('pms_token'))
self.setUserId(window('currUserId'))
self.setUsername(window('plex_username'))
# Counters to declare PMS dead or unauthorized # Counters to declare PMS dead or unauthorized
# Use window variables because start of movies will be called with a # Use window variables because start of movies will be called with a
@ -274,10 +260,11 @@ class DownloadUtils():
self.unauthorizedAttempts): self.unauthorizedAttempts):
log.warn('We seem to be truly unauthorized for PMS' log.warn('We seem to be truly unauthorized for PMS'
' %s ' % url) ' %s ' % url)
if window('plex_serverStatus') not in ('401', 'Auth'): if state.PMS_STATUS not in ('401', 'Auth'):
# Tell userclient token has been revoked. # Tell userclient token has been revoked.
log.debug('Setting PMS server status to ' log.debug('Setting PMS server status to '
'unauthorized') 'unauthorized')
state.PMS_STATUS = '401'
window('plex_serverStatus', value="401") window('plex_serverStatus', value="401")
dialog('notification', dialog('notification',
lang(29999), lang(29999),

View file

@ -12,7 +12,7 @@ from xbmc import sleep, executebuiltin, translatePath
from xbmcgui import ListItem from xbmcgui import ListItem
from utils import window, settings, language as lang, dialog, tryEncode, \ from utils import window, settings, language as lang, dialog, tryEncode, \
CatchExceptions, JSONRPC, exists_dir CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode
import downloadutils import downloadutils
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
@ -42,8 +42,8 @@ def chooseServer():
server = setup.PickPMS(showDialog=True) server = setup.PickPMS(showDialog=True)
if server is None: if server is None:
log.error('We did not connect to a new PMS, aborting') log.error('We did not connect to a new PMS, aborting')
window('suspend_Userclient', clear=True) plex_command('SUSPEND_USER_CLIENT', 'False')
window('suspend_LibraryThread', clear=True) plex_command('SUSPEND_LIBRARY_THREAD', 'False')
return return
log.info("User chose server %s" % server['name']) log.info("User chose server %s" % server['name'])
@ -81,7 +81,8 @@ def togglePlexTV():
settings('plex_status', value="Not logged in to plex.tv") settings('plex_status', value="Not logged in to plex.tv")
window('plex_token', clear=True) window('plex_token', clear=True)
window('plex_username', clear=True) plex_command('PLEX_TOKEN', '')
plex_command('PLEX_USERNAME', '')
else: else:
log.info('Login to plex.tv') log.info('Login to plex.tv')
import initialsetup import initialsetup
@ -100,7 +101,7 @@ def resetAuth():
resp = dialog('yesno', heading="{plex}", line1=lang(39206)) resp = dialog('yesno', heading="{plex}", line1=lang(39206))
if resp == 1: if resp == 1:
log.info("Reset login attempts.") log.info("Reset login attempts.")
window('plex_serverStatus', value="Auth") plex_command('PMS_STATUS', 'Auth')
else: else:
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
@ -146,7 +147,7 @@ def doMainListing(content_type=None):
addDirectoryItem(lang(30173), addDirectoryItem(lang(30173),
"plugin://%s?mode=channels" % v.ADDON_ID) "plugin://%s?mode=channels" % v.ADDON_ID)
# Plex user switch # Plex user switch
addDirectoryItem(lang(39200) + window('plex_username'), addDirectoryItem(lang(39200),
"plugin://%s?mode=switchuser" % v.ADDON_ID) "plugin://%s?mode=switchuser" % v.ADDON_ID)
# some extra entries for settings and stuff # some extra entries for settings and stuff
@ -488,7 +489,6 @@ def getVideoFiles(plexId, params):
except: except:
log.error('Could not get file path for item %s' % plexId) log.error('Could not get file path for item %s' % plexId)
return xbmcplugin.endOfDirectory(HANDLE) return xbmcplugin.endOfDirectory(HANDLE)
path = tryEncode(path)
# Assign network protocol # Assign network protocol
if path.startswith('\\\\'): if path.startswith('\\\\'):
path = path.replace('\\\\', 'smb://') path = path.replace('\\\\', 'smb://')
@ -501,14 +501,14 @@ def getVideoFiles(plexId, params):
if exists_dir(path): if exists_dir(path):
for root, dirs, files in walk(path): for root, dirs, files in walk(path):
for directory in dirs: for directory in dirs:
item_path = join(root, directory) item_path = tryEncode(join(root, directory))
li = ListItem(item_path, path=item_path) li = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=item_path, url=item_path,
listitem=li, listitem=li,
isFolder=True) isFolder=True)
for file in files: for file in files:
item_path = join(root, file) item_path = tryEncode(join(root, file))
li = ListItem(item_path, path=item_path) li = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=file, url=file,
@ -536,7 +536,8 @@ def getExtraFanArt(plexid, plexPath):
# We need to store the images locally for this to work # We need to store the images locally for this to work
# because of the caching system in xbmc # because of the caching system in xbmc
fanartDir = translatePath("special://thumbnails/plex/%s/" % plexid) fanartDir = tryDecode(translatePath(
"special://thumbnails/plex/%s/" % plexid))
if not exists_dir(fanartDir): if not exists_dir(fanartDir):
# Download the images to the cache directory # Download the images to the cache directory
makedirs(fanartDir) makedirs(fanartDir)
@ -549,19 +550,19 @@ def getExtraFanArt(plexid, plexPath):
backdrops = api.getAllArtwork()['Backdrop'] backdrops = api.getAllArtwork()['Backdrop']
for count, backdrop in enumerate(backdrops): for count, backdrop in enumerate(backdrops):
# Same ordering as in artwork # Same ordering as in artwork
fanartFile = join(fanartDir, "fanart%.3d.jpg" % count) fanartFile = tryEncode(join(fanartDir, "fanart%.3d.jpg" % count))
li = ListItem("%.3d" % count, path=fanartFile) li = ListItem("%.3d" % count, path=fanartFile)
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=HANDLE, handle=HANDLE,
url=fanartFile, url=fanartFile,
listitem=li) listitem=li)
copyfile(backdrop, fanartFile) copyfile(backdrop, tryDecode(fanartFile))
else: else:
log.info("Found cached backdrop.") log.info("Found cached backdrop.")
# Use existing cached images # Use existing cached images
for root, dirs, files in walk(fanartDir): for root, dirs, files in walk(fanartDir):
for file in files: for file in files:
fanartFile = join(root, file) fanartFile = tryEncode(join(root, file))
li = ListItem(file, path=fanartFile) li = ListItem(file, path=fanartFile)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=fanartFile, url=fanartFile,
@ -964,22 +965,19 @@ def enterPMS():
def __LogIn(): def __LogIn():
""" """
Resets (clears) window properties to enable (re-)login: Resets (clears) window properties to enable (re-)login
suspend_Userclient
plex_runLibScan: set to 'full' to trigger lib sync
suspend_LibraryThread is cleared in service.py if user was signed out! SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
out!
""" """
window('plex_runLibScan', value='full') window('plex_runLibScan', value='full')
# Restart user client # Restart user client
window('suspend_Userclient', clear=True) plex_command('SUSPEND_USER_CLIENT', 'False')
def __LogOut(): def __LogOut():
""" """
Finishes lib scans, logs out user. The following window attributes are set: Finishes lib scans, logs out user.
suspend_LibraryThread: 'true'
suspend_Userclient: 'true'
Returns True if successfully signed out, False otherwise Returns True if successfully signed out, False otherwise
""" """
@ -991,7 +989,7 @@ def __LogOut():
time=3000, time=3000,
sound=False) sound=False)
# Pause library sync thread # Pause library sync thread
window('suspend_LibraryThread', value='true') plex_command('SUSPEND_LIBRARY_THREAD', 'True')
# Wait max for 10 seconds for all lib scans to shutdown # Wait max for 10 seconds for all lib scans to shutdown
counter = 0 counter = 0
while window('plex_dbScan') == 'true': while window('plex_dbScan') == 'true':
@ -999,17 +997,18 @@ def __LogOut():
# Failed to reset PMS and plex.tv connects. Try to restart Kodi. # Failed to reset PMS and plex.tv connects. Try to restart Kodi.
dialog('ok', lang(29999), lang(39208)) dialog('ok', lang(29999), lang(39208))
# Resuming threads, just in case # Resuming threads, just in case
window('suspend_LibraryThread', clear=True) plex_command('SUSPEND_LIBRARY_THREAD', 'False')
log.error("Could not stop library sync, aborting") log.error("Could not stop library sync, aborting")
return False return False
counter += 1 counter += 1
sleep(50) sleep(50)
log.debug("Successfully stopped library sync") log.debug("Successfully stopped library sync")
# Log out currently signed in user:
window('plex_serverStatus', value="401")
# Above method needs to have run its course! Hence wait
counter = 0 counter = 0
# Log out currently signed in user:
window('plex_serverStatus', value='401')
plex_command('PMS_STATUS', '401')
# Above method needs to have run its course! Hence wait
while window('plex_serverStatus') == "401": while window('plex_serverStatus') == "401":
if counter > 100: if counter > 100:
# 'Failed to reset PKC. Try to restart Kodi.' # 'Failed to reset PKC. Try to restart Kodi.'
@ -1019,5 +1018,5 @@ def __LogOut():
counter += 1 counter += 1
sleep(50) sleep(50)
# Suspend the user client during procedure # Suspend the user client during procedure
window('suspend_Userclient', value='true') plex_command('SUSPEND_USER_CLIENT', 'True')
return True return True

View file

@ -13,6 +13,7 @@ from userclient import UserClient
from PlexAPI import PlexAPI from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import state
############################################################################### ###############################################################################
@ -156,7 +157,7 @@ class InitialSetup():
verifySSL = False verifySSL = False
else: else:
url = server['baseURL'] url = server['baseURL']
verifySSL = None verifySSL = True
chk = self.plx.CheckConnection(url, chk = self.plx.CheckConnection(url,
token=server['accesstoken'], token=server['accesstoken'],
verifySSL=verifySSL) verifySSL=verifySSL)
@ -450,6 +451,7 @@ class InitialSetup():
yeslabel="Native (Direct Paths)"): yeslabel="Native (Direct Paths)"):
log.debug("User opted to use direct paths.") log.debug("User opted to use direct paths.")
settings('useDirectPaths', value="1") settings('useDirectPaths', value="1")
state.DIRECT_PATHS = True
# Are you on a system where you would like to replace paths # Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if dialog.yesno(heading=lang(29999), line1=lang(39033)): if dialog.yesno(heading=lang(29999), line1=lang(39033)):
@ -477,9 +479,6 @@ class InitialSetup():
if dialog.yesno(heading=lang(29999), line1=lang(39016)): if dialog.yesno(heading=lang(29999), line1=lang(39016)):
log.debug("User opted to disable Plex music library.") log.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false") settings('enableMusic', value="false")
else:
from utils import advancedsettings_tweaks
advancedsettings_tweaks()
# Download additional art from FanArtTV # Download additional art from FanArtTV
if dialog.yesno(heading=lang(29999), line1=lang(39061)): if dialog.yesno(heading=lang(29999), line1=lang(39061)):
@ -496,12 +495,6 @@ class InitialSetup():
# Open Settings page now? You will need to restart! # Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
if goToSettings: if goToSettings:
window('plex_serverStatus', value="Stop") state.PMS_STATUS = 'Stop'
xbmc.executebuiltin( xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)') 'Addon.OpenSettings(plugin.video.plexkodiconnect)')
else:
# "Kodi will now restart to apply the changes"
dialog.ok(heading=lang(29999), line1=lang(33033))
xbmc.executebuiltin('RestartApp')
# We should always restart to ensure e.g. Kodi settings for Music
# are in use!

View file

@ -16,6 +16,7 @@ import kodidb_functions as kodidb
import PlexAPI import PlexAPI
from PlexFunctions import GetPlexMetadata from PlexFunctions import GetPlexMetadata
import variables as v import variables as v
import state
############################################################################### ###############################################################################
@ -35,10 +36,7 @@ class Items(object):
""" """
def __init__(self): def __init__(self):
self.directpath = window('useDirectPaths') == 'true'
self.artwork = artwork.Artwork() self.artwork = artwork.Artwork()
self.userid = window('currUserId')
self.server = window('pms_server') self.server = window('pms_server')
def __enter__(self): def __enter__(self):
@ -268,8 +266,8 @@ class Movies(Items):
break break
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
doIndirect = not self.directpath doIndirect = not state.DIRECT_PATHS
if self.directpath: if state.DIRECT_PATHS:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
playurl = API.getFilePath(forceFirstMediaStream=True) playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None: if playurl is None:
@ -569,8 +567,8 @@ class TVShows(Items):
studio = None studio = None
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
doIndirect = not self.directpath doIndirect = not state.DIRECT_PATHS
if self.directpath: if state.DIRECT_PATHS:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
playurl = API.getTVShowPath() playurl = API.getTVShowPath()
if playurl is None: if playurl is None:
@ -892,9 +890,9 @@ class TVShows(Items):
seasonid = self.kodi_db.addSeason(showid, season) seasonid = self.kodi_db.addSeason(showid, season)
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
doIndirect = not self.directpath doIndirect = not state.DIRECT_PATHS
playurl = API.getFilePath(forceFirstMediaStream=True) playurl = API.getFilePath(forceFirstMediaStream=True)
if self.directpath: if state.DIRECT_PATHS:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
if playurl is None: if playurl is None:
# Something went wrong, trying to use non-direct paths # Something went wrong, trying to use non-direct paths
@ -1116,7 +1114,7 @@ class TVShows(Items):
self.kodi_db.addStreams(fileid, streams, runtime) self.kodi_db.addStreams(fileid, streams, runtime)
# Process playstates # Process playstates
self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
if not self.directpath and resume: if not state.DIRECT_PATHS and resume:
# Create additional entry for widgets. This is only required for plugin/episode. # Create additional entry for widgets. This is only required for plugin/episode.
temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/") temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/")
tempfileid = self.kodi_db.addFile(filename, temppathid) tempfileid = self.kodi_db.addFile(filename, temppathid)
@ -1634,8 +1632,8 @@ class Music(Items):
mood = ' / '.join(moods) mood = ' / '.join(moods)
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
doIndirect = not self.directpath doIndirect = not state.DIRECT_PATHS
if self.directpath: if state.DIRECT_PATHS:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
playurl = API.getFilePath(forceFirstMediaStream=True) playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None: if playurl is None:

View file

@ -1409,8 +1409,8 @@ class Kodidb_Functions():
ID = 'idEpisode' ID = 'idEpisode'
elif kodi_type == v.KODI_TYPE_SONG: elif kodi_type == v.KODI_TYPE_SONG:
ID = 'idSong' ID = 'idSong'
query = '''UPDATE ? SET userrating = ? WHERE ? = ?''' query = '''UPDATE %s SET userrating = ? WHERE ? = ?''' % kodi_type
self.cursor.execute(query, (kodi_type, userrating, ID, kodi_id)) self.cursor.execute(query, (userrating, ID, kodi_id))
def create_entry_uniqueid(self): def create_entry_uniqueid(self):
self.cursor.execute( self.cursor.execute(

View file

@ -14,6 +14,7 @@ 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 from variables import REMAP_TYPE_FROM_PLEXTYPE
import state
############################################################################### ###############################################################################
@ -137,6 +138,10 @@ class KodiMonitor(Monitor):
sleep(5000) sleep(5000)
window('plex_runLibScan', value="full") window('plex_runLibScan', value="full")
elif method == "System.OnQuit":
log.info('Kodi OnQuit detected - shutting down')
state.STOP_PKC = True
def PlayBackStart(self, data): def PlayBackStart(self, data):
""" """
Called whenever a playback is started Called whenever a playback is started

View file

@ -5,8 +5,7 @@ from Queue import Empty
from xbmc import sleep from xbmc import sleep
from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ from utils import thread_methods
ThreadMethodsAdditionalSuspend
import plexdb_functions as plexdb import plexdb_functions as plexdb
import itemtypes import itemtypes
import variables as v import variables as v
@ -18,9 +17,8 @@ log = getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') @thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'],
@ThreadMethodsAdditionalStop('plex_shouldStop') add_stops=['STOP_SYNC'])
@ThreadMethods
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
@ -55,14 +53,14 @@ class Process_Fanart_Thread(Thread):
Do the work Do the work
""" """
log.debug("---===### Starting FanartSync ###===---") log.debug("---===### Starting FanartSync ###===---")
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
queue = self.queue queue = self.queue
while not threadStopped(): while not thread_stopped():
# In the event the server goes offline # In the event the server goes offline
while threadSuspended() or window('plex_dbScan'): while thread_suspended():
# Set in service.py # Set in service.py
if threadStopped(): if thread_stopped():
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("---===### Stopped FanartSync ###===---") log.info("---===### Stopped FanartSync ###===---")
return return

View file

@ -5,7 +5,7 @@ from Queue import Empty
from xbmc import sleep from xbmc import sleep
from utils import ThreadMethodsAdditionalStop, ThreadMethods, window from utils import thread_methods, window
from PlexFunctions import GetPlexMetadata, GetAllPlexChildren from PlexFunctions import GetPlexMetadata, GetAllPlexChildren
import sync_info import sync_info
@ -16,8 +16,7 @@ log = getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalStop('suspend_LibraryThread') @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@ThreadMethods
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.
@ -48,7 +47,7 @@ class Threaded_Get_Metadata(Thread):
continue continue
else: else:
self.queue.task_done() self.queue.task_done()
if self.threadStopped(): if self.thread_stopped():
# Shutdown from outside requested; purge out_queue as well # Shutdown from outside requested; purge out_queue as well
while not self.out_queue.empty(): while not self.out_queue.empty():
# Still try because remaining item might have been taken # Still try because remaining item might have been taken
@ -79,8 +78,8 @@ class Threaded_Get_Metadata(Thread):
# cache local variables because it's faster # cache local variables because it's faster
queue = self.queue queue = self.queue
out_queue = self.out_queue out_queue = self.out_queue
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
while threadStopped() is False: while thread_stopped() is False:
# grabs Plex item from queue # grabs Plex item from queue
try: try:
item = queue.get(block=False) item = queue.get(block=False)

View file

@ -5,19 +5,17 @@ from Queue import Empty
from xbmc import sleep from xbmc import sleep
from utils import ThreadMethodsAdditionalStop, ThreadMethods from utils import thread_methods
import itemtypes import itemtypes
import sync_info import sync_info
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) log = getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalStop('suspend_LibraryThread') @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@ThreadMethods
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
@ -70,9 +68,9 @@ class Threaded_Process_Metadata(Thread):
item_fct = getattr(itemtypes, self.item_type) item_fct = getattr(itemtypes, self.item_type)
# cache local variables because it's faster # cache local variables because it's faster
queue = self.queue queue = self.queue
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
with item_fct() as item_class: with item_fct() as item_class:
while threadStopped() is False: while thread_stopped() is False:
# grabs item from queue # grabs item from queue
try: try:
item = queue.get(block=False) item = queue.get(block=False)

View file

@ -4,7 +4,7 @@ from threading import Thread, Lock
from xbmc import sleep from xbmc import sleep
from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang from utils import thread_methods, language as lang
############################################################################### ###############################################################################
@ -18,8 +18,7 @@ LOCK = Lock()
############################################################################### ###############################################################################
@ThreadMethodsAdditionalStop('suspend_LibraryThread') @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
@ThreadMethods
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.
@ -53,13 +52,13 @@ class Threaded_Show_Sync_Info(Thread):
# cache local variables because it's faster # cache local variables because it's faster
total = self.total total = self.total
dialog = self.dialog dialog = self.dialog
threadStopped = self.threadStopped 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)))
total = 2 * total total = 2 * total
totalProgress = 0 totalProgress = 0
while threadStopped() is False: while thread_stopped() is False:
with LOCK: with LOCK:
get_progress = GET_METADATA_COUNT get_progress = GET_METADATA_COUNT
process_progress = PROCESS_METADATA_COUNT process_progress = PROCESS_METADATA_COUNT

View file

@ -10,10 +10,9 @@ import xbmcgui
from xbmcvfs import exists from xbmcvfs import exists
from utils import window, settings, getUnixTimestamp, sourcesXML,\ from utils import window, settings, getUnixTimestamp, sourcesXML,\
ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes, \ tryDecode, deletePlaylists, deleteNodes, tryEncode
ThreadMethodsAdditionalSuspend, create_actor_db_index, dialog
import downloadutils import downloadutils
import itemtypes import itemtypes
import plexdb_functions as plexdb import plexdb_functions as plexdb
@ -30,6 +29,7 @@ from library_sync.process_metadata import Threaded_Process_Metadata
import library_sync.sync_info as sync_info import library_sync.sync_info as sync_info
from library_sync.fanart import Process_Fanart_Thread from library_sync.fanart import Process_Fanart_Thread
import music import music
import state
############################################################################### ###############################################################################
@ -38,9 +38,8 @@ log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') @thread_methods(add_stops=['STOP_SYNC'],
@ThreadMethodsAdditionalStop('plex_shouldStop') add_suspends=['SUSPEND_LIBRARY_THREAD'])
@ThreadMethods
class LibrarySync(Thread): class LibrarySync(Thread):
""" """
""" """
@ -72,7 +71,6 @@ class LibrarySync(Thread):
self.enableMusic = settings('enableMusic') == "true" self.enableMusic = settings('enableMusic') == "true"
self.enableBackgroundSync = settings( self.enableBackgroundSync = settings(
'enableBackgroundSync') == "true" 'enableBackgroundSync') == "true"
self.direct_paths = settings('useDirectPaths') == '1'
# Init for replacing paths # Init for replacing paths
window('remapSMB', value=settings('remapSMB')) window('remapSMB', value=settings('remapSMB'))
@ -300,7 +298,7 @@ class LibrarySync(Thread):
# Do the processing # Do the processing
for itemtype in process: for itemtype in process:
if self.threadStopped(): if self.thread_stopped():
xbmc.executebuiltin('InhibitIdleShutdown(false)') xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver) setScreensaver(value=screensaver)
return False return False
@ -323,7 +321,7 @@ class LibrarySync(Thread):
window('plex_scancrashed', clear=True) window('plex_scancrashed', clear=True)
elif window('plex_scancrashed') == '401': elif window('plex_scancrashed') == '401':
window('plex_scancrashed', clear=True) window('plex_scancrashed', clear=True)
if window('plex_serverStatus') not in ('401', 'Auth'): if state.PMS_STATUS not in ('401', 'Auth'):
# Plex server had too much and returned ERROR # Plex server had too much and returned ERROR
self.dialog.ok(lang(29999), lang(39409)) self.dialog.ok(lang(29999), lang(39409))
@ -474,7 +472,7 @@ class LibrarySync(Thread):
""" """
Compare the views to Plex Compare the views to Plex
""" """
if self.direct_paths is True: if state.DIRECT_PATHS is True and self.enableMusic is True:
if music.set_excludefromscan_music_folders() is True: if music.set_excludefromscan_music_folders() is True:
log.info('Detected new Music library - restarting now') log.info('Detected new Music library - restarting now')
# 'New Plex music library detected. Sorry, but we need to # 'New Plex music library detected. Sorry, but we need to
@ -759,8 +757,8 @@ class LibrarySync(Thread):
for thread in threads: for thread in threads:
# Threads might already have quit by themselves (e.g. Kodi exit) # Threads might already have quit by themselves (e.g. Kodi exit)
try: try:
thread.stopThread() thread.stop_thread()
except: except AttributeError:
pass pass
log.debug("Stop sent to all threads") log.debug("Stop sent to all threads")
# Wait till threads are indeed dead # Wait till threads are indeed dead
@ -805,7 +803,7 @@ class LibrarySync(Thread):
# PROCESS MOVIES ##### # PROCESS MOVIES #####
self.updatelist = [] self.updatelist = []
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
# Get items per view # Get items per view
viewId = view['id'] viewId = view['id']
@ -826,7 +824,7 @@ class LibrarySync(Thread):
log.info("Processed view") log.info("Processed view")
# Update viewstate for EVERY item # Update viewstate for EVERY item
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
self.PlexUpdateWatched(view['id'], itemType) self.PlexUpdateWatched(view['id'], itemType)
@ -898,7 +896,7 @@ class LibrarySync(Thread):
# PROCESS TV Shows ##### # PROCESS TV Shows #####
self.updatelist = [] self.updatelist = []
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
# Get items per view # Get items per view
viewId = view['id'] viewId = view['id']
@ -927,7 +925,7 @@ class LibrarySync(Thread):
# PROCESS TV Seasons ##### # PROCESS TV Seasons #####
# Cycle through tv shows # Cycle through tv shows
for tvShowId in allPlexTvShowsId: for tvShowId in allPlexTvShowsId:
if self.threadStopped(): if self.thread_stopped():
return False return False
# Grab all seasons to tvshow from PMS # Grab all seasons to tvshow from PMS
seasons = GetAllPlexChildren(tvShowId) seasons = GetAllPlexChildren(tvShowId)
@ -952,7 +950,7 @@ class LibrarySync(Thread):
# PROCESS TV Episodes ##### # PROCESS TV Episodes #####
# Cycle through tv shows # Cycle through tv shows
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
# Grab all episodes to tvshow from PMS # Grab all episodes to tvshow from PMS
episodes = GetAllPlexLeaves(view['id']) episodes = GetAllPlexLeaves(view['id'])
@ -987,7 +985,7 @@ class LibrarySync(Thread):
# Update viewstate: # Update viewstate:
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
self.PlexUpdateWatched(view['id'], itemType) self.PlexUpdateWatched(view['id'], itemType)
@ -1024,7 +1022,7 @@ class LibrarySync(Thread):
for kind in (v.PLEX_TYPE_ARTIST, for kind in (v.PLEX_TYPE_ARTIST,
v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ALBUM,
v.PLEX_TYPE_SONG): v.PLEX_TYPE_SONG):
if self.threadStopped(): if self.thread_stopped():
return False return False
log.debug("Start processing music %s" % kind) log.debug("Start processing music %s" % kind)
self.allKodiElementsId = {} self.allKodiElementsId = {}
@ -1041,7 +1039,7 @@ class LibrarySync(Thread):
# Update viewstate for EVERY item # Update viewstate for EVERY item
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
self.PlexUpdateWatched(view['id'], itemType) self.PlexUpdateWatched(view['id'], itemType)
@ -1066,7 +1064,7 @@ class LibrarySync(Thread):
except ValueError: except ValueError:
pass pass
for view in views: for view in views:
if self.threadStopped(): if self.thread_stopped():
return False return False
# Get items per view # Get items per view
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
@ -1172,7 +1170,7 @@ class LibrarySync(Thread):
now = getUnixTimestamp() now = getUnixTimestamp()
deleteListe = [] deleteListe = []
for i, item in enumerate(self.itemsToProcess): for i, item in enumerate(self.itemsToProcess):
if self.threadStopped(): if self.thread_stopped():
# Chances are that Kodi gets shut down # Chances are that Kodi gets shut down
break break
if item['state'] == 9: if item['state'] == 9:
@ -1277,8 +1275,8 @@ class LibrarySync(Thread):
# movie or episode) # movie or episode)
continue continue
typus = int(item.get('type', 0)) typus = int(item.get('type', 0))
state = int(item.get('state', 0)) status = int(item.get('state', 0))
if state == 9 or (typus in (1, 4, 10) and state == 5): if status == 9 or (typus in (1, 4, 10) and status == 5):
# Only process deleted items OR movies, episodes, tracks/songs # Only process deleted items OR movies, episodes, tracks/songs
plex_id = str(item.get('itemID', '0')) plex_id = str(item.get('itemID', '0'))
if plex_id == '0': if plex_id == '0':
@ -1286,7 +1284,7 @@ class LibrarySync(Thread):
continue continue
try: try:
if (now - self.just_processed[plex_id] < if (now - self.just_processed[plex_id] <
self.ignore_just_processed and state != 9): self.ignore_just_processed and status != 9):
log.debug('We just processed %s: ignoring' % plex_id) log.debug('We just processed %s: ignoring' % plex_id)
continue continue
except KeyError: except KeyError:
@ -1299,7 +1297,7 @@ class LibrarySync(Thread):
else: else:
# Haven't added this element to the queue yet # Haven't added this element to the queue yet
self.itemsToProcess.append({ self.itemsToProcess.append({
'state': state, 'state': status,
'type': typus, 'type': typus,
'ratingKey': plex_id, 'ratingKey': plex_id,
'timestamp': getUnixTimestamp(), 'timestamp': getUnixTimestamp(),
@ -1315,8 +1313,8 @@ class LibrarySync(Thread):
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
for item in data: for item in data:
# Drop buffering messages immediately # Drop buffering messages immediately
state = item.get('state') status = item.get('state')
if state == 'buffering': if status == 'buffering':
continue continue
ratingKey = item.get('ratingKey') ratingKey = item.get('ratingKey')
kodiInfo = plex_db.getItem_byId(ratingKey) kodiInfo = plex_db.getItem_byId(ratingKey)
@ -1335,8 +1333,7 @@ class LibrarySync(Thread):
} }
else: else:
# PMS is ours - get all current sessions # PMS is ours - get all current sessions
self.sessionKeys = GetPMSStatus( self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN)
window('plex_token'))
log.debug('Updated current sessions. They are: %s' log.debug('Updated current sessions. They are: %s'
% self.sessionKeys) % self.sessionKeys)
if sessionKey not in self.sessionKeys: if sessionKey not in self.sessionKeys:
@ -1349,20 +1346,19 @@ class LibrarySync(Thread):
# Identify the user - same one as signed on with PKC? Skip # Identify the user - same one as signed on with PKC? Skip
# update if neither session's username nor userid match # update if neither session's username nor userid match
# (Owner sometime's returns id '1', not always) # (Owner sometime's returns id '1', not always)
if (window('plex_token') == '' and if (not state.PLEX_TOKEN and currSess['userId'] == '1'):
currSess['userId'] == '1'):
# PKC not signed in to plex.tv. Plus owner of PMS is # PKC not signed in to plex.tv. Plus owner of PMS is
# playing (the '1'). # playing (the '1').
# Hence must be us (since several users require plex.tv # Hence must be us (since several users require plex.tv
# token for PKC) # token for PKC)
pass pass
elif not (currSess['userId'] == window('currUserId') elif not (currSess['userId'] == state.PLEX_USER_ID
or or
currSess['username'] == window('plex_username')): currSess['username'] == state.PLEX_USERNAME):
log.debug('Our username %s, userid %s did not match ' log.debug('Our username %s, userid %s did not match '
'the session username %s with userid %s' 'the session username %s with userid %s'
% (window('plex_username'), % (state.PLEX_USERNAME,
window('currUserId'), state.PLEX_USER_ID,
currSess['username'], currSess['username'],
currSess['userId'])) currSess['userId']))
continue continue
@ -1394,14 +1390,14 @@ class LibrarySync(Thread):
'file_id': kodiInfo[1], 'file_id': kodiInfo[1],
'kodi_type': kodiInfo[4], 'kodi_type': kodiInfo[4],
'viewOffset': resume, 'viewOffset': resume,
'state': state, 'state': status,
'duration': currSess['duration'], 'duration': currSess['duration'],
'viewCount': currSess['viewCount'], 'viewCount': currSess['viewCount'],
'lastViewedAt': DateToKodi(getUnixTimestamp()) 'lastViewedAt': DateToKodi(getUnixTimestamp())
}) })
log.debug('Update playstate for user %s with id %s: %s' log.debug('Update playstate for user %s with id %s: %s'
% (window('plex_username'), % (state.PLEX_USERNAME,
window('currUserId'), state.PLEX_USER_ID,
items[-1])) items[-1]))
# Now tell Kodi where we are # Now tell Kodi where we are
for item in items: for item in items:
@ -1433,6 +1429,7 @@ class LibrarySync(Thread):
try: try:
self.run_internal() self.run_internal()
except Exception as e: except Exception as e:
state.DB_SCAN = False
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
log.error('LibrarySync thread crashed. Error message: %s' % e) log.error('LibrarySync thread crashed. Error message: %s' % e)
import traceback import traceback
@ -1443,8 +1440,8 @@ class LibrarySync(Thread):
def run_internal(self): def run_internal(self):
# Re-assign handles to have faster calls # Re-assign handles to have faster calls
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
installSyncDone = self.installSyncDone installSyncDone = self.installSyncDone
enableBackgroundSync = self.enableBackgroundSync enableBackgroundSync = self.enableBackgroundSync
fullSync = self.fullSync fullSync = self.fullSync
@ -1470,18 +1467,15 @@ class LibrarySync(Thread):
# Ensure that DBs exist if called for very first time # Ensure that DBs exist if called for very first time
self.initializeDBs() self.initializeDBs()
if self.enableMusic:
advancedsettings_tweaks()
if settings('FanartTV') == 'true': if settings('FanartTV') == 'true':
self.fanartthread.start() self.fanartthread.start()
while not threadStopped(): while not thread_stopped():
# In the event the server goes offline # In the event the server goes offline
while threadSuspended(): while thread_suspended():
# Set in service.py # Set in service.py
if threadStopped(): if thread_stopped():
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("###===--- LibrarySync Stopped ---===###") log.info("###===--- LibrarySync Stopped ---===###")
return return
@ -1513,7 +1507,7 @@ class LibrarySync(Thread):
# Also runs when first installed # Also runs when first installed
# Verify the video database can be found # Verify the video database can be found
videoDb = v.DB_VIDEO_PATH videoDb = v.DB_VIDEO_PATH
if not exists(videoDb): if not exists(tryEncode(videoDb)):
# Database does not exists # Database does not exists
log.error("The current Kodi version is incompatible " log.error("The current Kodi version is incompatible "
"to know which Kodi versions are supported.") "to know which Kodi versions are supported.")
@ -1523,6 +1517,7 @@ class LibrarySync(Thread):
self.dialog.ok(heading=lang(29999), line1=lang(39403)) self.dialog.ok(heading=lang(29999), line1=lang(39403))
break break
# Run start up sync # Run start up sync
state.DB_SCAN = True
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
log.info("Db version: %s" % settings('dbCreatedWithVersion')) log.info("Db version: %s" % settings('dbCreatedWithVersion'))
lastTimeSync = getUnixTimestamp() lastTimeSync = getUnixTimestamp()
@ -1547,6 +1542,7 @@ class LibrarySync(Thread):
log.info("Initial start-up full sync starting") log.info("Initial start-up full sync starting")
librarySync = fullSync() librarySync = fullSync()
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
if librarySync: if librarySync:
log.info("Initial start-up full sync successful") log.info("Initial start-up full sync successful")
startupComplete = True startupComplete = True
@ -1565,23 +1561,26 @@ class LibrarySync(Thread):
break break
# Currently no db scan, so we can start a new scan # Currently no db scan, so we can start a new scan
elif window('plex_dbScan') != "true": elif state.DB_SCAN is False:
# Full scan was requested from somewhere else, e.g. userclient # Full scan was requested from somewhere else, e.g. userclient
if window('plex_runLibScan') in ("full", "repair"): if window('plex_runLibScan') in ("full", "repair"):
log.info('Full library scan requested, starting') log.info('Full library scan requested, starting')
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
state.DB_SCAN = True
if window('plex_runLibScan') == "full": if window('plex_runLibScan') == "full":
fullSync() fullSync()
elif window('plex_runLibScan') == "repair": elif window('plex_runLibScan') == "repair":
fullSync(repair=True) fullSync(repair=True)
window('plex_runLibScan', clear=True) window('plex_runLibScan', clear=True)
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Full library sync finished # Full library sync finished
self.showKodiNote(lang(39407), forced=False) self.showKodiNote(lang(39407), forced=False)
# Reset views was requested from somewhere else # Reset views was requested from somewhere else
elif window('plex_runLibScan') == "views": elif window('plex_runLibScan') == "views":
log.info('Refresh playlist and nodes requested, starting') log.info('Refresh playlist and nodes requested, starting')
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
state.DB_SCAN = True
window('plex_runLibScan', clear=True) window('plex_runLibScan', clear=True)
# First remove playlists # First remove playlists
@ -1602,6 +1601,7 @@ class LibrarySync(Thread):
forced=True, forced=True,
icon="error") icon="error")
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif window('plex_runLibScan') == 'fanart': elif window('plex_runLibScan') == 'fanart':
window('plex_runLibScan', clear=True) window('plex_runLibScan', clear=True)
# Only look for missing fanart (No) # Only look for missing fanart (No)
@ -1613,31 +1613,37 @@ class LibrarySync(Thread):
yeslabel=lang(39225))) yeslabel=lang(39225)))
elif window('plex_runLibScan') == 'del_textures': elif window('plex_runLibScan') == 'del_textures':
window('plex_runLibScan', clear=True) window('plex_runLibScan', clear=True)
state.DB_SCAN = True
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
import artwork import artwork
artwork.Artwork().fullTextureCacheSync() artwork.Artwork().fullTextureCacheSync()
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
else: else:
now = getUnixTimestamp() now = getUnixTimestamp()
if (now - lastSync > fullSyncInterval and if (now - lastSync > fullSyncInterval and
not xbmcplayer.isPlaying()): not xbmcplayer.isPlaying()):
lastSync = now lastSync = now
log.info('Doing scheduled full library scan') log.info('Doing scheduled full library scan')
state.DB_SCAN = True
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
if fullSync() is False and not threadStopped(): if fullSync() is False and not thread_stopped():
log.error('Could not finish scheduled full sync') log.error('Could not finish scheduled full sync')
self.showKodiNote(lang(39410), self.showKodiNote(lang(39410),
forced=True, forced=True,
icon='error') icon='error')
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
# Full library sync finished # Full library sync finished
self.showKodiNote(lang(39407), forced=False) self.showKodiNote(lang(39407), forced=False)
elif now - lastTimeSync > oneDay: elif now - lastTimeSync > oneDay:
lastTimeSync = now lastTimeSync = now
log.info('Starting daily time sync') log.info('Starting daily time sync')
state.DB_SCAN = True
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
self.syncPMStime() self.syncPMStime()
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif enableBackgroundSync: elif enableBackgroundSync:
# Check back whether we should process something # Check back whether we should process something
# Only do this once every while (otherwise, potentially # Only do this once every while (otherwise, potentially

View file

@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import Thread
from Queue import Queue
from xbmc import sleep
from utils import window, ThreadMethods
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethods
class Monitor_Kodi_Play(Thread):
"""
Monitors for new plays initiated on the Kodi side with addon paths.
Immediately throws them into a queue to be processed by playback_starter
"""
# Borg - multiple instances, shared state
def __init__(self, callback=None):
self.mgr = callback
self.playback_queue = Queue()
Thread.__init__(self)
def run(self):
threadStopped = self.threadStopped
queue = self.playback_queue
log.info("----===## Starting Kodi_Play_Client ##===----")
while not threadStopped():
if window('plex_play_new_item'):
queue.put(window('plex_play_new_item'))
window('plex_play_new_item', clear=True)
else:
sleep(20)
# Put one last item into the queue to let playback_starter end
queue.put(None)
log.info("----===## Kodi_Play_Client stopped ##===----")

View file

@ -17,6 +17,7 @@ import variables as v
from downloadutils import DownloadUtils from downloadutils import DownloadUtils
from PKC_listitem import convert_PKC_to_listitem from PKC_listitem import convert_PKC_to_listitem
import plexdb_functions as plexdb import plexdb_functions as plexdb
import state
############################################################################### ###############################################################################
log = logging.getLogger("PLEX."+__name__) log = logging.getLogger("PLEX."+__name__)
@ -39,7 +40,7 @@ class Playback_Starter(Thread):
""" """
log.info("Process_play called with plex_id %s, kodi_id %s" log.info("Process_play called with plex_id %s, kodi_id %s"
% (plex_id, kodi_id)) % (plex_id, kodi_id))
if window('plex_authenticated') != "true": if not state.AUTHENTICATED:
log.error('Not yet authenticated for PMS, abort starting playback') log.error('Not yet authenticated for PMS, abort starting playback')
# Todo: Warn user with dialog # Todo: Warn user with dialog
return return
@ -152,12 +153,12 @@ class Playback_Starter(Thread):
pickle_me(result) pickle_me(result)
def run(self): def run(self):
queue = self.mgr.monitor_kodi_play.playback_queue queue = self.mgr.command_pipeline.playback_queue
log.info("----===## Starting Playback_Starter ##===----") log.info("----===## Starting Playback_Starter ##===----")
while True: while True:
item = queue.get() item = queue.get()
if item is None: if item is None:
# Need to shutdown - initiated by monitor_kodi_play # Need to shutdown - initiated by command_pipeline
break break
else: else:
self.triage(item) self.triage(item)

View file

@ -22,6 +22,7 @@ from playlist_func import add_item_to_kodi_playlist, \
from pickler import Playback_Successful from pickler import Playback_Successful
from plexdb_functions import Get_Plex_DB from plexdb_functions import Get_Plex_DB
import variables as v import variables as v
import state
############################################################################### ###############################################################################
@ -187,7 +188,7 @@ class PlaybackUtils():
kodi_type) kodi_type)
elif contextmenu_play: elif contextmenu_play:
if window('useDirectPaths') == 'true': if state.DIRECT_PATHS:
# Cannot add via JSON with full metadata because then we # Cannot add via JSON with full metadata because then we
# Would be using the direct path # Would be using the direct path
log.debug("Adding contextmenu item for direct paths") log.debug("Adding contextmenu item for direct paths")

View file

@ -13,6 +13,7 @@ import downloadutils
import plexdb_functions as plexdb import plexdb_functions as plexdb
import kodidb_functions as kodidb import kodidb_functions as kodidb
import variables as v import variables as v
import state
############################################################################### ###############################################################################
@ -308,6 +309,9 @@ class Player(xbmc.Player):
'plex_playbackProps', 'plex_playbackProps',
'plex_forcetranscode'): 'plex_forcetranscode'):
window(item, clear=True) window(item, clear=True)
# We might have saved a transient token from a user flinging media via
# Companion
state.PLEX_TRANSIENT_TOKEN = None
log.debug("Cleared playlist properties.") log.debug("Cleared playlist properties.")
def onPlayBackEnded(self): def onPlayBackEnded(self):

View file

@ -5,7 +5,7 @@ from threading import RLock, Thread
from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend from utils import window, thread_methods
import playlist_func as PL import playlist_func as PL
from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren
from PlexAPI import API from PlexAPI import API
@ -21,8 +21,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('plex_serverStatus') @thread_methods(add_suspends=['PMS_STATUS'])
@ThreadMethods
class Playqueue(Thread): class Playqueue(Thread):
""" """
Monitors Kodi's playqueues for changes on the Kodi side Monitors Kodi's playqueues for changes on the Kodi side
@ -147,20 +146,24 @@ class Playqueue(Thread):
index = list(range(0, len(old))) index = list(range(0, len(old)))
log.debug('Comparing new Kodi playqueue %s with our play queue %s' log.debug('Comparing new Kodi playqueue %s with our play queue %s'
% (new, old)) % (new, old))
if self.thread_stopped():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
for i, new_item in enumerate(new): for i, new_item in enumerate(new):
if (new_item['file'].startswith('plugin://') and if (new_item['file'].startswith('plugin://') and
not new_item['file'].startswith(PLUGIN)): not new_item['file'].startswith(PLUGIN)):
# Ignore new media added by other addons # Ignore new media added by other addons
continue continue
for j, old_item in enumerate(old): for j, old_item in enumerate(old):
if self.threadStopped(): try:
# Chances are that we got an empty Kodi playlist due to if (old_item.file.startswith('plugin://') and
# Kodi exit not old_item['file'].startswith(PLUGIN)):
return # Ignore media by other addons
if (old_item['file'].startswith('plugin://') and continue
not old_item['file'].startswith(PLUGIN)): except (TypeError, AttributeError):
# Ignore media by other addons # were not passed a filename; ignore
continue pass
if new_item.get('id') is None: if new_item.get('id') is None:
identical = old_item.file == new_item['file'] identical = old_item.file == new_item['file']
else: else:
@ -193,8 +196,8 @@ class Playqueue(Thread):
log.debug('Done comparing playqueues') log.debug('Done comparing playqueues')
def run(self): def run(self):
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
log.info("----===## Starting PlayQueue client ##===----") log.info("----===## Starting PlayQueue client ##===----")
# Initialize the playqueues, if Kodi already got items in them # Initialize the playqueues, if Kodi already got items in them
for playqueue in self.playqueues: for playqueue in self.playqueues:
@ -203,9 +206,9 @@ class Playqueue(Thread):
PL.init_Plex_playlist(playqueue, kodi_item=item) PL.init_Plex_playlist(playqueue, kodi_item=item)
else: else:
PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item) PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item)
while not threadStopped(): while not thread_stopped():
while threadSuspended(): while thread_suspended():
if threadStopped(): if thread_stopped():
break break
sleep(1000) sleep(1000)
with lock: with lock:

View file

@ -24,8 +24,6 @@ class PlayUtils():
self.API = PlexAPI.API(item) self.API = PlexAPI.API(item)
self.doUtils = DownloadUtils().downloadUrl self.doUtils = DownloadUtils().downloadUrl
self.userid = window('currUserId')
self.server = window('pms_server')
self.machineIdentifier = window('plex_machineIdentifier') self.machineIdentifier = window('plex_machineIdentifier')
def getPlayUrl(self, partNumber=None): def getPlayUrl(self, partNumber=None):
@ -335,7 +333,8 @@ class PlayUtils():
# We don't know the language - no need to download # We don't know the language - no need to download
else: else:
path = self.API.addPlexCredentialsToUrl( path = self.API.addPlexCredentialsToUrl(
"%s%s" % (self.server, stream.attrib['key'])) "%s%s" % (window('pms_server'),
stream.attrib['key']))
downloadable_streams.append(index) downloadable_streams.append(index)
download_subs.append(tryEncode(path)) download_subs.append(tryEncode(path))
else: else:

View file

@ -163,7 +163,7 @@ class MyHandler(BaseHTTPRequestHandler):
else: else:
# Throw it to companion.py # Throw it to companion.py
process_command(request_path, params, self.server.queue) process_command(request_path, params, self.server.queue)
self.response(getOKMsg(), js.getPlexHeaders()) self.response('', js.getPlexHeaders())
subMgr.notify() subMgr.notify()
except: except:
log.error('Error encountered. Traceback:') log.error('Error encountered. Traceback:')

View file

@ -3,8 +3,10 @@ import re
import threading import threading
import downloadutils import downloadutils
from clientinfo import getXArgsDeviceInfo
from utils import window from utils import window
import PlexFunctions as pf import PlexFunctions as pf
import state
from functions import * from functions import *
############################################################################### ###############################################################################
@ -68,19 +70,16 @@ class SubscriptionManager:
info = self.getPlayerProperties(playerid) info = self.getPlayerProperties(playerid)
# save this info off so the server update can use it too # save this info off so the server update can use it too
self.playerprops[playerid] = info; self.playerprops[playerid] = info;
state = info['state'] status = info['state']
time = info['time'] time = info['time']
else: else:
state = "stopped" status = "stopped"
time = 0 time = 0
ret = "\n"+' <Timeline state="%s" time="%s" type="%s"' % (state, time, ptype) ret = "\n"+' <Timeline state="%s" time="%s" type="%s"' % (status, time, ptype)
if playerid is None: if playerid is None:
ret += ' seekRange="0-0"'
ret += ' />' ret += ' />'
return ret return ret
# pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server'))
# userId = str(WINDOW.getProperty('currUserId'))
pbmc_server = window('pms_server') pbmc_server = window('pms_server')
if pbmc_server: if pbmc_server:
(self.protocol, self.server, self.port) = \ (self.protocol, self.server, self.port) = \
@ -112,7 +111,6 @@ class SubscriptionManager:
ret += ' containerKey="%s"' % self.containerKey ret += ' containerKey="%s"' % self.containerKey
ret += ' duration="%s"' % info['duration'] ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable() ret += ' controllable="%s"' % self.controllable()
ret += ' machineIdentifier="%s"' % serv.get('uuid', "") ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
ret += ' protocol="%s"' % serv.get('protocol', "http") ret += ' protocol="%s"' % serv.get('protocol', "http")
@ -123,6 +121,8 @@ class SubscriptionManager:
ret += ' mute="%s"' % self.mute ret += ' mute="%s"' % self.mute
ret += ' repeat="%s"' % info['repeat'] ret += ' repeat="%s"' % info['repeat']
ret += ' itemType="%s"' % info['itemType'] ret += ' itemType="%s"' % info['itemType']
if state.PLEX_TRANSIENT_TOKEN:
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
# Might need an update in the future # Might need an update in the future
if ptype == 'video': if ptype == 'video':
ret += ' subtitleStreamID="-1"' ret += ' subtitleStreamID="-1"'
@ -167,9 +167,10 @@ class SubscriptionManager:
# Process the players we have left (to signal a stop) # Process the players we have left (to signal a stop)
for typus, p in self.lastplayers.iteritems(): for typus, p in self.lastplayers.iteritems():
self.lastinfo[typus]['state'] = 'stopped' self.lastinfo[typus]['state'] = 'stopped'
self._sendNotification(self.lastinfo[typus]) # self._sendNotification(self.lastinfo[typus])
def _sendNotification(self, info): def _sendNotification(self, info):
xargs = getXArgsDeviceInfo()
params = { params = {
'containerKey': self.containerKey or "/library/metadata/900000", 'containerKey': self.containerKey or "/library/metadata/900000",
'key': self.lastkey or "/library/metadata/900000", 'key': self.lastkey or "/library/metadata/900000",
@ -178,6 +179,8 @@ class SubscriptionManager:
'time': info['time'], 'time': info['time'],
'duration': info['duration'] 'duration': info['duration']
} }
if state.PLEX_TRANSIENT_TOKEN:
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
if info.get('playQueueID'): if info.get('playQueueID'):
params['containerKey'] = '/playQueues/%s' % info['playQueueID'] params['containerKey'] = '/playQueues/%s' % info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion'] params['playQueueVersion'] = info['playQueueVersion']
@ -186,7 +189,7 @@ class SubscriptionManager:
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'), serv.get('server', 'localhost'),
serv.get('port', '32400')) serv.get('port', '32400'))
self.doUtils(url, parameters=params) self.doUtils(url, parameters=params, headerOptions=xargs)
log.debug("Sent server notification with parameters: %s to %s" log.debug("Sent server notification with parameters: %s to %s"
% (params, url)) % (params, url))

View file

@ -1,574 +0,0 @@
# -*- coding: utf-8 -*-
#################################################################################################
import logging
import xbmc
import downloadutils
from utils import window, settings, kodiSQL
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
#################################################################################################
class Read_EmbyServer():
limitIndex = int(settings('limitindex'))
def __init__(self):
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.userId = window('emby_currUser')
self.server = window('emby_server%s' % self.userId)
def split_list(self, itemlist, size):
# Split up list in pieces of size. Will generate a list of lists
return [itemlist[i:i+size] for i in range(0, len(itemlist), size)]
def getItem(self, itemid):
# This will return the full item
item = {}
result = self.doUtils("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid)
if result:
item = result
return item
def getItems(self, itemlist):
items = []
itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists:
# Will return basic information
params = {
'Ids': ",".join(itemlist),
'Fields': "Etag"
}
url = "{server}/emby/Users/{UserId}/Items?&format=json"
result = self.doUtils(url, parameters=params)
if result:
items.extend(result['Items'])
return items
def getFullItems(self, itemlist):
items = []
itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists:
params = {
"Ids": ",".join(itemlist),
"Fields": (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount"
)
}
url = "{server}/emby/Users/{UserId}/Items?format=json"
result = self.doUtils(url, parameters=params)
if result:
items.extend(result['Items'])
return items
def getView_embyId(self, itemid):
# Returns ancestors using embyId
viewId = None
url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid
for view in self.doUtils(url):
if view['Type'] == "CollectionFolder":
# Found view
viewId = view['Id']
# Compare to view table in emby database
emby = kodiSQL('plex')
cursor_emby = emby.cursor()
query = ' '.join((
"SELECT view_name, media_type",
"FROM view",
"WHERE view_id = ?"
))
cursor_emby.execute(query, (viewId,))
result = cursor_emby.fetchone()
try:
viewName = result[0]
mediatype = result[1]
except TypeError:
viewName = None
mediatype = None
cursor_emby.close()
return [viewName, viewId, mediatype]
def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True,
limit=None, sortorder="Ascending", filter_type=""):
params = {
'ParentId': parentid,
'IncludeItemTypes': itemtype,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'IsMissing': False,
'Recursive': recursive,
'Limit': limit,
'SortBy': sortby,
'SortOrder': sortorder,
'Filters': filter,
'Fields': (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers"
)
}
return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
def getTvChannels(self):
params = {
'EnableImages': True,
'Fields': (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers"
)
}
url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json"
return self.doUtils(url, parameters=params)
def getTvRecordings(self, groupid):
if groupid == "root":
groupid = ""
params = {
'GroupId': groupid,
'EnableImages': True,
'Fields': (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers"
)
}
url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
return self.doUtils(url, parameters=params)
def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
items = {
'Items': [],
'TotalRecordCount': 0
}
# Get total number of items
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = {
'ParentId': parentid,
'IncludeItemTypes': itemtype,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'IsMissing': False,
'Recursive': True,
'Limit': 1
}
result = self.doUtils(url, parameters=params)
try:
total = result['TotalRecordCount']
items['TotalRecordCount'] = total
except TypeError: # Failed to retrieve
log.debug("%s:%s Failed to retrieve the server response." % (url, params))
else:
index = 0
jump = self.limitIndex
throttled = False
highestjump = 0
while index < total:
# Get items by chunk to increase retrieval speed at scale
params = {
'ParentId': parentid,
'IncludeItemTypes': itemtype,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'IsMissing': False,
'Recursive': True,
'StartIndex': index,
'Limit': jump,
'SortBy': sortby,
'SortOrder': "Ascending",
}
if basic:
params['Fields'] = "Etag"
else:
params['Fields'] = (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount"
)
result = self.doUtils(url, parameters=params)
try:
items['Items'].extend(result['Items'])
except TypeError:
# Something happened to the connection
if not throttled:
throttled = True
log.info("Throttle activated.")
if jump == highestjump:
# We already tried with the highestjump, but it failed. Reset value.
log.info("Reset highest value.")
highestjump = 0
# Lower the number by half
if highestjump:
throttled = False
jump = highestjump
log.info("Throttle deactivated.")
else:
jump = int(jump/4)
log.debug("Set jump limit to recover: %s" % jump)
retry = 0
while window('emby_online') != "true":
# Wait server to come back online
if retry == 5:
log.info("Unable to reconnect to server. Abort process.")
return items
retry += 1
if xbmc.Monitor().waitForAbort(1):
# Abort was requested while waiting.
return items
else:
# Request succeeded
index += jump
if dialog:
percentage = int((float(index) / float(total))*100)
dialog.update(percentage)
if jump > highestjump:
# Adjust with the latest number, if it's greater
highestjump = jump
if throttled:
# We needed to adjust the number of item requested.
# keep increasing until the connection times out again
# to find the highest value
increment = int(jump*0.33)
if not increment: # Incase the increment is 0
increment = 10
jump += increment
log.info("Increase jump limit to: %s" % jump)
return items
def getViews(self, mediatype="", root=False, sortedlist=False):
# Build a list of user views
views = []
mediatype = mediatype.lower()
if not root:
url = "{server}/emby/Users/{UserId}/Views?format=json"
else: # Views ungrouped
url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json"
result = self.doUtils(url)
try:
items = result['Items']
except TypeError:
log.debug("Error retrieving views for type: %s" % mediatype)
else:
for item in items:
item['Name'] = item['Name']
if item['Type'] == "Channel":
# Filter view types
continue
# 3/4/2016 OriginalCollectionType is added
itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed"))
# 11/29/2015 Remove this once OriginalCollectionType is added to stable server.
# Assumed missing is mixed then.
'''if itemtype is None:
url = "{server}/emby/Library/MediaFolders?format=json"
result = self.doUtils(url)
for folder in result['Items']:
if item['Id'] == folder['Id']:
itemtype = folder.get('CollectionType', "mixed")'''
if item['Name'] not in ('Collections', 'Trailers'):
if sortedlist:
views.append({
'name': item['Name'],
'type': itemtype,
'id': item['Id']
})
elif (itemtype == mediatype or
(itemtype == "mixed" and mediatype in ("movies", "tvshows"))):
views.append({
'name': item['Name'],
'type': itemtype,
'id': item['Id']
})
return views
def verifyView(self, parentid, itemid):
belongs = False
params = {
'ParentId': parentid,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'IsMissing': False,
'Recursive': True,
'Ids': itemid
}
result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
try:
total = result['TotalRecordCount']
except TypeError:
# Something happened to the connection
pass
else:
if total:
belongs = True
return belongs
def getMovies(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
def getBoxset(self, dialog=None):
return self.getSection(None, "BoxSet", dialog=dialog)
def getMovies_byBoxset(self, boxsetid):
return self.getSection(boxsetid, "Movie")
def getMusicVideos(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
def getHomeVideos(self, parentId):
return self.getSection(parentId, "Video")
def getShows(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "Series", basic=basic, dialog=dialog)
def getSeasons(self, showId):
items = {
'Items': [],
'TotalRecordCount': 0
}
params = {
'IsVirtualUnaired': False,
'Fields': "Etag"
}
url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId
result = self.doUtils(url, parameters=params)
if result:
items = result
return items
def getEpisodes(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
def getEpisodesbyShow(self, showId):
return self.getSection(showId, "Episode")
def getEpisodesbySeason(self, seasonId):
return self.getSection(seasonId, "Episode")
def getArtists(self, dialog=None):
items = {
'Items': [],
'TotalRecordCount': 0
}
# Get total number of items
url = "{server}/emby/Artists?UserId={UserId}&format=json"
params = {
'Recursive': True,
'Limit': 1
}
result = self.doUtils(url, parameters=params)
try:
total = result['TotalRecordCount']
items['TotalRecordCount'] = total
except TypeError: # Failed to retrieve
log.debug("%s:%s Failed to retrieve the server response." % (url, params))
else:
index = 1
jump = self.limitIndex
while index < total:
# Get items by chunk to increase retrieval speed at scale
params = {
'Recursive': True,
'IsVirtualUnaired': False,
'IsMissing': False,
'StartIndex': index,
'Limit': jump,
'SortBy': "SortName",
'SortOrder': "Ascending",
'Fields': (
"Etag,Genres,SortName,Studios,Writer,ProductionYear,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore,"
"AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview"
)
}
result = self.doUtils(url, parameters=params)
items['Items'].extend(result['Items'])
index += jump
if dialog:
percentage = int((float(index) / float(total))*100)
dialog.update(percentage)
return items
def getAlbums(self, basic=False, dialog=None):
return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
def getAlbumsbyArtist(self, artistId):
return self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
def getSongs(self, basic=False, dialog=None):
return self.getSection(None, "Audio", basic=basic, dialog=dialog)
def getSongsbyAlbum(self, albumId):
return self.getSection(albumId, "Audio")
def getAdditionalParts(self, itemId):
items = {
'Items': [],
'TotalRecordCount': 0
}
url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId
result = self.doUtils(url)
if result:
items = result
return items
def sortby_mediatype(self, itemids):
sorted_items = {}
# Sort items
items = self.getFullItems(itemids)
for item in items:
mediatype = item.get('Type')
if mediatype:
sorted_items.setdefault(mediatype, []).append(item)
return sorted_items
def updateUserRating(self, itemid, favourite=None):
# Updates the user rating to Emby
doUtils = self.doUtils
if favourite:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
doUtils(url, action_type="POST")
elif not favourite:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
doUtils(url, action_type="DELETE")
else:
log.info("Error processing user rating.")
log.info("Update user rating to emby for itemid: %s | favourite: %s" % (itemid, favourite))
def refreshItem(self, itemid):
url = "{server}/emby/Items/%s/Refresh?format=json" % itemid
params = {
'Recursive': True,
'ImageRefreshMode': "FullRefresh",
'MetadataRefreshMode': "FullRefresh",
'ReplaceAllImages': False,
'ReplaceAllMetadata': True
}
self.doUtils(url, postBody=params, action_type="POST")
def deleteItem(self, itemid):
url = "{server}/emby/Items/%s?format=json" % itemid
self.doUtils(url, action_type="DELETE")

36
resources/lib/state.py Normal file
View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# THREAD SAFE
# Quit PKC
STOP_PKC = False
# Usually triggered by another Python instance - will have to be set (by
# polling window) through e.g. librarysync thread
SUSPEND_LIBRARY_THREAD = False
# Set if user decided to cancel sync
STOP_SYNC = False
# Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true'
DB_SCAN = False
# Plex Media Server Status - along with window('plex_serverStatus')
PMS_STATUS = False
# When the userclient needs to wait
SUSPEND_USER_CLIENT = False
# Plex home user? Then "False". Along with window('plex_restricteduser')
RESTRICTED_USER = False
# Direct Paths (True) or Addon Paths (False)? Along with
# window('useDirectPaths')
DIRECT_PATHS = False
# Along with window('plex_authenticated')
AUTHENTICATED = False
# plex.tv username
PLEX_USERNAME = None
# Token for that user for plex.tv
PLEX_TOKEN = None
# Plex ID of that user (e.g. for plex.tv) as a STRING
PLEX_USER_ID = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
PLEX_TRANSIENT_TOKEN = None

View file

@ -10,12 +10,12 @@ import xbmcaddon
from xbmcvfs import exists from xbmcvfs import exists
from utils import window, settings, language as lang, ThreadMethods, \ from utils import window, settings, language as lang, thread_methods
ThreadMethodsAdditionalSuspend
import downloadutils import downloadutils
import PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier from PlexFunctions import GetMachineIdentifier
import state
############################################################################### ###############################################################################
@ -24,8 +24,7 @@ log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_Userclient') @thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
@ThreadMethods
class UserClient(threading.Thread): class UserClient(threading.Thread):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
@ -40,7 +39,6 @@ class UserClient(threading.Thread):
self.retry = 0 self.retry = 0
self.currUser = None self.currUser = None
self.currUserId = None
self.currServer = None self.currServer = None
self.currToken = None self.currToken = None
self.HasAccess = True self.HasAccess = True
@ -118,37 +116,19 @@ class UserClient(threading.Thread):
def hasAccess(self): def hasAccess(self):
# Plex: always return True for now # Plex: always return True for now
return True return True
# hasAccess is verified in service.py
url = "{server}/emby/Users?format=json"
result = self.doUtils.downloadUrl(url)
if result is False:
# Access is restricted, set in downloadutils.py via exception
log.info("Access is restricted.")
self.HasAccess = False
elif window('plex_online') != "true":
# Server connection failed
pass
elif window('plex_serverStatus') == "restricted":
log.info("Access is granted.")
self.HasAccess = True
window('plex_serverStatus', clear=True)
xbmcgui.Dialog().notification(lang(29999),
lang(33007))
def loadCurrUser(self, username, userId, usertoken, authenticated=False): def loadCurrUser(self, username, userId, usertoken, authenticated=False):
log.debug('Loading current user') log.debug('Loading current user')
doUtils = self.doUtils doUtils = self.doUtils
self.currUserId = userId
self.currToken = usertoken self.currToken = usertoken
self.currServer = self.getServer() self.currServer = self.getServer()
self.ssl = self.getSSLverify() self.ssl = self.getSSLverify()
self.sslcert = self.getSSL() self.sslcert = self.getSSL()
if authenticated is False: if authenticated is False:
if self.currServer is None:
return False
log.debug('Testing validity of current token') log.debug('Testing validity of current token')
res = PlexAPI.PlexAPI().CheckConnection(self.currServer, res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
token=self.currToken, token=self.currToken,
@ -164,21 +144,27 @@ class UserClient(threading.Thread):
return False return False
# Set to windows property # Set to windows property
window('currUserId', value=userId) state.PLEX_USER_ID = userId or None
window('plex_username', value=username) state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '') # This is the token for the current PMS (might also be '')
window('pms_token', value=self.currToken) window('pms_token', value=self.currToken)
# This is the token for plex.tv for the current user # This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv # Is only '' if user is not signed in to plex.tv
window('plex_token', value=settings('plexToken')) window('plex_token', value=settings('plexToken'))
state.PLEX_TOKEN = settings('plexToken') or None
window('plex_restricteduser', value=settings('plex_restricteduser')) window('plex_restricteduser', value=settings('plex_restricteduser'))
state.RESTRICTED_USER = True \
if settings('plex_restricteduser') == 'true' else False
window('pms_server', value=self.currServer) window('pms_server', value=self.currServer)
window('plex_machineIdentifier', value=self.machineIdentifier) window('plex_machineIdentifier', value=self.machineIdentifier)
window('plex_servername', value=self.servername) window('plex_servername', value=self.servername)
window('plex_authenticated', value='true') window('plex_authenticated', value='true')
state.AUTHENTICATED = True
window('useDirectPaths', value='true' window('useDirectPaths', value='true'
if settings('useDirectPaths') == "1" else 'false') if settings('useDirectPaths') == "1" else 'false')
state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \
else False
window('plex_force_transcode_pix', value='true' window('plex_force_transcode_pix', value='true'
if settings('force_transcode_pix') == "1" else 'false') if settings('force_transcode_pix') == "1" else 'false')
@ -202,7 +188,7 @@ class UserClient(threading.Thread):
# Give attempts at entering password / selecting user # Give attempts at entering password / selecting user
if self.retry >= 2: if self.retry >= 2:
log.error("Too many retries to login.") log.error("Too many retries to login.")
window('plex_serverStatus', value="Stop") state.PMS_STATUS = 'Stop'
dialog.ok(lang(33001), dialog.ok(lang(33001),
lang(39023)) lang(39023))
xbmc.executebuiltin( xbmc.executebuiltin(
@ -283,14 +269,17 @@ class UserClient(threading.Thread):
self.doUtils.stopSession() self.doUtils.stopSession()
window('plex_authenticated', clear=True) window('plex_authenticated', clear=True)
state.AUTHENTICATED = False
window('pms_token', clear=True) window('pms_token', clear=True)
state.PLEX_TOKEN = None
window('plex_token', clear=True) window('plex_token', clear=True)
window('pms_server', clear=True) window('pms_server', clear=True)
window('plex_machineIdentifier', clear=True) window('plex_machineIdentifier', clear=True)
window('plex_servername', clear=True) window('plex_servername', clear=True)
window('currUserId', clear=True) state.PLEX_USER_ID = None
window('plex_username', clear=True) state.PLEX_USERNAME = None
window('plex_restricteduser', clear=True) window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False
settings('username', value='') settings('username', value='')
settings('userid', value='') settings('userid', value='')
@ -298,44 +287,42 @@ class UserClient(threading.Thread):
# Reset token in downloads # Reset token in downloads
self.doUtils.setToken('') self.doUtils.setToken('')
self.doUtils.setUserId('')
self.doUtils.setUsername('')
self.currToken = None self.currToken = None
self.auth = True self.auth = True
self.currUser = None self.currUser = None
self.currUserId = None
self.retry = 0 self.retry = 0
def run(self): def run(self):
log.info("----===## Starting UserClient ##===----") log.info("----===## Starting UserClient ##===----")
while not self.threadStopped(): thread_stopped = self.thread_stopped
while self.threadSuspended(): thread_suspended = self.thread_suspended
if self.threadStopped(): while not thread_stopped():
while thread_suspended():
if thread_stopped():
break break
xbmc.sleep(1000) xbmc.sleep(1000)
status = window('plex_serverStatus') if state.PMS_STATUS == "Stop":
if status == "Stop":
xbmc.sleep(500) xbmc.sleep(500)
continue continue
# Verify the connection status to server # Verify the connection status to server
elif status == "restricted": elif state.PMS_STATUS == "restricted":
# Parental control is restricting access # Parental control is restricting access
self.HasAccess = False self.HasAccess = False
elif status == "401": elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token # Unauthorized access, revoke token
window('plex_serverStatus', value="Auth") state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth')
self.resetClient() self.resetClient()
xbmc.sleep(2000) xbmc.sleep(3000)
if self.auth and (self.currUser is None): if self.auth and (self.currUser is None):
# Try to authenticate user # Try to authenticate user
if not status or status == "Auth": if not state.PMS_STATUS or state.PMS_STATUS == "Auth":
# Set auth flag because we no longer need # Set auth flag because we no longer need
# to authenticate the user # to authenticate the user
self.auth = False self.auth = False
@ -343,10 +330,11 @@ class UserClient(threading.Thread):
# Successfully authenticated and loaded a user # Successfully authenticated and loaded a user
log.info("Successfully authenticated!") log.info("Successfully authenticated!")
log.info("Current user: %s" % self.currUser) log.info("Current user: %s" % self.currUser)
log.info("Current userId: %s" % self.currUserId) log.info("Current userId: %s" % state.PLEX_USER_ID)
self.retry = 0 self.retry = 0
window('suspend_LibraryThread', clear=True) state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True) window('plex_serverStatus', clear=True)
state.PMS_STATUS = False
if not self.auth and (self.currUser is None): if not self.auth and (self.currUser is None):
# Loop if no server found # Loop if no server found
@ -354,7 +342,7 @@ class UserClient(threading.Thread):
# The status Stop is for when user cancelled password dialog. # The status Stop is for when user cancelled password dialog.
# Or retried too many times # Or retried too many times
if server and status != "Stop": if server and state.PMS_STATUS != "Stop":
# Only if there's information found to login # Only if there's information found to login
log.debug("Server found: %s" % server) log.debug("Server found: %s" % server)
self.auth = True self.auth = True
@ -362,5 +350,4 @@ class UserClient(threading.Thread):
# Minimize CPU load # Minimize CPU load
xbmc.sleep(100) xbmc.sleep(100)
self.doUtils.stopSession()
log.info("##===---- UserClient Stopped ----===##") log.info("##===---- UserClient Stopped ----===##")

View file

@ -11,7 +11,7 @@ from StringIO import StringIO
from time import localtime, strftime, strptime from time import localtime, strftime, strptime
from unicodedata import normalize from unicodedata import normalize
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from functools import wraps from functools import wraps, partial
from calendar import timegm from calendar import timegm
from os.path import join from os.path import join
from os import remove, walk, makedirs from os import remove, walk, makedirs
@ -25,6 +25,7 @@ from xbmcvfs import exists, delete
from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \ from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \
DB_PLEX_PATH, KODI_PROFILE, KODIVERSION DB_PLEX_PATH, KODI_PROFILE, KODIVERSION
import state
############################################################################### ###############################################################################
@ -76,6 +77,19 @@ def pickl_window(property, value=None, clear=False, windowid=10000):
return win.getProperty(property) return win.getProperty(property)
def plex_command(key, value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
key: state.py variable
value: either 'True' or 'False'
"""
while window('plex_command'):
xbmc.sleep(5)
window('plex_command', value='%s-%s' % (key, value))
def settings(setting, value=None): def settings(setting, value=None):
""" """
Get or add addon setting. Returns unicode Get or add addon setting. Returns unicode
@ -97,12 +111,12 @@ def exists_dir(path):
Safe way to check whether the directory path exists already (broken in Kodi Safe way to check whether the directory path exists already (broken in Kodi
<17) <17)
Feed with encoded string Feed with encoded string or unicode
""" """
if KODIVERSION >= 17: if KODIVERSION >= 17:
answ = exists(path) answ = exists(tryEncode(path))
else: else:
dummyfile = join(path, 'dummyfile.txt') dummyfile = join(tryDecode(path), 'dummyfile.txt')
try: try:
with open(dummyfile, 'w') as f: with open(dummyfile, 'w') as f:
f.write('text') f.write('text')
@ -111,7 +125,7 @@ def exists_dir(path):
answ = 0 answ = 0
else: else:
# Folder exists. Delete file again. # Folder exists. Delete file again.
delete(dummyfile) delete(tryEncode(dummyfile))
answ = 1 answ = 1
return answ return answ
@ -319,7 +333,7 @@ def reset():
return return
# first stop any db sync # first stop any db sync
window('plex_shouldStop', value="true") plex_command('STOP_SYNC', 'True')
count = 10 count = 10
while window('plex_dbScan') == "true": while window('plex_dbScan') == "true":
log.debug("Sync is running, will retry: %s..." % count) log.debug("Sync is running, will retry: %s..." % count)
@ -347,7 +361,7 @@ def reset():
for row in rows: for row in rows:
tablename = row[0] tablename = row[0]
if tablename != "version": if tablename != "version":
cursor.execute("DELETE FROM ?", (tablename,)) cursor.execute("DELETE FROM %s" % tablename)
connection.commit() connection.commit()
cursor.close() cursor.close()
@ -360,7 +374,7 @@ def reset():
for row in rows: for row in rows:
tablename = row[0] tablename = row[0]
if tablename != "version": if tablename != "version":
cursor.execute("DELETE FROM ?", (tablename, )) cursor.execute("DELETE FROM %s" % tablename)
connection.commit() connection.commit()
cursor.close() cursor.close()
@ -373,7 +387,7 @@ def reset():
for row in rows: for row in rows:
tablename = row[0] tablename = row[0]
if tablename != "version": if tablename != "version":
cursor.execute("DELETE FROM ?", (tablename, )) cursor.execute("DELETE FROM %s" % tablename)
cursor.execute('DROP table IF EXISTS plex') cursor.execute('DROP table IF EXISTS plex')
cursor.execute('DROP table IF EXISTS view') cursor.execute('DROP table IF EXISTS view')
connection.commit() connection.commit()
@ -387,7 +401,7 @@ def reset():
# Remove all existing textures first # Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/") path = xbmc.translatePath("special://thumbnails/")
if exists(path): if exists(path):
rmtree(path, ignore_errors=True) rmtree(tryDecode(path), ignore_errors=True)
# remove all existing data from texture DB # remove all existing data from texture DB
connection = kodiSQL('texture') connection = kodiSQL('texture')
cursor = connection.cursor() cursor = connection.cursor()
@ -397,7 +411,7 @@ def reset():
for row in rows: for row in rows:
tableName = row[0] tableName = row[0]
if(tableName != "version"): if(tableName != "version"):
cursor.execute("DELETE FROM ?", (tableName, )) cursor.execute("DELETE FROM %s" % tableName)
connection.commit() connection.commit()
cursor.close() cursor.close()
@ -411,7 +425,7 @@ def reset():
line1=language(39603)): line1=language(39603)):
# Delete the settings # Delete the settings
addon = xbmcaddon.Addon() addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')) addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
dataPath = "%ssettings.xml" % addondir dataPath = "%ssettings.xml" % addondir
log.info("Deleting: settings.xml") log.info("Deleting: settings.xml")
remove(dataPath) remove(dataPath)
@ -522,9 +536,16 @@ def guisettingsXML():
try: try:
xmlparse = etree.parse(xmlpath) xmlparse = etree.parse(xmlpath)
except: except IOError:
# Document is blank or missing # Document is blank or missing
root = etree.Element('settings') root = etree.Element('settings')
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
'guisettings.xml', 'http://kodi.wiki/view/userdata'))
return
else: else:
root = xmlparse.getroot() root = xmlparse.getroot()
return root return root
@ -606,6 +627,14 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None,
return None, None return None, None
# Create topmost xml entry # Create topmost xml entry
tree = etree.ElementTree(element=etree.Element('advancedsettings')) tree = etree.ElementTree(element=etree.Element('advancedsettings'))
except etree.ParseError:
log.error('Error parsing %s' % path)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
'advancedsettings.xml',
'http://kodi.wiki/view/Advancedsettings.xml'))
return None, None
root = tree.getroot() root = tree.getroot()
element = root element = root
@ -632,17 +661,6 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None,
return element, tree return element, tree
def advancedsettings_tweaks():
"""
Kodi tweaks
Changes advancedsettings.xml, musiclibrary:
backgroundupdate set to "true"
"""
advancedsettings_xml(['musiclibrary', 'backgroundupdate'],
new_value='true')
def sourcesXML(): def sourcesXML():
# To make Master lock compatible # To make Master lock compatible
path = tryDecode(xbmc.translatePath("special://profile/")) path = tryDecode(xbmc.translatePath("special://profile/"))
@ -650,8 +668,15 @@ def sourcesXML():
try: try:
xmlparse = etree.parse(xmlpath) xmlparse = etree.parse(xmlpath)
except: # Document is blank or missing except IOError: # Document is blank or missing
root = etree.Element('sources') root = etree.Element('sources')
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog('ok', language(29999), language(39716).format(
'sources.xml', 'http://kodi.wiki/view/sources.xml'))
return
else: else:
root = xmlparse.getroot() root = xmlparse.getroot()
@ -685,20 +710,27 @@ def sourcesXML():
def passwordsXML(): def passwordsXML():
# To add network credentials # To add network credentials
path = xbmc.translatePath("special://userdata/") path = tryDecode(xbmc.translatePath("special://userdata/"))
xmlpath = "%spasswords.xml" % path xmlpath = "%spasswords.xml" % path
dialog = xbmcgui.Dialog()
try: try:
xmlparse = etree.parse(xmlpath) xmlparse = etree.parse(xmlpath)
except: except IOError:
# Document is blank or missing # Document is blank or missing
root = etree.Element('passwords') root = etree.Element('passwords')
skipFind = True skipFind = True
except etree.ParseError:
log.error('Error parsing %s' % xmlpath)
# "Kodi cannot parse {0}. PKC will not function correctly. Please visit
# {1} and correct your file!"
dialog.ok(language(29999), language(39716).format(
'passwords.xml', 'http://forum.kodi.tv/'))
return
else: else:
root = xmlparse.getroot() root = xmlparse.getroot()
skipFind = False skipFind = False
dialog = xbmcgui.Dialog()
credentials = settings('networkCreds') credentials = settings('networkCreds')
if credentials: if credentials:
# Present user with options # Present user with options
@ -798,7 +830,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
""" """
Feed with tagname as unicode Feed with tagname as unicode
""" """
path = xbmc.translatePath("special://profile/playlists/video/") path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
if viewtype == "mixed": if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype) plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype) xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
@ -807,12 +839,12 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
xsppath = "%sPlex %s.xsp" % (path, viewid) xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory # Create the playlist directory
if not exists(path): if not exists(tryEncode(path)):
log.info("Creating directory: %s" % path) log.info("Creating directory: %s" % path)
makedirs(path) makedirs(path)
# Only add the playlist if it doesn't already exists # Only add the playlist if it doesn't already exists
if exists(xsppath): if exists(tryEncode(xsppath)):
log.info('Path %s does exist' % xsppath) log.info('Path %s does exist' % xsppath)
if delete: if delete:
remove(xsppath) remove(xsppath)
@ -827,27 +859,22 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
'show': 'tvshows' 'show': 'tvshows'
} }
log.info("Writing playlist file to: %s" % xsppath) log.info("Writing playlist file to: %s" % xsppath)
try: with open(xsppath, 'wb'):
with open(xsppath, 'wb'): tryEncode(
tryEncode( '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' '<smartplaylist type="%s">\n\t'
'<smartplaylist type="%s">\n\t' '<name>Plex %s</name>\n\t'
'<name>Plex %s</name>\n\t' '<match>all</match>\n\t'
'<match>all</match>\n\t' '<rule field="tag" operator="is">\n\t\t'
'<rule field="tag" operator="is">\n\t\t' '<value>%s</value>\n\t'
'<value>%s</value>\n\t' '</rule>\n'
'</rule>\n' '</smartplaylist>\n'
'</smartplaylist>\n' % (itemtypes.get(mediatype, mediatype), plname, tagname))
% (itemtypes.get(mediatype, mediatype), plname, tagname))
except Exception as e:
log.error("Failed to create playlist: %s" % xsppath)
log.error(e)
return
log.info("Successfully added playlist: %s" % tagname) log.info("Successfully added playlist: %s" % tagname)
def deletePlaylists(): def deletePlaylists():
# Clean up the playlists # Clean up the playlists
path = xbmc.translatePath("special://profile/playlists/video/") path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
for root, _, files in walk(path): for root, _, files in walk(path):
for file in files: for file in files:
if file.startswith('Plex'): if file.startswith('Plex'):
@ -855,7 +882,7 @@ def deletePlaylists():
def deleteNodes(): def deleteNodes():
# Clean up video nodes # Clean up video nodes
path = xbmc.translatePath("special://profile/library/video/") path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
for root, dirs, _ in walk(path): for root, dirs, _ in walk(path):
for directory in dirs: for directory in dirs:
if directory.startswith('Plex-'): if directory.startswith('Plex-'):
@ -906,78 +933,78 @@ def LogTime(func):
return wrapper return wrapper
def ThreadMethodsAdditionalStop(windowAttribute): def thread_methods(cls=None, add_stops=None, add_suspends=None):
"""
Decorator to replace stopThread method to include the Kodi windowAttribute
Use with any sync threads. @ThreadMethods still required FIRST
"""
def wrapper(cls):
def threadStopped(self):
return (self._threadStopped or
(window('plex_terminateNow') == "true") or
window(windowAttribute) == "true")
cls.threadStopped = threadStopped
return cls
return wrapper
def ThreadMethodsAdditionalSuspend(windowAttribute):
"""
Decorator to replace threadSuspended(): thread now also suspends if a
Kodi windowAttribute is set to 'true', e.g. 'suspend_LibraryThread'
Use with any library sync threads. @ThreadMethods still required FIRST
"""
def wrapper(cls):
def threadSuspended(self):
return (self._threadSuspended or
window(windowAttribute) == 'true')
cls.threadSuspended = threadSuspended
return cls
return wrapper
def ThreadMethods(cls):
""" """
Decorator to add the following methods to a threading class: Decorator to add the following methods to a threading class:
suspendThread(): pauses the thread suspend_thread(): pauses the thread
resumeThread(): resumes the thread resume_thread(): resumes the thread
stopThread(): stopps/kills the thread stop_thread(): stopps/kills the thread
threadSuspended(): returns True if thread is suspend_thread thread_suspended(): returns True if thread is suspended
threadStopped(): returns True if thread is stopped (or should stop ;-)) thread_stopped(): returns True if thread is stopped (or should stop ;-))
ALSO stops if Kodi is exited ALSO returns True if PKC should exit
Also adds the following class attributes: Also adds the following class attributes:
_threadStopped __thread_stopped
_threadSuspended __thread_suspended
__stops
__suspends
invoke with either
@Newthread_methods
class MyClass():
or
@Newthread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'],
add_suspends=['DB_SCAN', 'WHATEVER'])
class MyClass():
""" """
# So we don't need to invoke with ()
if cls is None:
return partial(thread_methods,
add_stops=add_stops,
add_suspends=add_suspends)
# Because we need a reference, not a copy of the immutable objects in
# state, we need to look up state every time explicitly
cls.__stops = ['STOP_PKC']
if add_stops is not None:
cls.__stops.extend(add_stops)
cls.__suspends = add_suspends or []
# Attach new attributes to class # Attach new attributes to class
cls._threadStopped = False cls.__thread_stopped = False
cls._threadSuspended = False cls.__thread_suspended = False
# Define new class methods and attach them to class # Define new class methods and attach them to class
def stopThread(self): def stop_thread(self):
self._threadStopped = True self.__thread_stopped = True
cls.stopThread = stopThread cls.stop_thread = stop_thread
def suspendThread(self): def suspend_thread(self):
self._threadSuspended = True self.__thread_suspended = True
cls.suspendThread = suspendThread cls.suspend_thread = suspend_thread
def resumeThread(self): def resume_thread(self):
self._threadSuspended = False self.__thread_suspended = False
cls.resumeThread = resumeThread cls.resume_thread = resume_thread
def threadSuspended(self): def thread_suspended(self):
return self._threadSuspended if self.__thread_suspended is True:
cls.threadSuspended = threadSuspended return True
for suspend in self.__suspends:
if getattr(state, suspend):
return True
return False
cls.thread_suspended = thread_suspended
def threadStopped(self): def thread_stopped(self):
return self._threadStopped or (window('plex_terminateNow') == 'true') if self.__thread_stopped is True:
cls.threadStopped = threadStopped return True
for stop in self.__stops:
if getattr(state, stop):
return True
return False
cls.thread_stopped = thread_stopped
# Return class to render this a decorator # Return class to render this a decorator
return cls return cls

View file

@ -2,7 +2,8 @@
import xbmc import xbmc
from xbmcaddon import Addon from xbmcaddon import Addon
# Paths are in string, not unicode! # Paths are in unicode, otherwise Windows will throw fits
# For any file operations with KODI function, use encoded strings!
def tryDecode(string, encoding='utf-8'): def tryDecode(string, encoding='utf-8'):
@ -29,7 +30,7 @@ ADDON_VERSION = _ADDON.getAddonInfo('version')
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = xbmc.translatePath("special://profile") KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
if xbmc.getCondVisibility('system.platform.osx'): if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX" PLATFORM = "MacOSX"
@ -70,8 +71,8 @@ _DB_VIDEO_VERSION = {
17: 107, # Krypton 17: 107, # Krypton
18: 108 # Leia 18: 108 # Leia
} }
DB_VIDEO_PATH = xbmc.translatePath( DB_VIDEO_PATH = tryDecode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]) "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = { _DB_MUSIC_VERSION = {
13: 46, # Gotham 13: 46, # Gotham
@ -81,8 +82,8 @@ _DB_MUSIC_VERSION = {
17: 60, # Krypton 17: 60, # Krypton
18: 62 # Leia 18: 62 # Leia
} }
DB_MUSIC_PATH = xbmc.translatePath( DB_MUSIC_PATH = tryDecode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]) "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = { _DB_TEXTURE_VERSION = {
13: 13, # Gotham 13: 13, # Gotham
@ -92,13 +93,13 @@ _DB_TEXTURE_VERSION = {
17: 13, # Krypton 17: 13, # Krypton
18: 13 # Leia 18: 13 # Leia
} }
DB_TEXTURE_PATH = xbmc.translatePath( DB_TEXTURE_PATH = tryDecode(xbmc.translatePath(
"special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]) "special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]))
DB_PLEX_PATH = xbmc.translatePath("special://database/plex.db") DB_PLEX_PATH = tryDecode(xbmc.translatePath("special://database/plex.db"))
EXTERNAL_SUBTITLE_TEMP_PATH = xbmc.translatePath( EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath(
"special://profile/addon_data/%s/temp/" % ADDON_ID) "special://profile/addon_data/%s/temp/" % ADDON_ID))
# Multiply Plex time by this factor to receive Kodi time # Multiply Plex time by this factor to receive Kodi time

View file

@ -3,14 +3,13 @@
import logging import logging
from shutil import copytree from shutil import copytree
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from os import remove, listdir, makedirs from os import makedirs
from os.path import isfile, join
import xbmc import xbmc
from xbmcvfs import exists from xbmcvfs import exists
from utils import window, settings, language as lang, tryEncode, indent, \ from utils import window, settings, language as lang, tryEncode, indent, \
normalize_nodes, exists_dir normalize_nodes, exists_dir, tryDecode
import variables as v import variables as v
############################################################################### ###############################################################################
@ -63,22 +62,25 @@ class VideoNodes(object):
dirname = viewid dirname = viewid
# Returns strings # Returns strings
path = xbmc.translatePath("special://profile/library/video/") path = tryDecode(xbmc.translatePath(
nodepath = xbmc.translatePath( "special://profile/library/video/"))
"special://profile/library/video/Plex-%s/" % dirname) nodepath = tryDecode(xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname))
if delete: if delete:
files = [f for f in listdir(nodepath) if isfile(join(nodepath, f))] if exists_dir(nodepath):
for file in files: from shutil import rmtree
remove(nodepath + file) rmtree(nodepath)
log.info("Sucessfully removed videonode: %s." % tagname) log.info("Sucessfully removed videonode: %s." % tagname)
return return
# Verify the video directory # Verify the video directory
if not exists_dir(path): if not exists_dir(path):
copytree( copytree(
src=xbmc.translatePath("special://xbmc/system/library/video"), src=tryDecode(xbmc.translatePath(
dst=xbmc.translatePath("special://profile/library/video")) "special://xbmc/system/library/video")),
dst=tryDecode(xbmc.translatePath(
"special://profile/library/video")))
# Create the node directory # Create the node directory
if mediatype != "photos": if mediatype != "photos":
@ -290,7 +292,7 @@ class VideoNodes(object):
# To do: add our photos nodes to kodi picture sources somehow # To do: add our photos nodes to kodi picture sources somehow
continue continue
if exists(nodeXML): if exists(tryEncode(nodeXML)):
# Don't recreate xml if already exists # Don't recreate xml if already exists
continue continue
@ -377,8 +379,9 @@ class VideoNodes(object):
def singleNode(self, indexnumber, tagname, mediatype, itemtype): def singleNode(self, indexnumber, tagname, mediatype, itemtype):
tagname = tryEncode(tagname) tagname = tryEncode(tagname)
cleantagname = normalize_nodes(tagname) cleantagname = tryDecode(normalize_nodes(tagname))
nodepath = xbmc.translatePath("special://profile/library/video/") nodepath = tryDecode(xbmc.translatePath(
"special://profile/library/video/"))
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname path = "library://video/plex_%s.xml" % cleantagname
if v.KODIVERSION >= 17: if v.KODIVERSION >= 17:
@ -391,8 +394,10 @@ class VideoNodes(object):
if not exists_dir(nodepath): if not exists_dir(nodepath):
# We need to copy over the default items # We need to copy over the default items
copytree( copytree(
src=xbmc.translatePath("special://xbmc/system/library/video"), src=tryDecode(xbmc.translatePath(
dst=xbmc.translatePath("special://profile/library/video")) "special://xbmc/system/library/video")),
dst=tryDecode(xbmc.translatePath(
"special://profile/library/video")))
labels = { labels = {
'Favorite movies': 30180, 'Favorite movies': 30180,
@ -406,7 +411,7 @@ class VideoNodes(object):
window('%s.content' % embynode, value=path) window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=itemtype) window('%s.type' % embynode, value=itemtype)
if exists(nodeXML): if exists(tryEncode(nodeXML)):
# Don't recreate xml if already exists # Don't recreate xml if already exists
return return

View file

@ -11,9 +11,9 @@ from ssl import CERT_NONE
from xbmc import sleep from xbmc import sleep
from utils import window, settings, ThreadMethodsAdditionalSuspend, \ from utils import window, settings, thread_methods
ThreadMethods
from companion import process_command from companion import process_command
import state
############################################################################### ###############################################################################
@ -22,8 +22,6 @@ log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@ThreadMethods
class WebSocket(Thread): class WebSocket(Thread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
@ -62,11 +60,11 @@ class WebSocket(Thread):
counter = 0 counter = 0
handshake_counter = 0 handshake_counter = 0
threadStopped = self.threadStopped thread_stopped = self.thread_stopped
threadSuspended = self.threadSuspended thread_suspended = self.thread_suspended
while not threadStopped(): while not thread_stopped():
# In the event the server goes offline # In the event the server goes offline
while threadSuspended(): while thread_suspended():
# Set in service.py # Set in service.py
if self.ws is not None: if self.ws is not None:
try: try:
@ -74,7 +72,7 @@ class WebSocket(Thread):
except: except:
pass pass
self.ws = None self.ws = None
if threadStopped(): if thread_stopped():
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("##===---- %s Stopped ----===##" log.info("##===---- %s Stopped ----===##"
% self.__class__.__name__) % self.__class__.__name__)
@ -141,16 +139,17 @@ class WebSocket(Thread):
def stopThread(self): def stopThread(self):
""" """
Overwrite this method from ThreadMethods to close websockets Overwrite this method from thread_methods to close websockets
""" """
log.info("Stopping %s thread." % self.__class__.__name__) log.info("Stopping %s thread." % self.__class__.__name__)
self._threadStopped = True self.__threadStopped = True
try: try:
self.ws.shutdown() self.ws.shutdown()
except: except:
pass pass
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD'])
class PMS_Websocket(WebSocket): class PMS_Websocket(WebSocket):
""" """
Websocket connection with the PMS for Plex Companion Websocket connection with the PMS for Plex Companion
@ -160,16 +159,15 @@ class PMS_Websocket(WebSocket):
def getUri(self): def getUri(self):
server = window('pms_server') server = window('pms_server')
# Need to use plex.tv token, if any. NOT user token
token = window('plex_token')
# Get the appropriate prefix for the websocket # Get the appropriate prefix for the websocket
if server.startswith('https'): if server.startswith('https'):
server = "wss%s" % server[5:] server = "wss%s" % server[5:]
else: else:
server = "ws%s" % server[4:] server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server uri = "%s/:/websockets/notifications" % server
if token: # Need to use plex.tv token, if any. NOT user token
uri += '?X-Plex-Token=%s' % token if state.PLEX_TOKEN:
uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN
sslopt = {} sslopt = {}
if settings('sslverify') == "false": if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE sslopt["cert_reqs"] = CERT_NONE
@ -213,14 +211,18 @@ class PMS_Websocket(WebSocket):
class Alexa_Websocket(WebSocket): class Alexa_Websocket(WebSocket):
""" """
Websocket connection to talk to Amazon Alexa Websocket connection to talk to Amazon Alexa.
Can't use thread_methods!
""" """
__thread_stopped = False
__thread_suspended = False
def getUri(self): def getUri(self):
self.plex_client_Id = window('plex_client_Id') self.plex_client_Id = window('plex_client_Id')
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (window('currUserId'), % (state.PLEX_USER_ID,
self.plex_client_Id, self.plex_client_Id, state.PLEX_TOKEN))
window('plex_token')))
sslopt = {} sslopt = {}
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt return uri, sslopt
@ -252,11 +254,32 @@ class Alexa_Websocket(WebSocket):
def IOError_response(self): def IOError_response(self):
pass pass
def threadSuspended(self): # Path in thread_methods
def stop_thread(self):
self.__thread_stopped = True
def suspend_thread(self):
self.__thread_suspended = True
def resume_thread(self):
self.__thread_suspended = False
def thread_stopped(self):
if self.__thread_stopped is True:
return True
if state.STOP_PKC:
return True
return False
# The culprit
def thread_suspended(self):
""" """
Overwrite to ignore library sync stuff and allow to check for Overwrite method since we need to check for plex token
plex_restricteduser
""" """
return (self._threadSuspended or if self.__thread_suspended is True:
window('plex_restricteduser') == 'true' or return True
not window('plex_token')) if not state.PLEX_TOKEN:
return True
if state.RESTRICTED_USER:
return True
return False

View file

@ -6,7 +6,7 @@ import logging
from os import path as os_path from os import path as os_path
from sys import path as sys_path, argv from sys import path as sys_path, argv
from xbmc import translatePath, Monitor, sleep from xbmc import translatePath, Monitor
from xbmcaddon import Addon from xbmcaddon import Addon
############################################################################### ###############################################################################
@ -30,7 +30,8 @@ sys_path.append(_base_resource)
############################################################################### ###############################################################################
from utils import settings, window, language as lang, dialog, tryEncode from utils import settings, window, language as lang, dialog, tryEncode, \
tryDecode
from userclient import UserClient from userclient import UserClient
import initialsetup import initialsetup
from kodimonitor import KodiMonitor from kodimonitor import KodiMonitor
@ -42,10 +43,11 @@ from playqueue import Playqueue
import PlexAPI import PlexAPI
from PlexCompanion import PlexCompanion from PlexCompanion import PlexCompanion
from monitor_kodi_play import Monitor_Kodi_Play from command_pipeline import Monitor_Window
from playback_starter import Playback_Starter from playback_starter import Playback_Starter
from artwork import Image_Cache_Thread from artwork import Image_Cache_Thread
import variables as v import variables as v
import state
############################################################################### ###############################################################################
@ -85,7 +87,7 @@ class Service():
window('plex_logLevel', value=str(logLevel)) window('plex_logLevel', value=str(logLevel))
window('plex_kodiProfile', window('plex_kodiProfile',
value=translatePath("special://profile")) value=tryDecode(translatePath("special://profile")))
window('plex_context', window('plex_context',
value='true' if settings('enableContext') == "true" else "") value='true' if settings('enableContext') == "true" else "")
window('fetch_pms_item_number', window('fetch_pms_item_number',
@ -105,18 +107,16 @@ class Service():
# 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_dbCheck", "plex_kodiScan",
"plex_shouldStop", "currUserId", "plex_dbScan", "plex_shouldStop", "plex_dbScan",
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps", "plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
"plex_runLibScan", "plex_username", "pms_token", "plex_token", "plex_runLibScan", "pms_token", "plex_token",
"pms_server", "plex_machineIdentifier", "plex_servername", "pms_server", "plex_machineIdentifier", "plex_servername",
"plex_authenticated", "PlexUserImage", "useDirectPaths", "plex_authenticated", "PlexUserImage", "useDirectPaths",
"suspend_LibraryThread", "plex_terminateNow",
"kodiplextimeoffset", "countError", "countUnauthorized", "kodiplextimeoffset", "countError", "countUnauthorized",
"plex_restricteduser", "plex_allows_mediaDeletion", "plex_restricteduser", "plex_allows_mediaDeletion",
"plex_play_new_item", "plex_result", "plex_force_transcode_pix" "plex_command", "plex_result", "plex_force_transcode_pix"
] ]
for prop in properties: for prop in properties:
window(prop, clear=True) window(prop, clear=True)
@ -134,15 +134,22 @@ class Service():
logLevel = 0 logLevel = 0
return logLevel return logLevel
def __stop_PKC(self):
"""
Kodi's abortRequested is really unreliable :-(
"""
return self.monitor.abortRequested() or state.STOP_PKC
def ServiceEntryPoint(self): def ServiceEntryPoint(self):
# Important: Threads depending on abortRequest will not trigger # Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once. # if profile switch happens more than once.
__stop_PKC = self.__stop_PKC
monitor = self.monitor monitor = self.monitor
kodiProfile = v.KODI_PROFILE kodiProfile = v.KODI_PROFILE
# Detect playback start early on # Detect playback start early on
self.monitor_kodi_play = Monitor_Kodi_Play(self) self.command_pipeline = Monitor_Window(self)
self.monitor_kodi_play.start() self.command_pipeline.start()
# Server auto-detect # Server auto-detect
initialsetup.InitialSetup().setup() initialsetup.InitialSetup().setup()
@ -162,14 +169,14 @@ class Service():
welcome_msg = True welcome_msg = True
counter = 0 counter = 0
while not monitor.abortRequested(): while not __stop_PKC():
if tryEncode(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.warn("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread." "Terminating old PlexKodiConnect thread."
% (kodiProfile, % (kodiProfile,
tryEncode(window('plex_kodiProfile')))) window('plex_kodiProfile')))
break break
# Before proceeding, need to make sure: # Before proceeding, need to make sure:
@ -242,14 +249,13 @@ class Service():
# Server went offline # Server went offline
break break
if monitor.waitForAbort(5): if monitor.waitForAbort(3):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
break break
sleep(50)
else: else:
# Wait until Plex server is online # Wait until Plex server is online
# or Kodi is shut down. # or Kodi is shut down.
while not monitor.abortRequested(): while not self.__stop_PKC():
server = self.user.getServer() server = self.user.getServer()
if server is False: if server is False:
# No server info set in add-on settings # No server info set in add-on settings
@ -261,7 +267,7 @@ class Service():
self.server_online = False self.server_online = False
window('plex_online', value="false") window('plex_online', value="false")
# Suspend threads # Suspend threads
window('suspend_LibraryThread', value='true') state.SUSPEND_LIBRARY_THREAD = True
log.error("Plex Media Server went offline") log.error("Plex Media Server went offline")
if settings('show_pms_offline') == 'true': if settings('show_pms_offline') == 'true':
dialog('notification', dialog('notification',
@ -298,10 +304,10 @@ class Service():
sound=False) sound=False)
log.info("Server %s is online and ready." % server) log.info("Server %s is online and ready." % server)
window('plex_online', value="true") window('plex_online', value="true")
if window('plex_authenticated') == 'true': if state.AUTHENTICATED:
# Server got offline when we were authenticated. # Server got offline when we were authenticated.
# Hence resume threads # Hence resume threads
window('suspend_LibraryThread', clear=True) state.SUSPEND_LIBRARY_THREAD = False
# Start the userclient thread # Start the userclient thread
if not self.user_running: if not self.user_running:
@ -317,31 +323,10 @@ class Service():
if monitor.waitForAbort(0.05): if monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
break break
# Terminating PlexKodiConnect # Terminating PlexKodiConnect
# Tell all threads to terminate (e.g. several lib sync threads) # Tell all threads to terminate (e.g. several lib sync threads)
window('plex_terminateNow', value='true') state.STOP_PKC = True
try:
self.plexCompanion.stopThread()
except:
log.warn('plexCompanion already shut down')
try:
self.library.stopThread()
except:
log.warn('Library sync already shut down')
try:
self.ws.stopThread()
except:
log.warn('Websocket client already shut down')
try:
self.alexa.stopThread()
except:
log.warn('Websocket client already shut down')
try:
self.user.stopThread()
except:
log.warn('User client already shut down')
try: try:
downloadutils.DownloadUtils().stopSession() downloadutils.DownloadUtils().stopSession()
except: except:
@ -349,6 +334,7 @@ class Service():
window('plex_service_started', clear=True) window('plex_service_started', clear=True)
log.warn("======== STOP %s ========" % v.ADDON_NAME) log.warn("======== STOP %s ========" % v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation! # Safety net - Kody starts PKC twice upon first installation!
if window('plex_service_started') == 'true': if window('plex_service_started') == 'true':
exit = True exit = True