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)
[![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)
[![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
+ Italian, thanks @nikkux, @chicco83
+ Dutch, thanks @mvanbaak
+ French, thanks @lflforce, @ahivert, @Nox71, @CotzaDev, @vinch100, @Polymorph31, @jbnitro
+ Chinese Traditional, thanks @old2tan
+ Chinese Simplified, thanks @everdream
+ [Please help translating](https://www.transifex.com/croneter/pkc)

View file

@ -1,5 +1,5 @@
<?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>
<import addon="xbmc.python" version="2.1.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="zh_TW">使用風險由您自己承擔</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
- Fix KeyError for Plex Companion
- 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)
- Don't add media by other add-ons to queue
- Fix KeyError for Plex Companion

View file

@ -170,10 +170,10 @@ class Main():
Start up playback_starter in main Python thread
"""
# Put the request into the 'queue'
while window('plex_play_new_item'):
while window('plex_command'):
sleep(50)
window('plex_play_new_item',
value='%s%s' % ('play', argv[2]))
window('plex_command',
value='play_%s' % argv[2])
# Wait for the result
while not pickl_window('plex_result'):
sleep(50)

View file

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

View file

@ -7,13 +7,14 @@ from urllib import urlencode
from xbmc import sleep, executebuiltin
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
from utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API
import player
import variables as v
import state
###############################################################################
@ -22,8 +23,7 @@ log = logging.getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
@ThreadMethods
@thread_methods(add_suspends=['PMS_STATUS'])
class PlexCompanion(Thread):
"""
"""
@ -77,6 +77,8 @@ class PlexCompanion(Thread):
log.debug('Processing: %s' % task)
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':
# e.g. Alexa
xml = GetPlexMetadata(data['key'])
@ -144,11 +146,28 @@ class PlexCompanion(Thread):
offset=data.get('offset'))
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
client = self.client
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
# Start up instances
requestMgr = httppersist.RequestMgr()
@ -196,12 +215,12 @@ class PlexCompanion(Thread):
if httpd:
t = Thread(target=httpd.handle_request)
while not threadStopped():
while not thread_stopped():
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
while threadSuspended():
if threadStopped():
while thread_suspended():
if thread_stopped():
break
sleep(1000)
try:
@ -245,11 +264,3 @@ class PlexCompanion(Thread):
sleep(50)
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 utils import window, settings, language as lang, kodiSQL, tryEncode, \
ThreadMethods, ThreadMethodsAdditionalStop, dialog, exists_dir
thread_methods, dialog, exists_dir, tryDecode
# Disable annoying requests warnings
import requests.packages.urllib3
@ -126,8 +126,8 @@ def double_urldecode(text):
return unquote(unquote(text))
@ThreadMethodsAdditionalStop('plex_shouldStop')
@ThreadMethods
@thread_methods(add_stops=['STOP_SYNC'],
add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'])
class Image_Cache_Thread(Thread):
xbmc_host = 'localhost'
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()
@ -140,22 +140,16 @@ class Image_Cache_Thread(Thread):
self.queue = ARTWORK_QUEUE
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):
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
queue = self.queue
sleep_between = self.sleep_between
while not threadStopped():
while not thread_stopped():
# In the event the server goes offline
while threadSuspended():
while thread_suspended():
# Set in service.py
if threadStopped():
if thread_stopped():
# Abort was requested while waiting. We should exit
log.info("---===### Stopped Image_Cache_Thread ###===---")
return
@ -178,7 +172,7 @@ class Image_Cache_Thread(Thread):
# download. All is well
break
except requests.ConnectionError:
if threadStopped():
if thread_stopped():
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
@ -228,7 +222,7 @@ class Artwork():
if dialog('yesno', "Image Texture Cache", lang(39251)):
log.info("Resetting all cache data first")
# Remove all existing textures first
path = translatePath("special://thumbnails/")
path = tryDecode(translatePath("special://thumbnails/"))
if exists_dir(path):
rmtree(path, ignore_errors=True)
@ -241,8 +235,7 @@ class Artwork():
for row in rows:
tableName = row[0]
if tableName != "version":
query = "DELETE FROM ?"
cursor.execute(query, (tableName,))
cursor.execute("DELETE FROM %s" % tableName)
connection.commit()
connection.close()
@ -430,7 +423,7 @@ class Artwork():
path = translatePath("special://thumbnails/%s" % cachedurl)
log.debug("Deleting cached thumbnail: %s" % path)
if exists(path):
rmtree(path, ignore_errors=True)
rmtree(tryDecode(path), ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
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
import clientinfo as client
import state
###############################################################################
# Disable annoying requests warnings
@ -40,20 +42,6 @@ class DownloadUtils():
def __init__(self):
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):
"""
Reserved for userclient only
@ -108,8 +96,6 @@ class DownloadUtils():
# Set other stuff
self.setServer(window('pms_server'))
self.setToken(window('pms_token'))
self.setUserId(window('currUserId'))
self.setUsername(window('plex_username'))
# Counters to declare PMS dead or unauthorized
# Use window variables because start of movies will be called with a
@ -274,10 +260,11 @@ class DownloadUtils():
self.unauthorizedAttempts):
log.warn('We seem to be truly unauthorized for PMS'
' %s ' % url)
if window('plex_serverStatus') not in ('401', 'Auth'):
if state.PMS_STATUS not in ('401', 'Auth'):
# Tell userclient token has been revoked.
log.debug('Setting PMS server status to '
'unauthorized')
state.PMS_STATUS = '401'
window('plex_serverStatus', value="401")
dialog('notification',
lang(29999),

View file

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

View file

@ -13,6 +13,7 @@ from userclient import UserClient
from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import state
###############################################################################
@ -156,7 +157,7 @@ class InitialSetup():
verifySSL = False
else:
url = server['baseURL']
verifySSL = None
verifySSL = True
chk = self.plx.CheckConnection(url,
token=server['accesstoken'],
verifySSL=verifySSL)
@ -450,6 +451,7 @@ class InitialSetup():
yeslabel="Native (Direct Paths)"):
log.debug("User opted to use direct paths.")
settings('useDirectPaths', value="1")
state.DIRECT_PATHS = True
# Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if dialog.yesno(heading=lang(29999), line1=lang(39033)):
@ -477,9 +479,6 @@ class InitialSetup():
if dialog.yesno(heading=lang(29999), line1=lang(39016)):
log.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false")
else:
from utils import advancedsettings_tweaks
advancedsettings_tweaks()
# Download additional art from FanArtTV
if dialog.yesno(heading=lang(29999), line1=lang(39061)):
@ -496,12 +495,6 @@ class InitialSetup():
# Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
if goToSettings:
window('plex_serverStatus', value="Stop")
state.PMS_STATUS = 'Stop'
xbmc.executebuiltin(
'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
from PlexFunctions import GetPlexMetadata
import variables as v
import state
###############################################################################
@ -35,10 +36,7 @@ class Items(object):
"""
def __init__(self):
self.directpath = window('useDirectPaths') == 'true'
self.artwork = artwork.Artwork()
self.userid = window('currUserId')
self.server = window('pms_server')
def __enter__(self):
@ -268,8 +266,8 @@ class Movies(Items):
break
# GET THE FILE AND PATH #####
doIndirect = not self.directpath
if self.directpath:
doIndirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
# Direct paths is set the Kodi way
playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None:
@ -569,8 +567,8 @@ class TVShows(Items):
studio = None
# GET THE FILE AND PATH #####
doIndirect = not self.directpath
if self.directpath:
doIndirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
# Direct paths is set the Kodi way
playurl = API.getTVShowPath()
if playurl is None:
@ -892,9 +890,9 @@ class TVShows(Items):
seasonid = self.kodi_db.addSeason(showid, season)
# GET THE FILE AND PATH #####
doIndirect = not self.directpath
doIndirect = not state.DIRECT_PATHS
playurl = API.getFilePath(forceFirstMediaStream=True)
if self.directpath:
if state.DIRECT_PATHS:
# Direct paths is set the Kodi way
if playurl is None:
# Something went wrong, trying to use non-direct paths
@ -1116,7 +1114,7 @@ class TVShows(Items):
self.kodi_db.addStreams(fileid, streams, runtime)
# Process playstates
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.
temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/")
tempfileid = self.kodi_db.addFile(filename, temppathid)
@ -1634,8 +1632,8 @@ class Music(Items):
mood = ' / '.join(moods)
# GET THE FILE AND PATH #####
doIndirect = not self.directpath
if self.directpath:
doIndirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
# Direct paths is set the Kodi way
playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None:

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ from Queue import Empty
from xbmc import sleep
from utils import ThreadMethodsAdditionalStop, ThreadMethods, window
from utils import thread_methods, window
from PlexFunctions import GetPlexMetadata, GetAllPlexChildren
import sync_info
@ -16,8 +16,7 @@ log = getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
@ThreadMethods
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
class Threaded_Get_Metadata(Thread):
"""
Threaded download of Plex XML metadata for a certain library item.
@ -48,7 +47,7 @@ class Threaded_Get_Metadata(Thread):
continue
else:
self.queue.task_done()
if self.threadStopped():
if self.thread_stopped():
# Shutdown from outside requested; purge out_queue as well
while not self.out_queue.empty():
# 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
queue = self.queue
out_queue = self.out_queue
threadStopped = self.threadStopped
while threadStopped() is False:
thread_stopped = self.thread_stopped
while thread_stopped() is False:
# grabs Plex item from queue
try:
item = queue.get(block=False)

View file

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

View file

@ -4,7 +4,7 @@ from threading import Thread, Lock
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')
@ThreadMethods
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD'])
class Threaded_Show_Sync_Info(Thread):
"""
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
total = self.total
dialog = self.dialog
threadStopped = self.threadStopped
thread_stopped = self.thread_stopped
dialog.create("%s %s: %s %s"
% (lang(39714), self.item_type, str(total), lang(39715)))
total = 2 * total
totalProgress = 0
while threadStopped() is False:
while thread_stopped() is False:
with LOCK:
get_progress = GET_METADATA_COUNT
process_progress = PROCESS_METADATA_COUNT

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,8 +3,10 @@ import re
import threading
import downloadutils
from clientinfo import getXArgsDeviceInfo
from utils import window
import PlexFunctions as pf
import state
from functions import *
###############################################################################
@ -68,19 +70,16 @@ class SubscriptionManager:
info = self.getPlayerProperties(playerid)
# save this info off so the server update can use it too
self.playerprops[playerid] = info;
state = info['state']
status = info['state']
time = info['time']
else:
state = "stopped"
status = "stopped"
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:
ret += ' seekRange="0-0"'
ret += ' />'
return ret
# pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server'))
# userId = str(WINDOW.getProperty('currUserId'))
pbmc_server = window('pms_server')
if pbmc_server:
(self.protocol, self.server, self.port) = \
@ -112,7 +111,6 @@ class SubscriptionManager:
ret += ' containerKey="%s"' % self.containerKey
ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable()
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
ret += ' protocol="%s"' % serv.get('protocol', "http")
@ -123,6 +121,8 @@ class SubscriptionManager:
ret += ' mute="%s"' % self.mute
ret += ' repeat="%s"' % info['repeat']
ret += ' itemType="%s"' % info['itemType']
if state.PLEX_TRANSIENT_TOKEN:
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
# Might need an update in the future
if ptype == 'video':
ret += ' subtitleStreamID="-1"'
@ -167,9 +167,10 @@ class SubscriptionManager:
# Process the players we have left (to signal a stop)
for typus, p in self.lastplayers.iteritems():
self.lastinfo[typus]['state'] = 'stopped'
self._sendNotification(self.lastinfo[typus])
# self._sendNotification(self.lastinfo[typus])
def _sendNotification(self, info):
xargs = getXArgsDeviceInfo()
params = {
'containerKey': self.containerKey or "/library/metadata/900000",
'key': self.lastkey or "/library/metadata/900000",
@ -178,6 +179,8 @@ class SubscriptionManager:
'time': info['time'],
'duration': info['duration']
}
if state.PLEX_TRANSIENT_TOKEN:
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
if info.get('playQueueID'):
params['containerKey'] = '/playQueues/%s' % info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion']
@ -186,7 +189,7 @@ class SubscriptionManager:
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'),
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"
% (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 utils import window, settings, language as lang, ThreadMethods, \
ThreadMethodsAdditionalSuspend
from utils import window, settings, language as lang, thread_methods
import downloadutils
import PlexAPI
from PlexFunctions import GetMachineIdentifier
import state
###############################################################################
@ -24,8 +24,7 @@ log = logging.getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_Userclient')
@ThreadMethods
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
class UserClient(threading.Thread):
# Borg - multiple instances, shared state
@ -40,7 +39,6 @@ class UserClient(threading.Thread):
self.retry = 0
self.currUser = None
self.currUserId = None
self.currServer = None
self.currToken = None
self.HasAccess = True
@ -118,37 +116,19 @@ class UserClient(threading.Thread):
def hasAccess(self):
# Plex: always return True for now
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):
log.debug('Loading current user')
doUtils = self.doUtils
self.currUserId = userId
self.currToken = usertoken
self.currServer = self.getServer()
self.ssl = self.getSSLverify()
self.sslcert = self.getSSL()
if authenticated is False:
if self.currServer is None:
return False
log.debug('Testing validity of current token')
res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
token=self.currToken,
@ -164,21 +144,27 @@ class UserClient(threading.Thread):
return False
# Set to windows property
window('currUserId', value=userId)
window('plex_username', value=username)
state.PLEX_USER_ID = userId or None
state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '')
window('pms_token', value=self.currToken)
# This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv
window('plex_token', value=settings('plexToken'))
state.PLEX_TOKEN = settings('plexToken') or None
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('plex_machineIdentifier', value=self.machineIdentifier)
window('plex_servername', value=self.servername)
window('plex_authenticated', value='true')
state.AUTHENTICATED = True
window('useDirectPaths', value='true'
if settings('useDirectPaths') == "1" else 'false')
state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \
else False
window('plex_force_transcode_pix', value='true'
if settings('force_transcode_pix') == "1" else 'false')
@ -202,7 +188,7 @@ class UserClient(threading.Thread):
# Give attempts at entering password / selecting user
if self.retry >= 2:
log.error("Too many retries to login.")
window('plex_serverStatus', value="Stop")
state.PMS_STATUS = 'Stop'
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
@ -283,14 +269,17 @@ class UserClient(threading.Thread):
self.doUtils.stopSession()
window('plex_authenticated', clear=True)
state.AUTHENTICATED = False
window('pms_token', clear=True)
state.PLEX_TOKEN = None
window('plex_token', clear=True)
window('pms_server', clear=True)
window('plex_machineIdentifier', clear=True)
window('plex_servername', clear=True)
window('currUserId', clear=True)
window('plex_username', clear=True)
state.PLEX_USER_ID = None
state.PLEX_USERNAME = None
window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False
settings('username', value='')
settings('userid', value='')
@ -298,44 +287,42 @@ class UserClient(threading.Thread):
# Reset token in downloads
self.doUtils.setToken('')
self.doUtils.setUserId('')
self.doUtils.setUsername('')
self.currToken = None
self.auth = True
self.currUser = None
self.currUserId = None
self.retry = 0
def run(self):
log.info("----===## Starting UserClient ##===----")
while not self.threadStopped():
while self.threadSuspended():
if self.threadStopped():
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
while not thread_stopped():
while thread_suspended():
if thread_stopped():
break
xbmc.sleep(1000)
status = window('plex_serverStatus')
if status == "Stop":
if state.PMS_STATUS == "Stop":
xbmc.sleep(500)
continue
# Verify the connection status to server
elif status == "restricted":
elif state.PMS_STATUS == "restricted":
# Parental control is restricting access
self.HasAccess = False
elif status == "401":
elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token
window('plex_serverStatus', value="Auth")
state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth')
self.resetClient()
xbmc.sleep(2000)
xbmc.sleep(3000)
if self.auth and (self.currUser is None):
# 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
# to authenticate the user
self.auth = False
@ -343,10 +330,11 @@ class UserClient(threading.Thread):
# Successfully authenticated and loaded a user
log.info("Successfully authenticated!")
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
window('suspend_LibraryThread', clear=True)
state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True)
state.PMS_STATUS = False
if not self.auth and (self.currUser is None):
# Loop if no server found
@ -354,7 +342,7 @@ class UserClient(threading.Thread):
# The status Stop is for when user cancelled password dialog.
# 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
log.debug("Server found: %s" % server)
self.auth = True
@ -362,5 +350,4 @@ class UserClient(threading.Thread):
# Minimize CPU load
xbmc.sleep(100)
self.doUtils.stopSession()
log.info("##===---- UserClient Stopped ----===##")

View file

@ -11,7 +11,7 @@ from StringIO import StringIO
from time import localtime, strftime, strptime
from unicodedata import normalize
import xml.etree.ElementTree as etree
from functools import wraps
from functools import wraps, partial
from calendar import timegm
from os.path import join
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, \
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)
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):
"""
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
<17)
Feed with encoded string
Feed with encoded string or unicode
"""
if KODIVERSION >= 17:
answ = exists(path)
answ = exists(tryEncode(path))
else:
dummyfile = join(path, 'dummyfile.txt')
dummyfile = join(tryDecode(path), 'dummyfile.txt')
try:
with open(dummyfile, 'w') as f:
f.write('text')
@ -111,7 +125,7 @@ def exists_dir(path):
answ = 0
else:
# Folder exists. Delete file again.
delete(dummyfile)
delete(tryEncode(dummyfile))
answ = 1
return answ
@ -319,7 +333,7 @@ def reset():
return
# first stop any db sync
window('plex_shouldStop', value="true")
plex_command('STOP_SYNC', 'True')
count = 10
while window('plex_dbScan') == "true":
log.debug("Sync is running, will retry: %s..." % count)
@ -347,7 +361,7 @@ def reset():
for row in rows:
tablename = row[0]
if tablename != "version":
cursor.execute("DELETE FROM ?", (tablename,))
cursor.execute("DELETE FROM %s" % tablename)
connection.commit()
cursor.close()
@ -360,7 +374,7 @@ def reset():
for row in rows:
tablename = row[0]
if tablename != "version":
cursor.execute("DELETE FROM ?", (tablename, ))
cursor.execute("DELETE FROM %s" % tablename)
connection.commit()
cursor.close()
@ -373,7 +387,7 @@ def reset():
for row in rows:
tablename = row[0]
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 view')
connection.commit()
@ -387,7 +401,7 @@ def reset():
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/")
if exists(path):
rmtree(path, ignore_errors=True)
rmtree(tryDecode(path), ignore_errors=True)
# remove all existing data from texture DB
connection = kodiSQL('texture')
cursor = connection.cursor()
@ -397,7 +411,7 @@ def reset():
for row in rows:
tableName = row[0]
if(tableName != "version"):
cursor.execute("DELETE FROM ?", (tableName, ))
cursor.execute("DELETE FROM %s" % tableName)
connection.commit()
cursor.close()
@ -411,7 +425,7 @@ def reset():
line1=language(39603)):
# Delete the settings
addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile'))
addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
dataPath = "%ssettings.xml" % addondir
log.info("Deleting: settings.xml")
remove(dataPath)
@ -522,9 +536,16 @@ def guisettingsXML():
try:
xmlparse = etree.parse(xmlpath)
except:
except IOError:
# Document is blank or missing
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:
root = xmlparse.getroot()
return root
@ -606,6 +627,14 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None,
return None, None
# Create topmost xml entry
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()
element = root
@ -632,17 +661,6 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None,
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():
# To make Master lock compatible
path = tryDecode(xbmc.translatePath("special://profile/"))
@ -650,8 +668,15 @@ def sourcesXML():
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank or missing
except IOError: # Document is blank or missing
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:
root = xmlparse.getroot()
@ -685,20 +710,27 @@ def sourcesXML():
def passwordsXML():
# To add network credentials
path = xbmc.translatePath("special://userdata/")
path = tryDecode(xbmc.translatePath("special://userdata/"))
xmlpath = "%spasswords.xml" % path
dialog = xbmcgui.Dialog()
try:
xmlparse = etree.parse(xmlpath)
except:
except IOError:
# Document is blank or missing
root = etree.Element('passwords')
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:
root = xmlparse.getroot()
skipFind = False
dialog = xbmcgui.Dialog()
credentials = settings('networkCreds')
if credentials:
# Present user with options
@ -798,7 +830,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
"""
Feed with tagname as unicode
"""
path = xbmc.translatePath("special://profile/playlists/video/")
path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
if viewtype == "mixed":
plname = "%s - %s" % (tagname, 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)
# Create the playlist directory
if not exists(path):
if not exists(tryEncode(path)):
log.info("Creating directory: %s" % path)
makedirs(path)
# Only add the playlist if it doesn't already exists
if exists(xsppath):
if exists(tryEncode(xsppath)):
log.info('Path %s does exist' % xsppath)
if delete:
remove(xsppath)
@ -827,27 +859,22 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
'show': 'tvshows'
}
log.info("Writing playlist file to: %s" % xsppath)
try:
with open(xsppath, 'wb'):
tryEncode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Plex %s</name>\n\t'
'<match>all</match>\n\t'
'<rule field="tag" operator="is">\n\t\t'
'<value>%s</value>\n\t'
'</rule>\n'
'</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
except Exception as e:
log.error("Failed to create playlist: %s" % xsppath)
log.error(e)
return
with open(xsppath, 'wb'):
tryEncode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Plex %s</name>\n\t'
'<match>all</match>\n\t'
'<rule field="tag" operator="is">\n\t\t'
'<value>%s</value>\n\t'
'</rule>\n'
'</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
log.info("Successfully added playlist: %s" % tagname)
def deletePlaylists():
# 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 file in files:
if file.startswith('Plex'):
@ -855,7 +882,7 @@ def deletePlaylists():
def deleteNodes():
# 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 directory in dirs:
if directory.startswith('Plex-'):
@ -906,78 +933,78 @@ def LogTime(func):
return wrapper
def ThreadMethodsAdditionalStop(windowAttribute):
"""
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):
def thread_methods(cls=None, add_stops=None, add_suspends=None):
"""
Decorator to add the following methods to a threading class:
suspendThread(): pauses the thread
resumeThread(): resumes the thread
stopThread(): stopps/kills the thread
suspend_thread(): pauses the thread
resume_thread(): resumes the thread
stop_thread(): stopps/kills the thread
threadSuspended(): returns True if thread is suspend_thread
threadStopped(): returns True if thread is stopped (or should stop ;-))
ALSO stops if Kodi is exited
thread_suspended(): returns True if thread is suspended
thread_stopped(): returns True if thread is stopped (or should stop ;-))
ALSO returns True if PKC should exit
Also adds the following class attributes:
_threadStopped
_threadSuspended
__thread_stopped
__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
cls._threadStopped = False
cls._threadSuspended = False
cls.__thread_stopped = False
cls.__thread_suspended = False
# Define new class methods and attach them to class
def stopThread(self):
self._threadStopped = True
cls.stopThread = stopThread
def stop_thread(self):
self.__thread_stopped = True
cls.stop_thread = stop_thread
def suspendThread(self):
self._threadSuspended = True
cls.suspendThread = suspendThread
def suspend_thread(self):
self.__thread_suspended = True
cls.suspend_thread = suspend_thread
def resumeThread(self):
self._threadSuspended = False
cls.resumeThread = resumeThread
def resume_thread(self):
self.__thread_suspended = False
cls.resume_thread = resume_thread
def threadSuspended(self):
return self._threadSuspended
cls.threadSuspended = threadSuspended
def thread_suspended(self):
if self.__thread_suspended is True:
return True
for suspend in self.__suspends:
if getattr(state, suspend):
return True
return False
cls.thread_suspended = thread_suspended
def threadStopped(self):
return self._threadStopped or (window('plex_terminateNow') == 'true')
cls.threadStopped = threadStopped
def thread_stopped(self):
if self.__thread_stopped is True:
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 cls

View file

@ -2,7 +2,8 @@
import xbmc
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'):
@ -29,7 +30,7 @@ ADDON_VERSION = _ADDON.getAddonInfo('version')
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = xbmc.translatePath("special://profile")
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX"
@ -70,8 +71,8 @@ _DB_VIDEO_VERSION = {
17: 107, # Krypton
18: 108 # Leia
}
DB_VIDEO_PATH = xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])
DB_VIDEO_PATH = tryDecode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = {
13: 46, # Gotham
@ -81,8 +82,8 @@ _DB_MUSIC_VERSION = {
17: 60, # Krypton
18: 62 # Leia
}
DB_MUSIC_PATH = xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])
DB_MUSIC_PATH = tryDecode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = {
13: 13, # Gotham
@ -92,13 +93,13 @@ _DB_TEXTURE_VERSION = {
17: 13, # Krypton
18: 13 # Leia
}
DB_TEXTURE_PATH = xbmc.translatePath(
"special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION])
DB_TEXTURE_PATH = tryDecode(xbmc.translatePath(
"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(
"special://profile/addon_data/%s/temp/" % ADDON_ID)
EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath(
"special://profile/addon_data/%s/temp/" % ADDON_ID))
# Multiply Plex time by this factor to receive Kodi time

View file

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

View file

@ -11,9 +11,9 @@ from ssl import CERT_NONE
from xbmc import sleep
from utils import window, settings, ThreadMethodsAdditionalSuspend, \
ThreadMethods
from utils import window, settings, thread_methods
from companion import process_command
import state
###############################################################################
@ -22,8 +22,6 @@ log = logging.getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@ThreadMethods
class WebSocket(Thread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
@ -62,11 +60,11 @@ class WebSocket(Thread):
counter = 0
handshake_counter = 0
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
while not threadStopped():
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
while not thread_stopped():
# In the event the server goes offline
while threadSuspended():
while thread_suspended():
# Set in service.py
if self.ws is not None:
try:
@ -74,7 +72,7 @@ class WebSocket(Thread):
except:
pass
self.ws = None
if threadStopped():
if thread_stopped():
# Abort was requested while waiting. We should exit
log.info("##===---- %s Stopped ----===##"
% self.__class__.__name__)
@ -141,16 +139,17 @@ class WebSocket(Thread):
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__)
self._threadStopped = True
self.__threadStopped = True
try:
self.ws.shutdown()
except:
pass
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD'])
class PMS_Websocket(WebSocket):
"""
Websocket connection with the PMS for Plex Companion
@ -160,16 +159,15 @@ class PMS_Websocket(WebSocket):
def getUri(self):
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
if server.startswith('https'):
server = "wss%s" % server[5:]
else:
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
if token:
uri += '?X-Plex-Token=%s' % token
# Need to use plex.tv token, if any. NOT user token
if state.PLEX_TOKEN:
uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
@ -213,14 +211,18 @@ class PMS_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):
self.plex_client_Id = window('plex_client_Id')
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (window('currUserId'),
self.plex_client_Id,
window('plex_token')))
% (state.PLEX_USER_ID,
self.plex_client_Id, state.PLEX_TOKEN))
sslopt = {}
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt
@ -252,11 +254,32 @@ class Alexa_Websocket(WebSocket):
def IOError_response(self):
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
plex_restricteduser
Overwrite method since we need to check for plex token
"""
return (self._threadSuspended or
window('plex_restricteduser') == 'true' or
not window('plex_token'))
if self.__thread_suspended is True:
return True
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 sys import path as sys_path, argv
from xbmc import translatePath, Monitor, sleep
from xbmc import translatePath, Monitor
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
import initialsetup
from kodimonitor import KodiMonitor
@ -42,10 +43,11 @@ from playqueue import Playqueue
import PlexAPI
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 artwork import Image_Cache_Thread
import variables as v
import state
###############################################################################
@ -85,7 +87,7 @@ class Service():
window('plex_logLevel', value=str(logLevel))
window('plex_kodiProfile',
value=translatePath("special://profile"))
value=tryDecode(translatePath("special://profile")))
window('plex_context',
value='true' if settings('enableContext') == "true" else "")
window('fetch_pms_item_number',
@ -105,18 +107,16 @@ class Service():
# Reset window props for profile switch
properties = [
"plex_online", "plex_serverStatus", "plex_onWake",
"plex_dbCheck", "plex_kodiScan",
"plex_shouldStop", "currUserId", "plex_dbScan",
"plex_shouldStop", "plex_dbScan",
"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",
"plex_authenticated", "PlexUserImage", "useDirectPaths",
"suspend_LibraryThread", "plex_terminateNow",
"kodiplextimeoffset", "countError", "countUnauthorized",
"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:
window(prop, clear=True)
@ -134,15 +134,22 @@ class Service():
logLevel = 0
return logLevel
def __stop_PKC(self):
"""
Kodi's abortRequested is really unreliable :-(
"""
return self.monitor.abortRequested() or state.STOP_PKC
def ServiceEntryPoint(self):
# Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once.
__stop_PKC = self.__stop_PKC
monitor = self.monitor
kodiProfile = v.KODI_PROFILE
# Detect playback start early on
self.monitor_kodi_play = Monitor_Kodi_Play(self)
self.monitor_kodi_play.start()
self.command_pipeline = Monitor_Window(self)
self.command_pipeline.start()
# Server auto-detect
initialsetup.InitialSetup().setup()
@ -162,14 +169,14 @@ class Service():
welcome_msg = True
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
log.warn("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread."
% (kodiProfile,
tryEncode(window('plex_kodiProfile'))))
window('plex_kodiProfile')))
break
# Before proceeding, need to make sure:
@ -242,14 +249,13 @@ class Service():
# Server went offline
break
if monitor.waitForAbort(5):
if monitor.waitForAbort(3):
# Abort was requested while waiting. We should exit
break
sleep(50)
else:
# Wait until Plex server is online
# or Kodi is shut down.
while not monitor.abortRequested():
while not self.__stop_PKC():
server = self.user.getServer()
if server is False:
# No server info set in add-on settings
@ -261,7 +267,7 @@ class Service():
self.server_online = False
window('plex_online', value="false")
# Suspend threads
window('suspend_LibraryThread', value='true')
state.SUSPEND_LIBRARY_THREAD = True
log.error("Plex Media Server went offline")
if settings('show_pms_offline') == 'true':
dialog('notification',
@ -298,10 +304,10 @@ class Service():
sound=False)
log.info("Server %s is online and ready." % server)
window('plex_online', value="true")
if window('plex_authenticated') == 'true':
if state.AUTHENTICATED:
# Server got offline when we were authenticated.
# Hence resume threads
window('suspend_LibraryThread', clear=True)
state.SUSPEND_LIBRARY_THREAD = False
# Start the userclient thread
if not self.user_running:
@ -317,31 +323,10 @@ class Service():
if monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit
break
# Terminating PlexKodiConnect
# Tell all threads to terminate (e.g. several lib sync threads)
window('plex_terminateNow', value='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')
state.STOP_PKC = True
try:
downloadutils.DownloadUtils().stopSession()
except:
@ -349,6 +334,7 @@ class Service():
window('plex_service_started', clear=True)
log.warn("======== STOP %s ========" % v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation!
if window('plex_service_started') == 'true':
exit = True