diff --git a/default.py b/default.py index ddc36b00..2c067b39 100644 --- a/default.py +++ b/default.py @@ -70,7 +70,8 @@ class Main: 'reConnect': entrypoint.reConnect, 'delete': entrypoint.deleteItem, 'browseplex': entrypoint.BrowsePlexContent, - 'ondeck': entrypoint.getOnDeck + 'ondeck': entrypoint.getOnDeck, + 'chooseServer': entrypoint.chooseServer } if "/extrafanart" in sys.argv[0]: diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 6c2eeace..25c80998 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -297,7 +297,7 @@ Duration of the video library pop up (in seconds) Duration of the music library pop up (in seconds) Server messages - Generate a new device Id + [COLOR yellow]Generate a new unique device Id (e.g. when cloning Kodi)[/COLOR] Users must log in every time when Kodi restarts Restart Kodi if you make changes Complete Re-Sync necessary @@ -392,6 +392,7 @@ On Deck: Append show title to episode On Deck: Append season- and episode-number (e.g. S3E2) Nothing works? Try a full reset! + [COLOR yellow]Choose Plex Server from a list[/COLOR] diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index a8d107bd..c13163ef 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -24,6 +24,7 @@ Bei Wiederaufnahme zurückspulen (in Sekunden) [COLOR yellow]Anzahl Login-Versuche zurücksetzen[/COLOR] + [COLOR yellow]Neue einzigartige Geräte-ID generieren (z.B. wenn Kodi geklont wurde)[/COLOR] Benutzer müssen sich bei jedem Neustart von Kodi neu anmelden Bei Änderungen Kodi neu starten Komplette Neusynchronisierung nötig @@ -329,6 +330,7 @@ "Aktuell": Serien- an Episoden-Titel anfügen "Aktuell": Staffel und Episode anfügen (z.B. S3E2) Nichts funktioniert? Setze mal alles zurück! + [COLOR yellow]Plex Server aus Liste auswählen[/COLOR] Plex Home Benutzer abmelden: diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f46ea86d..81df5a42 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -29,17 +29,6 @@ http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-python (and others...) """ - - -# Specific to PlexDB: -import clientinfo -import utils -import downloadutils -import xbmcaddon -import xbmcgui -import xbmc -import xbmcvfs - import struct import time import urllib2 @@ -52,13 +41,21 @@ import Queue import traceback import requests import xml.etree.ElementTree as etree +from uuid import uuid4 import re import json from urllib import urlencode, quote_plus, unquote -from PlexFunctions import PlexToKodiTimefactor, PMSHttpsEnabled +import clientinfo +import utils +import downloadutils +import xbmcaddon +import xbmcgui +import xbmc +import xbmcvfs +from PlexFunctions import PlexToKodiTimefactor, PMSHttpsEnabled # Disable requests logging from requests.packages.urllib3.exceptions import InsecureRequestWarning @@ -835,8 +832,6 @@ class PlexAPI(): 'X-Plex-Version': self.plexversion, 'X-Plex-Client-Identifier': self.clientId, 'X-Plex-Provides': 'player', - 'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac,dts{bitrate:800000&channels:8},ac3{bitrate:800000&channels:8}', - 'X-Plex-Client-Profile-Extra': 'add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=*&audioCodec=dca,ac3)', } if self.token: @@ -2173,15 +2168,17 @@ class API(): transcodePath = self.server + \ '/video/:/transcode/universal/start.m3u8?' args = { - 'copyts': 1, + 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' + 'session': str(uuid4()), + 'fastSeek': 1, 'path': path, 'mediaIndex': 0, # Probably refering to XML reply sheme 'partIndex': self.part, - 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' - 'session': self.clientId, + # 'copyts': 1, # 'offset': 0, # Resume point - 'fastSeek': 1 } + # Seem like PHT to let the PMS use the transcoding profile + xargs['X-Plex-Device'] = 'Plex Home Theater' # Currently not used! if action == "DirectStream": @@ -2199,9 +2196,7 @@ class API(): args.update(quality) args.update(argsUpdate) - url = transcodePath + \ - urlencode(xargs) + '&' + \ - urlencode(args) + url = transcodePath + urlencode(xargs) + '&' + urlencode(args) return url def externalSubs(self, playurl): diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 6e048df3..ab50e509 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -383,7 +383,7 @@ def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'): try: xml[0].tag except (IndexError, TypeError, AttributeError): - logMsg("Error retrieving metadata for %s" % url, -1) + logMsg(title, "Error retrieving metadata for %s" % url, -1) return None return xml @@ -425,16 +425,16 @@ def PMSHttpsEnabled(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) + logMsg(title, "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) + logMsg(title, "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) + logMsg(title, "Server timeout reached for Url %s" % url, -1) return None if res.status_code == requests.codes.ok: return answer @@ -442,6 +442,26 @@ def PMSHttpsEnabled(url): return None +def GetMachineIdentifier(url): + """ + Returns the unique PMS machine identifier of url + + Returns None if something went wrong + """ + xml = downloadutils.DownloadUtils().downloadUrl( + url + '/identity', type="GET") + try: + xml.attrib + except: + logMsg(title, 'Could not get the PMS machineIdentifier for %s' + % url, -1) + return None + machineIdentifier = xml.attrib.get('machineIdentifier') + logMsg(title, 'Found machineIdentifier %s for %s' + % (machineIdentifier, url), 1) + return machineIdentifier + + def scrobble(ratingKey, state): """ Tells the PMS to set an item's watched state to state="watched" or @@ -458,4 +478,4 @@ def scrobble(ratingKey, state): else: return downloadutils.DownloadUtils().downloadUrl(url, type="GET") - logMsg("Toggled watched state for Plex item %s" % ratingKey, 1) + logMsg(title, "Toggled watched state for Plex item %s" % ratingKey, 1) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 2596abaf..10752714 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -69,7 +69,7 @@ class ClientInfo(): If id does not exist, create one and save in Kodi settings file. """ - if reset: + if reset is True: utils.window('plex_client_Id', clear=True) utils.settings('plex_client_Id', value="") @@ -78,7 +78,8 @@ class ClientInfo(): return clientId clientId = utils.settings('plex_client_Id') - if clientId: + # Because Kodi appears to cache file settings!! + if clientId != "" and reset is False: utils.window('plex_client_Id', value=clientId) self.logMsg("Unique device Id plex_client_Id loaded: %s" % clientId, 1) return clientId diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 946b3345..9253d688 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -73,36 +73,100 @@ def plexCompanion(fullurl, params): title, "Not knowing what to do for now - no playQueue sent", -1) -def reConnect(): +def chooseServer(): """ - Triggers login to plex.tv and re-authorization + Lets user choose from list of PMS (signs out & signs in) """ string = xbmcaddon.Addon().getLocalizedString - utils.logMsg("entrypoint reConnect", - "Connection resets requested", 0) + utils.logMsg(title, "Choosing PMS server requested, starting", 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, + time=3000, 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 + # Wait max for 25 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)) + dialog.ok(addonName, + string(39208)) # Resuming threads, just in case utils.window('suspend_LibraryThread', clear=True) - # Abort reConnection + utils.logMsg(title, "Could not stop library sync, aborting", -1) return counter += 1 xbmc.sleep(50) + utils.logMsg(title, "Successfully stopped library sync", 0) + + # 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") + + # Above method needs to have run its course! Hence wait + counter = 0 + while utils.window('emby_serverStatus') == "401": + if counter > 100: + dialog.ok(addonName, + string(39208)) + utils.logMsg(title, "Could not sign out, aborting", -1) + return + counter += 1 + xbmc.sleep(50) + # Suspend the user client during procedure + utils.window('suspend_Userclient', value='true') + + import initialsetup + initialsetup.InitialSetup().setup(chooseServer=True) + # Request lib sync to get user view data (e.g. watched/unwatched) + utils.window('plex_runLibScan', value='full') + # Restart user client + utils.window('suspend_Userclient', clear=True) + utils.logMsg(title, "Choosing new PMS complete", 0) + + +def reConnect(): + """ + Triggers login to plex.tv and re-authorization + """ + string = xbmcaddon.Addon().getLocalizedString + utils.logMsg(title, "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=3000, + sound=False) + # Pause library sync thread - user needs to be auth in order to sync + utils.window('suspend_LibraryThread', value='true') + # Wait max for 25 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(addonName, + string(39208)) + # Resuming threads, just in case + utils.window('suspend_LibraryThread', clear=True) + utils.logMsg(title, "Could not stop library sync, aborting", -1) + return + counter += 1 + xbmc.sleep(50) + + utils.logMsg(title, "Successfully stopped library sync", 0) # Delete plex credentials in settings utils.settings('myplexlogin', value="true") @@ -126,9 +190,9 @@ def reConnect(): counter = 0 while utils.window('emby_serverStatus') == "401": if counter > 100: - dialog.ok(heading=addonName, - message=string(39208)) - # Abort reConnection + dialog.ok(addonName, + string(39208)) + utils.logMsg(title, "Could not sign out, aborting", -1) return counter += 1 xbmc.sleep(50) @@ -141,6 +205,7 @@ def reConnect(): utils.window('plex_runLibScan', value='full') # Restart user client utils.window('suspend_Userclient', clear=True) + utils.logMsg(title, "Complete reconnection to plex.tv and PMS complete", 0) def PassPlaylist(xml, resume=None): @@ -288,9 +353,8 @@ def resetDeviceId(): dialog = xbmcgui.Dialog() language = utils.language - deviceId_old = utils.window('emby_deviceId') + deviceId_old = utils.window('plex_client_Id') try: - utils.window('emby_deviceId', clear=True) deviceId = clientinfo.ClientInfo().getDeviceId(reset=True) except Exception as e: utils.logMsg(addonName, diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 7d1557a6..559fe2f1 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -26,7 +26,7 @@ class InitialSetup(): self.userClient = userclient.UserClient() self.plx = PlexAPI.PlexAPI() - def setup(self, forcePlexTV=False): + def setup(self, forcePlexTV=False, chooseServer=False): """ Initial setup. Run once upon startup. Check server, user, direct paths, music, direct stream if not direct @@ -51,7 +51,8 @@ class InitialSetup(): # Optionally sign into plex.tv. Will not be called on very first run # as plexToken will be '' - if (plexToken and myplexlogin == 'true' and forcePlexTV is False): + if (plexToken and myplexlogin == 'true' and forcePlexTV is False + and chooseServer is False): chk = self.plx.CheckConnection('plex.tv', plexToken) # HTTP Error: unauthorized. Token is no longer valid if chk == 401 or chk == 403: @@ -92,7 +93,7 @@ class InitialSetup(): self.logMsg('Failed to update Plex info from plex.tv', -1) # If a Plex server IP has already been set, return. - if server and forcePlexTV is False: + if server and forcePlexTV is False and chooseServer is False: self.logMsg("Server is already set.", 0) self.logMsg("url: %s, Plex machineIdentifier: %s" % (server, serverid), 0) @@ -100,7 +101,8 @@ class InitialSetup(): # If not already retrieved myplex info, optionally let user sign in # to plex.tv. This DOES get called on very first install run - if ((not plexToken and myplexlogin == 'true') or forcePlexTV): + if ((not plexToken and myplexlogin == 'true' and chooseServer is False) + or forcePlexTV): result = self.plx.PlexTvSignInWithPin() if result: plexLogin = result['username'] @@ -231,7 +233,7 @@ class InitialSetup(): # self.logMsg("User opted to use direct paths.", 1) # utils.settings('useDirectPaths', value="1") - if forcePlexTV: + if forcePlexTV is True or chooseServer is True: return goToSettings = False diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index a03be48a..bbd29b68 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -184,7 +184,7 @@ class PlaybackUtils(): # For transcoding only, ask for audio/subs pref if window('emby_%s.playmethod' % playurl) == "Transcode": window('emby_%s.playmethod' % playurl, clear=True) - playurl = playutils.audioSubsPref(playurl, listitem) + playurl = playutils.audioSubsPref(listitem, playurl) window('emby_%s.playmethod' % playurl, value="Transcode") listitem.setPath(playurl) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 8e4647ff..61823444 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -321,22 +321,24 @@ class PlayUtils(): # Set part where we're at self.API.setPartNumber(part) + if part is None: + part = 0 try: mediastreams = self.item[0][part] except (TypeError, KeyError, IndexError): - return + return url audioNum = 0 # Remember 'no subtitles' subNum = 1 for stream in mediastreams: # Since Emby returns all possible tracks together, have to sort them. - index = stream.attrib['id'] - type = stream.attrib['streamType'] + index = stream.attrib.get('id') + type = stream.attrib.get('streamType') # Audio if type == "2": - codec = stream.attrib['codec'] + codec = stream.attrib.get('codec') channelLayout = stream.attrib.get('audioChannelLayout', "") try: @@ -356,7 +358,7 @@ class PlayUtils(): try: track = "%s %s" % (subNum+1, stream.attrib['language']) except: - track = "%s 'unknown' (%s)" % (subNum+1, stream.attrib['codec']) + track = "%s 'unknown' (%s)" % (subNum+1, stream.attrib.get('codec')) default = stream.attrib.get('default') forced = stream.attrib.get('forced') diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index ea624fde..842bcf87 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -13,6 +13,7 @@ import utils import downloadutils import PlexAPI +from PlexFunctions import GetMachineIdentifier ############################################################################### @@ -76,11 +77,11 @@ class UserClient(threading.Thread): settings = utils.settings # Original host - self.machineIdentifier = settings('plex_machineIdentifier') self.servername = settings('plex_servername') HTTPS = settings('https') == "true" host = settings('ipaddress') port = settings('port') + self.machineIdentifier = settings('plex_machineIdentifier') server = host + ":" + port @@ -94,6 +95,12 @@ class UserClient(threading.Thread): # If https is false elif prefix and not HTTPS: server = "http://%s" % server + # User entered IP; we need to get the machineIdentifier + if self.machineIdentifier == '' and prefix is True: + self.machineIdentifier = GetMachineIdentifier(server) + if self.machineIdentifier is None: + self.machineIdentifier = '' + settings('plex_machineIdentifier', value=self.machineIdentifier) self.logMsg('Returning active server: %s' % server) return server diff --git a/resources/settings.xml b/resources/settings.xml index edb9f19a..329175ce 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -2,6 +2,7 @@ + @@ -122,6 +123,7 @@ +