diff --git a/README.md b/README.md index d4a0d901..f26c4dbd 100644 --- a/README.md +++ b/README.md @@ -55,18 +55,28 @@ Currently these features are working: - Play directly from network paths (e.g. "\\\\server\\Plex\\movie.mkv" on Windows or SMB paths "smb://server/Plex/movie.mkv") instead of slow HTTP (e.g. "192.168.1.1:32400"). You have to setup all your Plex libraries to point to such network paths. -**Known Issues:** -- **Plex Music:** You must have a static IP address for your Plex media server if you plan to use Plex Music features. This is due to the way Kodi works and cannot be helped. +**Known "Larger" Issues:** +Solutions are unlikely due to the nature of these issues +- **Plex Music:** You must have a static IP address for your Plex media server if you plan to use Plex Music features. This is due to the way Kodi works and cannot be helped. +- **Plex Music:** Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. (Plex puts each song in a "dedicated folder", e.g. 'http://192.168.1.1:32400/library/parts/749450/'. Kodi unsuccessfully tries to scan these folders) - **Plex updates:** PlexKodiConnect continuously polls the Plex Media Server for changes. If something on the PMS has changed, this change is synced to Kodi. Hence if you rescan your entire library, a long PlexKodiConnect re-sync is triggered. -- **Direct Paths:** If you use direct paths, your sync will be slower +- **Subtitles**: external Plex subtitles (separate file, e.g. mymovie.srt) can be used, but it is impossible to label them correctly/tell what language they are in +- **Direct Paths:** If you use direct paths, your (initial) sync will be slower + +**Known Bugs:** +- **Plex Music:** Plex Music for direct paths does not work yet. - **Video Nodes**: some nodes, e.g. "On Deck", are customized/hacked. Hence no access to movie metadata is possible, because Kodi does not know it's a library item -**What could be in the pipeline?** + +**What could be in the pipeline for future development?** - Watch Later - Playlists - Homevideos - Pictures - Music Videos +- Automatic updates +- Redesigned background sync process that puts less strain on the PMS +- Simultaneously connecting to several PMS - TV Shows Theme Music (ultra-low prio) diff --git a/addon.xml b/addon.xml index 95c70d7c..861e9c3e 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/changelog.txt b/changelog.txt index 84500448..d4487bc0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,20 @@ +version 1.0.16 +- Kodi profiles up and running; try assigning different Plex users per profile! +- Change "Switch User" to "Log Out Plex User: " +- TV shows On Deck: append season and episode number in the settings +- Shut down PKC correctly (useful for Kodi profiles) +- Don't de-authorize if several PMS are present +- Relabel to "Full PKC reset" in settings + +version 1.0.15 +- Enable external Plex subtitles if available +- TV On Deck: option to include show name +- Playback updates now if an item is resumed +- Fix PMS not being informed of playback stop +- Fix playback updates for remote PMS +- Deactivate info "Gathering information from files" +- Updated readme + version 1.0.14 - Fix TV shows rating not showing up - Fix music libraries being scanned twice diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 9527dbcc..6c2eeace 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -357,11 +357,10 @@ Please sign in to plex.tv. Problems connecting to server. Pick another server? Disable Plex music library? - Would you now like to go to the plugin's settings? - (This is hopefully unneccessary ;-)) + Would you now like to go to the plugin's settings to fine-tune PKC? You will need to RESTART Kodi! [COLOR yellow]Repair local database (force update all content)[/COLOR] - [COLOR yellow]Perform local database reset (full resync)[/COLOR] + [COLOR red]Partial or full reset of Database and PKC[/COLOR] [COLOR yellow]Cache all images to Kodi texture cache[/COLOR] [COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR] (local) @@ -388,8 +387,15 @@ Go a step further and complete replace all original Plex library paths (/volume1/media) with custom SMB paths (smb://NAS/MyStuff)? Please enter your custom smb paths in the settings under "Sync Options" and then restart Kodi + Appearance Tweaks + TV Shows + On Deck: Append show title to episode + On Deck: Append season- and episode-number (e.g. S3E2) + Nothing works? Try a full reset! + + - Switch Plex Home User + Log-out Plex Home User: Settings Network credentials Refresh Plex playlists/nodes diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index 009e9279..a8d107bd 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -294,10 +294,10 @@ Bitte loggen Sie sich in plex.tv ein. Beim Verbinden mit dem Server sind Probleme aufgetreten. Mit einem anderen Server versuchen? Plex Musik Bibliotheken deaktivieren? - Möchten Sie nun die Einstellungen des Plugins öffnen? (Was hoffentlich unnötig ist ;-))" + Möchten Sie nun die Einstellungen des Plugins öffnen? Kodi muss anschliessend neu gestartet werden! [COLOR yellow]Lokale Datenbank reparieren (allen Inhalt aktualisieren)[/COLOR] - [COLOR yellow]Lokale Datenbank zurücksetzen (kompletter Resync nötig)[/COLOR] + [COLOR red]Datenbank und auf Wunsch PKC zurücksetzen[/COLOR] [COLOR yellow]Alle Plex Bilder in Kodi zwischenspeichern[/COLOR] [COLOR yellow]Plex Themes zu Kodi synchronisieren[/COLOR] (lokal) @@ -324,8 +324,14 @@ Sollen sogar sämtliche Plex Pfade wie /volume1/Hans/medien durch benutzerdefinierte smb Pfade wie smb://NAS/Filme ersetzt werden? Bitte geben Sie Ihre benutzerdefinierten SMB Pfade nun in den Einstellungen unter Sync Optionen ein. Starten Sie dann Kodi neu. + Erscheinung + TV Serien + "Aktuell": Serien- an Episoden-Titel anfügen + "Aktuell": Staffel und Episode anfügen (z.B. S3E2) + Nichts funktioniert? Setze mal alles zurück! + - Plex Home Benutzer wechseln + Plex Home Benutzer abmelden: Einstellungen Netzwerk Credentials Plex Playlisten und Nodes zurücksetzen diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 7daa6d88..f46ea86d 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -51,6 +51,7 @@ from threading import Thread import Queue import traceback import requests +import xml.etree.ElementTree as etree import re import json @@ -58,10 +59,10 @@ from urllib import urlencode, quote_plus, unquote from PlexFunctions import PlexToKodiTimefactor, PMSHttpsEnabled -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree + +# Disable requests logging +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) @utils.logging @@ -194,12 +195,12 @@ class PlexAPI(): # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) while count < 30: xml = self.CheckPlexTvSignin(identifier) - if xml: + if xml is not False: break # Wait for 1 seconds xbmc.sleep(1000) count += 1 - if not xml: + if xml is False: # Could not sign in to plex.tv Try again later dialog.ok(self.addonName, string(39305)) return False @@ -263,7 +264,7 @@ class PlexAPI(): identifier = None # Download xml = self.TalkToPlexServer(url, talkType="POST") - if not xml: + if xml is False: return code, identifier try: code = xml.find('code').text @@ -648,7 +649,11 @@ class PlexAPI(): # Ping to check whether we need HTTPs or HTTP url = (self.getPMSProperty(ATV_udid, uuid_id, 'ip') + ':' + self.getPMSProperty(ATV_udid, uuid_id, 'port')) - if PMSHttpsEnabled(url): + https = PMSHttpsEnabled(url) + if https is None: + # Error contacting url + continue + elif https: self.updatePMSProperty(ATV_udid, uuid_id, 'scheme', 'https') else: self.updatePMSProperty(ATV_udid, uuid_id, 'scheme', 'http') @@ -2316,6 +2321,7 @@ class API(): utils.window('emby_shouldStop', value="true") playurl = False utils.window('emby_pathverified', value='true') + utils.settings('emby_pathverified', value='true') return playurl def askToValidate(self, url): diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 93275547..bd35daa1 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -2,7 +2,6 @@ import threading import traceback import socket -import requests import xbmc @@ -64,22 +63,19 @@ class PlexCompanion(threading.Thread): message_count = 0 is_running = False while not self.threadStopped(): - while self.threadSuspended(): - if self.threadStopped(): - break - xbmc.sleep(3000) # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - if window('emby_serverStatus'): - xbmc.sleep(3000) - continue + while self.threadSuspended() or window('emby_serverStatus'): + if self.threadStopped(): + break + xbmc.sleep(1000) try: httpd.handle_request() message_count += 1 - if message_count > 30: + if message_count > 100: if self.client.check_client_registration(): self.logMsg("Client is still registered", 1) else: @@ -97,13 +93,14 @@ class PlexCompanion(threading.Thread): xbmc.sleep(50) except: self.logMsg("Error in loop, continuing anyway", 1) - self.logMsg(traceback.print_exc(), 1) + self.logMsg(traceback.format_exc(), 1) xbmc.sleep(50) self.client.stop_all() try: httpd.socket.shutdown(socket.SHUT_RDWR) + except: + pass finally: httpd.socket.close() - requests.dumpConnections() self.logMsg("----===## STOP Plex Companion ##===----", 0) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index d00da0e7..6e048df3 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -4,12 +4,17 @@ from ast import literal_eval from urlparse import urlparse, parse_qs import re from copy import deepcopy +import requests from xbmcaddon import Addon import downloadutils from utils import logMsg, settings +# Disable requests logging +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + addonName = Addon().getAddonInfo('name') title = "%s %s" % (addonName, __name__) @@ -394,7 +399,8 @@ def getPlexRepeat(kodiRepeat): def PMSHttpsEnabled(url): """ - Returns True if the PMS wants to talk https, False otherwise + Returns True if the PMS wants to talk https, False otherwise. None if error + occured, e.g. the connection timed out With with e.g. url=192.168.0.1:32400 (NO http/https) @@ -403,17 +409,37 @@ def PMSHttpsEnabled(url): Prefers HTTPS over HTTP """ - xml = downloadutils.DownloadUtils().downloadUrl( - 'https://%s/identity' % url) + # True if https, False if http + answer = True try: - # received a valid XML - https connection is possible - xml.attrib - logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1) - return True - except: - # couldn't get an xml - switch to http traffic - logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1) - return False + # Don't use downloadutils here, otherwise we may get un-authorized! + res = requests.get('https://%s/identity' % url, + headers={}, + verify=False, + timeout=(3, 10)) + # Don't verify SSL since we can connect for sure then! + except requests.exceptions.ConnectionError as e: + # Might have SSL deactivated. Try with http + try: + res = requests.get('http://%s/identity' % url, + headers={}, + timeout=(3, 10)) + except requests.exceptions.ConnectionError as e: + logMsg("Server is offline or cannot be reached. Url: %s, " + "Error message: %s" % (url, e), -1) + return None + except requests.exceptions.ReadTimeout: + logMsg("Server timeout reached for Url %s" % url, -1) + return None + else: + answer = False + except requests.exceptions.ReadTimeout: + logMsg("Server timeout reached for Url %s" % url, -1) + return None + if res.status_code == requests.codes.ok: + return answer + else: + return None def scrobble(ratingKey, state): diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 9d9da4ec..3b2c21f3 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -15,6 +15,10 @@ import xbmcvfs import utils import image_cache_thread +# Disable requests logging +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + ############################################################################### diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index d7e1b387..80d97eb4 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -4,6 +4,7 @@ # import json import requests +import xml.etree.ElementTree as etree # import logging # import xbmc @@ -13,10 +14,6 @@ import utils import clientinfo import PlexAPI -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree ############################################################################### @@ -392,7 +389,8 @@ class DownloadUtils(): elif status not in ("401", "Auth"): # Tell userclient token has been revoked. - self.logMsg('Setting emby_serverStatus to 401') + self.logMsg('Error 401 contacting %s' % url, 0) + self.logMsg('Setting emby_serverStatus to 401', 0) utils.window('emby_serverStatus', value="401") self.logMsg("HTTP Error: %s" % e, 0) xbmcgui.Dialog().notification( diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 59e82fc8..946b3345 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -81,13 +81,28 @@ def reConnect(): utils.logMsg("entrypoint reConnect", "Connection resets requested", 0) dialog = xbmcgui.Dialog() + # Resetting, please wait dialog.notification( heading=addonName, message=string(39207), icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", + time=2000, sound=False) # Pause library sync thread - user needs to be auth in order to sync utils.window('suspend_LibraryThread', value='true') + # Wait max for 5 seconds for all lib scans to finish + counter = 0 + while utils.window('emby_dbScan') == 'true': + if counter > 500: + # Failed to reset PMS and plex.tv connects. Try to restart Kodi. + dialog.ok(heading=addonName, + message=string(39208)) + # Resuming threads, just in case + utils.window('suspend_LibraryThread', clear=True) + # Abort reConnection + return + counter += 1 + xbmc.sleep(50) # Delete plex credentials in settings utils.settings('myplexlogin', value="true") @@ -97,18 +112,13 @@ def reConnect(): utils.settings('plexHomeSize', value="1") utils.settings('plexAvatar', value="") - # Wait max for 5 seconds for all lib scans to finish - counter = 0 - while utils.window('emby_dbScan') == 'true': - if counter > 100: - dialog.ok(heading=addonName, - message=string(39208)) - # Resuming threads, just in case - utils.window('suspend_LibraryThread', clear=True) - # Abort reConnection - return - counter += 1 - xbmc.sleep(50) + # Reset connection details + utils.settings('plex_machineIdentifier', value="") + utils.settings('plex_servername', value="") + utils.settings('https', value="") + utils.settings('ipaddress', value="") + utils.settings('port', value="") + # Log out currently signed in user: utils.window('emby_serverStatus', value="401") @@ -255,7 +265,7 @@ def doMainListing(): addDirectoryItem(label, path) # Plex user switch - addDirectoryItem(string(39200), + addDirectoryItem(string(39200) + utils.window('plex_username'), "plugin://plugin.video.plexkodiconnect/" "?mode=switchuser") @@ -1446,6 +1456,19 @@ def getOnDeck(viewid, mediatype, tagname, limit): for episode in episodes: # There will always be only 1 episode ('limit=1') li = createListItem(episode) + # Fix some skin shortcomings + title = episode.get('title', '') + if utils.settings('OnDeckTvAppendSeason') == 'true': + seasonid = episode.get('season') + episodeid = episode.get('episode') + if seasonid and episodeid: + title = ('S' + str(seasonid) + 'E' + str(episodeid) + + ' - ' + title) + if utils.settings('OnDeckTvAppendShow') == 'true': + show = episode.get('showtitle') + if show: + title = show + ' - ' + title + li.setLabel(title) xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), url=episode['file'], diff --git a/resources/lib/image_cache_thread.py b/resources/lib/image_cache_thread.py index 69003b69..d40164e7 100644 --- a/resources/lib/image_cache_thread.py +++ b/resources/lib/image_cache_thread.py @@ -3,6 +3,10 @@ import utils import xbmc import requests +# Disable requests logging +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + @utils.logging class image_cache_thread(threading.Thread): diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 1a332fe0..7d1557a6 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -261,8 +261,6 @@ class InitialSetup(): dialog.ok(heading=self.addonName, line1=string(39044)) goToSettings = True - # Don't start anything because we need these paths first! - utils.window('emby_serverStatus', value="Stop") # Go to network credentials? if dialog.yesno(heading=self.addonName, @@ -280,8 +278,9 @@ class InitialSetup(): xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') else: - # Open Settings page now? + # Open Settings page now? You will need to restart! if dialog.yesno(heading=self.addonName, line1=string(39017)): + utils.window('emby_serverStatus', value="Stop") xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 94564bf3..c31720e7 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -2055,17 +2055,18 @@ class Music(Items): if doIndirect: # Plex works a bit differently path = "%s%s" % (self.server, item[0][0].attrib.get('key')) - filename = API.addPlexCredentialsToUrl(path) - # Keep path empty to not let Kodi scan it - path = "" + path = API.addPlexCredentialsToUrl(path) + filename = path.rsplit('/', 1)[1] + path = path.replace(filename, '') # UPDATE THE SONG ##### if update_item: self.logMsg("UPDATE song itemid: %s - Title: %s with path: %s" % (itemid, title, path), 1) # Update path - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - kodicursor.execute(query, (path, pathid)) + # Use dummy strHash '123' for Kodi + query = "UPDATE path SET strPath = ?, strHash = ? WHERE idPath = ?" + kodicursor.execute(query, (path, '123', pathid)) # Update the song entry query = ' '.join(( @@ -2087,7 +2088,7 @@ class Music(Items): self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1) # Add path - pathid = kodi_db.addPath(path) + pathid = kodi_db.addPath(path, strHash="123") try: # Get the album diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index c9dc2011..6bb37d8a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -47,7 +47,7 @@ class Kodidb_Functions(): self.clientInfo = clientinfo.ClientInfo() self.artwork = artwork.Artwork() - def addPath(self, path): + def addPath(self, path, strHash=None): # SQL won't return existing paths otherwise if path is None: path = "" @@ -64,15 +64,26 @@ class Kodidb_Functions(): except TypeError: cursor.execute("select coalesce(max(idPath),0) from path") pathid = cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO path( - idPath, strPath) + if strHash is None: + query = ( + ''' + INSERT INTO path( + idPath, strPath) - VALUES (?, ?) - ''' - ) - cursor.execute(query, (pathid, path)) + VALUES (?, ?) + ''' + ) + cursor.execute(query, (pathid, path)) + else: + query = ( + ''' + INSERT INTO path( + idPath, strPath, strHash) + + VALUES (?, ?, ?) + ''' + ) + cursor.execute(query, (pathid, path, strHash)) return pathid @@ -744,6 +755,88 @@ class Kodidb_Functions(): ids.append(row[0]) return ids + def getIdFromTitle(self, itemdetails): + """ + Returns the Kodi id (e.g. idMovie, idEpisode) from the item's + title (c00), if there is exactly ONE found for the itemtype. + (False otherwise) + + itemdetails is the data['item'] response from Kodi + + itemdetails for movies: + { + "title":"Kung Fu Panda", + "type":"movie", + "year":2008 + } + + itemdetails for episodes: + { + "episode":5 + "season":5, + "showtitle":"Girls", + "title":"Queen for Two Days", + "type":"episode" + } + """ + try: + type = itemdetails['type'] + except: + return False + + if type == 'movie': + query = ' '.join(( + "SELECT idMovie", + "FROM movie", + "WHERE c00 = ?" + )) + try: + rows = self.cursor.execute(query, (itemdetails['title'],)) + except: + return False + elif type == 'episode': + query = ' '.join(( + "SELECT idShow", + "FROM tvshow", + "WHERE c00 = ?" + )) + try: + rows = self.cursor.execute(query, (itemdetails['showtitle'],)) + except: + return False + ids = [] + for row in rows: + ids.append(row[0]) + if len(ids) > 1: + # No unique match possible + return False + showid = ids[0] + + query = ' '.join(( + "SELECT idEpisode", + "FROM episode", + "WHERE c12 = ? AND c13 = ? AND idShow = ?" + )) + try: + rows = self.cursor.execute( + query, + (itemdetails['season'], + itemdetails['episode'], + showid)) + except: + return False + else: + return False + + ids = [] + for row in rows: + ids.append(row[0]) + if len(ids) > 1: + # No unique match possible + return False + else: + return ids[0] + def getUnplayedItems(self): """ VIDEOS diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d16bb26a..502d86d4 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,6 +10,7 @@ import xbmcgui import downloadutils import embydb_functions as embydb +import kodidb_functions as kodidb import playbackutils as pbutils import utils from PlexFunctions import scrobble @@ -154,11 +155,21 @@ class KodiMonitor(xbmc.Monitor): # Try to get a Kodi ID item = data.get('item') try: - kodiid = item['id'] type = item['type'] - except (KeyError, TypeError): - log("Item is invalid for Plex playstate update.", 0) + except: + log("Item is invalid for PMS playstate update.", 0) return + try: + kodiid = item['id'] + except (KeyError, TypeError): + log('Kodi did not give us a Kodi item id, trying to get from item ' + 'title', 0) + # Try to get itemid with the element's title + with kodidb.GetKodiDB('video') as kodi_db: + kodiid = kodi_db.getIdFromTitle(item) + if kodiid is False: + log("Item is invalid for PMS playstate update.", 0) + return # Get Plex' item id with embydb.GetEmbyDB() as emby_db: diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 27c017c1..f097d1df 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -4,10 +4,7 @@ from threading import Thread, Lock import Queue -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree +import xml.etree.ElementTree as etree import xbmc import xbmcgui @@ -239,6 +236,9 @@ class LibrarySync(Thread): 'enableBackgroundSync') == "true" else False self.limitindex = int(utils.settings('limitindex')) + if utils.settings('emby_pathverified') == 'true': + utils.window('emby_pathverified', value='true') + # Time offset between Kodi and PMS in seconds (=Koditime - PMStime) self.timeoffset = 0 # Time in seconds to look into the past when looking for PMS changes @@ -326,7 +326,7 @@ class LibrarySync(Thread): # Get the Plex item's metadata xml = PlexFunctions.GetPlexMetadata(plexId) - if not xml: + if xml is None: self.logMsg("Could not download metadata, aborting time sync", -1) return libraryId = xml[0].attrib['librarySectionID'] @@ -493,7 +493,7 @@ class LibrarySync(Thread): updatedAt=self.getPMSfromKodiTime(lastSync), containerSize=self.limitindex) # Just skip if something went wrong - if not items: + if items is None: continue # Get one itemtype, because they're the same in the PMS section try: @@ -528,7 +528,7 @@ class LibrarySync(Thread): view['id'], lastViewedAt=self.getPMSfromKodiTime(lastSync), containerSize=self.limitindex) - if not items: + if items is None: continue for item in items: itemId = item.attrib.get('ratingKey') @@ -635,6 +635,12 @@ class LibrarySync(Thread): # Add sources utils.sourcesXML() + # Deactivate Kodi popup showing that it's (unsuccessfully) trying to + # scan music folders + if self.enableMusic: + utils.musiclibXML() + utils.advancedSettingsXML() + # Set new timestamp NOW because sync might take a while self.saveLastSync() @@ -1075,7 +1081,7 @@ class LibrarySync(Thread): viewName = view['name'] all_plexmovies = PlexFunctions.GetPlexSectionResults( viewId, args=None, containerSize=self.limitindex) - if not all_plexmovies: + if all_plexmovies is None: self.logMsg("Couldnt get section items, aborting for view.", 1) continue # Populate self.updatelist and self.allPlexElementsId @@ -1209,7 +1215,7 @@ class LibrarySync(Thread): viewName = view['name'] allPlexTvShows = PlexFunctions.GetPlexSectionResults( viewId, containerSize=self.limitindex) - if not allPlexTvShows: + if allPlexTvShows is None: self.logMsg( "Error downloading show view xml for view %s" % viewId, -1) continue @@ -1236,7 +1242,7 @@ class LibrarySync(Thread): # Grab all seasons to tvshow from PMS seasons = PlexFunctions.GetAllPlexChildren( tvShowId, containerSize=self.limitindex) - if not seasons: + if seasons is None: self.logMsg( "Error downloading season xml for show %s" % tvShowId, -1) continue @@ -1261,7 +1267,7 @@ class LibrarySync(Thread): # Grab all episodes to tvshow from PMS episodes = PlexFunctions.GetAllPlexLeaves( view['id'], containerSize=self.limitindex) - if not episodes: + if episodes is None: self.logMsg( "Error downloading episod xml for view %s" % view.get('name'), -1) @@ -1359,7 +1365,7 @@ class LibrarySync(Thread): viewName = view['name'] itemsXML = PlexFunctions.GetPlexSectionResults( viewId, args=urlArgs, containerSize=self.limitindex) - if not itemsXML: + if itemsXML is None: self.logMsg("Error downloading xml for view %s" % viewId, -1) continue @@ -1401,6 +1407,7 @@ class LibrarySync(Thread): except Exception as e: utils.window('emby_dbScan', clear=True) self.logMsg('LibrarySync thread crashed', -1) + self.logMsg('Error message: %s' % e, -1) # Library sync thread has crashed xbmcgui.Dialog().ok( heading=self.addonName, diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 1a1b1539..a03be48a 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -275,9 +275,7 @@ class PlaybackUtils(): # Append external subtitles to stream playmethod = utils.window('%s.playmethod' % embyitem) - # Only for direct stream - if playmethod in ("DirectStream"): - # Direct play automatically appends external + if playmethod in ("DirectStream", "DirectPlay"): subtitles = self.API.externalSubs(playurl) listitem.setSubtitles(subtitles) diff --git a/resources/lib/player.py b/resources/lib/player.py index 77a4dd73..e494fe4a 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -537,6 +537,7 @@ class Player(xbmc.Player): url = "{server}/emby/Items/%s?format=json" % itemid log("Deleting request: %s" % itemid, 1) doUtils(url, type="DELETE") + self.stopPlayback(data) # Clean the WINDOW properties for filename in self.played_info: @@ -552,7 +553,6 @@ class Player(xbmc.Player): ) for item in cleanup: utils.window(item, clear=True) - self.stopPlayback(data) # Stop transcoding if playMethod == "Transcode": @@ -564,7 +564,7 @@ class Player(xbmc.Player): self.played_info.clear() def stopPlayback(self, data): - self.logMsg("stopPlayback called", 2) + self.logMsg("stopPlayback called", 1) itemId = data['item_id'] playTime = data['currentPosition'] diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 597eedc3..f78626e5 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -32,8 +32,11 @@ import threading import time import urllib2 +import xbmc + import downloadutils from PlexFunctions import PMSHttpsEnabled +from utils import window class plexgdm: @@ -119,7 +122,7 @@ class plexgdm: self.__printDebug("Sending registration data: HTTP/1.0 200 OK\r\n%s" % (self.client_data), 3) self.client_registered = True - time.sleep(0.5) + xbmc.sleep(500) self.__printDebug("Client Update loop stopped",1) @@ -141,9 +144,15 @@ class plexgdm: return False try: - media_server=self.server_list[0]['server'] - media_port=self.server_list[0]['port'] - scheme = self.server_list[0]['protocol'] + for server in self.server_list: + if server['uuid'] == window('plex_machineIdentifier'): + media_server = server['server'] + media_port = server['port'] + scheme = server['protocol'] + break + else: + self.__printDebug("Did not find our server!", 2) + return False self.__printDebug("Checking server [%s] on port [%s]" % (media_server, media_port) ,2) client_result = self.download( @@ -240,15 +249,47 @@ class plexgdm: update['class'] = each.split(':')[1].strip() # Quickly test if we need https - if PMSHttpsEnabled( - '%s:%s' % (update['server'], update['port'])): + https = PMSHttpsEnabled( + '%s:%s' % (update['server'], update['port'])) + if https is None: + # Error contacting server + continue + elif https: update['protocol'] = 'https' else: update['protocol'] = 'http' discovered_servers.append(update) + # Append REMOTE PMS that we haven't found yet; if necessary + currServer = window('pms_server') + if currServer: + currServerProt, currServerIP, currServerPort = \ + currServer.split(':') + currServerIP = currServerIP.replace('/', '') + for server in discovered_servers: + if server['server'] == currServerIP: + break + else: + # Currently active server was not discovered via GDM; ADD + update = { + 'port': currServerPort, + 'protocol': currServerProt, + 'class': None, + 'content-type': 'plex/media-server', + 'discovery': 'auto', + 'master': 1, + 'owned': '1', + 'role': 'master', + 'server': currServerIP, + 'serverName': window('plex_servername'), + 'updated': int(time.time()), + 'uuid': window('plex_machineIdentifier'), + 'version': 'irrelevant' + } + discovered_servers.append(update) + self.server_list = discovered_servers - + if not self.server_list: self.__printDebug("No servers have been discovered",1) else: @@ -292,7 +333,7 @@ class plexgdm: if discovery_count > self.discovery_interval: self.discover() discovery_count=0 - time.sleep(1) + xbmc.sleep(1000) def start_discovery(self, daemon = False): if not self._discovery_is_running: @@ -326,8 +367,8 @@ if __name__ == '__main__': client.start_all() while not client.discovery_complete: print "Waiting for results" - time.sleep(1) - time.sleep(20) + xbmc.sleep(1000) + xbmc.sleep(20000) print client.getServerList() if client.check_client_registration(): print "Successfully registered" diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 2305e9e9..c85238c7 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -1,12 +1,12 @@ import re import threading -from xml.dom.minidom import parseString +# from xml.dom.minidom import parseString from functions import * from settings import settings from httppersist import requests from xbmc import Player -import xbmcgui +# import xbmcgui import downloadutils from utils import window import PlexFunctions as pf @@ -67,18 +67,19 @@ class SubscriptionManager: ret += ' />' return ret - WINDOW = xbmcgui.Window(10000) - # pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server')) # userId = str(WINDOW.getProperty('currUserId')) - # pbmc_server = str(WINDOW.getProperty('pms_server')) - pbmc_server = None + pbmc_server = window('pms_server') + if pbmc_server: + (self.protocol, self.server, self.port) = \ + pbmc_server.split(':') + self.server = self.server.replace('/', '') keyid = None count = 0 while not keyid: if count > 300: break - keyid = WINDOW.getProperty('Plex_currently_playing_itemid') + keyid = window('Plex_currently_playing_itemid') xbmc.sleep(100) count += 1 if keyid: @@ -87,8 +88,6 @@ class SubscriptionManager: ret += ' location="%s"' % (self.mainlocation) ret += ' key="%s"' % (self.lastkey) ret += ' ratingKey="%s"' % (self.lastratingkey) - if pbmc_server: - (self.server, self.port) = pbmc_server.split(':') serv = getServerByHost(self.server) if info.get('playQueueID'): self.containerKey = "/playQueues/%s" % info.get('playQueueID') diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 7a4e92b0..ea624fde 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -76,8 +76,8 @@ class UserClient(threading.Thread): settings = utils.settings # Original host - self.machineIdentifier = utils.settings('plex_machineIdentifier') - self.servername = utils.settings('plex_servername') + self.machineIdentifier = settings('plex_machineIdentifier') + self.servername = settings('plex_servername') HTTPS = settings('https') == "true" host = settings('ipaddress') port = settings('port') @@ -91,14 +91,11 @@ class UserClient(threading.Thread): # If https is true if prefix and HTTPS: server = "https://%s" % server - return server # If https is false elif prefix and not HTTPS: server = "http://%s" % server - return server - # If only the host:port is required - elif not prefix: - return server + self.logMsg('Returning active server: %s' % server) + return server def getSSLverify(self): # Verify host certificate @@ -410,6 +407,7 @@ class UserClient(threading.Thread): self.auth = False if self.authenticate(): # Successfully authenticated and loaded a user + log("Successfully authenticated!", 1) log("Current user: %s" % self.currUser, 1) log("Current userId: %s" % self.currUserId, 1) log("Current accessToken: xxxx", 1) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index f4167133..aadeca3a 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -148,7 +148,7 @@ def ThreadMethodsAdditionalStop(windowAttribute): def wrapper(cls): def threadStopped(self): return (self._threadStopped or - (window('terminateNow') == "true") or + (window('plex_terminateNow') == "true") or window(windowAttribute) == "true") cls.threadStopped = threadStopped return cls @@ -209,7 +209,7 @@ def ThreadMethods(cls): cls.threadSuspended = threadSuspended def threadStopped(self): - return self._threadStopped or (window('terminateNow') == 'true') + return self._threadStopped or (window('plex_terminateNow') == 'true') cls.threadStopped = threadStopped # Return class to render this a decorator @@ -625,6 +625,85 @@ def indent(elem, level=0): if level and (not elem.tail or not elem.tail.strip()): elem.tail = i + +def musiclibXML(): + """ + Deactivates Kodi trying to scan music library on startup + + Changes guisettings.xml in Kodi userdata folder: + updateonstartup: set to "false" + """ + path = xbmc.translatePath("special://profile/").decode('utf-8') + xmlpath = "%sguisettings.xml" % path + + try: + xmlparse = etree.parse(xmlpath) + except: + # Document is blank or missing + root = etree.Element('settings') + else: + root = xmlparse.getroot() + + music = root.find('musiclibrary') + if music is None: + music = etree.SubElement(root, 'musiclibrary') + + startup = music.find('updateonstartup') + if startup is None: + # Setting does not exist yet; create it + startup = etree.SubElement(music, + 'updateonstartup', + attrib={'default': "true"}).text = "false" + else: + startup.text = "false" + + # Prettify and write to file + try: + indent(root) + except: + pass + etree.ElementTree(root).write(xmlpath) + + +def advancedSettingsXML(): + """ + Deactivates Kodi popup for scanning of music library + + Changes advancedsettings.xml, musiclibrary: + backgroundupdate set to "true" + """ + path = xbmc.translatePath("special://profile/").decode('utf-8') + xmlpath = "%sadvancedsettings.xml" % path + + try: + xmlparse = etree.parse(xmlpath) + except: + # Document is blank or missing + root = etree.Element('advancedsettings') + else: + root = xmlparse.getroot() + + music = root.find('musiclibrary') + if music is None: + music = etree.SubElement(root, 'musiclibrary') + + backgroundupdate = music.find('backgroundupdate') + if backgroundupdate is None: + # Setting does not exist yet; create it + backgroundupdate = etree.SubElement( + music, + 'backgroundupdate').text = "true" + else: + backgroundupdate.text = "true" + + # Prettify and write to file + try: + indent(root) + except: + pass + etree.ElementTree(root).write(xmlpath) + + def sourcesXML(): # To make Master lock compatible path = xbmc.translatePath("special://profile/").decode('utf-8') diff --git a/resources/settings.xml b/resources/settings.xml index 3012f1fd..edb9f19a 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -74,6 +74,7 @@ + @@ -108,13 +109,21 @@ --> + + + + + + + - + + diff --git a/service.py b/service.py index 065f279a..d1fdc473 100644 --- a/service.py +++ b/service.py @@ -28,6 +28,7 @@ import player import utils import videonodes import websocket_client as wsc +import downloadutils import PlexAPI import PlexCompanion @@ -80,7 +81,10 @@ class Service(): "emby_initialScan", "emby_customplaylist", "emby_playbackProps", "plex_runLibScan", "plex_username", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", - "plex_authenticated" + "plex_authenticated", "EmbyUserImage", "useDirectPaths", + "replaceSMB", "remapSMB", "remapSMBmovieOrg", "remapSMBtvOrg", + "remapSMBmusicOrg", "remapSMBmovieNew", "remapSMBtvNew", + "remapSMBmusicNew", "suspend_LibraryThread", "plex_terminateNow" ] for prop in properties: window(prop, clear=True) @@ -132,12 +136,10 @@ class Service(): # 3. User has access to the server if window('emby_online') == "true": - # Emby server is online # Verify if user is set and has access to the server if (user.currUser is not None) and user.HasAccess: - - # If an item is playing + # If an item is playing if xplayer.isPlaying(): try: # Update and report progress @@ -190,10 +192,15 @@ class Service(): ws.start() # Start the syncing thread if not self.library_running: + log('Starting libary sync thread', 1) self.library_running = True library.start() + # Start the Plex Companion thread + if not self.plexCompanion_running and \ + self.runPlexCompanion == "true": + self.plexCompanion_running = True + plexCompanion.start() else: - if (user.currUser is None) and self.warn_auth: # Alert user is not authenticated and suppress future warning self.warn_auth = False @@ -213,6 +220,7 @@ class Service(): if monitor.waitForAbort(5): # Abort was requested while waiting. We should exit break + xbmc.sleep(50) else: # Wait until Emby server is online # or Kodi is shut down. @@ -235,9 +243,7 @@ class Service(): icon="special://home/addons/plugin.video." "plexkodiconnect/icon.png", sound=False) - self.server_online = False - else: # Server is online if not self.server_online: @@ -254,9 +260,8 @@ class Service(): "plexkodiconnect/icon.png", time=2000, sound=False) - self.server_online = True - log("Server is online and ready.", 1) + log("Server %s is online and ready." % server, 1) window('emby_online', value="true") # Start the userclient thread @@ -264,36 +269,31 @@ class Service(): self.userclient_running = True user.start() - # Start the Plex Companion thread - if not self.plexCompanion_running and \ - self.runPlexCompanion == "true": - self.plexCompanion_running = True - plexCompanion.start() + break if monitor.waitForAbort(1): # Abort was requested while waiting. break + xbmc.sleep(50) if monitor.waitForAbort(1): # Abort was requested while waiting. We should exit break - ##### Emby thread is terminating. ##### + # Terminating PlexKodiConnect - # Tell all threads to terminate - utils.window('terminateNow', value='true') + # Tell all threads to terminate (e.g. several lib sync threads) + utils.window('plex_terminateNow', value='true') try: - if self.plexCompanion_running: - plexCompanion.stopThread() + plexCompanion.stopThread() except: xbmc.log('plexCompanion already shut down') try: - if self.library_running: - library.stopThread() + library.stopThread() except: xbmc.log('Library sync already shut down') @@ -303,8 +303,7 @@ class Service(): xbmc.log('Websocket client already shut down') try: - if self.userclient_running: - user.stopThread() + user.stopThread() except: xbmc.log('User client already shut down') @@ -314,8 +313,8 @@ class Service(): delay = int(utils.settings('startupDelay')) xbmc.log("Delaying Plex startup by: %s sec..." % delay) -# Plex: add 3 seconds just for good measure -if delay and xbmc.Monitor().waitForAbort(delay+3): +# Plex: add 5 seconds just for good measure +if delay and xbmc.Monitor().waitForAbort(delay+5): # Start the service xbmc.log("Abort requested while waiting. Emby for kodi not started.") else: