From 091ca6e9975d8325e2fb46610e2761391f9a3998 Mon Sep 17 00:00:00 2001 From: shaun Date: Thu, 25 Jan 2018 08:19:32 +1100 Subject: [plugin.video.embycon] 1.4.39 --- plugin.video.embycon/addon.xml | 14 +- .../language/resource.language.en_gb/strings.po | 113 +- plugin.video.embycon/resources/lib/action_menu.py | 2 +- plugin.video.embycon/resources/lib/clientinfo.py | 14 +- plugin.video.embycon/resources/lib/datamanager.py | 142 +-- .../resources/lib/downloadutils.py | 253 +++-- plugin.video.embycon/resources/lib/error.py | 152 +++ plugin.video.embycon/resources/lib/functions.py | 974 ++++++------------ .../resources/lib/item_functions.py | 556 ++++++++++ plugin.video.embycon/resources/lib/kodi_utils.py | 10 +- .../resources/lib/menu_functions.py | 232 +++-- plugin.video.embycon/resources/lib/play_utils.py | 687 +++++++++++-- .../resources/lib/server_detect.py | 123 +-- .../resources/lib/server_sessions.py | 17 +- .../resources/lib/simple_logging.py | 39 +- plugin.video.embycon/resources/lib/trakttokodi.py | 248 +++++ plugin.video.embycon/resources/lib/translation.py | 19 +- plugin.video.embycon/resources/lib/utils.py | 155 +-- plugin.video.embycon/resources/lib/websocket.py | 930 +++++++++++++++++ .../resources/lib/websocket_client.py | 302 ++++++ plugin.video.embycon/resources/lib/widgets.py | 155 +-- plugin.video.embycon/resources/settings.xml | 18 +- .../resources/skins/skin.estuary/xml/Home-17.1.xml | 2 +- .../skins/skin.estuary/xml/Home-17.3-elec.xml | 2 +- .../resources/skins/skin.estuary/xml/Home-17.3.xml | 2 +- .../resources/skins/skin.estuary/xml/Home-17.6.xml | 1080 ++++++++++++++++++++ plugin.video.embycon/service.py | 328 +----- 27 files changed, 4956 insertions(+), 1613 deletions(-) create mode 100644 plugin.video.embycon/resources/lib/error.py create mode 100644 plugin.video.embycon/resources/lib/item_functions.py create mode 100644 plugin.video.embycon/resources/lib/trakttokodi.py create mode 100644 plugin.video.embycon/resources/lib/websocket.py create mode 100644 plugin.video.embycon/resources/lib/websocket_client.py create mode 100644 plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.6.xml diff --git a/plugin.video.embycon/addon.xml b/plugin.video.embycon/addon.xml index eb1e52f..908f69f 100644 --- a/plugin.video.embycon/addon.xml +++ b/plugin.video.embycon/addon.xml @@ -1,13 +1,13 @@ - + - video + video audio @@ -15,10 +15,10 @@ all en GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 - https://emby.media/community/index.php?/topic/46651-embycon/ - https://emby.media/community/index.php?/topic/46651-embycon/ + https://emby.media/community/index.php?/forum/99-kodi/ + https://faush01.github.io/plugin.video.embycon/ https://github.com/faush01/plugin.video.embycon - View and play your Emby media library. - An addon to allow you to view and playback your Emby (www.emby.media) Movie and TV Show collection. + Browse and play your Emby server media library. + An addon to allow you to browse and playback your Emby (www.emby.media) Movie, TV Show and Music collections. diff --git a/plugin.video.embycon/resources/language/resource.language.en_gb/strings.po b/plugin.video.embycon/resources/language/resource.language.en_gb/strings.po index 4647f75..8222ecb 100644 --- a/plugin.video.embycon/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.embycon/resources/language/resource.language.en_gb/strings.po @@ -47,7 +47,7 @@ msgid "Enable performance profiling" msgstr "" msgctxt "#30011" -msgid "Detect Server" +msgid "Detect Local Server" msgstr "" msgctxt "#30012" @@ -82,10 +82,18 @@ msgctxt "#30020" msgid "Flatten single season" msgstr "" +msgctxt "#30021" +msgid "Show all episodes item" +msgstr "" + msgctxt "#30022" msgid "Advanced" msgstr "" +msgctxt "#30023" +msgid "Hide unwatched episode details" +msgstr "" + msgctxt "#30024" msgid "Username:" msgstr "" @@ -159,19 +167,11 @@ msgid "On Resume Jump Back Seconds" msgstr "" msgctxt "#30116" -msgid "Add Item and Played Counts" -msgstr "" - -msgctxt "#30117" -msgid "Background Art Refresh Rate (seconds)" +msgid "Add unwatched counts to names" msgstr "" msgctxt "#30118" -msgid "Add Resume Percent" -msgstr "" - -msgctxt "#30119" -msgid "Add Episode Number" +msgid "Add resume percent to names" msgstr "" msgctxt "#30120" @@ -210,10 +210,6 @@ msgctxt "#30137" msgid "Please restart Kodi" msgstr "" -msgctxt "#30138" -msgid "Cache Emby Data Locally" -msgstr "" - msgctxt "#30139" msgid "No Media Type Set" msgstr "" @@ -226,10 +222,6 @@ msgctxt "#30151" msgid "Select item action (Requires Restart)" msgstr "" -msgctxt "#30162" -msgid "Add Season Number" -msgstr "" - msgctxt "#30163" msgid "Add (cc) if subtitle is available" msgstr "" @@ -255,15 +247,15 @@ msgid "Select User" msgstr "" msgctxt "#30181" -msgid "Include Overview" +msgid "Include plot" msgstr "" msgctxt "#30182" -msgid "Include Media Info" +msgid "Include media stream info" msgstr "" msgctxt "#30183" -msgid "Include People" +msgid "Include people" msgstr "" msgctxt "#30200" @@ -430,6 +422,10 @@ msgctxt "#30254" msgid "Show Settings" msgstr "" +msgctxt "#30255" +msgid "Movies (Year)" +msgstr "" + msgctxt "#30256" msgid "Movies - All" msgstr "" @@ -535,7 +531,7 @@ msgid "Your skin %s is currently not supported" msgstr "" msgctxt "#30282" -msgid "No servers detected" +msgid "No Emby servers on your local network detected" msgstr "" msgctxt "#30283" @@ -626,3 +622,74 @@ msgctxt "#30304" msgid "Loaded Textures: " msgstr "" +msgctxt "#30305" +msgid "Not Found: %s" +msgstr "" + +msgctxt "#30306" +msgid "Playback starting: %s" +msgstr "" + +msgctxt "#30307" +msgid "Emby: Play Trailer" +msgstr "" + +msgctxt "#30308" +msgid "Select Trailer" +msgstr "" + +msgctxt "#30309" +msgid "Select Media Source" +msgstr "" + +msgctxt "#30310" +msgid "Enable Emby remote control" +msgstr "" + +msgctxt "#30311" +msgid "EmbyCon encountered an error!" +msgstr "" + +msgctxt "#30312" +msgid "Do you want to send this error to the devs?" +msgstr "" + +msgctxt "#30313" +msgid "Menu" +msgstr "" + +msgctxt "#30314" +msgid "Play" +msgstr "" + +msgctxt "#30315" +msgid "Suppress notifications for connection errors" +msgstr "" + +msgctxt "#30316" +msgid "Connection Error" +msgstr "" + +msgctxt "#30317" +msgid "Emby: Play All" +msgstr "" + +msgctxt "#30318" +msgid "Music - All Albums" +msgstr "" + +msgctxt "#30319" +msgid "Music - All Album Artists" +msgstr "" + +msgctxt "#30320" +msgid " - Albums" +msgstr "" + +msgctxt "#30321" +msgid " - Album Artists" +msgstr "" + +msgctxt "#30322" +msgid "Auto Resume" +msgstr "" diff --git a/plugin.video.embycon/resources/lib/action_menu.py b/plugin.video.embycon/resources/lib/action_menu.py index 3521e44..96f099a 100644 --- a/plugin.video.embycon/resources/lib/action_menu.py +++ b/plugin.video.embycon/resources/lib/action_menu.py @@ -35,7 +35,7 @@ class ActionMenu(xbmcgui.WindowXMLDialog): def onClick(self, controlID): if (controlID == 3000): self.selected_action = self.listControl.getSelectedItem() - log.debug("ActionMenu: Selected Item:" + str(self.selected_action)) + log.debug("ActionMenu: Selected Item: {0}", self.selected_action) self.close() def setActionItems(self, action_items): diff --git a/plugin.video.embycon/resources/lib/clientinfo.py b/plugin.video.embycon/resources/lib/clientinfo.py index 4d046a9..bf5ae99 100644 --- a/plugin.video.embycon/resources/lib/clientinfo.py +++ b/plugin.video.embycon/resources/lib/clientinfo.py @@ -9,10 +9,9 @@ from kodi_utils import HomeWindow from simple_logging import SimpleLogging log = SimpleLogging(__name__) -__addon__ = xbmcaddon.Addon(id="plugin.video.embycon") - class ClientInformation(): + def getDeviceId(self): WINDOW = HomeWindow() @@ -22,26 +21,27 @@ class ClientInformation(): return client_id emby_guid_path = xbmc.translatePath("special://temp/embycon_guid").decode('utf-8') - log.debug("emby_guid_path: " + emby_guid_path) + log.debug("emby_guid_path: {0}", emby_guid_path) guid = xbmcvfs.File(emby_guid_path) client_id = guid.read() guid.close() if not client_id: client_id = str("%012X" % uuid4()) - log.debug("Generating a new guid: " + client_id) + log.debug("Generating a new guid: {0}", client_id) guid = xbmcvfs.File(emby_guid_path, 'w') guid.write(client_id) guid.close() - log.debug("emby_client_id (NEW): " + client_id) + log.debug("emby_client_id (NEW): {0}", client_id) else: - log.debug("emby_client_id: " + client_id) + log.debug("emby_client_id: {0}", client_id) WINDOW.setProperty("client_id", client_id) return client_id def getVersion(self): - version = __addon__.getAddonInfo("version") + addon = xbmcaddon.Addon(id="plugin.video.embycon") + version = addon.getAddonInfo("version") return version def getClient(self): diff --git a/plugin.video.embycon/resources/lib/datamanager.py b/plugin.video.embycon/resources/lib/datamanager.py index f5f69b5..feda9f0 100644 --- a/plugin.video.embycon/resources/lib/datamanager.py +++ b/plugin.video.embycon/resources/lib/datamanager.py @@ -1,22 +1,13 @@ # Gnu General Public License - see LICENSE.TXT -import hashlib -import os -import threading import json -import encodings - -import xbmcaddon -import xbmc +from collections import defaultdict from downloadutils import DownloadUtils from simple_logging import SimpleLogging -from utils import getChecksum -from kodi_utils import HomeWindow log = SimpleLogging(__name__) - class DataManager(): cacheDataResult = None dataUrl = None @@ -26,135 +17,12 @@ class DataManager(): def __init__(self, *args): log.debug("DataManager __init__") - def getCacheValidatorFromData(self, result): - key = 'Items' - results = result.get(key) - if results is None: - key = 'SearchHints' - results = result.get(key) - if results is None: - results = [] - - itemCount = 0 - dataHashString = "" - - for item in results: - item_hash_string = getChecksum(item) - item_hash_string = str(itemCount) + "_" + key + "_" + item.get("Name", "-") + "_" + item_hash_string + "|" - log.debug("ITEM_HASH: " + item_hash_string) - dataHashString += item_hash_string - itemCount += 1 - - # hash the data - dataHashString = dataHashString.encode("UTF-8") - m = hashlib.md5() - m.update(dataHashString) - validatorString = m.hexdigest() - - log.debug("getCacheValidatorFromData : RawData : " + dataHashString) - log.debug("getCacheValidatorFromData : hashData : " + validatorString) - - return validatorString - def loadJasonData(self, jsonData): - return json.loads(jsonData) + return json.loads(jsonData, object_hook=lambda d: defaultdict(lambda: None, d)) def GetContent(self, url): + jsonData = DownloadUtils().downloadUrl(url) + result = self.loadJasonData(jsonData) + return result - __addon__ = xbmcaddon.Addon(id='plugin.video.embycon') - use_cache_system = __addon__.getSetting('cacheEmbyData') == "true" - - if use_cache_system == False: - # dont use cache system at all, just get the result and return - log.debug("GetContent - Not using cache system") - jsonData = DownloadUtils().downloadUrl(url, suppress=False, popup=1) - result = self.loadJasonData(jsonData) - log.debug("Returning Loaded Result") - return result - - # first get the url hash - m = hashlib.md5() - m.update(url) - urlHash = m.hexdigest() - - # build cache data path - - __addondir__ = xbmc.translatePath(__addon__.getAddonInfo('profile')) - if not os.path.exists(os.path.join(__addondir__, "cache")): - os.makedirs(os.path.join(__addondir__, "cache")) - cacheDataPath = os.path.join(__addondir__, "cache", urlHash) - - log.debug("Cache_Data_Manager:" + cacheDataPath) - - # are we forcing a reload - WINDOW = HomeWindow() - force_data_reload = WINDOW.getProperty("force_data_reload") == "true" - WINDOW.clearProperty("force_data_reload") - - if os.path.exists(cacheDataPath) and not force_data_reload: - # load data from cache if it is available and trigger a background - # verification process to test cache validity - log.debug("Loading Cached File") - with open(cacheDataPath, 'r') as f: - result = self.loadJasonData(f.read()) - - # start a worker thread to process the cache validity - self.cacheDataResult = result - self.dataUrl = url - self.cacheDataPath = cacheDataPath - actionThread = CacheManagerThread() - actionThread.setCacheData(self) - actionThread.start() - - log.debug("Returning Cached Result") - return result - else: - # no cache data so load the url and save it - jsonData = DownloadUtils().downloadUrl(url, suppress=False, popup=1) - log.debug("Loading URL and saving to cache") - with open(cacheDataPath, 'w') as f: - f.write(jsonData) - result = self.loadJasonData(jsonData) - self.cacheManagerFinished = True - log.debug("Returning Loaded Result") - return result - - -class CacheManagerThread(threading.Thread): - dataManager = None - - def __init__(self, *args): - threading.Thread.__init__(self, *args) - - def setCacheData(self, data): - self.dataManager = data - - def run(self): - - log.debug("CacheManagerThread Started") - - cacheValidatorString = self.dataManager.getCacheValidatorFromData(self.dataManager.cacheDataResult) - log.debug("Cache Validator String (" + cacheValidatorString + ")") - - jsonData = DownloadUtils().downloadUrl(self.dataManager.dataUrl, suppress=False, popup=1) - loadedResult = self.dataManager.loadJasonData(jsonData) - loadedValidatorString = self.dataManager.getCacheValidatorFromData(loadedResult) - log.debug("Loaded Validator String (" + loadedValidatorString + ")") - - # if they dont match then save the data and trigger a content reload - if (cacheValidatorString != loadedValidatorString): - log.debug("CacheManagerThread Saving new cache data and reloading container") - with open(self.dataManager.cacheDataPath, 'w') as f: - f.write(jsonData) - - # we need to refresh but will wait until the main function has finished - loops = 0 - while (self.dataManager.canRefreshNow == False and loops < 200 and not xbmc.Monitor().abortRequested()): - log.debug("Cache_Data_Manager: Not finished yet") - xbmc.sleep(100) - loops = loops + 1 - - log.debug("Sending container refresh (" + str(loops) + ")") - xbmc.executebuiltin("Container.Refresh") - log.debug("CacheManagerThread Exited") diff --git a/plugin.video.embycon/resources/lib/downloadutils.py b/plugin.video.embycon/resources/lib/downloadutils.py index 5bfce6e..6a52b62 100644 --- a/plugin.video.embycon/resources/lib/downloadutils.py +++ b/plugin.video.embycon/resources/lib/downloadutils.py @@ -10,6 +10,8 @@ import ssl import StringIO import gzip import json +from urlparse import urlparse +import urllib from kodi_utils import HomeWindow from clientinfo import ClientInformation @@ -18,6 +20,25 @@ from translation import i18n log = SimpleLogging(__name__) +def getDetailsString(): + + addonSettings = xbmcaddon.Addon(id='plugin.video.embycon') + include_media = addonSettings.getSetting("include_media") == "true" + include_people = addonSettings.getSetting("include_people") == "true" + include_overview = addonSettings.getSetting("include_overview") == "true" + + detailsString = "DateCreated,EpisodeCount,SeasonCount,Path,Genres,Studios,Etag" + + if include_media: + detailsString += ",MediaStreams" + + if include_people: + detailsString += ",People" + + if include_overview: + detailsString += ",Overview" + + return detailsString class DownloadUtils(): getString = None @@ -33,6 +54,17 @@ class DownloadUtils(): if (len(host) == 0) or (host == "") or (len(port) == 0): return None + # if user entered a full path i.e. http://some_host:port + if host.lower().strip().startswith("http://") or host.lower().strip().startswith("https://"): + log.debug("Extracting host info from url: {0}", host) + url_bits = urlparse(host.strip()) + if url_bits.hostname is not None and len(url_bits.hostname) > 0: + host = url_bits.hostname + settings.setSetting("ipaddress", host) + if url_bits.port is not None and url_bits.port > 0: + port = str(url_bits.port) + settings.setSetting("port", port) + server = host + ":" + port use_https = settings.getSetting('use_https') == 'true' if use_https: @@ -42,61 +74,64 @@ class DownloadUtils(): return server - def getArtwork(self, data, art_type, parent=False, index="0", width=10000, height=10000, server=None): + def getArtwork(self, data, art_type, parent=False, index=0, width=10000, height=10000, server=None): - id = data.get("Id") + id = data["Id"] + item_type = data["Type"] - if data.get("Type") in ["Episode", "Season"]: + if item_type in ["Episode", "Season"]: if art_type != "Primary" or parent == True: - id = data.get("SeriesId") + id = data["SeriesId"] imageTag = "" # "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format - itemType = data.get("Type") - # for episodes always use the parent BG - if (itemType == "Episode" and art_type == "Backdrop"): - id = data.get("ParentBackdropItemId") - bgItemTags = data.get("ParentBackdropImageTags") - if (bgItemTags != None and len(bgItemTags) > 0): + if item_type == "Episode" and art_type == "Backdrop": + id = data["ParentBackdropItemId"] + bgItemTags = data["ParentBackdropImageTags"] + if bgItemTags is not None and len(bgItemTags) > 0: imageTag = bgItemTags[0] - elif (art_type == "Backdrop") and (parent == True): - id = data.get("ParentBackdropItemId") - bgItemTags = data.get("ParentBackdropImageTags") - if (bgItemTags != None and len(bgItemTags) > 0): + elif art_type == "Backdrop" and parent is True: + id = data["ParentBackdropItemId"] + bgItemTags = data["ParentBackdropImageTags"] + if bgItemTags is not None and len(bgItemTags) > 0: imageTag = bgItemTags[0] - elif (art_type == "Backdrop"): - BGTags = data.get("BackdropImageTags") - if (BGTags != None and len(BGTags) > 0): - bgIndex = int(index) - imageTag = data.get("BackdropImageTags")[bgIndex] - log.debug("Background Image Tag:" + imageTag) - elif (parent == False): - if (data.get("ImageTags") != None and data.get("ImageTags").get(art_type) != None): - imageTag = data.get("ImageTags").get(art_type) - log.debug("Image Tag:" + imageTag) - elif (parent == True): - if (itemType == "Episode" or itemType == "Season") and art_type == 'Primary': + elif art_type == "Backdrop": + BGTags = data["BackdropImageTags"] + if BGTags is not None and len(BGTags) > index: + imageTag = BGTags[index] + log.debug("Background Image Tag: {0}", imageTag) + elif parent is False: + image_tags = data["ImageTags"] + if image_tags is not None: + image_tag_type = image_tags[art_type] + if image_tag_type is not None: + imageTag = image_tag_type + log.debug("Image Tag: {0}", imageTag) + elif parent is True: + if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary': tagName = 'SeriesPrimaryImageTag' idName = 'SeriesId' else: - tagName = 'Parent%sTag' % art_type + tagName = 'Parent%sImageTag' % art_type idName = 'Parent%sItemId' % art_type - if (data.get(idName) != None and data.get(tagName) != None): - id = data.get(idName) - imageTag = data.get(tagName) - log.debug("Parent Image Tag:" + imageTag) + parent_image_id = data[idName] + parent_image_tag = data[tagName] + if parent_image_id is not None and parent_image_tag is not None: + id = parent_image_id + imageTag = parent_image_tag + log.debug("Parent Image Tag: {0}", imageTag) if (imageTag == "" or imageTag == None) and (art_type != 'Banner'): # ParentTag not passed for Banner - log.debug("No Image Tag for request:" + art_type + " item:" + itemType + " parent:" + str(parent)) + log.debug("No Image Tag for request:{0} item:{1} parent:{2}", art_type, item_type, parent) return "" query = "" artwork = "%s/emby/Items/%s/Images/%s/%s?MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" % (server, id, art_type, index, width, height, imageTag, query) - log.debug("getArtwork : " + artwork) + log.debug("getArtwork: {0}", artwork) ''' # do not return non-existing images @@ -119,13 +154,17 @@ class DownloadUtils(): artwork += '&MaxHeight=%s' % height return artwork + def get_user_artwork(self, item_id, item_type): + # Load user information set by UserClient + return "%s/emby/Users/%s/Images/%s?Format=original" % (self.getServer(), item_id, item_type) + def getUserId(self): WINDOW = HomeWindow() userid = WINDOW.getProperty("userid") if (userid != None and userid != ""): - log.debug("EmbyCon DownloadUtils -> Returning saved UserID : " + userid) + log.debug("EmbyCon DownloadUtils -> Returning saved UserID: {0}", userid) return userid settings = xbmcaddon.Addon('plugin.video.embycon') @@ -133,56 +172,63 @@ class DownloadUtils(): if not userName: return "" - log.debug("Looking for user name: " + userName) + log.debug("Looking for user name: {0}", userName) jsonData = None try: jsonData = self.downloadUrl("{server}/emby/Users/Public?format=json", suppress=True, authenticate=False) except Exception, msg: - error = "Get User unable to connect: " + str(msg) - log.error(error) + log.error("Get User unable to connect: {0}", msg) return "" - log.debug("GETUSER_JSONDATA_01:" + str(jsonData)) + log.debug("GETUSER_JSONDATA_01: {0}", jsonData) result = [] try: result = json.loads(jsonData) except Exception, e: - log.debug("jsonload : " + str(e) + " (" + jsonData + ")") + log.debug("Could not load user data: {0}", e) return "" if result is None: return "" - log.debug("GETUSER_JSONDATA_02:" + str(result)) + log.debug("GETUSER_JSONDATA_02: {0}", result) userid = "" + userImage = "" secure = False for user in result: - if (user.get("Name") == userName): + if (user.get("Name") == unicode(userName, "utf-8")): userid = user.get("Id") - log.debug("Username Found:" + user.get("Name")) + if "PrimaryImageTag" in user: + userImage = self.get_user_artwork(userid, 'Primary') + log.debug("Username Found: {0}", user.get("Name")) if (user.get("HasPassword") == True): secure = True log.debug("Username Is Secure (HasPassword=True)") break - if (secure) or (not userid): + if secure or not userid: authOk = self.authenticate() - if (authOk == ""): - xbmcgui.Dialog().ok(self.addon_name, i18n('incorrect_user_pass')) + if authOk == "": + xbmcgui.Dialog().notification(i18n("connection_error"), + i18n('incorrect_user_pass'), + icon="special://home/addons/plugin.video.embycon/icon.png") return "" if not userid: userid = WINDOW.getProperty("userid") if userid == "": - xbmcgui.Dialog().ok(self.addon_name, i18n('username_not_found')) + xbmcgui.Dialog().notification(i18n("connection_error"), + i18n('username_not_found'), + icon="special://home/addons/plugin.video.embycon/icon.png") - log.debug("userid : " + userid) + log.debug("userid: {0}", userid) WINDOW.setProperty("userid", userid) + WINDOW.setProperty("userimage", userImage) return userid @@ -191,31 +237,27 @@ class DownloadUtils(): WINDOW = HomeWindow() token = WINDOW.getProperty("AccessToken") - if (token != None and token != ""): - log.debug("EmbyCon DownloadUtils -> Returning saved AccessToken : " + token) + if token is not None and token != "": + log.debug("EmbyCon DownloadUtils -> Returning saved AccessToken: {0}", token) return token settings = xbmcaddon.Addon('plugin.video.embycon') port = settings.getSetting("port") host = settings.getSetting("ipaddress") - if (host == None or host == "" or port == None or port == ""): + if host is None or host == "" or port is None or port == "": return "" url = "{server}/emby/Users/AuthenticateByName?format=json" - clientInfo = ClientInformation() - txt_mac = clientInfo.getDeviceId() - version = clientInfo.getVersion() - client = clientInfo.getClient() - - deviceName = settings.getSetting('deviceName') - deviceName = deviceName.replace("\"", "_") + pwd_sha = hashlib.sha1(settings.getSetting('password')).hexdigest() + user_name = urllib.quote(settings.getSetting('username')) + pwd_text = urllib.quote(settings.getSetting('password')) - authString = "Mediabrowser Client=\"" + client + "\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" - headers = {'Accept-encoding': 'gzip', 'Authorization': authString} - sha1 = hashlib.sha1(settings.getSetting('password')) + messageData = "username=" + user_name + "&password=" + pwd_sha - messageData = "username=" + settings.getSetting('username') + "&password=" + sha1.hexdigest() + use_https = settings.getSetting('use_https') == 'true' + if use_https: + messageData += "&pw=" + pwd_text resp = self.downloadUrl(url, postBody=messageData, method="POST", suppress=True, authenticate=False) @@ -228,8 +270,8 @@ class DownloadUtils(): except: pass - if (accessToken != None): - log.debug("User Authenticated : " + accessToken) + if accessToken is not None: + log.debug("User Authenticated: {0}", accessToken) WINDOW.setProperty("AccessToken", accessToken) WINDOW.setProperty("userid", userid) return accessToken @@ -247,7 +289,12 @@ class DownloadUtils(): settings = xbmcaddon.Addon('plugin.video.embycon') deviceName = settings.getSetting('deviceName') + # remove none ascii chars + deviceName = deviceName.decode("ascii", errors='ignore') + # remove some chars not valid for names deviceName = deviceName.replace("\"", "_") + if len(deviceName) == 0: + deviceName = "EmbyCon" headers = {} headers["Accept-encoding"] = "gzip" @@ -268,23 +315,34 @@ class DownloadUtils(): if (authToken != ""): headers["X-MediaBrowser-Token"] = authToken - log.debug("EmbyCon Authentication Header : " + str(headers)) + log.debug("EmbyCon Authentication Header: {0}", headers) return headers - def downloadUrl(self, url, suppress=False, postBody=None, method="GET", popup=0, authenticate=True, headers=None): + def downloadUrl(self, url, suppress=False, postBody=None, method="GET", authenticate=True, headers=None): log.debug("downloadUrl") + + return_data = "null" settings = xbmcaddon.Addon(id='plugin.video.embycon') - log.debug(url) + if settings.getSetting("suppressErrors") == "true": + suppress = True + + log.debug("Before: {0}", url) + if url.find("{server}") != -1: server = self.getServer() + if server is None: + return return_data url = url.replace("{server}", server) + if url.find("{userid}") != -1: userid = self.getUserId() url = url.replace("{userid}", userid) + if url.find("{ItemLimit}") != -1: show_x_filtered_items = settings.getSetting("show_x_filtered_items") url = url.replace("{ItemLimit}", show_x_filtered_items) + if url.find("{IsUnplayed}") != -1 or url.find("{,IsUnplayed}") != -1 or url.find("{IsUnplayed,}") != -1 \ or url.find("{,IsUnplayed,}") != -1: show_latest_unplayed = settings.getSetting("show_latest_unplayed") == "true" @@ -301,9 +359,13 @@ class DownloadUtils(): url = url.replace("{IsUnplayed,}", "IsUnplayed,") elif url.find("{,IsUnplayed,}") != -1: url = url.replace("{,IsUnplayed,}", ",IsUnplayed,") - log.debug(url) - return_data = "null" + if url.find("{field_filters}") != -1: + filter_string = getDetailsString() + url = url.replace("{field_filters}", filter_string) + + log.debug("After: {0}", url) + try: if url.startswith('http'): serversplit = 2 @@ -315,9 +377,9 @@ class DownloadUtils(): server = url.split('/')[serversplit] urlPath = "/" + "/".join(url.split('/')[urlsplit:]) - log.debug("DOWNLOAD_URL = " + url) - log.debug("server = " + str(server)) - log.debug("urlPath = " + str(urlPath)) + log.debug("DOWNLOAD_URL: {0}", url) + log.debug("server: {0}", server) + log.debug("urlPath: {0}", urlPath) # check the server details tokens = server.split(':') @@ -326,7 +388,6 @@ class DownloadUtils(): if (host == "" or host == "" or port == ""): return "" - settings = xbmcaddon.Addon('plugin.video.embycon') use_https = settings.getSetting('use_https') == 'true' verify_cert = settings.getSetting('verify_cert') == 'true' @@ -341,7 +402,7 @@ class DownloadUtils(): conn = httplib.HTTPConnection(server, timeout=40) head = self.getAuthHeader(authenticate) - log.debug("HEADERS : " + str(head)) + log.debug("HEADERS: {0}", head) if (postBody != None): if isinstance(postBody, dict): @@ -351,20 +412,20 @@ class DownloadUtils(): content_type = "application/x-www-form-urlencoded" head["Content-Type"] = content_type - log.debug("Content-Type : " + content_type) + log.debug("Content-Type: {0}", content_type) - log.debug("POST DATA : " + postBody) + log.debug("POST DATA: {0}", postBody) conn.request(method=method, url=urlPath, body=postBody, headers=head) else: conn.request(method=method, url=urlPath, headers=head) data = conn.getresponse() - log.debug("GET URL HEADERS : " + str(data.getheaders())) + log.debug("GET URL HEADERS: {0}", data.getheaders()) if int(data.status) == 200: retData = data.read() contentType = data.getheader('content-encoding') - log.debug("Data Len Before : " + str(len(retData))) + log.debug("Data Len Before: {0}", len(retData)) if (contentType == "gzip"): retData = StringIO.StringIO(retData) gzipper = gzip.GzipFile(fileobj=retData) @@ -373,41 +434,29 @@ class DownloadUtils(): return_data = retData if headers is not None and isinstance(headers, dict): headers.update(data.getheaders()) - log.debug("Data Len After : " + str(len(return_data))) + log.debug("Data Len After: {0}", len(return_data)) log.debug("====== 200 returned =======") - log.debug("Content-Type : " + str(contentType)) - log.debug(return_data) + log.debug("Content-Type: {0}", contentType) + log.debug("{0}", return_data) log.debug("====== 200 finished ======") - #elif (int(data.status) == 301) or (int(data.status) == 302): - # try: - # conn.close() - # except: - # pass - # return data.getheader('Location') - elif int(data.status) >= 400: - error = "HTTP response error: " + str(data.status) + " " + str(data.reason) - log.error(error) + log.error("HTTP response error: {0} {1}", data.status, data.reason) if suppress is False: - if popup == 0: - xbmcgui.Dialog().notification(self.addon_name, i18n('url_error_') % str(data.reason)) - else: - xbmcgui.Dialog().ok(self.addon_name, i18n('url_error_') % str(data.reason)) - log.error(error) + xbmcgui.Dialog().notification(i18n("connection_error"), + i18n('url_error_') % str(data.reason), + icon="special://home/addons/plugin.video.embycon/icon.png") except Exception, msg: - error = "Unable to connect to " + str(server) + " : " + str(msg) - log.error(error) + log.error("Unable to connect to {0} : {1}", server, msg) if suppress is False: - if popup == 0: - xbmcgui.Dialog().notification(self.addon_name, i18n('url_error_') % str(msg)) - else: - xbmcgui.Dialog().ok(self.addon_name, i18n('url_error_') % i18n('unable_connect_server'), str(msg)) - #raise + xbmcgui.Dialog().notification(i18n("connection_error"), + str(msg), + icon="special://home/addons/plugin.video.embycon/icon.png") + finally: try: - log.debug("Closing HTTP connection: " + str(conn)) + log.debug("Closing HTTP connection: {0}", conn) conn.close() except: pass diff --git a/plugin.video.embycon/resources/lib/error.py b/plugin.video.embycon/resources/lib/error.py new file mode 100644 index 0000000..903ad70 --- /dev/null +++ b/plugin.video.embycon/resources/lib/error.py @@ -0,0 +1,152 @@ + +import traceback +import sys +import os +import httplib +import json + +import xbmcgui +import xbmc + +from simple_logging import SimpleLogging +from clientinfo import ClientInformation +from translation import i18n + +log = SimpleLogging(__name__) + +def catch_except(errors=(Exception, ), default_value=False): + # Will wrap method with try/except and print parameters for easier debugging + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except errors as error: + if not (hasattr(error, 'quiet') and error.quiet): + return_value = xbmcgui.Dialog().yesno(i18n('error'), i18n('embycon_error'), i18n('embycon_error_submit')) + if return_value: + log.debug("Sending Error Data") + try: + submit_error_data() + except Exception as error: + log.debug("Sending Error Data Failed: {0}", error) + raise + return default_value + return wrapper + return decorator + +def submit_error_data(): + + data = {} + + try: + error_type, error_short, error_stack, machine_state = format_exception() + + data["error_stack"] = error_stack + data["error_type"] = error_type + data["error_short"] = error_short + data["machine_state"] = machine_state + data["sys.argv"] = sys.argv + data["kodi_version"] = xbmc.getInfoLabel("System.BuildVersion") + + client_info = ClientInformation() + data["addon_version"] = client_info.getVersion() + data["device_id"] = client_info.getDeviceId() + + except Exception as error: + exc_type, exc_obj, exc_tb = sys.exc_info() + stack_trace_data = traceback.format_tb(exc_tb) + data["report_error"] = str(error) + data["report_error_stack"] = str(stack_trace_data) + + post_data = json.dumps(data) + log.debug("ERROR_DATA: {0}", post_data) + + server = "allthedata.pythonanywhere.com" + url_path = "/submit" + conn = httplib.HTTPConnection(server, timeout=40) + head = {} + head["Content-Type"] = "application/json" + conn.request(method="POST", url=url_path, body=post_data, headers=head) + data = conn.getresponse() + log.debug("Submit Responce Code: {0}", data.status) + + +def format_exception(): + + stack = traceback.extract_stack() + exc_type, exc_obj, exc_tb = sys.exc_info() + + frames = [] + tb = exc_tb + while tb: + frame = tb.tb_frame + frames.append(frame) + tb = tb.tb_next + + machine_state = [] + for frame in frames: + state = {} + state["filename"] = frame.f_code.co_filename + state["line"] = frame.f_lineno + state["function"] = frame.f_code.co_name + locals = {} + for key, value in frame.f_locals.items(): + locals[key] = str(value) + state["locals"] = locals + machine_state.append(state) + + stack_trace_data = traceback.format_tb(exc_tb) + tb = traceback.extract_tb(exc_tb) + full_tb = stack[:-1] + tb + # log.error(str(full_tb)) + + # get last stack frame + latestStackFrame = None + if (len(tb) > 0): + latestStackFrame = tb[-1] + # log.error(str(tb)) + + fileStackTrace = "" + try: + # get files from stack + stackFileList = [] + for frame in full_tb: + # log.error(str(frame)) + frameFile = (os.path.split(frame[0])[1])[:-3] + frameLine = frame[1] + if (len(stackFileList) == 0 or stackFileList[-1][0] != frameFile): + stackFileList.append([frameFile, [str(frameLine)]]) + else: + file = stackFileList[-1][0] + lines = stackFileList[-1][1] + lines.append(str(frameLine)) + stackFileList[-1] = [file, lines] + # log.error(str(stackFileList)) + + for item in stackFileList: + lines = ",".join(item[1]) + fileStackTrace += item[0] + "," + lines + ":" + # log.error(str(fileStackTrace)) + except Exception as e: + fileStackTrace = None + log.error("{0}", e) + + errorType = "NA" + errorFile = "NA" + + if latestStackFrame is not None: + if fileStackTrace is None: + fileStackTrace = os.path.split(latestStackFrame[0])[1] + ":" + str(latestStackFrame[1]) + + codeLine = "NA" + if (len(latestStackFrame) > 3 and latestStackFrame[3] != None): + codeLine = latestStackFrame[3].strip() + + errorFile = "%s(%s)(%s)" % (fileStackTrace, exc_obj.message, codeLine) + errorFile = errorFile[0:499] + errorType = "%s" % (exc_type.__name__) + # log.error(errorType + " - " + errorFile) + + del (exc_type, exc_obj, exc_tb) + + return errorType, errorFile, stack_trace_data, machine_state diff --git a/plugin.video.embycon/resources/lib/functions.py b/plugin.video.embycon/resources/lib/functions.py index aa2953e..46dbf70 100644 --- a/plugin.video.embycon/resources/lib/functions.py +++ b/plugin.video.embycon/resources/lib/functions.py @@ -9,24 +9,29 @@ import pstats import json import StringIO import encodings +import binascii import xbmcplugin import xbmcgui import xbmcaddon import xbmc +from resources.lib.error import catch_except from downloadutils import DownloadUtils -from utils import getDetailsString, getArt, cache_artwork +from utils import getArt, cache_artwork, send_event_notification from kodi_utils import HomeWindow from clientinfo import ClientInformation from datamanager import DataManager from server_detect import checkServer from simple_logging import SimpleLogging -from menu_functions import displaySections, showMovieAlphaList, showGenreList, showWidgets, showSearch +from menu_functions import displaySections, showMovieAlphaList, showGenreList, showWidgets, showSearch, showYearsList from translation import i18n from server_sessions import showServerSessions from action_menu import ActionMenu from widgets import getWidgetContent, getWidgetContentCast, getWidgetContentSimilar, getWidgetContentNextUp, getSuggestions, getWidgetUrlContent, checkForNewContent +import trakttokodi +from item_functions import add_gui_item, extract_item_info, ItemDetails, add_context_menu + __addon__ = xbmcaddon.Addon(id='plugin.video.embycon') __addondir__ = xbmc.translatePath(__addon__.getAddonInfo('profile')) @@ -41,6 +46,7 @@ downloadUtils = DownloadUtils() dataManager = DataManager() +@catch_except() def mainEntryPoint(): log.debug("===== EmbyCon START =====") @@ -54,11 +60,11 @@ def mainEntryPoint(): pr.enable() ADDON_VERSION = ClientInformation().getVersion() - log.debug("Running Python: " + str(sys.version_info)) - log.debug("Running EmbyCon: " + str(ADDON_VERSION)) - log.debug("Kodi BuildVersion: " + xbmc.getInfoLabel("System.BuildVersion")) - log.debug("Kodi Version: " + str(kodi_version)) - log.debug("Script argument data: " + str(sys.argv)) + log.debug("Running Python: {0}", sys.version_info) + log.debug("Running EmbyCon: {0}", ADDON_VERSION) + log.debug("Kodi BuildVersion: {0}", xbmc.getInfoLabel("System.BuildVersion")) + log.debug("Kodi Version: {0}", kodi_version) + log.debug("Script argument data: {0}", sys.argv) try: params = get_params(sys.argv[2]) @@ -69,7 +75,7 @@ def mainEntryPoint(): if (len(params) == 0): windowParams = home_window.getProperty("Params") - log.debug("windowParams : " + windowParams) + log.debug("windowParams: {0}", windowParams) # home_window.clearProperty("Params") if (windowParams): try: @@ -77,7 +83,7 @@ def mainEntryPoint(): except: params = {} - log.debug("Script params = " + str(params)) + log.debug("Script params: {0}", params) param_url = params.get('url', None) @@ -109,12 +115,17 @@ def mainEntryPoint(): elif sys.argv[1] == "delete": item_id = sys.argv[2] delete(item_id) + elif mode == "playTrailer": + item_id = params["id"] + playTrailer(item_id) elif mode == "MOVIE_ALPHA": showMovieAlphaList() elif mode == "MOVIE_GENRE": showGenreList() elif mode == "SERIES_GENRE": showGenreList(item_type="series") + elif mode == "MOVIE_YEARS": + showYearsList() elif mode == "WIDGETS": showWidgets() elif mode == "SHOW_MENU": @@ -142,7 +153,7 @@ def mainEntryPoint(): getWidgetUrlContent(int(sys.argv[1]), params) elif mode == "PARENT_CONTENT": checkServer(notify=False) - showParentContent(sys.argv[0], int(sys.argv[1]), params) + showParentContent(params) elif mode == "SHOW_CONTENT": # plugin://plugin.video.embycon?mode=SHOW_CONTENT&item_type=Movie|Series checkServer(notify=False) @@ -153,23 +164,18 @@ def mainEntryPoint(): xbmcplugin.setContent(int(sys.argv[1]), 'files') showSearch() elif mode == "NEW_SEARCH": - # plugin://plugin.video.embycon?mode=NEW_SEARCH&item_type= - if 'SEARCH_RESULTS' not in xbmc.getInfoLabel('Container.FolderPath'): # don't ask for input on '..' - checkServer(notify=False) - search(int(sys.argv[1]), params) - else: - return - elif mode == "SEARCH_RESULTS": - # plugin://plugin.video.embycon?mode=SEARCH_RESULTS&item_type=&query=&index=<[0-9]+> checkServer(notify=False) searchResults(params) elif mode == "SHOW_SERVER_SESSIONS": checkServer(notify=False) showServerSessions() + elif mode == "TRAKTTOKODI": + checkServer(notify=False) + trakttokodi.entry_point(params) else: checkServer(notify=False) - log.debug("EmbyCon -> Mode: " + str(mode)) - log.debug("EmbyCon -> URL: " + str(param_url)) + log.debug("EmbyCon -> Mode: {0}", mode) + log.debug("EmbyCon -> URL: {0}", param_url) if mode == "GET_CONTENT": getContent(param_url, params) @@ -199,7 +205,7 @@ def mainEntryPoint(): def markWatched(item_id): - log.debug("Mark Item Watched : " + item_id) + log.debug("Mark Item Watched: {0}", item_id) url = "{server}/emby/Users/{userid}/PlayedItems/" + item_id downloadUtils.downloadUrl(url, postBody="", method="POST") home_window = HomeWindow() @@ -209,7 +215,7 @@ def markWatched(item_id): def markUnwatched(item_id): - log.debug("Mark Item UnWatched : " + item_id) + log.debug("Mark Item UnWatched: {0}", item_id) url = "{server}/emby/Users/{userid}/PlayedItems/" + item_id downloadUtils.downloadUrl(url, method="DELETE") home_window = HomeWindow() @@ -219,7 +225,7 @@ def markUnwatched(item_id): def markFavorite(item_id): - log.debug("Add item to favourites : " + item_id) + log.debug("Add item to favourites: {0}", item_id) url = "{server}/emby/Users/{userid}/FavoriteItems/" + item_id downloadUtils.downloadUrl(url, postBody="", method="POST") home_window = HomeWindow() @@ -229,7 +235,7 @@ def markFavorite(item_id): def unmarkFavorite(item_id): - log.debug("Remove item from favourites : " + item_id) + log.debug("Remove item from favourites: {0}", item_id) url = "{server}/emby/Users/{userid}/FavoriteItems/" + item_id downloadUtils.downloadUrl(url, method="DELETE") home_window = HomeWindow() @@ -241,7 +247,7 @@ def unmarkFavorite(item_id): def delete(item_id): return_value = xbmcgui.Dialog().yesno(i18n('confirm_file_delete'), i18n('file_delete_confirm')) if return_value: - log.debug('Deleting Item : ' + item_id) + log.debug('Deleting Item: {0}', item_id) url = '{server}/emby/Items/' + item_id progress = xbmcgui.DialogProgress() progress.create(i18n('deleting'), i18n('waiting_server_delete')) @@ -252,224 +258,8 @@ def delete(item_id): xbmc.executebuiltin("Container.Refresh") -def addGUIItem(url, details, extraData, display_options, folder=True): - - url = url.encode('utf-8') - - log.debug("Adding GuiItem for [%s]" % details.get('title', i18n('unknown'))) - log.debug("Passed details: " + str(details)) - log.debug("Passed extraData: " + str(extraData)) - - if details.get('title', '') == '': - return - - if extraData.get('mode', None) is None: - mode = "&mode=0" - else: - mode = "&mode=%s" % extraData['mode'] - - # Create the URL to pass to the item - if folder: - u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + extraData["itemtype"] - if extraData.get("name_format"): - u += '&name_format=' + urllib.quote(extraData.get("name_format")) - else: - u = sys.argv[0] + "?item_id=" + url + "&mode=PLAY" - - # Create the ListItem that will be displayed - thumbPath = str(extraData.get('thumb', '')) - - listItemName = details.get('title', i18n('unknown')) - - # calculate percentage - cappedPercentage = 0 - if (extraData.get('resumetime') != None and int(extraData.get('resumetime')) > 0): - duration = float(extraData.get('duration')) - if (duration > 0): - resume = float(extraData.get('resumetime')) - percentage = int((resume / duration) * 100.0) - cappedPercentage = percentage - - if (extraData.get('TotalEpisodes') != None and extraData.get('TotalEpisodes') != "0"): - totalItems = int(extraData.get('TotalEpisodes')) - watched = int(extraData.get('WatchedEpisodes')) - percentage = int((float(watched) / float(totalItems)) * 100.0) - cappedPercentage = percentage - - countsAdded = False - addCounts = display_options.get("addCounts", True) - if addCounts and extraData.get('UnWatchedEpisodes') != "0": - countsAdded = True - listItemName = listItemName + " (" + extraData.get('UnWatchedEpisodes') + ")" - - addResumePercent = display_options.get("addResumePercent", True) - if (countsAdded == False - and addResumePercent - and details.get('title') != None - and cappedPercentage not in [0, 100]): - listItemName = listItemName + " (" + str(cappedPercentage) + "%)" - - subtitle_available = display_options.get("addSubtitleAvailable", False) - if subtitle_available and extraData.get("SubtitleAvailable", False): - listItemName += " (cc)" - - # update title with new name, this sets the new name in the deailts that are later passed to video info - details['title'] = listItemName - - if kodi_version > 17: - list_item = xbmcgui.ListItem(listItemName, offscreen=True) - else: - list_item = xbmcgui.ListItem(listItemName, iconImage=thumbPath, thumbnailImage=thumbPath) - - log.debug("Setting thumbnail as " + thumbPath) - - # calculate percentage - if (cappedPercentage != 0): - list_item.setProperty("complete_percentage", str(cappedPercentage)) - - # For all end items - if (not folder): - # list_item.setProperty('IsPlayable', 'true') - if extraData.get('type', 'video').lower() == "video": - list_item.setProperty('TotalTime', str(extraData.get('duration'))) - list_item.setProperty('ResumeTime', str(int(extraData.get('resumetime')))) - - # StartPercent - - artTypes = ['thumb', 'poster', 'fanart', 'clearlogo', 'discart', 'banner', 'clearart', - 'landscape', 'tvshow.poster', 'tvshow.clearart', 'tvshow.banner', 'tvshow.landscape'] - artLinks = {} - for artType in artTypes: - artLinks[artType] = extraData.get(artType, '') - log.debug("Setting " + artType + " as " + artLinks[artType]) - list_item.setProperty('fanart_image', artLinks['fanart']) # back compat - list_item.setProperty('discart', artLinks['discart']) # not avail to setArt - list_item.setProperty('tvshow.poster', artLinks['tvshow.poster']) # not avail to setArt - list_item.setArt(artLinks) - - menuItems = addContextMenu(details, extraData, folder) - if (len(menuItems) > 0): - list_item.addContextMenuItems(menuItems, True) - - # new way - videoInfoLabels = {} - - # add cast - people = extraData.get('cast') - if people is not None: - if kodi_version >= 17: - list_item.setCast(people) - else: - videoInfoLabels['cast'] = videoInfoLabels['castandrole'] = [(cast_member['name'], cast_member['role']) for cast_member in people] - - if (extraData.get('type') == None or extraData.get('type') == "Video"): - videoInfoLabels.update(details) - else: - list_item.setInfo(type=extraData.get('type', 'Video'), infoLabels=details) - - videoInfoLabels["duration"] = extraData.get("duration") - videoInfoLabels["playcount"] = extraData.get("playcount") - if (extraData.get('favorite') == 'true'): - videoInfoLabels["top250"] = "1" - - videoInfoLabels["mpaa"] = extraData.get('mpaa') - videoInfoLabels["rating"] = extraData.get('rating') - videoInfoLabels["director"] = extraData.get('director') - videoInfoLabels["writer"] = extraData.get('writer') - videoInfoLabels["year"] = extraData.get('year') - videoInfoLabels["premiered"] = extraData.get('premieredate') - videoInfoLabels["dateadded"] = extraData.get('dateadded') - videoInfoLabels["studio"] = extraData.get('studio') - videoInfoLabels["genre"] = extraData.get('genre') - - item_type = extraData.get('itemtype').lower() - mediatype = 'video' - - if (item_type == 'movie') or (item_type == 'boxset'): - mediatype = 'movie' - elif (item_type == 'series'): - mediatype = 'tvshow' - elif (item_type == 'season'): - mediatype = 'season' - elif (item_type == 'episode'): - mediatype = 'episode' - - videoInfoLabels["mediatype"] = mediatype - - if mediatype == 'episode': - videoInfoLabels["episode"] = details.get('episode') - - if (mediatype == 'season') or (mediatype == 'episode'): - videoInfoLabels["season"] = details.get('season') - - list_item.setInfo('video', videoInfoLabels) - - list_item.addStreamInfo('video', - {'duration': extraData.get('duration'), 'aspect': extraData.get('aspectratio'), - 'codec': extraData.get('videocodec'), 'width': extraData.get('width'), - 'height': extraData.get('height')}) - list_item.addStreamInfo('audio', {'codec': extraData.get('audiocodec'), 'channels': extraData.get('channels')}) - if extraData.get('SubtitleLang', '') != '': - list_item.addStreamInfo('subtitle', {'language': extraData.get('SubtitleLang', '')}) - - list_item.setProperty('CriticRating', str(extraData.get('criticrating'))) - list_item.setProperty('ItemType', extraData.get('itemtype')) - - if extraData.get('totaltime') != None: - list_item.setProperty('TotalTime', extraData.get('totaltime')) - if extraData.get('TotalSeasons') != None: - list_item.setProperty('TotalSeasons', extraData.get('TotalSeasons')) - if extraData.get('TotalEpisodes') != None: - list_item.setProperty('TotalEpisodes', extraData.get('TotalEpisodes')) - if extraData.get('WatchedEpisodes') != None: - list_item.setProperty('WatchedEpisodes', extraData.get('WatchedEpisodes')) - if extraData.get('UnWatchedEpisodes') != None: - list_item.setProperty('UnWatchedEpisodes', extraData.get('UnWatchedEpisodes')) - if extraData.get('NumEpisodes') != None: - list_item.setProperty('NumEpisodes', extraData.get('NumEpisodes')) - - #list_item.setProperty('ItemGUID', extraData.get('guiid')) - list_item.setProperty('id', extraData.get('id')) - - return (u, list_item, folder) - - -def addContextMenu(details, extraData, folder): - commands = [] - - item_id = extraData.get('id') - if item_id != None: - scriptToRun = PLUGINPATH + "/default.py" - - if not folder: - argsToPass = "?mode=PLAY&item_id=" + item_id + "&force_transcode=true" - commands.append((i18n('emby_force_transcode'), "RunPlugin(plugin://plugin.video.embycon" + argsToPass + ")")) - - # watched/unwatched - if extraData.get("playcount") == "0": - argsToPass = 'markWatched,' + item_id - commands.append((i18n('emby_mark_watched'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) - elif extraData.get("playcount"): - argsToPass = 'markUnwatched,' + item_id - commands.append((i18n('emby_mark_unwatched'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) - - # favourite add/remove - if extraData.get('favorite') == 'false': - argsToPass = 'markFavorite,' + item_id - commands.append((i18n('emby_set_favorite'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) - elif extraData.get('favorite') == 'true': - argsToPass = 'unmarkFavorite,' + item_id - commands.append((i18n('emby_unset_favorite'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) - - # delete - argsToPass = 'delete,' + item_id - commands.append((i18n('emby_delete'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) - - return (commands) - - def get_params(paramstring): - log.debug("Parameter string: " + paramstring) + log.debug("Parameter string: {0}", paramstring) param = {} if len(paramstring) >= 2: params = paramstring @@ -491,15 +281,19 @@ def get_params(paramstring): elif (len(splitparams)) == 3: param[splitparams[0]] = splitparams[1] + "=" + splitparams[2] - log.debug("EmbyCon -> Detected parameters: " + str(param)) + log.debug("EmbyCon -> Detected parameters: {0}", param) return param def setSort(pluginhandle, viewType): - log.debug("SETTING_SORT for media type: " + str(viewType)) + log.debug("SETTING_SORT for media type: {0}", viewType) if viewType == "BoxSets": xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE) + elif viewType == "Episodes": + xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_EPISODE) + elif viewType == "Music": + xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_TRACKNUM) else: xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE) xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) @@ -518,8 +312,8 @@ def getContent(url, params): if not media_type: xbmcgui.Dialog().ok(i18n('error'), i18n('no_media_type')) - log.debug("URL: " + str(url)) - log.debug("MediaType: " + str(media_type)) + log.debug("URL: {0}", url) + log.debug("MediaType: {0}", media_type) pluginhandle = int(sys.argv[1]) settings = xbmcaddon.Addon(id='plugin.video.embycon') @@ -529,6 +323,21 @@ def getContent(url, params): if media_type.startswith("movie"): viewType = "Movies" xbmcplugin.setContent(pluginhandle, 'movies') + elif media_type == "musicalbums": + viewType = "Albums" + xbmcplugin.setContent(pluginhandle, 'albums') + elif media_type == "musicartists": + viewType = "Artists" + xbmcplugin.setContent(pluginhandle, 'artists') + elif media_type == "musicartist": + viewType = "Albums" + xbmcplugin.setContent(pluginhandle, 'albums') + elif media_type == "music" or media_type == "audio" or media_type == "musicalbum": + viewType = "Music" + xbmcplugin.setContent(pluginhandle, 'songs') + elif media_type.startswith("boxsets"): + viewType = "Movies" + xbmcplugin.setContent(pluginhandle, 'sets') elif media_type.startswith("boxset"): viewType = "BoxSets" xbmcplugin.setContent(pluginhandle, 'movies') @@ -541,9 +350,7 @@ def getContent(url, params): elif media_type == "season" or media_type == "episodes": viewType = "Episodes" xbmcplugin.setContent(pluginhandle, 'episodes') - log.debug("ViewType: " + viewType) - - setSort(pluginhandle, viewType) + log.debug("ViewType: {0} media_type: {1}", viewType, media_type) # show a progress indicator if needed progress = None @@ -555,26 +362,19 @@ def getContent(url, params): # use the data manager to get the data result = dataManager.GetContent(url) - if result == None or len(result) == 0: - if (progress != None): - progress.close() - return - dirItems = processDirectory(result, progress, params) if dirItems is None: return - xbmcplugin.addDirectoryItems(pluginhandle, dirItems) + # set the sort items + setSort(pluginhandle, viewType) + + xbmcplugin.addDirectoryItems(pluginhandle, dirItems) xbmcplugin.endOfDirectory(pluginhandle, cacheToDisc=False) - # if the view master addon is available then run the script - try: - view_addon = xbmcaddon.Addon("script.viewmaster") - if view_addon is not None: - xbmc.executebuiltin('RunScript(' + xbmc.translatePath( - "special://home/addons/script.viewmaster/default.py") + ',' + viewType + ')') - except: - pass + # send display items event + display_items_notification = {"view_type": viewType} + send_event_notification("display_items", display_items_notification) if (progress != None): progress.update(100, i18n('done')) @@ -588,9 +388,9 @@ def processDirectory(results, progress, params): settings = xbmcaddon.Addon(id='plugin.video.embycon') server = downloadUtils.getServer() - detailsString = getDetailsString() name_format = params.get("name_format", None) + name_format_type = None if name_format is not None: name_format = urllib.unquote(name_format) tokens = name_format.split("|") @@ -599,23 +399,22 @@ def processDirectory(results, progress, params): dirItems = [] if results is None: - result = [] + results = [] + if isinstance(results, dict): - result = results.get("Items") - else: - result = results + results = results.get("Items", []) # flatten single season # if there is only one result and it is a season and you have flatten signle season turned on then # build a new url, set the content media type and call get content again flatten_single_season = settings.getSetting("flatten_single_season") == "true" - if flatten_single_season and len(result) == 1 and result[0].get("Type", "") == "Season": - season_id = result[0].get("Id") + if flatten_single_season and len(results) == 1 and results[0].get("Type", "") == "Season": + season_id = results[0].get("Id") season_url = ('{server}/emby/Users/{userid}/items' + '?ParentId=' + season_id + '&IsVirtualUnAired=false' + '&IsMissing=false' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&format=json') if progress is not None: progress.close() @@ -623,324 +422,89 @@ def processDirectory(results, progress, params): getContent(season_url, params) return None - add_season_number = settings.getSetting('addSeasonNumber') == 'true' - add_episode_number = settings.getSetting('addEpisodeNumber') == 'true' + hide_unwatched_details = settings.getSetting('hide_unwatched_details') == 'true' display_options = {} display_options["addCounts"] = settings.getSetting("addCounts") == 'true' display_options["addResumePercent"] = settings.getSetting("addResumePercent") == 'true' display_options["addSubtitleAvailable"] = settings.getSetting("addSubtitleAvailable") == 'true' - item_count = len(result) + item_count = len(results) current_item = 1 first_season_item = None total_unwatched = 0 total_episodes = 0 total_watched = 0 - for item in result: + + gui_options = {} + gui_options["server"] = server + + gui_options["name_format"] = name_format + gui_options["name_format_type"] = name_format_type + + for item in results: if (progress != None): percentDone = (float(current_item) / float(item_count)) * 100 progress.update(int(percentDone), i18n('processing_item:') + str(current_item)) current_item = current_item + 1 - id = str(item.get("Id")).encode('utf-8') - isFolder = item.get("IsFolder") + # get the infofrom the item + item_details = extract_item_info(item, gui_options) - item_type = str(item.get("Type")).encode('utf-8') - - if item_type == "Season" and first_season_item is None: + if item_details.item_type == "Season" and first_season_item is None: first_season_item = item - # set the episode number - tempEpisode = "" - if item_type == "Episode": - tempEpisode = item.get("IndexNumber") - if tempEpisode is not None: - if tempEpisode < 10: - tempEpisode = "0" + str(tempEpisode) - else: - tempEpisode = str(tempEpisode) - else: - tempEpisode = "" - - # set the season number - tempSeason = None - if item_type == "Episode": - tempSeason = item.get("ParentIndexNumber") - elif item_type == "Season": - tempSeason = item.get("IndexNumber") - if tempSeason is not None: - if tempSeason < 10: - tempSeason = "0" + str(tempSeason) - else: - tempSeason = str(tempSeason) - else: - tempSeason = "" - - # set the item name - # override with name format string from request - if name_format is not None and item.get("Type", "") == name_format_type: - nameInfo = {} - nameInfo["ItemName"] = item.get("Name", "").encode('utf-8') - nameInfo["SeriesName"] = item.get("SeriesName", "").encode('utf-8') - nameInfo["SeasonIndex"] = tempSeason - nameInfo["EpisodeIndex"] = tempEpisode - log.debug("FormatName : %s | %s" % (name_format, nameInfo)) - tempTitle = name_format.format(**nameInfo).strip() + total_unwatched += item_details.unwatched_episodes + total_episodes += item_details.total_episodes + total_watched += item_details.watched_episodes - else: - if (item.get("Name") != None): - tempTitle = item.get("Name").encode('utf-8') - else: - tempTitle = i18n('missing_title') - - if item.get("Type") == "Episode": - prefix = '' - if add_season_number: - prefix = "S" + str(tempSeason) - if add_episode_number: - prefix = prefix + "E" - if add_episode_number: - prefix = prefix + str(tempEpisode) - if prefix != '': - tempTitle = prefix + ' - ' + tempTitle - - production_year = item.get("ProductionYear") - if not production_year and item.get("PremiereDate"): - production_year = int(item.get("PremiereDate")[:4]) - - premiere_date = "" - if item.get("PremiereDate") != None: - tokens = (item.get("PremiereDate")).split("T") - premiere_date = tokens[0] - - try: - date_added = item['DateCreated'] - date_added = date_added.split('.')[0].replace('T', " ") - except KeyError: - date_added = "" - - # add the premiered date for Upcoming TV - if item.get("LocationType") == "Virtual": - airtime = item.get("AirTime") - tempTitle = tempTitle + ' - ' + str(premiere_date) + ' - ' + str(airtime) - - # Process MediaStreams - channels = '' - videocodec = '' - audiocodec = '' - height = '' - width = '' - aspectfloat = 0.0 - subtitle_lang = '' - subtitle_available = False - mediaStreams = item.get("MediaStreams") - if (mediaStreams != None): - for mediaStream in mediaStreams: - if mediaStream.get("Type") == "Video": - videocodec = mediaStream.get("Codec") - height = str(mediaStream.get("Height")) - width = str(mediaStream.get("Width")) - aspectratio = mediaStream.get("AspectRatio") - if aspectratio is not None and len(aspectratio) >= 3: - try: - aspectwidth, aspectheight = aspectratio.split(':') - aspectfloat = float(aspectwidth) / float(aspectheight) - except: - aspectfloat = 1.85 - if mediaStream.get("Type") == "Audio": - audiocodec = mediaStream.get("Codec") - channels = mediaStream.get("Channels") - if mediaStream.get("Type") == "Subtitle": - subtitle_available = True - if mediaStream.get("Language") is not None: - subtitle_lang = mediaStream.get("Language") - - # Process People - director = '' - writer = '' - cast = None - people = item.get("People") - if (people != None): - cast = [] - for person in people: - if (person.get("Type") == "Director"): - director = director + person.get("Name") + ' ' - if (person.get("Type") == "Writing"): - writer = person.get("Name") - if (person.get("Type") == "Writer"): - writer = person.get("Name") - if (person.get("Type") == "Actor"): - person_name = person.get("Name") - person_role = person.get("Role") - if person_role == None: - person_role = '' - person_id = person.get("Id") - person_tag = person.get("PrimaryImageTag") - person_thumbnail = downloadUtils.imageUrl(person_id, "Primary", 0, 400, 400, person_tag, server=server) - person = {"name": person_name, "role": person_role, "thumbnail": person_thumbnail} - cast.append(person) - - # Process Studios - studio = "" - studios = item.get("Studios") - if (studios != None): - for studio_string in studios: - if studio == "": # Just take the first one - temp = studio_string.get("Name") - studio = temp.encode('utf-8') - - # Process Genres - genre = "" - genres = item.get("Genres") - if (genres != None and genres != []): - for genre_string in genres: - if genre == "": # Just take the first genre - genre = genre_string - elif genre_string != None: - genre = genre + " / " + genre_string - - # Process UserData - userData = item.get("UserData") - overlay = "0" - favorite = "false" - seekTime = 0 - if (userData != None): - if userData.get("Played") != True: - overlay = "7" - watched = "true" - else: - overlay = "6" - watched = "false" + # if set, for unwatched episodes dont show some of the info + if hide_unwatched_details and item_details.item_type == "Episode" and item_details.play_count == 0: + item_details.plot = "[Spoiler Alert]" + item_details.art["poster"] = item_details.art["tvshow.poster"] + item_details.art["thumb"] = item_details.art["tvshow.poster"] - if userData.get("IsFavorite") == True: - overlay = "5" - favorite = "true" - else: - favorite = "false" - - if userData.get("PlaybackPositionTicks") != None: - reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 - seekTime = reasonableTicks / 10000 - - playCount = 0 - if (userData != None and userData.get("Played") == True): - playCount = 1 - # Populate the details list - details = {'title': tempTitle, - 'plot': item.get("Overview"), - 'Overlay': overlay, - 'playcount': str(playCount), - # 'aired' : episode.get('originallyAvailableAt','') , - 'TVShowTitle': item.get("SeriesName"), - } - - if item_type == "Episode": - details['episode'] = tempEpisode - if item_type == "Episode" or item_type == "Season": - details['season'] = tempSeason - - try: - tempDuration = str(int(item.get("RunTimeTicks", "0")) / (10000000)) - except TypeError: - try: - tempDuration = str(int(item.get("CumulativeRunTimeTicks")) / (10000000)) - except TypeError: - tempDuration = "0" - - TotalSeasons = 0 if item.get("ChildCount") == None else item.get("ChildCount") - TotalEpisodes = 0 if item.get("RecursiveItemCount") == None else item.get("RecursiveItemCount") - WatchedEpisodes = 0 if userData.get("UnplayedItemCount") == None else TotalEpisodes - userData.get("UnplayedItemCount") - UnWatchedEpisodes = 0 if userData.get("UnplayedItemCount") == None else userData.get("UnplayedItemCount") - NumEpisodes = TotalEpisodes - total_unwatched += UnWatchedEpisodes - total_episodes += TotalEpisodes - total_watched += WatchedEpisodes - - art = getArt(item, server) - # Populate the extraData list - extraData = {'thumb': art['thumb'], - 'fanart': art['fanart'], - 'poster': art['poster'], - 'banner': art['banner'], - 'clearlogo': art['clearlogo'], - 'discart': art['discart'], - 'clearart': art['clearart'], - 'landscape': art['landscape'], - 'tvshow.poster': art['tvshow.poster'], - 'tvshow.clearart': art['tvshow.clearart'], - 'tvshow.banner': art['tvshow.banner'], - 'tvshow.landscape': art['tvshow.landscape'], - 'id': id, - 'mpaa': item.get("OfficialRating"), - 'rating': item.get("CommunityRating"), - 'criticrating': item.get("CriticRating"), - 'year': production_year, - 'premieredate': premiere_date, - 'dateadded': date_added, - 'locationtype': item.get("LocationType"), - 'studio': studio, - 'genre': genre, - 'playcount': str(playCount), - 'director': director, - 'writer': writer, - 'channels': channels, - 'videocodec': videocodec, - 'aspectratio': str(aspectfloat), - 'audiocodec': audiocodec, - 'height': height, - 'width': width, - 'cast': cast, - 'favorite': favorite, - 'resumetime': str(seekTime), - 'totaltime': tempDuration, - 'duration': tempDuration, - 'RecursiveItemCount': item.get("RecursiveItemCount"), - 'RecursiveUnplayedItemCount': userData.get("UnplayedItemCount"), - 'TotalSeasons': str(TotalSeasons), - 'TotalEpisodes': str(TotalEpisodes), - 'WatchedEpisodes': str(WatchedEpisodes), - 'UnWatchedEpisodes': str(UnWatchedEpisodes), - 'NumEpisodes': str(NumEpisodes), - 'OriginalTitle': item.get("Name").encode('utf-8'), - 'itemtype': item_type, - 'SubtitleLang': subtitle_lang, - 'SubtitleAvailable': subtitle_available} - - extraData["Path"] = item.get("Path") - - extraData['mode'] = "GET_CONTENT" - - if isFolder == True: - - if item.get("Type", "") == "Series": - u = ('{server}/emby/Shows/' + id + + if item["IsFolder"] is True: + if item_details.item_type == "Series": + u = ('{server}/emby/Shows/' + item_details.id + '/Seasons' '?userId={userid}' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&format=json') + else: u = ('{server}/emby/Users/{userid}/items' + - '?ParentId=' + id + + '?ParentId=' + item_details.id + '&IsVirtualUnAired=false' + - '&IsMissing=false&' + - 'Fields=' + detailsString + + '&IsMissing=false' + + '&Fields={field_filters}' + '&format=json') - if item.get("RecursiveItemCount") != 0: - dirItems.append(addGUIItem(u, details, extraData, display_options)) + if item["RecursiveItemCount"] != 0: + dirItems.append(add_gui_item(u, item_details, display_options)) + + elif item_details.item_type == "MusicArtist": + u = ('{server}/emby/Users/{userid}/items' + + '?ArtistIds=' + item_details.id + + '&IncludeItemTypes=MusicAlbum' + + '&CollapseBoxSetItems=false' + + '&Recursive=true' + + '&format=json') + dirItems.append(add_gui_item(u, item_details, display_options)) + else: - u = id - dirItems.append(addGUIItem(u, details, extraData, display_options, folder=False)) + u = item_details.id + dirItems.append(add_gui_item(u, item_details, display_options, folder=False)) # add the all episodes item - if first_season_item is not None: + show_all_episodes = settings.getSetting('show_all_episodes') == 'true' + if show_all_episodes and first_season_item is not None and len(dirItems) > 1: series_url = ('{server}/emby/Users/{userid}/items' + - '?ParentId=' + str(first_season_item.get("SeriesId")).encode('utf-8') + + '?ParentId=' + first_season_item.get("SeriesId") + '&IsVirtualUnAired=false' + '&IsMissing=false' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Recursive=true' + '&IncludeItemTypes=Episode' + '&format=json') @@ -949,55 +513,69 @@ def processDirectory(results, progress, params): if total_unwatched == 0: played = 1 overlay = "6" - details = {'title': i18n('all'), - 'Overlay': overlay, - 'playcount': str(played) - } - art = getArt(first_season_item, server) - # Populate the extraData list - extraData = {'thumb': art['tvshow.poster'], - 'fanart': art['fanart'], - 'poster': art['tvshow.poster'], - 'banner': art['tvshow.banner'], - 'clearlogo': art['clearlogo'], - 'discart': art['discart'], - 'clearart': art['clearart'], - 'landscape': art['landscape'], - 'tvshow.poster': art['tvshow.poster'], - 'tvshow.clearart': art['tvshow.clearart'], - 'tvshow.banner': art['tvshow.banner'], - 'tvshow.landscape': art['tvshow.landscape'], - 'itemtype': 'Episodes', - 'UnWatchedEpisodes': str(total_unwatched), - 'TotalEpisodes': str(total_episodes), - 'WatchedEpisodes': str(total_watched), - 'playcount': str(played), - 'mode': 'GET_CONTENT', - 'name_format': 'Episode|episode_name_format'} - dirItems.append(addGUIItem(series_url, details, extraData, {}, folder=True)) + + item_details = ItemDetails() + + item_details.name = i18n('all') + item_details.art = getArt(first_season_item, server) + item_details.play_count = played + item_details.overlay = overlay + item_details.name_format = "Episode|episode_name_format" + item_details.series_name = first_season_item.get("SeriesName") + item_details.item_type = "Season" + item_details.unwatched_episodes = total_unwatched + item_details.total_episodes = total_episodes + item_details.watched_episodes = total_watched + item_details.mode = "GET_CONTENT" + + dirItems.append(add_gui_item(series_url, item_details, display_options, folder=True)) return dirItems def showMenu(params): - log.debug("showMenu(): " + str(params)) + log.debug("showMenu(): {0}", params) + + id = params["item_id"] + + url = "{server}/emby/Users/{userid}/Items/" + id + "?format=json" + data_manager = DataManager() + result = data_manager.GetContent(url) + log.debug("Playfile item info: {0}", result) + + if result is None: + return action_items = [] li = xbmcgui.ListItem("Play") li.setProperty('menu_id', 'play') action_items.append(li) + + if result["Type"] == "Episode" and result["ParentId"] is not None: + li = xbmcgui.ListItem("Show Season") + li.setProperty('menu_id', 'show_season') + action_items.append(li) + li = xbmcgui.ListItem("Force Transcode") li.setProperty('menu_id', 'transcode') action_items.append(li) - li = xbmcgui.ListItem("Mark Watched") - li.setProperty('menu_id', 'mark_watched') - action_items.append(li) - li = xbmcgui.ListItem("Mark Unwatched") - li.setProperty('menu_id', 'mark_unwatched') - action_items.append(li) + + user_data = result["UserData"] + if user_data.get("Played", False) is False or user_data.get("PlaybackPositionTicks", 0) > 0: + li = xbmcgui.ListItem("Mark Watched") + li.setProperty('menu_id', 'mark_watched') + action_items.append(li) + + if user_data.get("Played", False) is True or user_data.get("PlaybackPositionTicks", 0) > 0: + li = xbmcgui.ListItem("Mark Unwatched") + li.setProperty('menu_id', 'mark_unwatched') + action_items.append(li) + li = xbmcgui.ListItem("Delete") li.setProperty('menu_id', 'delete') action_items.append(li) + #xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False) + action_menu = ActionMenu("ActionMenu.xml", PLUGINPATH, "default", "720p") action_menu.setActionItems(action_items) action_menu.doModal() @@ -1005,11 +583,14 @@ def showMenu(params): selected_action = "" if selected_action_item is not None: selected_action = selected_action_item.getProperty('menu_id') - log.debug("Menu Action Selected: " + str(selected_action_item)) + log.debug("Menu Action Selected: {0}", selected_action_item) del action_menu if selected_action == "play": log.debug("Play Item") + #list_item = populate_listitem(params["item_id"]) + #result = xbmcgui.Dialog().info(list_item) + #log.debug("xbmcgui.Dialog().info: {0}", result) PLAY(params) elif selected_action == "transcode": params['force_transcode'] = 'true' @@ -1023,10 +604,66 @@ def showMenu(params): elif selected_action == "delete": delete(params["item_id"]) xbmc.executebuiltin("XBMC.ReloadSkin()") + elif selected_action == "show_season": + parent_id = result["ParentId"] + xbmc.executebuiltin( + 'ActivateWindow(Videos, plugin://plugin.video.embycon/?mode=PARENT_CONTENT&ParentId={0}&media_type=episodes, return)'.format(parent_id)) + + +def populate_listitem(item_id): + log.debug("populate_listitem: {0}", item_id) + + url = "{server}/emby/Users/{userid}/Items/" + item_id + "?format=json" + jsonData = downloadUtils.downloadUrl(url) + result = json.loads(jsonData) + log.debug("populate_listitem item info: {0}", result) + + ''' + server = downloadUtils.getServer() + gui_options = {} + gui_options["server"] = server + + gui_options["name_format"] = None + gui_options["name_format_type"] = None + + details, extraData = extract_item_info(result,gui_options ) + u, list_item, folder = add_gui_item(result["Id"], details, extraData, {}, folder=False) + + log.debug("list_item path: {0}", u) + + #list_item.setProperty('IsPlayable', 'false') + #list_item.setPath(u) + ''' + + item_title = result.get("Name", i18n('missing_title')) + + list_item = xbmcgui.ListItem(label=item_title) + + server = downloadUtils.getServer() + + art = getArt(result, server=server) + list_item.setIconImage(art['thumb']) # back compat + list_item.setProperty('fanart_image', art['fanart']) # back compat + list_item.setProperty('discart', art['discart']) # not avail to setArt + list_item.setArt(art) + + list_item.setProperty('IsPlayable', 'false') + list_item.setProperty('IsFolder', 'false') + list_item.setProperty('id', result.get("Id")) + + # play info + details = { + 'title': item_title, + 'plot': result.get("Overview") + } + + list_item.setInfo("Video", infoLabels=details) + + return list_item def showContent(pluginName, handle, params): - log.debug("showContent Called: " + str(params)) + log.debug("showContent Called: {0}", params) item_type = params.get("item_type") @@ -1034,40 +671,37 @@ def showContent(pluginName, handle, params): "?format=json" "&ImageTypeLimit=1" "&IsMissing=False" - "&Fields=" + getDetailsString() + + "&Fields={field_filters}" + "&Recursive=true" "&IsVirtualUnaired=false" "&IsMissing=False" "&IncludeItemTypes=" + item_type) - log.debug("showContent Content Url : " + str(contentUrl)) + log.debug("showContent Content Url: {0}", contentUrl) getContent(contentUrl, params) -def showParentContent(pluginName, handle, params): - log.debug("showParentContent Called: " + str(params)) - settings = xbmcaddon.Addon(id='plugin.video.embycon') +def showParentContent(params): + log.debug("showParentContent Called: {0}", params) parentId = params.get("ParentId") - detailsString = getDetailsString() contentUrl = ( "{server}/emby/Users/{userid}/items?ParentId=" + parentId + "&IsVirtualUnaired=false" + "&IsMissing=False" + "&ImageTypeLimit=1" + - "&Fields=" + detailsString + + "&Fields={field_filters}" + "&format=json") - log.debug("showParentContent Content Url : " + str(contentUrl)) + log.debug("showParentContent Content Url: {0}", contentUrl) getContent(contentUrl, params) -def search(handle, params): - log.debug('search Called: ' + str(params)) + +def searchResults(params): + item_type = params.get('item_type') - if not item_type: - return - kb = xbmc.Keyboard() + if item_type.lower() == 'movie': heading_type = i18n('movies') elif item_type.lower() == 'series': @@ -1076,26 +710,26 @@ def search(handle, params): heading_type = i18n('episodes') else: heading_type = item_type + + home_window = HomeWindow() + + last_search = home_window.getProperty("last_search") + kb = xbmc.Keyboard() kb.setHeading(heading_type.capitalize() + ' ' + i18n('search').lower()) + kb.setDefault(last_search) kb.doModal() + if kb.isConfirmed(): user_input = kb.getText().strip() - if user_input: - xbmcplugin.endOfDirectory(handle, cacheToDisc=False) - user_input = urllib.quote(user_input) - xbmc.executebuiltin('Container.Update(plugin://plugin.video.embycon/?mode=SEARCH_RESULTS&query={user_input}&item_type={item_type}&index=0)' - .format(user_input=user_input, item_type=item_type)) # redirect for results to avoid page refreshing issues - else: - return else: return + home_window.setProperty("last_search", user_input) -def searchResults(params): - log.debug('searchResults Called: ' + str(params)) + log.debug('searchResults Called: {0}', params) handle = int(sys.argv[1]) - query = params.get('query') + query = user_input item_type = params.get('item_type') if (not item_type) or (not query): return @@ -1105,15 +739,18 @@ def searchResults(params): settings = xbmcaddon.Addon(id='plugin.video.embycon') server = downloadUtils.getServer() - userid = downloadUtils.getUserId() - details_string = getDetailsString() content_url = ('{server}/emby/Search/Hints?searchTerm=' + query + - '&IncludeItemTypes=' + item_type + - '&UserId={userid}' - '&StartIndex=' + str(index) + - '&Limit=' + str(limit) + - '&IncludePeople=false&IncludeMedia=true&IncludeGenres=false&IncludeStudios=false&IncludeArtists=false') + '&IncludeItemTypes=' + item_type + + '&ExcludeItemTypes=LiveTvProgram' + + '&UserId={userid}' + '&StartIndex=' + str(index) + + '&Limit=' + str(limit) + + '&IncludePeople=false' + + '&IncludeMedia=true' + + '&IncludeGenres=false' + + '&IncludeStudios=false' + + '&IncludeArtists=false') if item_type.lower() == 'movie': xbmcplugin.setContent(handle, 'movies') @@ -1142,7 +779,10 @@ def searchResults(params): progress.update(0, i18n('retrieving_data')) result = dataManager.GetContent(content_url) - log.debug('SearchHints jsonData: ' + str(result)) + log.debug('SearchHints jsonData: {0}', result) + + if result is None: + result = {} results = result.get('SearchHints') if results is None: @@ -1150,13 +790,13 @@ def searchResults(params): item_count = 1 total_results = int(result.get('TotalRecordCount', 0)) - log.debug('SEARCH_TOTAL_RESULTS: ' + str(total_results)) + log.debug('SEARCH_TOTAL_RESULTS: {0}', total_results) list_items = [] for item in results: item_id = item.get('ItemId') name = title = item.get('Name') - log.debug('SEARCH_RESULT_NAME: ' + name) + log.debug('SEARCH_RESULT_NAME: {0}', name) if progress is not None: percent_complete = (float(item_count) / float(total_results)) * 100 @@ -1215,7 +855,7 @@ def searchResults(params): list_item = xbmcgui.ListItem(label=name, iconImage=art['thumb']) info = {'title': title, 'tvshowtitle': tvshowtitle, 'mediatype': media_type} - log.debug('SEARCH_RESULT_ART: ' + str(art)) + log.debug('SEARCH_RESULT_ART: {0}', art) list_item.setProperty('fanart_image', art['fanart']) list_item.setProperty('discart', art['discart']) list_item.setArt(art) @@ -1227,21 +867,23 @@ def searchResults(params): if item.get('MediaType') == 'Video': total_time = str(int(float(item.get('RunTimeTicks', '0')) / (10000000 * 60))) list_item.setProperty('TotalTime', str(total_time)) - list_item.setProperty('IsPlayable', 'true') + list_item.setProperty('IsPlayable', 'false') list_item_url = 'plugin://plugin.video.embycon/?item_id=' + item_id + '&mode=PLAY' is_folder = False else: item_url = ('{server}/emby/Users/{userid}' + '/items?ParentId=' + item_id + '&IsVirtualUnAired=false&IsMissing=false' + - '&Fields=' + details_string + + '&Fields={field_filters}' + '&format=json') list_item_url = 'plugin://plugin.video.embycon/?mode=GET_CONTENT&media_type={item_type}&url={item_url}'\ .format(item_type=item_type, item_url=urllib.quote(item_url)) list_item.setProperty('IsPlayable', 'false') is_folder = True - menu_items = addContextMenu({}, {'id': item_id}, is_folder) + item_details = ItemDetails() + item_details.id = item_id + menu_items = add_context_menu(item_details, is_folder) if len(menu_items) > 0: list_item.addContextMenuItems(menu_items, True) @@ -1251,7 +893,7 @@ def searchResults(params): info['year'] = item.get('ProductionYear', '') - log.debug('SEARCH_RESULT_INFO: ' + str(info)) + log.debug('SEARCH_RESULT_INFO: {0}', info) list_item.setInfo('Video', infoLabels=info) item_tuple = (list_item_url, list_item, is_folder) @@ -1268,14 +910,20 @@ def searchResults(params): def PLAY(params): log.debug("== ENTER: PLAY ==") - log.debug("PLAY ACTION PARAMS: " + str(params)) + log.debug("PLAY ACTION PARAMS: {0}", params) item_id = params.get("item_id") auto_resume = int(params.get("auto_resume", "-1")) - log.debug("AUTO_RESUME: " + str(auto_resume)) + log.debug("AUTO_RESUME: {0}", auto_resume) forceTranscode = params.get("force_transcode", None) is not None - log.debug("FORCE_TRANSCODE: " + str(forceTranscode)) + log.debug("FORCE_TRANSCODE: {0}", forceTranscode) + + media_source_id = params.get("media_source_id", "") + log.debug("media_source_id: {0}", media_source_id) + + use_default = params.get("use_default", "false") == "true" + log.debug("use_default: {0}", use_default) # set the current playing item id # set all the playback info, this will be picked up by the service @@ -1284,13 +932,69 @@ def PLAY(params): xbmc.Player().stop() play_info = {} - play_info["item_id"] = item_id + play_info["item_id"] = item_id play_info["auto_resume"] = str(auto_resume) play_info["force_transcode"] = forceTranscode - play_data = json.dumps(play_info) + play_info["media_source_id"] = media_source_id + play_info["use_default"] = use_default + send_event_notification("embycon_play_action", play_info) + + +def playTrailer(id): + log.debug("== ENTER: playTrailer ==") + + url = ("{server}/emby/Users/{userid}/Items/%s/LocalTrailers?format=json" % id) + + jsonData = downloadUtils.downloadUrl(url) + result = json.loads(jsonData) + log.debug("LocalTrailers {0}", result) + + trailer_list = [] + for trailer in result: + info = {} + info["type"] = "local" + info["name"] = trailer.get("Name", "na") + info["id"] = trailer.get("Id") + trailer_list.append(info) + + url = ("{server}/emby/Users/{userid}/Items/%s?format=json&Fields=RemoteTrailers" % id) + jsonData = downloadUtils.downloadUrl(url) + result = json.loads(jsonData) + log.debug("RemoteTrailers: {0}", result) + + remote_trailers = result.get("RemoteTrailers", []) + for trailer in remote_trailers: + info = {} + info["type"] = "remote" + info["name"] = trailer.get("Name", "na") + url = trailer.get("Url", "none") + if url.lower().find("youtube"): + info["url"] = url + trailer_list.append(info) + + log.debug("TrailerList: {0}", trailer_list) + + trailer_text = [] + for trailer in trailer_list: + name = trailer.get("name") + " (" + trailer.get("type") + ")" + trailer_text.append(name) + + dialog = xbmcgui.Dialog() + resp = dialog.select(i18n('select_trailer'), trailer_text) + if resp > -1: + trailer = trailer_list[resp] + log.debug("SelectedTrailer: {0}", trailer) + + if trailer.get("type") == "local": + params = {} + params["item_id"] = trailer.get("id") + PLAY(params) + + elif trailer.get("type") == "remote": + youtube_id = trailer.get("url").rsplit('=', 1)[1] + log.debug("YoutubeID: {0}", youtube_id) + youtube_plugin = "PlayMedia(plugin://plugin.video.youtube/?action=play_video&videoid=%s)" % youtube_id + xbmc.executebuiltin(youtube_plugin) + - home_window = HomeWindow() - home_window.setProperty("item_id", item_id) - home_window.setProperty("play_item_message", play_data) - #xbmcgui.Dialog().notification("EmbyCon", "Starting Playback") diff --git a/plugin.video.embycon/resources/lib/item_functions.py b/plugin.video.embycon/resources/lib/item_functions.py new file mode 100644 index 0000000..3818613 --- /dev/null +++ b/plugin.video.embycon/resources/lib/item_functions.py @@ -0,0 +1,556 @@ + +import sys +import os +import urllib +import json +from collections import defaultdict + +import xbmc +import xbmcaddon +import xbmcgui + +from utils import getArt +from simple_logging import SimpleLogging +from translation import i18n +from downloadutils import DownloadUtils +from datamanager import DataManager + +log = SimpleLogging(__name__) +kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + +addon_instance = xbmcaddon.Addon(id='plugin.video.embycon') +addon_path = addon_instance.getAddonInfo('path') +PLUGINPATH = xbmc.translatePath(os.path.join(addon_path)) + +download_utils = DownloadUtils() + +class ItemDetails(): + + name = None + id = None + path = None + is_folder = False + plot = None + series_name = None + episode_number = 0 + season_number = 0 + track_number = 0 + + art = None + + mpaa = None + rating = None + critic_rating = 0.0 + year = None + premiere_date = "" + date_added = "" + location_type = None + studio = None + genre = "" + play_count = 0 + director = "" + writer = "" + channels = "" + video_codec = "" + aspect_ratio = 0.0 + audio_codec = "" + height = 0 + width = 0 + cast = None + + resume_time = 0 + duration = 0 + recursive_item_count = 0 + recursive_unplayed_items_count = 0 + total_seasons = 0 + total_episodes = 0 + watched_episodes = 0 + unwatched_episodes = 0 + number_episodes = 0 + original_title = None + item_type = None + subtitle_lang = "" + subtitle_available = False + + song_artist = "" + album_artist = "" + album_name = "" + + favorite = "false" + overlay = "0" + + name_format = "" + mode = "" + +def extract_item_info(item, gui_options): + + item_details = ItemDetails() + + item_details.id = item["Id"] + item_details.is_folder = item["IsFolder"] + item_details.item_type = item["Type"] + item_details.location_type = item["LocationType"] + item_details.name = item["Name"] + item_details.original_title = item_details.name + + if item_details.item_type == "Episode": + item_details.episode_number = item["IndexNumber"] + + if item_details.item_type == "Episode": + item_details.season_number = item["ParentIndexNumber"] + elif item_details.item_type == "Season": + item_details.season_number = item["IndexNumber"] + + if item_details.season_number is None: + item_details.season_number = 0 + if item_details.episode_number is None: + item_details.episode_number = 0 + + if item_details.item_type == "Audio": + item_details.track_number = item["IndexNumber"] + item_details.album_name = item["Album"] + artists = item["Artists"] + if artists is not None and len(artists) > 0: + item_details.song_artist = artists[0] # get first artist + + if item_details.item_type == "MusicAlbum": + item_details.album_artist = item["AlbumArtist"] + item_details.album_name = item_details.name + + # set the item name + # override with name format string from request + name_format = gui_options["name_format"] + name_format_type = gui_options["name_format_type"] + + if name_format is not None and item_details.item_type == name_format_type: + nameInfo = {} + nameInfo["ItemName"] = item["Name"] + season_name = item["SeriesName"] + if season_name: + nameInfo["SeriesName"] = season_name + else: + nameInfo["SeriesName"] = "" + nameInfo["SeasonIndex"] = u"%02d" % item_details.season_number + nameInfo["EpisodeIndex"] = u"%02d" % item_details.episode_number + log.debug("FormatName: {0} | {1}", name_format, nameInfo) + item_details.name = unicode(name_format).format(**nameInfo).strip() + + year = item["ProductionYear"] + prem_date = item["PremiereDate"] + + if year is not None: + item_details.year = year + elif item_details.year is None and prem_date is not None: + item_details.year = int(prem_date[:4]) + + if prem_date is not None: + tokens = prem_date.split("T") + item_details.premiere_date = tokens[0] + + create_date = item["DateCreated"] + if create_date is not None: + item_details.date_added = create_date.split('.')[0].replace('T', " ") + + # add the premiered date for Upcoming TV + if item_details.location_type == "Virtual": + airtime = item["AirTime"] + item_details.name = item_details.name + ' - ' + item_details.premiere_date + ' - ' + str(airtime) + + # Process MediaStreams + mediaStreams = item["MediaStreams"] + if mediaStreams is not None: + for mediaStream in mediaStreams: + stream_type = mediaStream["Type"] + if stream_type == "Video": + item_details.video_codec = mediaStream["Codec"] + item_details.height = mediaStream["Height"] + item_details.width = mediaStream["Width"] + aspect = mediaStream["AspectRatio"] + if aspect is not None and len(aspect) >= 3: + try: + aspect_width, aspect_height = aspect.split(':') + item_details.aspect_ratio = float(aspect_width) / float(aspect_height) + except: + item_details.aspect_ratio = 1.85 + if stream_type == "Audio": + item_details.audio_codec = mediaStream["Codec"] + item_details.channels = mediaStream["Channels"] + if stream_type == "Subtitle": + item_details.subtitle_available = True + sub_lang = mediaStream["Language"] + if sub_lang is not None: + item_details.subtitle_lang = sub_lang + + # Process People + people = item["People"] + if people is not None: + cast = [] + for person in people: + person_type = person["Type"] + if person_type == "Director": + item_details.director = item_details.director + person["Name"] + ' ' + elif person_type == "Writing": + item_details.writer = person["Name"] + elif person_type == "Actor": + log.debug("Person: {0}", person) + person_name = person["Name"] + person_role = person["Role"] + person_id = person["Id"] + person_tag = person["PrimaryImageTag"] + if person_tag is not None: + person_thumbnail = download_utils.imageUrl(person_id, "Primary", 0, 400, 400, person_tag, server = gui_options["server"]) + else: + person_thumbnail = "" + person = {"name": person_name, "role": person_role, "thumbnail": person_thumbnail} + cast.append(person) + item_details.cast = cast + + # Process Studios + studios = item["Studios"] + if studios is not None: + for studio in studios: + if item_details.studio == "": # Just take the first one + studio_name = studio["Name"] + item_details.studio = studio_name + break + + # Process Genres + genres = item["Genres"] + if genres is not None: + for genre in genres: + item_details.genre = item_details.genre + " / " + genre + + # Process UserData + userData = item["UserData"] + if userData is None: + userData = defaultdict(lambda: None, {}) + + if userData["Played"] == True: + item_details.overlay = "6" + item_details.play_count = 1 + else: + item_details.overlay = "7" + item_details.play_count = 0 + + if userData["IsFavorite"] == True: + item_details.overlay = "5" + item_details.favorite = "true" + else: + item_details.favorite = "false" + + reasonableTicks = userData["PlaybackPositionTicks"] + if reasonableTicks is not None: + reasonableTicks = int(reasonableTicks) / 1000 + item_details.resume_time = int(reasonableTicks / 10000) + + item_details.series_name = item["SeriesName"] + item_details.plot = item["Overview"] + + runtime = item["RunTimeTicks"] + if item_details.is_folder == False and runtime is not None: + item_details.duration = long(runtime) / 10000000 + + child_count = item["ChildCount"] + if child_count is not None: + item_details.total_seasons = child_count + + recursive_item_count = item["RecursiveItemCount"] + if recursive_item_count is not None: + item_details.total_episodes = recursive_item_count + + unplayed_item_count = userData["UnplayedItemCount"] + if unplayed_item_count is not None: + item_details.unwatched_episodes = unplayed_item_count + item_details.watched_episodes = item_details.total_episodes - unplayed_item_count + + item_details.number_episodes = item_details.total_episodes + + item_details.art = getArt(item, gui_options["server"]) + item_details.rating = item["OfficialRating"] + item_details.mpaa = item["OfficialRating"] + item_details.critic_rating = item["CommunityRating"] + if item_details.critic_rating is None: + item_details.critic_rating = 0.0 + item_details.location_type = item["LocationType"] + item_details.recursive_item_count = item["RecursiveItemCount"] + item_details.recursive_unplayed_items_count = userData["UnplayedItemCount"] + + item_details.mode = "GET_CONTENT" + + return item_details + +def add_gui_item(url, item_details, display_options, folder=True): + + log.debug("Passed item_details: {0}", item_details.__dict__) + + if not item_details.name: + return + + if item_details.mode: + mode = "&mode=%s" % item_details.mode + else: + mode = "&mode=0" + + # Create the URL to pass to the item + if folder: + u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + item_details.item_type + if item_details.name_format: + u += '&name_format=' + urllib.quote(item_details.name_format) + else: + u = sys.argv[0] + "?item_id=" + url + "&mode=PLAY" + + # Create the ListItem that will be displayed + thumbPath = item_details.art["thumb"] + + listItemName = item_details.name + item_type = item_details.item_type.lower() + is_video = item_type not in ['musicalbum', 'audio', 'music'] + + # calculate percentage + cappedPercentage = 0 + if item_details.resume_time > 0: + duration = float(item_details.duration) + if (duration > 0): + resume = float(item_details.resume_time) + percentage = int((resume / duration) * 100.0) + cappedPercentage = percentage + + totalItems = item_details.total_episodes + if totalItems != 0: + watched = float(item_details.watched_episodes) + percentage = int((watched / float(totalItems)) * 100.0) + cappedPercentage = percentage + + countsAdded = False + addCounts = display_options["addCounts"] + if addCounts and item_details.unwatched_episodes != 0: + countsAdded = True + listItemName = listItemName + (" (%s)" % item_details.unwatched_episodes) + + addResumePercent = display_options["addResumePercent"] + if (not countsAdded + and addResumePercent + and cappedPercentage not in [0, 100]): + listItemName = listItemName + (" (%s%%)" % cappedPercentage) + + subtitle_available = display_options["addSubtitleAvailable"] + if subtitle_available and item_details.subtitle_available: + listItemName += " (cc)" + + if kodi_version > 17: + list_item = xbmcgui.ListItem(listItemName, offscreen=True) + else: + list_item = xbmcgui.ListItem(listItemName, iconImage=thumbPath, thumbnailImage=thumbPath) + + log.debug("Setting thumbnail as: {0}", thumbPath) + + # calculate percentage + if (cappedPercentage != 0): + list_item.setProperty("complete_percentage", str(cappedPercentage)) + + list_item.setProperty('IsPlayable', 'false') + + if folder == False and is_video: + list_item.setProperty('TotalTime', str(item_details.duration)) + list_item.setProperty('ResumeTime', str(item_details.resume_time)) + + list_item.setArt(item_details.art) + + list_item.setProperty('fanart_image', item_details.art['fanart']) # back compat + list_item.setProperty('discart', item_details.art['discart']) # not avail to setArt + list_item.setProperty('tvshow.poster', item_details.art['tvshow.poster']) # not avail to setArt + + # add context menu + menu_items = add_context_menu(item_details, folder) + if len(menu_items) > 0: + list_item.addContextMenuItems(menu_items, True) + + # new way + info_labels = {} + + # add cast + if item_details.cast is not None: + if kodi_version >= 17: + list_item.setCast(item_details.cast) + else: + info_labels['cast'] = info_labels['castandrole'] = [(cast_member['name'], cast_member['role']) for cast_member in item_details.cast] + + info_labels["title"] = listItemName + info_labels["plot"] = item_details.plot + info_labels["Overlay"] = item_details.overlay + info_labels["playcount"] = str(item_details.play_count) + info_labels["TVShowTitle"] = item_details.series_name + + info_labels["duration"] = item_details.duration + info_labels["playcount"] = item_details.play_count + if item_details.favorite == 'true': + info_labels["top250"] = "1" + + info_labels["mpaa"] = item_details.mpaa + info_labels["rating"] = item_details.rating + info_labels["director"] = item_details.director + info_labels["writer"] = item_details.writer + info_labels["year"] = item_details.year + info_labels["premiered"] = item_details.premiere_date + info_labels["dateadded"] = item_details.date_added + info_labels["studio"] = item_details.studio + info_labels["genre"] = item_details.genre + + mediatype = 'video' + + if item_type == 'movie': + mediatype = 'movie' + elif item_type == 'boxset': + mediatype = 'set' + elif item_type == 'series': + mediatype = 'tvshow' + elif item_type == 'season': + mediatype = 'season' + elif item_type == 'episode': + mediatype = 'episode' + elif item_type == 'musicalbum': + mediatype = 'album' + elif item_type == 'musicartist': + mediatype = 'artist' + elif item_type == 'audio' or item_type == 'music': + mediatype = 'song' + + info_labels["mediatype"] = mediatype + + if mediatype == 'episode': + info_labels["episode"] = item_details.episode_number + + if (mediatype == 'season') or (mediatype == 'episode'): + info_labels["season"] = item_details.season_number + + if is_video: + list_item.setInfo('video', info_labels) + log.debug("info_labels: {0}", info_labels) + list_item.addStreamInfo('video', + {'duration': item_details.duration, + 'aspect': item_details.aspect_ratio, + 'codec': item_details.video_codec, + 'width': item_details.width, + 'height': item_details.height}) + list_item.addStreamInfo('audio', + {'codec': item_details.audio_codec, + 'channels': item_details.channels}) + + list_item.setProperty('TotalSeasons', str(item_details.total_seasons)) + list_item.setProperty('TotalEpisodes', str(item_details.total_episodes)) + list_item.setProperty('WatchedEpisodes', str(item_details.watched_episodes)) + list_item.setProperty('UnWatchedEpisodes', str(item_details.unwatched_episodes)) + list_item.setProperty('NumEpisodes', str(item_details.number_episodes)) + + if item_details.subtitle_lang != '': + list_item.addStreamInfo('subtitle', {'language': item_details.subtitle_lang}) + + list_item.setRating("imdb", item_details.critic_rating, 0, True) + list_item.setProperty('TotalTime', str(item_details.duration)) + + else: + info_labels["tracknumber"] = item_details.track_number + if item_details.album_artist: + info_labels["artist"] = item_details.album_artist + elif item_details.song_artist: + info_labels["artist"] = item_details.song_artist + info_labels["album"] = item_details.album_name + + log.debug("info_labels: {0}", info_labels) + list_item.setInfo('music', info_labels) + + list_item.setContentLookup(False) + list_item.setProperty('ItemType', item_details.item_type) + list_item.setProperty('id', item_details.id) + + return (u, list_item, folder) + + +def add_context_menu(item_details, folder): + commands = [] + + if item_details.id is None: + return commands + + scriptToRun = PLUGINPATH + "/default.py" + + if item_details.item_type == "Season" or item_details.item_type == "MusicAlbum": + argsToPass = "?mode=PLAY&item_id=" + item_details.id + commands.append((i18n('play_all'), "RunPlugin(plugin://plugin.video.embycon" + argsToPass + ")")) + + if not folder: + argsToPass = "?mode=PLAY&item_id=" + item_details.id + "&force_transcode=true" + commands.append((i18n('emby_force_transcode'), "RunPlugin(plugin://plugin.video.embycon" + argsToPass + ")")) + + if not folder and item_details.item_type == "Movie": + argsToPass = "?mode=playTrailer&id=" + item_details.id + commands.append((i18n('play_trailer'), "RunPlugin(plugin://plugin.video.embycon" + argsToPass + ")")) + + # watched/unwatched + if item_details.play_count == 0: + argsToPass = 'markWatched,' + item_details.id + commands.append((i18n('emby_mark_watched'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) + else: + argsToPass = 'markUnwatched,' + item_details.id + commands.append((i18n('emby_mark_unwatched'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + # favourite add/remove + if item_details.favorite == 'false': + argsToPass = 'markFavorite,' + item_details.id + commands.append((i18n('emby_set_favorite'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) + else: + argsToPass = 'unmarkFavorite,' + item_details.id + commands.append((i18n('emby_unset_favorite'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + # delete + argsToPass = 'delete,' + item_details.id + commands.append((i18n('emby_delete'), "RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + return commands + + +def get_next_episode(item): + + if item.get("Type", "na") != "Episode": + log.debug("Not an episode, can not get next") + return None + + parendId = item.get("ParentId", "na") + item_index = item.get("IndexNumber", -1) + + if parendId == "na": + log.debug("No parent id, can not get next") + return None + + if item_index == -1: + log.debug("No episode number, can not get next") + return None + + url = ( '{server}/emby/Users/{userid}/Items?' + + '?Recursive=true' + + '&ParentId=' + parendId + + '&IsVirtualUnaired=false' + + '&IsMissing=False' + + '&IncludeItemTypes=Episode' + + '&ImageTypeLimit=1' + + '&format=json') + + data_manager = DataManager() + items_result = data_manager.GetContent(url) + log.debug("get_next_episode, sibling list: {0}", items_result) + + if items_result is None: + log.debug("get_next_episode no results") + return None + + item_list = items_result.get("Items", []) + + for item in item_list: + index = item.get("IndexNumber", -1) + # find the very next episode in the season + if index == item_index + 1: + log.debug("get_next_episode, found next episode: {0}", item) + return item + + return None + diff --git a/plugin.video.embycon/resources/lib/kodi_utils.py b/plugin.video.embycon/resources/lib/kodi_utils.py index 0bf3ab5..21071d4 100644 --- a/plugin.video.embycon/resources/lib/kodi_utils.py +++ b/plugin.video.embycon/resources/lib/kodi_utils.py @@ -23,17 +23,17 @@ class HomeWindow(): def getProperty(self, key): key = self.id_string % key value = self.window.getProperty(key) - # log.debug('HomeWindow: getProperty |%s| -> |%s|' % (key, value)) + # log.debug('HomeWindow: getProperty |{0}| -> |{1}|', key, value) return value def setProperty(self, key, value): key = self.id_string % key - # log.debug('HomeWindow: setProperty |%s| -> |%s|' % (key, value)) + # log.debug('HomeWindow: setProperty |{0}| -> |{1}|', key, value) self.window.setProperty(key, value) def clearProperty(self, key): key = self.id_string % key - # log.debug('HomeWindow: clearProperty |%s|' % key) + # log.debug('HomeWindow: clearProperty |{0}|', key) self.window.clearProperty(key) @@ -59,9 +59,9 @@ def getKodiVersion(): result = result.get("result") versionData = result.get("version") version = float(str(versionData.get("major")) + "." + str(versionData.get("minor"))) - log.debug("Version : " + str(version) + " - " + str(versionData)) + log.debug("Version: {0} - {1}", version, versionData) except: version = 0.0 - log.error("Version Error : RAW Version Data : " + str(result)) + log.error("Version Error : RAW Version Data: {0}", result) return version diff --git a/plugin.video.embycon/resources/lib/menu_functions.py b/plugin.video.embycon/resources/lib/menu_functions.py index 4e9ca0f..89809e9 100644 --- a/plugin.video.embycon/resources/lib/menu_functions.py +++ b/plugin.video.embycon/resources/lib/menu_functions.py @@ -9,10 +9,10 @@ import xbmcplugin import xbmcaddon from downloadutils import DownloadUtils -from utils import getDetailsString from kodi_utils import addMenuDirectoryItem from simple_logging import SimpleLogging from translation import i18n +from datamanager import DataManager log = SimpleLogging(__name__) downloadUtils = DownloadUtils() @@ -27,29 +27,27 @@ def showGenreList(item_type=None): if server is None: return - detailsString = getDetailsString() - kodi_type = "Movies" emby_type = "Movie" if item_type is not None and item_type == "series": emby_type = "Series" kodi_type = "tvshows" - try: - jsonData = downloadUtils.downloadUrl("{server}/emby/Genres?" + - "SortBy=SortName" + - "&SortOrder=Ascending" + - "&IncludeItemTypes=" + emby_type + - "&Recursive=true" + - "&UserId={userid}" + - "&format=json") - log.debug("GENRE_LIST_DATA : " + jsonData) - except Exception, msg: - error = "Get connect : " + str(msg) - log.error(error) + url = ("{server}/emby/Genres?" + + "SortBy=SortName" + + "&SortOrder=Ascending" + + "&IncludeItemTypes=" + emby_type + + "&Recursive=true" + + "&UserId={userid}" + + "&format=json") - result = json.loads(jsonData) - result = result.get("Items") + data_manager = DataManager() + result = data_manager.GetContent(url) + + if result is not None: + result = result.get("Items") + else: + result = [] collections = [] @@ -58,7 +56,7 @@ def showGenreList(item_type=None): item_data['title'] = genre.get("Name") item_data['media_type'] = kodi_type item_data['thumbnail'] = downloadUtils.getArtwork(genre, "Thumb", server=server) - item_data['path'] = ('{server}/emby/Users/{userid}/Items?Fields=' + detailsString + + item_data['path'] = ('{server}/emby/Users/{userid}/Items?Fields={field_filters}' + '&Recursive=true&GenreIds=' + genre.get("Id") + '&IncludeItemTypes=' + emby_type + '&ImageTypeLimit=1&format=json') @@ -68,7 +66,7 @@ def showGenreList(item_type=None): url = sys.argv[0] + ("?url=" + urllib.quote(collection['path']) + "&mode=GET_CONTENT" + "&media_type=" + collection["media_type"]) - log.debug("addMenuDirectoryItem: " + collection.get('title', i18n('unknown')) + " " + str(url)) + log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url) addMenuDirectoryItem(collection.get('title', i18n('unknown')), url, thumbnail=collection.get("thumbnail")) xbmcplugin.endOfDirectory(int(sys.argv[1])) @@ -81,15 +79,14 @@ def showMovieAlphaList(): server = downloadUtils.getServer() if server is None: return - detailsString = getDetailsString() collections = [] item_data = {} item_data['title'] = "#" item_data['media_type'] = "Movies" - item_data['path'] = ('{server}/emby/Users/{userid}' + - '/Items?Fields=' + detailsString + + item_data['path'] = ('{server}/emby/Users/{userid}/Items' + + '?Fields={field_filters}' + '&Recursive=true' + '&NameLessThan=A' + '&IncludeItemTypes=Movie' + @@ -103,8 +100,8 @@ def showMovieAlphaList(): item_data = {} item_data['title'] = alphaName item_data['media_type'] = "Movies" - item_data['path'] = ('{server}/emby/Users/{userid}' + - '/Items?Fields=' + detailsString + + item_data['path'] = ('{server}/emby/Users/{userid}/Items' + + '?Fields={field_filters}' + '&Recursive=true' + '&NameStartsWith=' + alphaName + '&IncludeItemTypes=Movie' + @@ -114,11 +111,58 @@ def showMovieAlphaList(): for collection in collections: url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) + "&mode=GET_CONTENT&media_type=" + collection["media_type"]) - log.debug("addMenuDirectoryItem: " + collection.get('title', i18n('unknown')) + " " + str(url)) + log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url) addMenuDirectoryItem(collection.get('title', i18n('unknown')), url) xbmcplugin.endOfDirectory(int(sys.argv[1])) +def showYearsList(): + + server = downloadUtils.getServer() + if server is None: + return + + jsonData = downloadUtils.downloadUrl("{server}/emby/Years" + + "?SortBy=SortName" + + "&SortOrder=Descending" + + "&IncludeItemTypes=Movie" + + "&Recursive=true" + + "&UserId={userid}" + + "&format=json") + log.debug("YEAR_LIST_DATA: {0}", jsonData) + + result = json.loads(jsonData) + if result is not None: + result = result.get("Items") + else: + result = [] + + collections = [] + + for year in result: + item_data = {} + item_data['title'] = year.get("Name") + item_data['media_type'] = "Movies" + #item_data['thumbnail'] = server + "/Years/" + year.get("Name") + "/Images/Thumb" + item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Fields={field_filters}' + + '&Recursive=true' + + '&Years=' + year.get("Name") + + '&IncludeItemTypes=Movie' + + '&ImageTypeLimit=1' + + '&format=json') + collections.append(item_data) + + for collection in collections: + url = sys.argv[0] + ("?url=" + urllib.quote(collection['path']) + + "&mode=GET_CONTENT" + + "&media_type=" + collection["media_type"]) + log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url) + addMenuDirectoryItem(collection.get('title', i18n('unknown')), url)#, thumbnail=collection.get("thumbnail")) + + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_LABEL) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + def displaySections(): log.debug("== ENTER: displaySections() ==") @@ -129,8 +173,7 @@ def displaySections(): return # Add collections - detailsString = getDetailsString() - collections = getCollections(detailsString) + collections = getCollections() if collections: for collection in collections: @@ -138,9 +181,10 @@ def displaySections(): "&mode=GET_CONTENT&media_type=" + collection["media_type"]) if collection.get("name_format") is not None: url += "&name_format=" + urllib.quote(collection.get("name_format")) - log.debug("addMenuDirectoryItem: " + collection.get('title', i18n('unknown')) + " " + str(url)) + log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url) addMenuDirectoryItem(collection.get('title', i18n('unknown')), url, thumbnail=collection.get("thumbnail")) + addMenuDirectoryItem(i18n('movies_year'), "plugin://plugin.video.embycon/?mode=MOVIE_YEARS") addMenuDirectoryItem(i18n('movies_genre'), "plugin://plugin.video.embycon/?mode=MOVIE_GENRE") addMenuDirectoryItem(i18n('movies_az'), "plugin://plugin.video.embycon/?mode=MOVIE_ALPHA") addMenuDirectoryItem(i18n('tvshow_genre'), "plugin://plugin.video.embycon/?mode=SERIES_GENRE") @@ -151,15 +195,16 @@ def displaySections(): addMenuDirectoryItem(i18n('detect_server'), "plugin://plugin.video.embycon/?mode=DETECT_SERVER_USER") addMenuDirectoryItem(i18n('show_settings'), "plugin://plugin.video.embycon/?mode=SHOW_SETTINGS") - addMenuDirectoryItem(i18n('cache_textures'), "plugin://plugin.video.embycon/?mode=CACHE_ARTWORK") + # only add these if we have other collection which means we have a valid server conn if collections: + addMenuDirectoryItem(i18n('cache_textures'), "plugin://plugin.video.embycon/?mode=CACHE_ARTWORK") addMenuDirectoryItem(i18n('widgets'), "plugin://plugin.video.embycon/?mode=WIDGETS") xbmcplugin.endOfDirectory(int(sys.argv[1])) -def getCollections(detailsString): +def getCollections(): log.debug("== ENTER: getCollections ==") server = downloadUtils.getServer() @@ -172,39 +217,56 @@ def getCollections(detailsString): log.debug("No userid so returning []") return [] - try: - jsonData = downloadUtils.downloadUrl("{server}/emby/Users/{userid}/Items/Root?format=json") - except Exception, msg: - error = "Get connect : " + str(msg) - log.error(error) - return [] - - log.debug("jsonData : " + jsonData) - result = json.loads(jsonData) + data_manager = DataManager() + result = data_manager.GetContent("{server}/emby/Users/{userid}/Items/Root?format=json") if result is None: return [] parentid = result.get("Id") - log.debug("parentid : " + parentid) + log.debug("parentid: {0}", parentid) htmlpath = "{server}/emby/Users/{userid}/items?ParentId=" + parentid + "&Sortby=SortName&format=json" - jsonData = downloadUtils.downloadUrl(htmlpath) - log.debug("jsonData : " + jsonData) - collections = [] + result = data_manager.GetContent(htmlpath) - result = [] - try: - result = json.loads(jsonData) + if result is not None: result = result.get("Items") - except Exception as error: - log.error("Error parsing user collection: " + str(error)) + else: + result = [] + + collections = [] for item in result: - item_name = (item.get("Name")).encode('utf-8') + item_name = item.get("Name") collection_type = item.get('CollectionType', None) - log.debug("CollectionType: " + str(collection_type)) - log.debug("Title: " + item_name) + log.debug("CollectionType: {0}", collection_type) + log.debug("Title: {0}", item_name) + + if collection_type == "music": + item_data = {} + item_data['title'] = item_name + i18n('_all_albums') + item_data['thumbnail'] = downloadUtils.getArtwork(item, "Primary", server=server) + item_data['media_type'] = 'MusicAlbums' + item_data['path'] = ('{server}/emby/Users/{userid}/Items' + + '?Recursive=true' + + '&ParentId=' + item.get("Id") + + '&IncludeItemTypes=MusicAlbum' + + '&ImageTypeLimit=1' + + '&EnableImageTypes=Primary,Backdrop,Banner,Thumb' + + '&format=json') + collections.append(item_data) + + item_data = {} + item_data['title'] = item_name + i18n('_all_artists') + item_data['thumbnail'] = downloadUtils.getArtwork(item, "Primary", server=server) + item_data['media_type'] = 'MusicArtists' + item_data['path'] = ('{server}/emby/Artists/AlbumArtists' + + '?Recursive=true' + + '&ParentId=' + item.get("Id") + + '&ImageTypeLimit=1' + + '&EnableImageTypes=Primary,Backdrop,Banner,Thumb' + + '&format=json') + collections.append(item_data) if collection_type in ["tvshows", "movies", "boxsets"]: collections.append({ @@ -214,7 +276,7 @@ def getCollections(detailsString): '?ParentId=' + item.get("Id") + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&ImageTypeLimit=1' + '&format=json'), 'media_type': collection_type}) @@ -227,7 +289,7 @@ def getCollections(detailsString): '?ParentId=' + item.get("Id") + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsUnplayed' + '&Recursive=true' + '&IncludeItemTypes=Series' + @@ -242,7 +304,7 @@ def getCollections(detailsString): '&Limit={ItemLimit}' + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsResumable' + '&Recursive=true' + '&IncludeItemTypes=Episode' + @@ -258,7 +320,7 @@ def getCollections(detailsString): '&Limit={ItemLimit}' + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortBy=DateCreated' + '&SortOrder=Descending' + '&Filters=IsUnplayed' + @@ -276,7 +338,7 @@ def getCollections(detailsString): '&Limit={ItemLimit}' + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortBy=DateCreated' + '&SortOrder=Descending' + '&Filters={IsUnplayed,}IsNotFolder' + @@ -293,7 +355,7 @@ def getCollections(detailsString): '&ParentId=' + item.get("Id") + '&Limit={ItemLimit}' + '&Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsUnplayed,IsNotFolder' + '&IsVirtualUnaired=false' + '&IsMissing=False' + @@ -311,7 +373,7 @@ def getCollections(detailsString): '?ParentId=' + item.get("Id") + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsUnplayed' + '&ImageTypeLimit=1' + '&format=json'), @@ -324,7 +386,7 @@ def getCollections(detailsString): '&Limit={ItemLimit}' + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsResumable' + '&ImageTypeLimit=1' + '&format=json'), @@ -337,7 +399,7 @@ def getCollections(detailsString): '&Limit={ItemLimit}' + '&IsVirtualUnaired=false' + '&IsMissing=False' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortBy=DateCreated' + '&SortOrder=Descending' + '&Filters={IsUnplayed,}IsNotFolder' + @@ -350,7 +412,7 @@ def getCollections(detailsString): item_data['title'] = i18n('movies_all') item_data['media_type'] = 'Movies' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + - '?Fields=' + detailsString + + '?Fields={field_filters}' + '&Recursive=true' + '&IncludeItemTypes=Movie' + '&ImageTypeLimit=1' + @@ -362,7 +424,7 @@ def getCollections(detailsString): item_data['media_type'] = 'Movies' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsUnplayed' + '&IncludeItemTypes=Movie' + '&ImageTypeLimit=1' + @@ -375,7 +437,7 @@ def getCollections(detailsString): item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Limit={ItemLimit}' + '&Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsResumable' + '&IncludeItemTypes=Movie' + '&ImageTypeLimit=1' + @@ -389,7 +451,7 @@ def getCollections(detailsString): '?Limit={ItemLimit}' + '&Recursive=true' + '&SortBy=DateCreated' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortOrder=Descending' + '&Filters={IsUnplayed,}IsNotFolder' + '&IncludeItemTypes=Movie' + @@ -401,7 +463,7 @@ def getCollections(detailsString): item_data['title'] = i18n('movies_favorites') item_data['media_type'] = 'Movies' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + - '?Fields=' + detailsString + + '?Fields={field_filters}' + '&Recursive=true' + '&Filters=IsFavorite' + '&IncludeItemTypes=Movie' + @@ -414,7 +476,7 @@ def getCollections(detailsString): item_data['media_type'] = 'BoxSets' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&IncludeItemTypes=BoxSet' + '&ImageTypeLimit=1' + '&format=json') @@ -424,7 +486,7 @@ def getCollections(detailsString): item_data['title'] = i18n('tvshows_all') item_data['media_type'] = 'tvshows' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + - '?Fields=' + detailsString + + '?Fields={field_filters}' + '&Recursive=true' + '&IncludeItemTypes=Series' + '&ImageTypeLimit=1' + @@ -435,7 +497,7 @@ def getCollections(detailsString): item_data['title'] = i18n('tvshows_unwatched') item_data['media_type'] = 'tvshows' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + - '?Fields=' + detailsString + + '?Fields={field_filters}' + '&Recursive=true' + '&Filters=IsUnplayed' + '&IncludeItemTypes=Series' + @@ -447,7 +509,7 @@ def getCollections(detailsString): item_data['title'] = i18n('tvshows_favorites') item_data['media_type'] = 'tvshows' item_data['path'] = ('{server}/emby/Users/{userid}/Items' + - '?Fields=' + detailsString + + '?Fields={field_filters}' + '&Recursive=true' + '&Filters=IsFavorite' + '&IncludeItemTypes=Series' + @@ -463,7 +525,7 @@ def getCollections(detailsString): '&Recursive=true' + '&GroupItems=true' + '&SortBy=DateCreated' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortOrder=Descending' + '&Filters={IsUnplayed}' + '&IsVirtualUnaired=false' + @@ -480,7 +542,7 @@ def getCollections(detailsString): item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Limit={ItemLimit}' + '&Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsResumable' + '&IncludeItemTypes=Episode' + '&ImageTypeLimit=1' + @@ -495,7 +557,7 @@ def getCollections(detailsString): '?Limit={ItemLimit}' + '&Recursive=true' + '&SortBy=DateCreated' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortOrder=Descending' + '&Filters={IsUnplayed,}IsNotFolder' + '&IsVirtualUnaired=false' + @@ -512,7 +574,7 @@ def getCollections(detailsString): item_data['path'] = ('{server}/emby/Shows/NextUp/?Userid={userid}' + '&Limit={ItemLimit}' + '&Recursive=true' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&Filters=IsUnplayed,IsNotFolder' + '&IsVirtualUnaired=false' + '&IsMissing=False' + @@ -528,7 +590,7 @@ def getCollections(detailsString): item_data['path'] = ('{server}/emby/Users/{userid}/Items' + '?Recursive=true' + '&SortBy=PremiereDate' + - '&Fields=' + detailsString + + '&Fields={field_filters}' + '&SortOrder=Ascending' + '&IsVirtualUnaired=true' + '&IsNotFolder' + @@ -537,6 +599,28 @@ def getCollections(detailsString): '&format=json') collections.append(item_data) + item_data = {} + item_data['title'] = i18n('music_all_albums') + item_data['media_type'] = 'MusicAlbums' + item_data['path'] = ('{server}/emby/Users/{userid}/Items' + + '?Recursive=true' + + '&IncludeItemTypes=MusicAlbum' + + '&ImageTypeLimit=1' + + '&EnableImageTypes=Primary,Backdrop,Banner,Thumb' + + '&format=json') + collections.append(item_data) + + item_data = {} + item_data['title'] = i18n('music_all_artists') + item_data['media_type'] = 'MusicArtists' + item_data['path'] = ('{server}/emby/Artists/AlbumArtists' + + '?Recursive=true' + + '&ImageTypeLimit=1' + + '&EnableImageTypes=Primary,Backdrop,Banner,Thumb' + + '&format=json') + collections.append(item_data) + + return collections diff --git a/plugin.video.embycon/resources/lib/play_utils.py b/plugin.video.embycon/resources/lib/play_utils.py index e6455c5..16146d9 100644 --- a/plugin.video.embycon/resources/lib/play_utils.py +++ b/plugin.video.embycon/resources/lib/play_utils.py @@ -1,5 +1,7 @@ # Gnu General Public License - see LICENSE.TXT +import binascii + import xbmc import xbmcgui import xbmcaddon @@ -9,42 +11,175 @@ import time import json import hashlib +from resources.lib.error import catch_except from simple_logging import SimpleLogging from downloadutils import DownloadUtils from resume_dialog import ResumeDialog -from utils import PlayUtils, getArt, id_generator +from utils import PlayUtils, getArt, id_generator, send_event_notification from kodi_utils import HomeWindow from translation import i18n from json_rpc import json_rpc +from datamanager import DataManager +from item_functions import get_next_episode, extract_item_info log = SimpleLogging(__name__) -downloadUtils = DownloadUtils() +download_utils = DownloadUtils() + +@catch_except() +def playAllFiles(id, monitor): + log.debug("PlayAllFiles for parent item id: {0}", id) + + url = ('{server}/emby/Users/{userid}/items' + + '?ParentId=' + id + + '&Fields=MediaSources' + + '&format=json') + data_manager = DataManager() + result = data_manager.GetContent(url) + log.debug("PlayAllFiles items info: {0}", result) + + # process each item + items = result["Items"] + if items is None: + items = [] + + settings = xbmcaddon.Addon('plugin.video.embycon') + server = download_utils.getServer() + + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() + for item in items: -def playFile(play_info): + item_id = item.get("Id") + sources = item.get("MediaSources") + selected_media_source = sources[0] + + listitem_props = [] + playback_type = "0" + playurl = None + play_session_id = id_generator() + log.debug("play_session_id: {0}", play_session_id) + + # check if strm file, path will contain contain strm contents + if selected_media_source.get('Container') == 'strm': + playurl, listitem_props = PlayUtils().getStrmDetails(selected_media_source) + if playurl is None: + return + + if not playurl: + playurl, playback_type = PlayUtils().getPlayUrl(item_id, selected_media_source, False, play_session_id) + + log.debug("Play URL: {0} ListItem Properties: {1}", playurl, listitem_props) + + playback_type_string = "DirectPlay" + if playback_type == "2": + playback_type_string = "Transcode" + elif playback_type == "1": + playback_type_string = "DirectStream" + + # add the playback type into the overview + if item.get("Overview", None) is not None: + item["Overview"] = playback_type_string + "\n" + item.get("Overview") + else: + item["Overview"] = playback_type_string + + # add title decoration is needed + item_title = item.get("Name", i18n('missing_title')) + list_item = xbmcgui.ListItem(label=item_title) + + # add playurl and data to the monitor + data = {} + data["item_id"] = item_id + data["playback_type"] = playback_type_string + data["play_session_id"] = play_session_id + data["play_action_type"] = "play_all" + monitor.played_information[playurl] = data + log.debug("Add to played_information: {0}", monitor.played_information) + + list_item.setPath(playurl) + list_item = setListItemProps(item_id, list_item, item, server, listitem_props, item_title) + + playlist.add(playurl, list_item) + + xbmc.Player().play(playlist) + + +def playFile(play_info, monitor): id = play_info.get("item_id") - auto_resume = play_info.get("auto_resume") - force_transcode = play_info.get("force_transcode") + auto_resume = play_info.get("auto_resume", "-1") + force_transcode = play_info.get("force_transcode", False) + media_source_id = play_info.get("media_source_id", "") + use_default = play_info.get("use_default", False) - log.debug("playFile id(%s) resume(%s) force_transcode(%s)" % (id, auto_resume, force_transcode)) + log.debug("playFile id({0}) resume({1}) force_transcode({2})", id, auto_resume, force_transcode) settings = xbmcaddon.Addon('plugin.video.embycon') addon_path = settings.getAddonInfo('path') + force_auto_resume = settings.getSetting('forceAutoResume') == 'true' jump_back_amount = int(settings.getSetting("jump_back_amount")) - server = downloadUtils.getServer() + server = download_utils.getServer() - jsonData = downloadUtils.downloadUrl("{server}/emby/Users/{userid}/Items/" + id + "?format=json", - suppress=False, popup=1) - result = json.loads(jsonData) - log.debug("Playfile item info: " + str(result)) + url = "{server}/emby/Users/{userid}/Items/" + id + "?format=json" + data_manager = DataManager() + result = data_manager.GetContent(url) + log.debug("Playfile item info: {0}", result) + + if result is None: + log.debug("Playfile item was None, so can not play!") + return + + # if this is a season, tv show or album then play all items + if result.get("Type") == "Season" or result.get("Type") == "MusicAlbum": + return playAllFiles(id, monitor) + + # select the media source to use + media_sources = result.get('MediaSources') + selected_media_source = None + + if media_sources is None or len(media_sources) == 0: + log.debug("Play Failed! There is no MediaSources data!") + return + + elif len(media_sources) == 1: + selected_media_source = media_sources[0] + + elif media_source_id != "": + for source in media_sources: + if source.get("Id", "na") == media_source_id: + selected_media_source = source + break + + elif len(media_sources) > 1: + sourceNames = [] + for source in media_sources: + sourceNames.append(source.get("Name", "na")) + + dialog = xbmcgui.Dialog() + resp = dialog.select(i18n('select_source'), sourceNames) + if resp > -1: + selected_media_source = media_sources[resp] + else: + log.debug("Play Aborted, user did not select a MediaSource") + return + + if selected_media_source is None: + log.debug("Play Aborted, MediaSource was None") + return seekTime = 0 auto_resume = int(auto_resume) + # process user data for resume points if auto_resume != -1: seekTime = (auto_resume / 1000) / 10000 + + elif force_auto_resume: + userData = result.get("UserData") + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + else: userData = result.get("UserData") if userData.get("PlaybackPositionTicks") != 0: @@ -58,13 +193,13 @@ def playFile(play_info): resumeDialog.doModal() resume_result = resumeDialog.getResumeAction() del resumeDialog - log.debug("Resume Dialog Result: " + str(resume_result)) + log.debug("Resume Dialog Result: {0}", resume_result) # check system settings for play action # if prompt is set ask to set it to auto resume params = {"setting": "myvideos.selectaction"} setting_result = json_rpc('Settings.getSettingValue').execute(params) - log.debug("Current Setting (myvideos.selectaction): %s" % setting_result) + log.debug("Current Setting (myvideos.selectaction): {0}", setting_result) current_value = setting_result.get("result", None) if current_value is not None: current_value = current_value.get("value", -1) @@ -73,7 +208,7 @@ def playFile(play_info): if return_value: params = {"setting": "myvideos.selectaction", "value": 2} json_rpc_result = json_rpc('Settings.setSettingValue').execute(params) - log.debug("Save Setting (myvideos.selectaction): %s" % json_rpc_result) + log.debug("Save Setting (myvideos.selectaction): {0}", json_rpc_result) if resume_result == 1: seekTime = 0 @@ -81,20 +216,21 @@ def playFile(play_info): return listitem_props = [] + playback_type = "0" playurl = None play_session_id = id_generator() - log.debug("play_session_id: %s" % play_session_id) + log.debug("play_session_id: {0}", play_session_id) # check if strm file, path will contain contain strm contents - if result.get('MediaSources'): - source = result['MediaSources'][0] - if source.get('Container') == 'strm': - playurl, listitem_props = PlayUtils().getStrmDetails(result) + if selected_media_source.get('Container') == 'strm': + playurl, listitem_props = PlayUtils().getStrmDetails(selected_media_source) + if playurl is None: + return if not playurl: - playurl, playback_type = PlayUtils().getPlayUrl(id, result, force_transcode, play_session_id) + playurl, playback_type = PlayUtils().getPlayUrl(id, selected_media_source, force_transcode, play_session_id) - log.debug("Play URL: " + playurl + " ListItem Properties: " + str(listitem_props)) + log.debug("Play URL: {0} ListItem Properties: {1}", playurl, listitem_props) playback_type_string = "DirectPlay" if playback_type == "2": @@ -102,37 +238,31 @@ def playFile(play_info): elif playback_type == "1": playback_type_string = "DirectStream" - home_window = HomeWindow() - home_window.setProperty("PlaybackType_" + id, playback_type_string) - home_window.setProperty("PlaySessionId_" + id, play_session_id) - # add the playback type into the overview if result.get("Overview", None) is not None: result["Overview"] = playback_type_string + "\n" + result.get("Overview") else: result["Overview"] = playback_type_string + # add title decoration is needed item_title = result.get("Name", i18n('missing_title')) - add_episode_number = settings.getSetting('addEpisodeNumber') == 'true' - if result.get("Type") == "Episode" and add_episode_number: - episode_num = result.get("IndexNumber") - if episode_num is not None: - if episode_num < 10: - episode_num = "0" + str(episode_num) - else: - episode_num = str(episode_num) - else: - episode_num = "" - item_title = episode_num + " - " + item_title - list_item = xbmcgui.ListItem(label=item_title) - # if transcoding then prompt for audio and subtitle - if playback_type == "2": - playurl = audioSubsPref(playurl, list_item, result) - log.debug("New playurl for transcoding : " + playurl) - elif playback_type == "1": - externalSubs(result, list_item) + if playback_type == "2": # if transcoding then prompt for audio and subtitle + playurl = audioSubsPref(playurl, list_item, selected_media_source, id, use_default) + log.debug("New playurl for transcoding: {0}", playurl) + + elif playback_type == "1": # for direct stream add any streamable subtitles + externalSubs(selected_media_source, list_item, id) + + # add playurl and data to the monitor + data = {} + data["item_id"] = id + data["playback_type"] = playback_type_string + data["play_session_id"] = play_session_id + data["play_action_type"] = "play" + monitor.played_information[playurl] = data + log.debug("Add to played_information: {0}", monitor.played_information) list_item.setPath(playurl) list_item = setListItemProps(id, list_item, result, server, listitem_props, item_title) @@ -142,6 +272,8 @@ def playFile(play_info): playlist.add(playurl, list_item) xbmc.Player().play(playlist) + send_next_episode_details(result) + if seekTime == 0: return @@ -156,18 +288,74 @@ def playFile(play_info): seekTime = seekTime - jump_back_amount - while xbmc.Player().getTime() < (seekTime - 5): + target_seek = (seekTime - 5) + current_position = 0 + while current_position < target_seek: # xbmc.Player().pause() xbmc.sleep(100) xbmc.Player().seekTime(seekTime) xbmc.sleep(100) # xbmc.Player().play() + current_position = xbmc.Player().getTime() + log.debug("Playback_Start_Seek target:{0} current:{1}", target_seek, current_position) + + +def send_next_episode_details(item): + + next_episode = get_next_episode(item) + + if next_episode is None: + log.debug("No next episode") + return + + gui_options = {} + gui_options["server"] = download_utils.getServer() + + gui_options["name_format"] = None + gui_options["name_format_type"] = "" + + item_details = extract_item_info(item, gui_options) + next_item_details = extract_item_info(next_episode, gui_options) + + current_item = {} + current_item["id"] = item_details.id + current_item["title"] = item_details.name + current_item["image"] = item_details.art.get('tvshow.poster', '') + current_item["thumb"] = item_details.art.get('thumb', '') + current_item["fanartimage"] = item_details.art.get('tvshow.fanart', '') + current_item["overview"] = item_details.plot + current_item["tvshowtitle"] = item_details.series_name + current_item["playcount"] = item_details.play_count + current_item["season"] = item_details.season_number + current_item["episode"] = item_details.episode_number + current_item["rating"] = item_details.critic_rating + current_item["year"] = item_details.year + + next_item = {} + next_item["id"] = next_item_details.id + next_item["title"] = next_item_details.name + next_item["image"] = next_item_details.art.get('tvshow.poster', '') + next_item["thumb"] = next_item_details.art.get('thumb', '') + next_item["fanartimage"] = next_item_details.art.get('tvshow.fanart', '') + next_item["overview"] = next_item_details.plot + next_item["tvshowtitle"] = next_item_details.series_name + next_item["playcount"] = next_item_details.play_count + next_item["season"] = next_item_details.season_number + next_item["episode"] = next_item_details.episode_number + next_item["rating"] = next_item_details.critic_rating + next_item["year"] = next_item_details.year + + next_info = { + "current_item": current_item, + "next_item": next_item + } + + log.debug("send_next_episode_details: {0}", next_info) + send_event_notification("embycon_next_episode", next_info) + def setListItemProps(id, listItem, result, server, extra_props, title): # set up item and item info - thumbID = id - eppNum = -1 - seasonNum = -1 art = getArt(result, server=server) listItem.setIconImage(art['thumb']) # back compat @@ -175,34 +363,69 @@ def setListItemProps(id, listItem, result, server, extra_props, title): listItem.setProperty('discart', art['discart']) # not avail to setArt listItem.setArt(art) - listItem.setProperty('IsPlayable', 'true') + listItem.setProperty('IsPlayable', 'false') listItem.setProperty('IsFolder', 'false') listItem.setProperty('id', result.get("Id")) for prop in extra_props: listItem.setProperty(prop[0], prop[1]) - # play info - details = { - 'title': title, - 'plot': result.get("Overview") - } + item_type = result.get("Type", "").lower() + mediatype = 'video' - if (eppNum > -1): - details["episode"] = str(eppNum) + if item_type == 'movie' or item_type == 'boxset': + mediatype = 'movie' + elif item_type == 'series': + mediatype = 'tvshow' + elif item_type == 'season': + mediatype = 'season' + elif item_type == 'episode': + mediatype = 'episode' + elif item_type == 'audio': + mediatype = 'song' - if (seasonNum > -1): - details["season"] = str(seasonNum) + if item_type == "audio": - listItem.setInfo("Video", infoLabels=details) + details = { + 'title': title, + 'mediatype': mediatype + } + listItem.setInfo("Music", infoLabels=details) + + else: + + details = { + 'title': title, + 'plot': result.get("Overview"), + 'mediatype': mediatype + } + + tv_show_name = result.get("SeriesName") + if tv_show_name is not None: + details['tvshowtitle'] = tv_show_name + + if item_type == "episode": + episode_number = result.get("IndexNumber", -1) + details["episode"] = str(episode_number) + season_number = result.get("ParentIndexNumber", -1) + details["season"] = str(season_number) + elif item_type == "season": + season_number = result.get("IndexNumber", -1) + details["season"] = str(season_number) + + details["plotoutline"] = "emby_id:" + id + #listItem.setUniqueIDs({'emby': id}) + + listItem.setInfo("Video", infoLabels=details) return listItem + # For transcoding only # Present the list of audio and subtitles to select from # for external streamable subtitles add the URL to the Kodi item and let Kodi handle it # else ask for the subtitles to be burnt in when transcoding -def audioSubsPref(url, list_item, emby_item): +def audioSubsPref(url, list_item, media_source, item_id, use_default): dialog = xbmcgui.Dialog() audioStreamsList = {} @@ -214,14 +437,12 @@ def audioSubsPref(url, list_item, emby_item): selectAudioIndex = "" selectSubsIndex = "" playurlprefs = "%s" % url + default_audio = media_source.get('DefaultAudioStreamIndex', 1) + default_sub = media_source.get('DefaultSubtitleStreamIndex', "") - try: - mediasources = emby_item['MediaSources'][0] - mediastreams = mediasources['MediaStreams'] - except (TypeError, KeyError, IndexError): - return + media_streams = media_source['MediaStreams'] - for stream in mediastreams: + for stream in media_streams: # Since Emby returns all possible tracks together, have to sort them. index = stream['Index'] @@ -258,7 +479,10 @@ def audioSubsPref(url, list_item, emby_item): subtitleStreamsList[track] = index subtitleStreams.append(track) - if len(audioStreams) > 1: + if use_default: + playurlprefs += "&AudioStreamIndex=%s" % default_audio + + elif len(audioStreams) > 1: resp = dialog.select(i18n('select_audio_stream'), audioStreams) if resp > -1: # User selected audio @@ -266,35 +490,38 @@ def audioSubsPref(url, list_item, emby_item): selectAudioIndex = audioStreamsList[selected] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] - else: # There's only one audiotrack. + playurlprefs += "&AudioStreamIndex=%s" % default_audio + + elif len(audioStreams) == 1: # There's only one audiotrack. selectAudioIndex = audioStreamsList[audioStreams[0]] playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex if len(subtitleStreams) > 1: - resp = dialog.select(i18n('select_subtitle'), subtitleStreams) - if resp == 0: - # User selected no subtitles - pass - elif resp > -1: - # User selected subtitles - selected = subtitleStreams[resp] - selectSubsIndex = subtitleStreamsList[selected] - - # Load subtitles in the listitem if downloadable - if selectSubsIndex in downloadableStreams: - - itemid = emby_item['Id'] - url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (downloadUtils.getServer(), itemid, itemid, selectSubsIndex))] - log.debug("Streaming subtitles url: %s %s" % (selectSubsIndex, url)) - list_item.setSubtitles(url) - else: - # Burn subtitles - playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex + if use_default: + playurlprefs += "&SubtitleStreamIndex=%s" % default_sub - else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") + else: + resp = dialog.select(i18n('select_subtitle'), subtitleStreams) + if resp == 0: + # User selected no subtitles + pass + elif resp > -1: + # User selected subtitles + selected = subtitleStreams[resp] + selectSubsIndex = subtitleStreamsList[selected] + + # Load subtitles in the listitem if downloadable + if selectSubsIndex in downloadableStreams: + url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (download_utils.getServer(), item_id, item_id, selectSubsIndex))] + log.debug("Streaming subtitles url: {0} {1}", selectSubsIndex, url) + list_item.setSubtitles(url) + else: + # Burn subtitles + playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex + + else: # User backed out of selection + playurlprefs += "&SubtitleStreamIndex=%s" % default_sub # Get number of channels for selected audio track audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) @@ -305,17 +532,14 @@ def audioSubsPref(url, list_item, emby_item): return playurlprefs + # direct stream, set any available subtitle streams -def externalSubs(emby_item, list_item): +def externalSubs(media_source, list_item, item_id): externalsubs = [] - itemid = emby_item['Id'] - try: - mediastreams = emby_item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return + media_streams = media_source['MediaStreams'] - for stream in mediastreams: + for stream in media_streams: if (stream['Type'] == "Subtitle" and stream['IsExternal'] @@ -324,8 +548,273 @@ def externalSubs(emby_item, list_item): index = stream['Index'] url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.%s" - % (downloadUtils.getServer(), itemid, itemid, index, stream['Codec'])) + % (download_utils.getServer(), item_id, item_id, index, stream['Codec'])) externalsubs.append(url) - list_item.setSubtitles(externalsubs) \ No newline at end of file + list_item.setSubtitles(externalsubs) + + +def sendProgress(monitor): + playing_file = xbmc.Player().getPlayingFile() + + ''' + video_tag_info = xbmc.Player().getVideoInfoTag() + plotoutline = video_tag_info.getPlotOutline() + log.debug("Player().getVideoInfoTag().getPlotOutline(): {0}", plotoutline) + emby_id = None + if plotoutline is not None and plotoutline.startswith("emby_id:"): + emby_id = plotoutline[8:] + log.debug("EmbyId: {0}", emby_id) + ''' + + play_data = monitor.played_information.get(playing_file) + + if play_data is None: + return + + log.debug("Sending Progress Update") + + play_time = xbmc.Player().getTime() + play_data["currentPossition"] = play_time + play_data["currently_playing"] = True + + item_id = play_data.get("item_id") + if item_id is None: + return + + ticks = int(play_time * 10000000) + paused = play_data.get("paused", False) + playback_type = play_data.get("playback_type") + play_session_id = play_data.get("play_session_id") + + postdata = { + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': item_id, + 'MediaSourceId': item_id, + 'PositionTicks': ticks, + 'IsPaused': paused, + 'IsMuted': False, + 'PlayMethod': playback_type, + 'PlaySessionId': play_session_id + } + + log.debug("Sending POST progress started: {0}", postdata) + + url = "{server}/emby/Sessions/Playing/Progress" + download_utils.downloadUrl(url, postBody=postdata, method="POST") + + +@catch_except() +def promptForStopActions(item_id, current_possition): + + settings = xbmcaddon.Addon(id='plugin.video.embycon') + + prompt_next_percentage = int(settings.getSetting('promptPlayNextEpisodePercentage')) + play_prompt = settings.getSetting('promptPlayNextEpisodePercentage_prompt') == "true" + prompt_delete_episode_percentage = int(settings.getSetting('promptDeleteEpisodePercentage')) + prompt_delete_movie_percentage = int(settings.getSetting('promptDeleteMoviePercentage')) + + # everything is off so return + if (prompt_next_percentage == 100 and + prompt_delete_episode_percentage == 100 and + prompt_delete_movie_percentage == 100): + return + + jsonData = download_utils.downloadUrl("{server}/emby/Users/{userid}/Items/" + item_id + "?format=json") + result = json.loads(jsonData) + + if result is None: + log.debug("promptForStopActions failed! no result from server.") + return + + prompt_to_delete = False + runtime = result.get("RunTimeTicks", 0) + + # if no runtime we cant calculate perceantge so just return + if runtime == 0: + log.debug("No runtime so returing") + return + + # item percentage complete + percenatge_complete = int(((current_possition * 10000000) / runtime) * 100) + log.debug("Episode Percentage Complete: {0}", percenatge_complete) + + if (prompt_delete_episode_percentage < 100 and + result.get("Type", "na") == "Episode" and + percenatge_complete > prompt_delete_episode_percentage): + prompt_to_delete = True + + if (prompt_delete_movie_percentage < 100 and + result.get("Type", "na") == "Movie" and + percenatge_complete > prompt_delete_movie_percentage): + prompt_to_delete = True + + if prompt_to_delete: + log.debug("Prompting for delete") + resp = xbmcgui.Dialog().yesno(i18n('confirm_file_delete'), i18n('file_delete_confirm'), autoclose=10000) + if resp: + log.debug("Deleting item: {0}", item_id) + url = "{server}/emby/Items/%s?format=json" % item_id + download_utils.downloadUrl(url, method="DELETE") + xbmc.executebuiltin("Container.Refresh") + + # prompt for next episode + if (prompt_next_percentage < 100 and + result.get("Type", "na") == "Episode" and + percenatge_complete > prompt_next_percentage): + + next_episode = get_next_episode(result) + + if next_episode is not None: + resp = True + index = next_episode.get("IndexNumber", -1) + if play_prompt: + next_epp_name = "%02d - %s" % (index, next_episode.get("Name", "n/a")) + resp = xbmcgui.Dialog().yesno(i18n("play_next_title"), i18n("play_next_question"), next_epp_name, autoclose=10000) + + if resp: + next_item_id = next_episode.get("Id") + log.debug("Playing Next Episode: {0}", next_item_id) + + play_info = {} + play_info["item_id"] = next_item_id + play_info["auto_resume"] = "-1" + play_info["force_transcode"] = False + send_event_notification("embycon_play_action", play_info) + + +@catch_except() +def stopAll(played_information): + if len(played_information) == 0: + return + + log.debug("played_information: {0}", played_information) + + for item_url in played_information: + data = played_information.get(item_url) + if data.get("currently_playing", False) is True: + log.debug("item_url: {0}", item_url) + log.debug("item_data: {0}", data) + + current_possition = data.get("currentPossition", 0) + emby_item_id = data.get("item_id") + + if emby_item_id is not None and len(emby_item_id) != 0 and emby_item_id != "None": + log.debug("Playback Stopped at: {0}", current_possition) + + url = "{server}/emby/Sessions/Playing/Stopped" + postdata = { + 'ItemId': emby_item_id, + 'MediaSourceId': emby_item_id, + 'PositionTicks': int(current_possition * 10000000) + } + download_utils.downloadUrl(url, postBody=postdata, method="POST") + data["currently_playing"] = False + + if data.get("play_action_type", "") == "play": + promptForStopActions(emby_item_id, current_possition) + + +class Service(xbmc.Player): + + def __init__(self, *args): + log.debug("Starting monitor service: {0}", args) + self.played_information = {} + + def onPlayBackStarted(self): + # Will be called when xbmc starts playing a file + stopAll(self.played_information) + + current_playing_file = xbmc.Player().getPlayingFile() + log.debug("onPlayBackStarted: {0}", current_playing_file) + log.debug("played_information: {0}", self.played_information) + + if current_playing_file not in self.played_information: + log.debug("This file was not started by EmbyCon") + return + + data = self.played_information[current_playing_file] + data["paused"] = False + data["currently_playing"] = True + + emby_item_id = data["item_id"] + playback_type = data["playback_type"] + play_session_id = data["play_session_id"] + + # if we could not find the ID of the current item then return + if emby_item_id is None or len(emby_item_id) == 0: + return + + log.debug("Sending Playback Started") + postdata = { + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': emby_item_id, + 'MediaSourceId': emby_item_id, + 'PlayMethod': playback_type, + 'PlaySessionId': play_session_id + } + + log.debug("Sending POST play started: {0}", postdata) + + url = "{server}/emby/Sessions/Playing" + download_utils.downloadUrl(url, postBody=postdata, method="POST") + + def onPlayBackEnded(self): + # Will be called when kodi stops playing a file + log.debug("EmbyCon Service -> onPlayBackEnded") + stopAll(self.played_information) + + def onPlayBackStopped(self): + # Will be called when user stops kodi playing a file + log.debug("onPlayBackStopped") + stopAll(self.played_information) + + def onPlayBackPaused(self): + # Will be called when kodi pauses the video + log.debug("onPlayBackPaused") + current_file = xbmc.Player().getPlayingFile() + play_data = self.played_information.get(current_file) + + if play_data is not None: + play_data['paused'] = True + sendProgress(self) + + def onPlayBackResumed(self): + # Will be called when kodi resumes the video + log.debug("onPlayBackResumed") + current_file = xbmc.Player().getPlayingFile() + play_data = self.played_information.get(current_file) + + if play_data is not None: + play_data['paused'] = False + sendProgress(self) + + def onPlayBackSeek(self, time, seekOffset): + # Will be called when kodi seeks in video + log.debug("onPlayBackSeek") + sendProgress(self) + + +class PlaybackService(xbmc.Monitor): + + def __init__(self, monitor): + self.monitor = monitor + + def onNotification(self, sender, method, data): + log.debug("PlaybackService:onNotification:{0}:{1}:{2}", sender, method, data) + if sender[-7:] != '.SIGNAL': + return + + signal = method.split('.', 1)[-1] + if signal != "embycon_play_action": + return + + data_json = json.loads(data) + hex_data = data_json[0] + log.debug("PlaybackService:onNotification:{0}", hex_data) + decoded_data = binascii.unhexlify(hex_data) + play_info = json.loads(decoded_data) + playFile(play_info, self.monitor) diff --git a/plugin.video.embycon/resources/lib/server_detect.py b/plugin.video.embycon/resources/lib/server_detect.py index 392a9df..a3fbf23 100644 --- a/plugin.video.embycon/resources/lib/server_detect.py +++ b/plugin.video.embycon/resources/lib/server_detect.py @@ -35,8 +35,8 @@ def getServerDetails(): sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - log.debug("MutliGroup : " + str(MULTI_GROUP)) - log.debug("Sending UDP Data : " + MESSAGE) + log.debug("MutliGroup: {0}", MULTI_GROUP) + log.debug("Sending UDP Data: {0}", MESSAGE) sock.sendto(MESSAGE, MULTI_GROUP) servers = [] @@ -46,10 +46,10 @@ def getServerDetails(): data, addr = sock.recvfrom(1024) # buffer size servers.append(json.loads(data)) except Exception as e: - log.error("Read UPD responce: %s" % e) + log.error("Read UPD responce: {0}", e) # break - log.debug("Found Servers: %s" % servers) + log.debug("Found Servers: {0}", servers) return servers @@ -83,14 +83,14 @@ def checkServer(force=False, change_user=False, notify=False): return serverUrl = serverInfo[return_index]["Address"] - log.debug("Selected server: " + serverUrl) + log.debug("Selected server: {0}", serverUrl) # parse the url url_bits = urlparse(serverUrl) server_address = url_bits.hostname server_port = str(url_bits.port) server_protocol = url_bits.scheme - log.debug("Detected server info " + server_protocol + " - " + server_address + " - " + server_port) + log.debug("Detected server info {0} - {1} - {2}", server_protocol, server_address, server_port) # save the server info settings.setSetting("port", server_port) @@ -107,6 +107,7 @@ def checkServer(force=False, change_user=False, notify=False): # we need to change the user current_username = settings.getSetting("username") + current_username = unicode(current_username, "utf-8") # if asked or we have no current user then show user selection screen if change_user or len(current_username) == 0: @@ -114,69 +115,71 @@ def checkServer(force=False, change_user=False, notify=False): log.debug("Getting user list") jsonData = downloadUtils.downloadUrl(serverUrl + "/emby/Users/Public?format=json", authenticate=False) - log.debug("jsonData : " + str(jsonData)) + log.debug("jsonData: {0}", jsonData) result = json.loads(jsonData) if result is None: - result = [] - - names = [] - user_list = [] - secured = [] - for user in result: - config = user.get("Configuration") - if (config != None): - if (config.get("IsHidden") is None) or (config.get("IsHidden") is False): - name = user.get("Name") - user_list.append(name) - if (user.get("HasPassword") is True): - secured.append(True) - name = i18n('username_secured') % name - else: - secured.append(False) - names.append(name) - - if (len(current_username) > 0) and (not any(n == current_username for n in user_list)): - names.insert(0, i18n('username_userdefined') % current_username) - user_list.insert(0, current_username) + xbmcgui.Dialog().ok(i18n('error'), + i18n('unable_connect_server'), + i18n('address:') + serverUrl) + else: + names = [] + user_list = [] + secured = [] + for user in result: + config = user.get("Configuration") + if (config != None): + if (config.get("IsHidden") is None) or (config.get("IsHidden") is False): + name = user.get("Name") + user_list.append(name) + if (user.get("HasPassword") is True): + secured.append(True) + name = i18n('username_secured') % name + else: + secured.append(False) + names.append(name) + + if (len(current_username) > 0) and (not any(n == current_username for n in user_list)): + names.insert(0, i18n('username_userdefined') % current_username) + user_list.insert(0, current_username) + secured.insert(0, True) + + names.insert(0, i18n('username_userinput')) + user_list.insert(0, '') secured.insert(0, True) + log.debug("User List: {0}", names) + log.debug("User List: {0}", user_list) - names.insert(0, i18n('username_userinput')) - user_list.insert(0, '') - secured.insert(0, True) - log.debug("User List : " + str(names)) - log.debug("User List : " + str(user_list)) - - return_value = xbmcgui.Dialog().select(i18n('select_user'), names) - - if (return_value > -1): - log.debug("Selected User Index : " + str(return_value)) - if return_value == 0: - kb = xbmc.Keyboard() - kb.setHeading(i18n('username:')) - kb.doModal() - if kb.isConfirmed(): - selected_user = kb.getText() - else: - selected_user = None - else: - selected_user = user_list[return_value] + return_value = xbmcgui.Dialog().select(i18n('select_user'), names) - log.debug("Selected User Name : " + str(selected_user)) - - if selected_user: - # we have a user so save it - log.debug("Saving Username : " + selected_user) - settings.setSetting("username", selected_user) - if secured[return_value] is True: + if (return_value > -1): + log.debug("Selected User Index: {0}", return_value) + if return_value == 0: kb = xbmc.Keyboard() - kb.setHeading(i18n('password:')) - kb.setHiddenInput(True) + kb.setHeading(i18n('username:')) kb.doModal() if kb.isConfirmed(): - log.debug("Saving Password for Username : " + selected_user) - settings.setSetting('password', kb.getText()) + selected_user = kb.getText() + else: + selected_user = None else: - settings.setSetting('password', '') + selected_user = user_list[return_value] + + log.debug("Selected User Name: {0}", selected_user) + + if selected_user: + # we have a user so save it + log.debug("Saving Username: {0}", selected_user) + settings.setSetting("username", selected_user) + if secured[return_value] is True: + kb = xbmc.Keyboard() + kb.setHeading(i18n('password:')) + kb.setHiddenInput(True) + kb.doModal() + if kb.isConfirmed(): + log.debug("Saving Password for Username: {0}", selected_user) + settings.setSetting('password', kb.getText()) + else: + settings.setSetting('password', '') home_window = HomeWindow() home_window.clearProperty("userid") diff --git a/plugin.video.embycon/resources/lib/server_sessions.py b/plugin.video.embycon/resources/lib/server_sessions.py index 85e7a9d..feb9efd 100644 --- a/plugin.video.embycon/resources/lib/server_sessions.py +++ b/plugin.video.embycon/resources/lib/server_sessions.py @@ -35,28 +35,27 @@ def showServerSessions(): if play_state is not None: runtime = 0 media_id = play_state.get("MediaSourceId", None) - log.debug("Media ID " + str(media_id)) + log.debug("Media ID: {0}", media_id) if media_id is not None: - jsonData = downloadUtils.downloadUrl("{server}/emby/Users/{userid}/Items/" + - media_id + "?format=json", - suppress=False, popup=1) + url = "{server}/emby/Users/{userid}/Items/" + media_id + "?format=json" + jsonData = downloadUtils.downloadUrl(url) media_info = json.loads(jsonData) - log.debug("Media Info " + str(media_info)) + log.debug("Media Info: {0}", media_info) runtime = media_info.get("RunTimeTicks", 0) - log.debug("Media Runtime " + str(runtime)) + log.debug("Media Runtime: {0}", runtime) position_ticks = play_state.get("PositionTicks", 0) - log.debug("Media PositionTicks " + str(position_ticks)) + log.debug("Media PositionTicks: {0}", position_ticks) if position_ticks > 0 and runtime > 0: percenatge_played = (position_ticks / float(runtime)) * 100.0 percenatge_played = int(percenatge_played) now_playing = session.get("NowPlayingItem", None) - log.debug("NOW_PLAYING: " + str(now_playing)) + log.debug("NOW_PLAYING: {0}", now_playing) if now_playing is not None: session_info += " (" + now_playing.get("Name", "na") + " " + str(percenatge_played) + "%)" - log.debug(session_info) + log.debug("session_info: {0}", session_info) list_item = xbmcgui.ListItem(label=session_info) item_tuple = ("", list_item, False) list_items.append(item_tuple) diff --git a/plugin.video.embycon/resources/lib/simple_logging.py b/plugin.video.embycon/resources/lib/simple_logging.py index 63ce6ac..05230d9 100644 --- a/plugin.video.embycon/resources/lib/simple_logging.py +++ b/plugin.video.embycon/resources/lib/simple_logging.py @@ -19,23 +19,30 @@ class SimpleLogging(): current_value = setting_result.get("result", None) if current_value is not None: self.enable_logging = current_value.get("value", False) - #xbmc.log("LOGGING_ENABLED %s: %s" % (self.name, str(self.enable_logging)), level=xbmc.LOGDEBUG) + xbmc.log("LOGGING_ENABLED %s : %s" % (self.name, str(self.enable_logging)), level=xbmc.LOGDEBUG) def __str__(self): return "LoggingEnabled: " + str(self.enable_logging) - def error(self, msg): - try: - xbmc.log(self.format(msg, "ERROR"), level=xbmc.LOGERROR) - except UnicodeEncodeError: - xbmc.log(self.format(msg, "ERROR").encode('utf-8'), level=xbmc.LOGERROR) - - def debug(self, msg): - if (self.enable_logging): - try: - xbmc.log(self.format(msg, "DEBUG"), level=xbmc.LOGDEBUG) - except UnicodeEncodeError: - xbmc.log(self.format(msg, "DEBUG").encode('utf-8'), level=xbmc.LOGDEBUG) - - def format(self, msg, levelValue): - return self.name + "(" + str(levelValue) + ") -> " + msg + def error(self, fmt, *args, **kwargs): + new_args = [] + # convert any unicode to utf-8 strings + for arg in args: + if isinstance(arg, unicode): + new_args.append(arg.encode("utf-8")) + else: + new_args.append(arg) + log_line = self.name + " (ERROR) -> " + fmt.format(*new_args) + xbmc.log(log_line, level=xbmc.LOGDEBUG) + + def debug(self, fmt, *args, **kwargs): + if self.enable_logging: + new_args = [] + # convert any unicode to utf-8 strings + for arg in args: + if isinstance(arg, unicode): + new_args.append(arg.encode("utf-8")) + else: + new_args.append(arg) + log_line = self.name + " (DEBUG) -> " + fmt.format(*new_args) + xbmc.log(log_line, level=xbmc.LOGDEBUG) diff --git a/plugin.video.embycon/resources/lib/trakttokodi.py b/plugin.video.embycon/resources/lib/trakttokodi.py new file mode 100644 index 0000000..3e27740 --- /dev/null +++ b/plugin.video.embycon/resources/lib/trakttokodi.py @@ -0,0 +1,248 @@ +# Gnu General Public License - see LICENSE.TXT + +import urllib +import encodings + +import xbmc +import xbmcgui + +from simple_logging import SimpleLogging +from datamanager import DataManager + +from translation import i18n + +log = SimpleLogging(__name__) +dataManager = DataManager() + +details_string = 'EpisodeCount,SeasonCount,Path,Etag,MediaStreams' +icon = xbmc.translatePath('special://home/addons/plugin.video.embycon/icon.png') + + +def not_found(content_string): + xbmcgui.Dialog().notification('EmbyCon', i18n('not_found_') % content_string, icon=icon, sound=False) + + +def playback_starting(content_string): + xbmcgui.Dialog().notification('EmbyCon', i18n('playback_starting_') % content_string, icon=icon, sound=False) + + +def search(item_type, query): + content_url = ('{server}/emby/Search/Hints?searchTerm=' + query + + '&IncludeItemTypes=' + item_type + + '&UserId={userid}' + '&StartIndex=0' + + '&Limit=25' + + '&IncludePeople=false&IncludeMedia=true&IncludeGenres=false&IncludeStudios=false&IncludeArtists=false') + + result = dataManager.GetContent(content_url) + return result + + +def get_items(video_type, item_id=None, parent_id=None): + content_url = None + result = dict() + + if video_type == 'season': + content_url = ('{server}/emby/Shows/' + item_id + + '/Seasons' + '?userId={userid}' + + '&Fields=' + details_string + + '&format=json') + + elif video_type == 'movie' or video_type == 'episode': + content_url = ('{server}/emby/Users/{userid}/items' + + '?ParentId=' + parent_id + + '&IsVirtualUnAired=false' + + '&IsMissing=false' + + '&Fields=' + details_string + + '&format=json') + + if content_url: + result = dataManager.GetContent(content_url) + + return result + + +def get_item(item_id): + result = dataManager.GetContent('{server}/emby/Users/{userid}/Items/' + item_id + '?Fields=ProviderIds&format=json') + return result + + +def get_imdb_id(item_id): + item = get_item(item_id) + imdb = item.get('ProviderIds', {}).get('Imdb') + return imdb + + +def get_season_id(parent_id, season): + season_items = get_items('season', parent_id) + season_items = season_items.get('Items') + + if season_items is None: + season_items = [] + + for season_item in season_items: + if season_item.get('IndexNumber') == int(season): + season_id = season_item.get('Id') + return season_id + + return None + + +def get_episode_id(parent_id, episode): + episode_items = get_items('episode', parent_id=parent_id) + episode_items = episode_items.get('Items') + + if episode_items is None: + episode_items = [] + + for episode_item in episode_items: + if episode_item.get('IndexNumber') == int(episode): + episode_id = episode_item.get('Id') + return episode_id + + return None + + +def get_match(item_type, title, year, imdb_id): + query = urllib.quote(title) + + results = search(item_type, query=query) + results = results.get('SearchHints') + if results is None: + results = [] + log.debug('SearchHints jsonData: {0}', results) + + potential_matches = [] + + for item in results: + name = item.get('Name') + production_year = item.get('ProductionYear') + if (name == title and int(year) == production_year) or (int(year) == production_year): + potential_matches.append(item) + + log.debug('Potential matches: {0}', potential_matches) + + for item in potential_matches: + item_imdb_id = get_imdb_id(item.get('ItemId')) + if item_imdb_id == imdb_id: + log.debug('Found match: {0}', item) + return item + + return None + + +def entry_point(parameters): + item_type = None + action = parameters.get('action', None) + video_type = parameters.get('video_type', None) + + title = urllib.unquote(parameters.get('title', '')) + + year = parameters.get('year', '') + episode = parameters.get('episode', '') + season = parameters.get('season', '') + imdb_id = parameters.get('imdb_id', '') + + if video_type == 'show' or video_type == 'season' or video_type == 'episode': + item_type = 'Series' + elif video_type == 'movie': + item_type = 'Movie' + + if not item_type: + return + + match = get_match(item_type, title, year, imdb_id) + + if not match: + title_search_word = '' + title_words = title.split(' ') + + for word in title_words: + if len(word) > len(title_search_word): + title_search_word = word + + title_search_word = title_search_word.replace(':', '') + + if title_search_word: + match = get_match(item_type, title_search_word, year, imdb_id) + + str_season = str(season) + if len(str_season) == 1: + str_season = '0' + str_season + str_episode = str(episode) + if len(str_episode) == 1: + str_episode = '0' + str_episode + + if action == 'play': + play_item_id = None + + if video_type == 'movie': + if match: + play_item_id = match.get('ItemId') + + if not play_item_id: + not_found('{title} ({year})'.format(title=title, year=year)) + + elif video_type == 'episode': + if not season or not episode: + return + + if match: + item_id = match.get('ItemId') + season_id = get_season_id(item_id, season) + + if season_id: + episode_id = get_episode_id(season_id, episode) + if episode_id: + play_item_id = episode_id + + if not play_item_id: + not_found('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode)) + + if play_item_id: + if video_type == 'episode': + playback_starting('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode)) + else: + playback_starting('{title} ({year})'.format(title=title, year=year)) + xbmc.executebuiltin('RunPlugin(plugin://plugin.video.embycon/?mode=PLAY&item_id={item_id})'.format(item_id=play_item_id)) + + elif action == 'open': + url = media_type = None + + if video_type == 'show': + if match: + item_id = match.get('ItemId') + media_type = 'series' + url = ('{server}/emby/Shows/' + item_id + + '/Seasons' + '?userId={userid}' + + '&Fields=' + details_string + + '&format=json') + + if not url: + not_found('{title} ({year})'.format(title=title, year=year)) + + elif video_type == 'season': + if not season: + return + + if match: + item_id = match.get('ItemId') + season_id = get_season_id(item_id, season) + + if season_id: + media_type = 'episodes' + + url = ('{server}/emby/Users/{userid}/items' + + '?ParentId=' + season_id + + '&IsVirtualUnAired=false' + + '&IsMissing=false' + + '&Fields=' + details_string + + '&format=json') + + if not url: + not_found('{title} ({year}) - S{season}'.format(title=title, year=year, season=str_season)) + + if url and media_type: + xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.embycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=urllib.quote(url), media_type=media_type)) diff --git a/plugin.video.embycon/resources/lib/translation.py b/plugin.video.embycon/resources/lib/translation.py index 4df1142..58b0c7e 100644 --- a/plugin.video.embycon/resources/lib/translation.py +++ b/plugin.video.embycon/resources/lib/translation.py @@ -10,7 +10,7 @@ def i18n(string_id): try: return addon.getLocalizedString(STRINGS[string_id]).encode('utf-8', 'ignore') except Exception as e: - log.error('Failed String Lookup: %s (%s)' % (string_id, e)) + log.error('Failed String Lookup: {0} ({1})', string_id, e) return string_id @@ -61,6 +61,7 @@ STRINGS = { 'movies_az': 30252, 'change_user': 30253, 'show_settings': 30254, + 'movies_year': 30255, 'movies_all': 30256, 'movies_recently_added': 30257, 'movies_in_progress': 30258, @@ -111,5 +112,19 @@ STRINGS = { 'caching_textures': 30301, 'existing_textures': 30302, 'missing_textures': 30303, - 'loaded_textures': 30304 + 'loaded_textures': 30304, + 'not_found_': 30305, + 'playback_starting_': 30306, + 'play_trailer': 30307, + 'select_trailer': 30308, + 'select_source': 30309, + 'embycon_error': 30311, + 'embycon_error_submit': 30312, + 'connection_error': 30316, + 'play_all': 30317, + 'music_all_albums': 30318, + 'music_all_artists': 30319, + '_all_albums': 30320, + '_all_artists': 30321 + } diff --git a/plugin.video.embycon/resources/lib/utils.py b/plugin.video.embycon/resources/lib/utils.py index b91aa59..945ad7e 100644 --- a/plugin.video.embycon/resources/lib/utils.py +++ b/plugin.video.embycon/resources/lib/utils.py @@ -12,12 +12,14 @@ import json import httplib import base64 import sys +import binascii from downloadutils import DownloadUtils from simple_logging import SimpleLogging from clientinfo import ClientInformation from json_rpc import json_rpc from translation import i18n +from datamanager import DataManager # define our global download utils downloadUtils = DownloadUtils() @@ -26,19 +28,21 @@ log = SimpleLogging(__name__) ########################################################################### class PlayUtils(): - def getPlayUrl(self, id, result, force_transcode, play_session_id): + def getPlayUrl(self, id, media_source, force_transcode, play_session_id): log.debug("getPlayUrl") addonSettings = xbmcaddon.Addon(id='plugin.video.embycon') playback_type = addonSettings.getSetting("playback_type") server = downloadUtils.getServer() - log.debug("playback_type: " + playback_type) + log.debug("playback_type: {0}", playback_type) if force_transcode: log.debug("playback_type: FORCED_TRANSCODE") playurl = None - log.debug("play_session_id: " + play_session_id) + log.debug("play_session_id: {0}", play_session_id) + media_source_id = media_source.get("Id") + log.debug("media_source_id: {0}", media_source_id) is_h265 = False - streams = result.get("MediaStreams", []) + streams = media_source.get("MediaStreams", []) for stream in streams: if stream.get("Type", "") == "Video" and stream.get("Codec", "") in ["hevc", "h265"]: is_h265 = True @@ -60,7 +64,7 @@ class PlayUtils(): if playback_type == "2": playback_bitrate = addonSettings.getSetting("playback_bitrate") - log.debug("playback_bitrate: " + playback_bitrate) + log.debug("playback_bitrate: {0}", playback_bitrate) playback_max_width = addonSettings.getSetting("playback_max_width") playback_video_force_8 = addonSettings.getSetting("playback_video_force_8") == "true" @@ -70,25 +74,28 @@ class PlayUtils(): bitrate = int(playback_bitrate) * 1000 user_token = downloadUtils.authenticate() - playurl = ( - "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s&PlaySessionId=%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" - % (server, id, id, play_session_id, deviceId, bitrate)) - - playurl = playurl + "&maxWidth=" + playback_max_width - + playurl = ("%s/emby/Videos/%s/master.m3u8" + + "?MediaSourceId=%s" + + "&PlaySessionId=%s" + + "&VideoCodec=h264" + + "&AudioCodec=ac3" + + "&MaxAudioChannels=6" + + "&deviceId=%s" + + "&VideoBitrate=%s" + + "&maxWidth=%s") + playurl = playurl % (server, id, media_source_id, play_session_id, deviceId, bitrate, playback_max_width) if playback_video_force_8: playurl = playurl + "&MaxVideoBitDepth=8" - playurl = playurl + "&api_key=" + user_token # do direct path playback elif playback_type == "0": - playurl = result.get("Path") + playurl = media_source.get("Path") # handle DVD structure - if (result.get("VideoType") == "Dvd"): + if (media_source.get("VideoType") == "Dvd"): playurl = playurl + "/VIDEO_TS/VIDEO_TS.IFO" - elif (result.get("VideoType") == "BluRay"): + elif (media_source.get("VideoType") == "BluRay"): playurl = playurl + "/BDMV/index.bdmv" smb_username = addonSettings.getSetting('smbusername') @@ -104,59 +111,53 @@ class PlayUtils(): # do direct http streaming playback elif playback_type == "1": - playurl = "%s/emby/Videos/%s/stream?static=true&PlaySessionId=%s" % (server, id, play_session_id) + playurl = ("%s/emby/Videos/%s/stream" + + "?static=true" + + "&PlaySessionId=%s" + + "&MediaSourceId=%s") + playurl = playurl % (server, id, play_session_id, media_source_id) user_token = downloadUtils.authenticate() playurl = playurl + "&api_key=" + user_token - log.debug("Playback URL: " + playurl) - return playurl.encode('utf-8'), playback_type + log.debug("Playback URL: {0}", playurl) + return playurl, playback_type - def getStrmDetails(self, result): + def getStrmDetails(self, media_source): playurl = None listitem_props = [] - source = result['MediaSources'][0] - contents = source.get('Path').encode('utf-8') # contains contents of strm file with linebreaks + contents = media_source.get('Path').encode('utf-8') # contains contents of strm file with linebreaks line_break = '\r' if '\r\n' in contents: - line_break += '\n' + line_break = '\r\n' + elif '\n' in contents: + line_break = '\n' lines = contents.split(line_break) for line in lines: line = line.strip() + log.debug("STRM Line: {0}", line) if line.startswith('#KODIPROP:'): match = re.search('#KODIPROP:(?P[^=]+?)=(?P.+)', line) if match: - listitem_props.append((match.group('item_property'), match.group('property_value'))) + item_property = match.group('item_property') + property_value = match.group('property_value') + log.debug("STRM property found: {0} value: {1}", item_property, property_value) + listitem_props.append((item_property, property_value)) + else: + log.debug("STRM #KODIPROP incorrect format") + elif line.startswith('#'): + # unrecognized, treat as comment + log.debug("STRM unrecognized line identifier, ignored") elif line != '': playurl = line + log.debug("STRM playback url found") - log.debug("Playback URL: " + playurl + " ListItem Properties: " + str(listitem_props)) + log.debug("Playback URL: {0} ListItem Properties: {1}", playurl, listitem_props) return playurl, listitem_props -def getDetailsString(): - - addonSettings = xbmcaddon.Addon(id='plugin.video.embycon') - include_media = addonSettings.getSetting("include_media") == "true" - include_people = addonSettings.getSetting("include_people") == "true" - include_overview = addonSettings.getSetting("include_overview") == "true" - - detailsString = "DateCreated,EpisodeCount,SeasonCount,Path,Genres,Studios,CumulativeRunTimeTicks,Etag" - - if include_media: - detailsString += ",MediaStreams" - - if include_people: - detailsString += ",People" - - if include_overview: - detailsString += ",Overview" - - return detailsString - - def getChecksum(item): userdata = item['UserData'] checksum = "%s_%s_%s_%s_%s_%s_%s" % ( @@ -182,36 +183,43 @@ def getArt(item, server, widget=False): 'clearart': '', 'discart': '', 'landscape': '', + 'tvshow.fanart': '', 'tvshow.poster': '', 'tvshow.clearart': '', + 'tvshow.clearlogo': '', 'tvshow.banner': '', 'tvshow.landscape': '' } - item_id = item.get("Id") + item_id = item["Id"] image_id = item_id - imageTags = item.get("ImageTags") - if (imageTags is not None) and (imageTags.get("Primary") is not None): - image_tag = imageTags.get("Primary") + imageTags = item["ImageTags"] + if imageTags is not None and imageTags["Primary"] is not None: + image_tag = imageTags["Primary"] if widget: art['thumb'] = downloadUtils.imageUrl(image_id, "Primary", 0, 400, 400, image_tag, server=server) else: art['thumb'] = downloadUtils.getArtwork(item, "Primary", server=server) - if item.get("Type") == "Episode" or item.get("Type") == "Season": + item_type = item["Type"] + + if item_type == "Episode" or item_type == "Season": art['tvshow.poster'] = downloadUtils.getArtwork(item, "Primary", parent=True, server=server) - art['tvshow.clearart'] = downloadUtils.getArtwork(item, "Logo", parent=True, server=server) + art['tvshow.clearart'] = downloadUtils.getArtwork(item, "Art", parent=True, server=server) + art['tvshow.clearlogo'] = downloadUtils.getArtwork(item, "Logo", parent=True, server=server) art['tvshow.banner'] = downloadUtils.getArtwork(item, "Banner", parent=True, server=server) art['tvshow.landscape'] = downloadUtils.getArtwork(item, "Thumb", parent=True, server=server) - elif item.get("Type") == "Series": + art['tvshow.fanart'] = downloadUtils.getArtwork(item, "Backdrop", parent=True, server=server) + elif item_type == "Series": art['tvshow.poster'] = downloadUtils.getArtwork(item, "Primary", parent=False, server=server) - art['tvshow.clearart'] = downloadUtils.getArtwork(item, "Logo", parent=False, server=server) + art['tvshow.clearart'] = downloadUtils.getArtwork(item, "Art", parent=False, server=server) + art['tvshow.clearlogo'] = downloadUtils.getArtwork(item, "Logo", parent=False, server=server) art['tvshow.banner'] = downloadUtils.getArtwork(item, "Banner", parent=False, server=server) art['tvshow.landscape'] = downloadUtils.getArtwork(item, "Thumb", parent=False, server=server) + art['tvshow.fanart'] = downloadUtils.getArtwork(item, "Backdrop", parent=False, server=server) - if item.get("Type") == "Episode": - art['thumb'] = art['thumb'] if art['thumb'] else downloadUtils.getArtwork(item, "Thumb", server=server) - art['landscape'] = art['thumb'] if art['thumb'] else downloadUtils.getArtwork(item, "Thumb", parent=True, server=server) + if item_type == "Episode": + art['landscape'] = downloadUtils.getArtwork(item, "Thumb", parent=True, server=server) else: art['poster'] = art['thumb'] @@ -256,13 +264,13 @@ def cache_artwork(): web_port = {"setting": "services.webserverport"} result = json_rpc('Settings.GetSettingValue').execute(web_port) xbmc_port = result['result']['value'] - log.debug("xbmc_port: " + str(xbmc_port)) + log.debug("xbmc_port: {0}", xbmc_port) # get the user web_user = {"setting": "services.webserverusername"} result = json_rpc('Settings.GetSettingValue').execute(web_user) xbmc_username = result['result']['value'] - log.debug("xbmc_username: " + str(xbmc_username)) + log.debug("xbmc_username: {0}", xbmc_username) # get the password web_pass = {"setting": "services.webserverpassword"} @@ -278,7 +286,7 @@ def cache_artwork(): json_result = json_rpc('Textures.GetTextures').execute() textures = json_result.get("result", {}).get("textures", []) - log.debug("texture ids: " + str(textures)) + log.debug("texture ids: {0}", textures) total = len(textures) for texture in textures: texture_id = texture["textureid"] @@ -320,11 +328,10 @@ def cache_artwork(): '&ImageTypeLimit=1' + '&format=json') - results = downloadUtils.downloadUrl(url, method="GET") + data_manager = DataManager() + results = data_manager.GetContent(url) if results is None: results = [] - else: - results = json.loads(results) if isinstance(results, dict): results = results.get("Items") @@ -340,10 +347,10 @@ def cache_artwork(): if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0: missing_texture_urls.add(image_url) - log.debug("texture_urls:" + str(texture_urls)) - log.debug("missing_texture_urls: " + str(missing_texture_urls)) - log.debug("Number of existing textures: %s" % len(texture_urls)) - log.debug("Number of missing textures: %s" % len(missing_texture_urls)) + log.debug("texture_urls: {0}", texture_urls) + log.debug("missing_texture_urls: {0}", missing_texture_urls) + log.debug("Number of existing textures: {0}", len(texture_urls)) + log.debug("Number of missing textures: {0}", len(missing_texture_urls)) kodi_http_server = "localhost:" + str(xbmc_port) headers = {} @@ -358,10 +365,10 @@ def cache_artwork(): count_done = 0 for get_url in missing_texture_urls: - log.debug("texture_url:" + get_url) + log.debug("texture_url: {0}", get_url) url = double_urlencode(get_url) kodi_texture_url = ("/image/image://%s" % url) - log.debug("kodi_texture_url: " + kodi_texture_url) + log.debug("kodi_texture_url: {0}", kodi_texture_url) percentage = int((float(index) / float(total)) * 100) message = "%s of %s" % (index, total) @@ -372,7 +379,7 @@ def cache_artwork(): data = conn.getresponse() if data.status == 200: count_done += 1 - log.debug("Get Image Result: " + str(data.status)) + log.debug("Get Image Result: {0}", data.status) index += 1 if pdialog.iscanceled(): @@ -398,3 +405,13 @@ def single_urlencode(text): text = urllib.urlencode({'blahblahblah': text.encode('utf-8')}) text = text[13:] return text.decode('utf-8') #return the result again as unicode + + +def send_event_notification(method, data): + next_data = json.dumps(data) + source_id = "embycon" + data = '\\"[\\"{0}\\"]\\"'.format(binascii.hexlify(next_data)) + command = 'XBMC.NotifyAll({0}.SIGNAL,{1},{2})'.format(source_id, method, data) + log.debug("Sending notification event data: {0}", command) + xbmc.executebuiltin(command) + diff --git a/plugin.video.embycon/resources/lib/websocket.py b/plugin.video.embycon/resources/lib/websocket.py new file mode 100644 index 0000000..a533652 --- /dev/null +++ b/plugin.video.embycon/resources/lib/websocket.py @@ -0,0 +1,930 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + + +import socket + +try: + import ssl + from ssl import SSLError + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + HAVE_SSL = False + +from urlparse import urlparse +import os +import array +import struct +import uuid +import hashlib +import base64 +import threading +import time +import logging +import traceback +import sys + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +# websocket supported version. +VERSION = 13 + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +logger = logging.getLogger() + + +class WebSocketException(Exception): + """ + websocket exeception class. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + +default_timeout = None +traceEnabled = False + + +def enableTrace(tracable): + """ + turn on/off the tracability. + + tracable: boolean value. if set True, tracability is enabled. + """ + global traceEnabled + traceEnabled = tracable + if tracable: + if not logger.handlers: + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global default_timeout + default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return default_timeout + + +def _wrap_sni_socket(sock, sslopt, hostname): + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + capath = ssl.get_default_verify_paths().capath + context.load_verify_locations(cafile=sslopt.get('ca_certs', None), + capath=sslopt.get('ca_cert_path', capath)) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="http") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return (hostname, port, resource, is_secure) + + +def create_connection(url, timeout=None, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, the custom HTTP headers are added. + """ + sockopt = options.get("sockopt", []) + sslopt = options.get("sslopt", {}) + websock = WebSocket(sockopt=sockopt, sslopt=sslopt) + websock.settimeout(timeout if timeout is not None else default_timeout) + websock.connect(url, **options) + return websock + +_MAX_INTEGER = (1 << 32) -1 +_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) +_MAX_CHAR_BYTE = (1<<8) -1 + +# ref. Websocket gets an update, and it breaks stuff. +# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html + + +def _create_sec_websocket_key(): + uid = uuid.uuid4() + return base64.encodestring(uid.bytes).strip() + + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", + } + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threashold. + LENGTH_7 = 0x7d + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask_value = mask + self.data = data + self.get_mask_key = os.urandom + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is uniocde, + data value is conveted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(1, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask_value << 7 | length) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask_value << 7 | 0x7e) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask_value << 7 | 0x7f) + frame_header += struct.pack("!Q", length) + + if not self.mask_value: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + return mask_key + "".join(s) + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + _m = array.array("B", mask_key) + _d = array.array("B", data) + for i in xrange(len(_d)): + _d[i] ^= _m[i % 4] + return _d.tostring() + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/recieve data. + The following example is a echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: dict object for ssl socket option. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None): + """ + Initalize WebSocket object. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.connected = False + self.sock = socket.socket() + for opts in sockopt: + self.sock.setsockopt(*opts) + self.sslopt = sslopt + self.get_mask_key = get_mask_key + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self._recv_buffer = [] + # These buffer over the build-up of a single frame. + self._frame_header = None + self._frame_length = None + self._frame_mask = None + self._cont_data = None + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can custumize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the fuct must 1 argument as integer. + The argument means length of mask key. + This func must be return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock.gettimeout() + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" dict object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header={"User-Agent: MyProgram", + ... "x-custom: header"}) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, + the custom HTTP headers are added. + + """ + hostname, port, resource, is_secure = _parse_url(url) + # TODO: we need to support proxy + self.sock.connect((hostname, port)) + if is_secure: + if HAVE_SSL: + if self.sslopt is None: + sslopt = {} + else: + sslopt = self.sslopt + if ssl.HAS_SNI: + self.sock = _wrap_sni_socket(self.sock, sslopt, hostname) + else: + self.sock = ssl.wrap_socket(self.sock, **sslopt) + else: + raise WebSocketException("SSL not available.") + + self._handshake(hostname, port, resource, **options) + + def _handshake(self, host, port, resource, **options): + headers = [] + headers.append("GET %s HTTP/1.1" % resource) + headers.append("Upgrade: websocket") + headers.append("Connection: Upgrade") + if port == 80: + hostport = host + else: + hostport = "%s:%d" % (host, port) + headers.append("Host: %s" % hostport) + + if "origin" in options: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Version: %s" % VERSION) + if "header" in options: + headers.extend(options["header"]) + + headers.append("") + headers.append("") + + header_str = "\r\n".join(headers) + self._send(header_str) + if traceEnabled: + logger.debug("--- request header ---") + logger.debug(header_str) + logger.debug("-----------------------") + + status, resp_headers = self._read_headers() + if status != 101: + self.close() + raise WebSocketException("Handshake Status %d" % status) + + success = self._validate_header(resp_headers, key) + if not success: + self.close() + raise WebSocketException("Invalid WebSocket Header") + + self.connected = True + + def _validate_header(self, headers, key): + for k, v in _HEADERS_TO_CHECK.iteritems(): + r = headers.get(k, None) + if not r: + return False + r = r.lower() + if v != r: + return False + + result = headers.get("sec-websocket-accept", None) + if not result: + return False + result = result.lower() + + value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower() + return hashed == result + + def _read_headers(self): + status = None + headers = {} + if traceEnabled: + logger.debug("--- response header ---") + + while True: + line = self._recv_line() + if line == "\r\n": + break + line = line.strip() + if traceEnabled: + logger.debug(line) + if not status: + status_info = line.split(" ", 2) + status = int(status_info[1]) + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip().lower() + else: + raise WebSocketException("Invalid header") + + if traceEnabled: + logger.debug("-----------------------") + + return status, headers + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicoce, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + frame = ABNF.create_frame(payload, opcode) + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + if traceEnabled: + logger.debug("send: " + repr(data)) + while data: + l = self._send(data) + data = data[l:] + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + opcode, data = self.recv_data() + return data + + def recv_data(self): + """ + Recieve data with operation code. + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketException("Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data: + raise WebSocketException("Illegal frame") + if self._cont_data: + self._cont_data[1] += frame.data + else: + self._cont_data = [frame.opcode, frame.data] + + if frame.fin: + data = self._cont_data + self._cont_data = None + return data + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return (frame.opcode, None) + elif frame.opcode == ABNF.OPCODE_PING: + self.pong(frame.data) + + def recv_frame(self): + """ + recieve data as frame from server. + + return value: ABNF frame object. + """ + # Header + if self._frame_header is None: + self._frame_header = self._recv_strict(2) + b1 = ord(self._frame_header[0]) + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = ord(self._frame_header[1]) + has_mask = b2 >> 7 & 1 + # Frame length + if self._frame_length is None: + length_bits = b2 & 0x7f + if length_bits == 0x7e: + length_data = self._recv_strict(2) + self._frame_length = struct.unpack("!H", length_data)[0] + elif length_bits == 0x7f: + length_data = self._recv_strict(8) + self._frame_length = struct.unpack("!Q", length_data)[0] + else: + self._frame_length = length_bits + # Mask + if self._frame_mask is None: + self._frame_mask = self._recv_strict(4) if has_mask else "" + # Payload + payload = self._recv_strict(self._frame_length) + if has_mask: + payload = ABNF.mask(self._frame_mask, payload) + # Reset for next frame + self._frame_header = None + self._frame_length = None + self._frame_mask = None + return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + + + def send_close(self, status=STATUS_NORMAL, reason=""): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=""): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + + try: + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + ''' + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + timeout = self.sock.gettimeout() + self.sock.settimeout(3) + try: + frame = self.recv_frame() + if logger.isEnabledFor(logging.ERROR): + recv_status = struct.unpack("!H", frame.data)[0] + if recv_status != STATUS_NORMAL: + logger.error("close status: " + repr(recv_status)) + except: + pass + self.sock.settimeout(timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + ''' + self._closeInternal() + + def _closeInternal(self): + self.connected = False + self.sock.close() + + def _send(self, data): + try: + return self.sock.send(data) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except Exception as e: + if "timed out" in e.args[0]: + raise WebSocketTimeoutException(e.args[0]) + else: + raise e + + def _recv(self, bufsize): + try: + bytes = self.sock.recv(bufsize) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except SSLError as e: + if e.args[0] == "The read operation timed out": + raise WebSocketTimeoutException(e.args[0]) + else: + raise + if not bytes: + raise WebSocketConnectionClosedException() + return bytes + + + def _recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self._recv_buffer) + while shortage > 0: + bytes = self._recv(shortage) + self._recv_buffer.append(bytes) + shortage -= len(bytes) + unified = "".join(self._recv_buffer) + if shortage == 0: + self._recv_buffer = [] + return unified + else: + self._recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + + def _recv_line(self): + line = [] + while True: + c = self._recv(1) + line.append(c) + if c == "\n": + break + return "".join(line) + + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + def __init__(self, url, header=[], + on_open=None, on_message=None, on_error=None, + on_close=None, keep_running=True, get_mask_key=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The arugment is this class object. + on_message: callbale object which is called when recieved data. + on_message has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The arugment is this class object. + keep_running: a boolean flag indicating whether the app's main loop should + keep running, defaults to True + get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's + docstring for more information + """ + self.url = url + self.header = header + self.on_open = on_open + self.on_message = on_message + self.on_error = on_error + self.on_close = on_close + self.keep_running = keep_running + self.get_mask_key = get_mask_key + self.sock = None + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + if self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException() + + def close(self): + """ + close websocket connection. + """ + self.keep_running = False + if(self.sock != None): + self.sock.close() + + def _send_ping(self, interval): + while True: + for i in range(interval): + time.sleep(1) + if not self.keep_running: + return + self.sock.ping() + + def run_forever(self, sockopt=None, sslopt=None, ping_interval=0): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command every specified period(second) + if set to 0, not send automatically. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + self.keep_running = True + + try: + self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt) + self.sock.settimeout(default_timeout) + self.sock.connect(self.url, header=self.header) + self._callback(self.on_open) + + if ping_interval: + thread = threading.Thread(target=self._send_ping, args=(ping_interval,)) + thread.setDaemon(True) + thread.start() + + while self.keep_running: + + try: + data = self.sock.recv() + + if data is None or self.keep_running == False: + break + self._callback(self.on_message, data) + + except Exception, e: + #print str(e.args[0]) + if "timed out" not in e.args[0]: + raise e + + except Exception, e: + self._callback(self.on_error, e) + finally: + if thread: + self.keep_running = False + self.sock.close() + self._callback(self.on_close) + self.sock = None + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + except Exception, e: + logger.error(e) + if True:#logger.isEnabledFor(logging.DEBUG): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) + + +if __name__ == "__main__": + enableTrace(True) + ws = create_connection("ws://echo.websocket.org/") + print("Sending 'Hello, World'...") + ws.send("Hello, World") + print("Sent") + print("Receiving...") + result = ws.recv() + print("Received '%s'" % result) + ws.close() diff --git a/plugin.video.embycon/resources/lib/websocket_client.py b/plugin.video.embycon/resources/lib/websocket_client.py new file mode 100644 index 0000000..db0a073 --- /dev/null +++ b/plugin.video.embycon/resources/lib/websocket_client.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import threading +import websocket + +import xbmc +import xbmcgui + +from functions import PLAY +from simple_logging import SimpleLogging +import clientinfo +import downloadutils +from json_rpc import json_rpc + +log = SimpleLogging(__name__) + +class WebSocketClient(threading.Thread): + + _shared_state = {} + + _client = None + _stop_websocket = False + + def __init__(self): + + self.__dict__ = self._shared_state + self.monitor = xbmc.Monitor() + + self.client_info = clientinfo.ClientInformation() + self.device_id = self.client_info.getDeviceId() + + threading.Thread.__init__(self) + + def on_message(self, ws, message): + + result = json.loads(message) + message_type = result['MessageType'] + + if message_type == 'Play': + data = result['Data'] + self._play(data) + + elif message_type == 'Playstate': + data = result['Data'] + self._playstate(data) + + elif message_type == "UserDataChanged": + log.debug("WebSocket Message UserDataChanged: {0}", message) + + elif message_type == "LibraryChanged": + log.debug("WebSocket Message LibraryChanged: {0}", message) + + elif message_type == "GeneralCommand": + data = result['Data'] + self._general_commands(data) + + else: + log.debug("WebSocket Message Type: {0}", message) + + def _play(cls, data): + + item_ids = data['ItemIds'] + command = data['PlayCommand'] + + if command == 'PlayNow': + startat = data.get('StartPositionTicks', 0) + log.debug("WebSocket Message PlayNow: {0}", data) + + media_source_id = data.get("MediaSourceId", "") + + params = {} + params["item_id"] = item_ids[0] + params["auto_resume"] = str(startat) + params["media_source_id"] = media_source_id + params["use_default"] = "true" + PLAY(params) + + + def _playstate(cls, data): + + command = data['Command'] + player = xbmc.Player() + + actions = { + + 'Stop': player.stop, + 'Unpause': player.pause, + 'Pause': player.pause, + 'PlayPause': player.pause, + 'NextTrack': player.playnext, + 'PreviousTrack': player.playprevious + } + if command == 'Seek': + + if player.isPlaying(): + seek_to = data['SeekPositionTicks'] + seek_time = seek_to / 10000000.0 + player.seekTime(seek_time) + log.debug("Seek to {0}", seek_time) + + elif command in actions: + actions[command]() + log.debug("Command: {0} completed", command) + + else: + log.debug("Unknown command: {0}", command) + return + + def _general_commands(cls, data): + + command = data['Name'] + arguments = data['Arguments'] + + if command in ('Mute', + 'Unmute', + 'SetVolume', + 'SetSubtitleStreamIndex', + 'SetAudioStreamIndex', + 'SetRepeatMode'): + + player = xbmc.Player() + # These commands need to be reported back + if command == 'Mute': + xbmc.executebuiltin('Mute') + + elif command == 'Unmute': + xbmc.executebuiltin('Mute') + + elif command == 'SetVolume': + volume = arguments['Volume'] + xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) + + elif command == 'SetAudioStreamIndex': + index = int(arguments['Index']) + player.setAudioStream(index - 1) + + elif command == 'SetRepeatMode': + mode = arguments['RepeatMode'] + xbmc.executebuiltin('xbmc.PlayerControl(%s)' % mode) + + elif command == 'DisplayMessage': + + header = arguments['Header'] + text = arguments['Text'] + # show notification here + log.debug("WebSocket DisplayMessage: {0}", text) + xbmcgui.Dialog().notification("EmbyCon", text) + + elif command == 'SendString': + + params = { + + 'text': arguments['String'], + 'done': False + } + json_rpc('Input.SendText').execute(params) + + elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): + # Commands that should wake up display + actions = { + + 'MoveUp': "Input.Up", + 'MoveDown': "Input.Down", + 'MoveRight': "Input.Right", + 'MoveLeft': "Input.Left" + } + json_rpc(actions[command]).execute() + + elif command == 'GoHome': + json_rpc('GUI.ActivateWindow').execute({'window': "home"}) + + elif command == "Guide": + json_rpc('GUI.ActivateWindow').execute({'window': "tvguide"}) + + else: + builtin = { + + 'ToggleFullscreen': 'Action(FullScreen)', + 'ToggleOsdMenu': 'Action(OSD)', + 'ToggleContextMenu': 'Action(ContextMenu)', + 'Select': 'Action(Select)', + 'Back': 'Action(back)', + 'PageUp': 'Action(PageUp)', + 'NextLetter': 'Action(NextLetter)', + 'GoToSearch': 'VideoLibrary.Search', + 'GoToSettings': 'ActivateWindow(Settings)', + 'PageDown': 'Action(PageDown)', + 'PreviousLetter': 'Action(PrevLetter)', + 'TakeScreenshot': 'TakeScreenshot', + 'ToggleMute': 'Mute', + 'VolumeUp': 'Action(VolumeUp)', + 'VolumeDown': 'Action(VolumeDown)', + } + if command in builtin: + xbmc.executebuiltin(builtin[command]) + + def on_close(self, ws): + log.debug("closed") + + def on_open(self, ws): + self.post_capabilities() + + def on_error(self, ws, error): + log.debug("Error: {0}", error) + + def run(self): + + # websocket.enableTrace(True) + download_utils = downloadutils.DownloadUtils() + + token = None + while token is None or token == "": + token = download_utils.authenticate() + if self.monitor.waitForAbort(10): + return + + # Get the appropriate prefix for the websocket + server = download_utils.getServer() + if "https" in server: + server = server.replace('https', "wss") + else: + server = server.replace('http', "ws") + + websocket_url = "%s/embywebsocket?api_key=%s&deviceId=%s" % (server, token, self.device_id) + log.debug("websocket url: {0}", websocket_url) + + self._client = websocket.WebSocketApp(websocket_url, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + self._client.on_open = self.on_open + log.debug("Starting WebSocketClient") + + while not self.monitor.abortRequested(): + + self._client.run_forever(ping_interval=10) + + if self._stop_websocket: + break + + if self.monitor.waitForAbort(20): + # Abort was requested, exit + break + + log.debug("Reconnecting WebSocket") + + log.debug("WebSocketClient Stopped") + + def stop_client(self): + + self._stop_websocket = True + if self._client is not None: + self._client.close() + log.debug("Stopping WebSocket (stop_client called)") + + def post_capabilities(self): + + url = "{server}/emby/Sessions/Capabilities/Full?format=json" + data = { + 'SupportsMediaControl': True, + 'PlayableMediaTypes': ["Video"], + 'SupportedCommands': ["MoveUp", + "MoveDown", + "MoveLeft", + "MoveRight", + "Select", + "Back", + "ToggleContextMenu", + "ToggleFullscreen", + "ToggleOsdMenu", + "GoHome", + "PageUp", + "NextLetter", + "GoToSearch", + "GoToSettings", + "PageDown", + "PreviousLetter", + "TakeScreenshot", + "VolumeUp", + "VolumeDown", + "ToggleMute", + "SendString", + "DisplayMessage", + #"SetAudioStreamIndex", + #"SetSubtitleStreamIndex", + "SetRepeatMode", + "Mute", + "Unmute", + "SetVolume", + "PlayNext", + "Play", + "Playstate", + "PlayMediaSource"] + } + + download_utils = downloadutils.DownloadUtils() + download_utils.downloadUrl(url, postBody=data, method="POST") + log.debug("Posted Capabilities: {0}", data) + diff --git a/plugin.video.embycon/resources/lib/widgets.py b/plugin.video.embycon/resources/lib/widgets.py index c3098d4..6a63c2d 100644 --- a/plugin.video.embycon/resources/lib/widgets.py +++ b/plugin.video.embycon/resources/lib/widgets.py @@ -12,12 +12,14 @@ from utils import getArt from datamanager import DataManager from simple_logging import SimpleLogging from kodi_utils import HomeWindow +from resources.lib.error import catch_except log = SimpleLogging(__name__) downloadUtils = DownloadUtils() dataManager = DataManager() kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + def checkForNewContent(): log.debug("checkForNewContent Called") @@ -33,7 +35,7 @@ def checkForNewContent(): added_result = downloadUtils.downloadUrl(added_url, suppress=True) result = json.loads(added_result) - log.debug("LATEST_ADDED_ITEM:" + str(result)) + log.debug("LATEST_ADDED_ITEM: {0}", result) last_added_date = "" if result is not None: @@ -41,7 +43,7 @@ def checkForNewContent(): if len(items) > 0: item = items[0] last_added_date = item.get("Etag", "") - log.debug("last_added_date: " + last_added_date) + log.debug("last_added_date: {0}", last_added_date) played_url = ('{server}/emby/Users/{userid}/Items' + '?Recursive=true' + @@ -55,7 +57,7 @@ def checkForNewContent(): played_result = downloadUtils.downloadUrl(played_url, suppress=True) result = json.loads(played_result) - log.debug("LATEST_PLAYED_ITEM:" + str(result)) + log.debug("LATEST_PLAYED_ITEM: {0}", result) last_played_date = "" if result is not None: @@ -63,29 +65,29 @@ def checkForNewContent(): if len(items) > 0: item = items[0] last_played_date = item.get("Etag", "") - log.debug("last_played_date: " + last_played_date) + log.debug("last_played_date: {0}", last_played_date) home_window = HomeWindow() current_widget_hash = home_window.getProperty("embycon_widget_reload") - log.debug("Current Widget Hash: " + str(current_widget_hash)) + log.debug("Current Widget Hash: {0}", current_widget_hash) m = hashlib.md5() m.update(last_played_date + last_added_date) new_widget_hash = m.hexdigest() - log.debug("New Widget Hash: " + str(new_widget_hash)) + log.debug("New Widget Hash: {0}", new_widget_hash) if current_widget_hash != new_widget_hash: home_window.setProperty("embycon_widget_reload", new_widget_hash) - log.debug("Setting New Widget Hash: " + str(new_widget_hash)) + log.debug("Setting New Widget Hash: {0}", new_widget_hash) def getWidgetUrlContent(handle, params): - log.debug("getWidgetUrlContent Called" + str(params)) + log.debug("getWidgetUrlContent Called: {0}", params) request = params["url"] request = urllib.unquote(request) request = "{server}/emby/" + request + "&ImageTypeLimit=1&format=json" - log.debug("getWidgetUrlContent URL:" + request) + log.debug("getWidgetUrlContent URL: {0}", request) select_action = params.get("action", None) @@ -96,7 +98,7 @@ def getWidgetUrlContent(handle, params): def getSuggestions(handle, params): - log.debug("getSuggestions Called" + str(params)) + log.debug("getSuggestions Called: {0}", params) itemsUrl = ("{server}/emby/Movies/Recommendations" + "?userId={userid}" + @@ -112,7 +114,7 @@ def getSuggestions(handle, params): xbmcplugin.endOfDirectory(handle, cacheToDisc=False) def getWidgetContentNextUp(handle, params): - log.debug("getWidgetContentNextUp Called" + str(params)) + log.debug("getWidgetContentNextUp Called: {0}", params) itemsUrl = ("{server}/emby/Shows/NextUp?SeriesId=" + params["id"] + "&userId={userid}" + @@ -128,7 +130,7 @@ def getWidgetContentNextUp(handle, params): def getWidgetContentSimilar(handle, params): - log.debug("getWisgetContentSimilarMovies Called" + str(params)) + log.debug("getWisgetContentSimilarMovies Called: {0}", params) itemsUrl = ("{server}/emby/Items/" + params["id"] + "/Similar" "?userId={userid}" + @@ -145,18 +147,21 @@ def getWidgetContentSimilar(handle, params): def getWidgetContentCast(handle, params): - log.debug("getWigetContentCast Called" + str(params)) + log.debug("getWigetContentCast Called: {0}", params) server = downloadUtils.getServer() id = params["id"] - jsonData = downloadUtils.downloadUrl("{server}/emby/Users/{userid}/Items/" + id + "?format=json", - suppress=False, popup=1) - result = json.loads(jsonData) - log.debug("ItemInfo: " + str(result)) + data_manager = DataManager() + result = data_manager.GetContent("{server}/emby/Users/{userid}/Items/" + id + "?format=json") + log.debug("ItemInfo: {0}", result) listItems = [] - people = result.get("People") - if (people != None): + if result is not None: + people = result.get("People") + else: + people = None + + if people is not None: for person in people: #if (person.get("Type") == "Director"): # director = director + person.get("Name") + ' ' @@ -181,12 +186,17 @@ def getWidgetContentCast(handle, params): artLinks["poster"] = person_thumbnail list_item.setArt(artLinks) + labels = {} + labels["mediatype"] = "artist" + list_item.setInfo(type="music", infoLabels=labels) + if person_role: list_item.setLabel2(person_role) itemTupple = ("", list_item, False) listItems.append(itemTupple) + xbmcplugin.setContent(handle, 'artists') xbmcplugin.addDirectoryItems(handle, listItems) xbmcplugin.endOfDirectory(handle, cacheToDisc=False) @@ -194,18 +204,20 @@ def getWidgetContentCast(handle, params): def populateWidgetItems(itemsUrl, override_select_action=None): server = downloadUtils.getServer() + if server is None: + return [] + settings = xbmcaddon.Addon(id='plugin.video.embycon') select_action = settings.getSetting("widget_select_action") if override_select_action is not None: select_action = str(override_select_action) - log.debug("WIDGET_DATE_URL: " + itemsUrl) + log.debug("WIDGET_DATE_URL: {0}", itemsUrl) # get the items - jsonData = downloadUtils.downloadUrl(itemsUrl, suppress=False, popup=1) - log.debug("Widget(Items) jsonData: " + jsonData) - result = json.loads(jsonData) + data_manager = DataManager() + result = data_manager.GetContent(itemsUrl) if result is not None and isinstance(result, dict) and result.get("Items") is not None: simmilarTo = result.get("BaselineItemName", None) @@ -219,35 +231,30 @@ def populateWidgetItems(itemsUrl, override_select_action=None): itemCount = 1 listItems = [] for item in result: - item_id = item.get("Id") - name = item.get("Name") + item_id = item["Id"] + name = item["Name"] episodeDetails = "" - log.debug("WIDGET_DATE_NAME: " + name) + log.debug("WIDGET_DATE_NAME: {0}", name) - title = item.get("Name") + title = name tvshowtitle = "" + item_type = item["Type"] + series_name = item["SeriesName"] - if (item.get("Type") == "Episode" and item.get("SeriesName") != None): + if item_type == "Episode" and series_name is not None: - eppNumber = "X" - tempEpisodeNumber = "0" - if (item.get("IndexNumber") != None): - eppNumber = item.get("IndexNumber") - if eppNumber < 10: - tempEpisodeNumber = "0" + str(eppNumber) - else: - tempEpisodeNumber = str(eppNumber) + episode_number = item["IndexNumber"] + if episode_number is None: + episode_number = 0 - seasonNumber = item.get("ParentIndexNumber") - if seasonNumber < 10: - tempSeasonNumber = "0" + str(seasonNumber) - else: - tempSeasonNumber = str(seasonNumber) + season_number = item["ParentIndexNumber"] + if season_number is None: + season_number = 0 - episodeDetails = "S" + tempSeasonNumber + "E" + tempEpisodeNumber - name = item.get("SeriesName") + " " + episodeDetails - tvshowtitle = episodeDetails - title = item.get("SeriesName") + name = series_name + " " + episodeDetails + name = "%s S%02dE%02d" % (series_name, season_number, episode_number) + tvshowtitle = "S%02dE%02d" % (season_number, episode_number) + title = series_name art = getArt(item, server, widget=True) @@ -258,31 +265,30 @@ def populateWidgetItems(itemsUrl, override_select_action=None): # list_item.setLabel2(episodeDetails) - production_year = item.get("ProductionYear") - if not production_year and item.get("PremiereDate"): - production_year = int(item.get("PremiereDate")[:4]) - - overlay = "0" - playCount = "0" + production_year = item["ProductionYear"] + prem_year = item["PremiereDate"] + if production_year is None and prem_year is not None: + production_year = int(prem_year[:4]) # add progress percent - userData = item.get("UserData") - if (userData != None): - if userData.get("Played") == True: - playCount = "1" - overlay = "5" - else: - overlay = "6" - - playBackTicks = float(userData.get("PlaybackPositionTicks")) - if (playBackTicks != None and playBackTicks > 0): - runTimeTicks = float(item.get("RunTimeTicks", "0")) - if (runTimeTicks > 0): - playBackPos = int(((playBackTicks / 1000) / 10000) / 60) - list_item.setProperty('ResumeTime', str(playBackPos)) - - percentage = int((playBackTicks / runTimeTicks) * 100.0) - list_item.setProperty("complete_percentage", str(percentage)) + userData = item["UserData"] + if userData["Played"] == True: + playCount = "1" + overlay = "5" + else: + playCount = "0" + overlay = "6" + + runtime = item["RunTimeTicks"] + playBackTicks = userData["PlaybackPositionTicks"] + + if playBackTicks is not None and runtime is not None and runtime > 0: + runtime = float(runtime) + playBackTicks = float(playBackTicks) + playBackPos = int(((playBackTicks / 1000) / 10000) / 60) + list_item.setProperty('ResumeTime', str(playBackPos)) + percentage = int((playBackTicks / runtime) * 100.0) + list_item.setProperty("complete_percentage", str(percentage)) video_info_label = {"title": title, "tvshowtitle": tvshowtitle, @@ -295,13 +301,14 @@ def populateWidgetItems(itemsUrl, override_select_action=None): list_item.setProperty('discart', art['discart']) # not avail to setArt list_item.setArt(art) # add count - list_item.setProperty("item_index", str(itemCount)) - itemCount = itemCount + 1 + #list_item.setProperty("item_index", str(itemCount)) + #itemCount = itemCount + 1 - list_item.setProperty('IsPlayable', 'true') + list_item.setProperty('IsPlayable', 'false') - totalTime = str(int(float(item.get("RunTimeTicks", "0")) / (10000000 * 60))) - list_item.setProperty('TotalTime', str(totalTime)) + if runtime is not None: + totalTime = str(int(float(runtime) / (10000000 * 60))) + list_item.setProperty('TotalTime', str(totalTime)) list_item.setProperty('id', item_id) @@ -320,7 +327,7 @@ def populateWidgetItems(itemsUrl, override_select_action=None): def getWidgetContent(handle, params): - log.debug("getWigetContent Called" + str(params)) + log.debug("getWigetContent Called: {0}", params) type = params.get("type") if (type == None): diff --git a/plugin.video.embycon/resources/settings.xml b/plugin.video.embycon/resources/settings.xml index 301caf6..ddf3b8d 100644 --- a/plugin.video.embycon/resources/settings.xml +++ b/plugin.video.embycon/resources/settings.xml @@ -36,26 +36,28 @@ + - - - - - + + + + - - + + - + + + \ No newline at end of file diff --git a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.1.xml b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.1.xml index cb9b540..6742452 100644 --- a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.1.xml +++ b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.1.xml @@ -88,7 +88,7 @@ - + diff --git a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3-elec.xml b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3-elec.xml index cb996ed..bfc4121 100644 --- a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3-elec.xml +++ b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3-elec.xml @@ -89,7 +89,7 @@ - + diff --git a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3.xml b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3.xml index 8c73c20..14cf80e 100644 --- a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3.xml +++ b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.3.xml @@ -88,7 +88,7 @@ - + diff --git a/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.6.xml b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.6.xml new file mode 100644 index 0000000..13987e7 --- /dev/null +++ b/plugin.video.embycon/resources/skins/skin.estuary/xml/Home-17.6.xml @@ -0,0 +1,1080 @@ + + + ClearProperty(first_load_done, 10000) + 9000 + background + + + HiddenObject + Focus + SetFocus(2000) + noop + Control.HasFocus(20000) + + + HiddenObject + Focus + SetFocus(2000) + noop + Control.HasFocus(20001) + + DefaultBackground + + DepthBackground + FullScreenDimensions + scale + 600 + conditional + WindowOpen + WindowClose + VisibleChange + $VAR[HomeFanartVar] + !Player.HasMedia + + + Conditional + + 462 + + + + + + + + + OpenClose_Right + + + String.IsEqual(Container(9000).ListItem.Property(id),emby_movies) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),emby_tvshows) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),movies) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),tvshows) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),music) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),addons) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),video) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),livetv) + + + + + WidgetGroupListCommon + + 390 + 0 + 0 + 36 + horizontal + PVR.IsRecordingTV | PVR.HasNonRecordingTVTimer + center + + 674 + PVR.IsRecordingTV + + + + + + + + + 674 + PVR.HasNonRecordingTVTimer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),radio) + + + + + WidgetGroupListCommon + + 390 + 25 + 36 + horizontal + right + 1360 + PVR.IsRecordingRadio | PVR.HasNonRecordingRadioTimer + + 680 + PVR.IsRecordingRadio + + + + + + + + + PVR.HasNonRecordingRadioTimer + 680 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),favorites) + + + + + 65 + 0 + 0 + 0 + 9000 + 9000 + 14100 + 14100 + $INFO[ListItem.FileNameAndPath] + 2 + 500 + vertical + Integer.IsGreater(Container(14100).NumItems,0) | Container(14100).IsUpdating + + + 130 + + + + + + + + + DepthContentPopout + 130 + Focus + UnFocus + + + + + + + favourites:// + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),weather) + + + + + WidgetGroupListCommon + + Weather info + 68 + 70 + 102 + 300 + !String.IsEmpty(Weather.plugin) + + 90 + 100% + dialogs/dialog-bg.png + + + 840 + 60 + center + 24 + 60 + right + font30_title + + + + 840 + 120 + center + 24 + 60 + right + font14 + + + + 50 + 50 + 20 + horizontal + left + -110 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),musicvideos) + + + + + WidgetGroupListCommon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),pictures) + + + + + WidgetGroupListCommon + + + + + + + + + + String.IsEqual(Container(9000).ListItem.Property(id),disc) + + + + + + + + + + + + + + + + DepthContentPanel + OpenClose_Left + + + + + 0 + 240 + 462 + -10 + 6 + 1 + ClearProperty(listposition,home) + SetFocus($INFO[Container(9000).ListItem.Property(menu_id)]) + PageDown + PageUp + 500 + + + UnFocus + + 0 + 0 + 462 + 95 + lists/focus.png + Conditional + + + -3 + 1 + 95 + 95 + $INFO[ListItem.Art(thumb)] + Focus + + + 0 + 0 + 95 + 95 + colors/black.png + Conditional + + + + -3 + 1 + 95 + 95 + $INFO[ListItem.Art(thumb)] + + + 104 + 0 + 95 + 560 + center + font37 + + text_shadow + + + + + -3 + 1 + 95 + 95 + $INFO[ListItem.Art(thumb)] + + + 104 + 0 + 95 + 560 + center + font37 + + text_shadow + + + + + + + ActivateWindow(Videos,videodb://movies/titles/,return) + ActivateWindow(Videos,plugin://plugin.video.embycon/?mode=SHOW_CONTENT&item_type=Movie&media_type=movies,return) + SetProperty(first_load_done, done, 10000) + $NUMBER[3000] + icons/sidemenu/movies.png + emby_movies + + + + ActivateWindow(Videos,videodb://movies/titles/,return) + ActivateWindow(Videos,plugin://plugin.video.embycon/?mode=SHOW_CONTENT&item_type=Series&media_type=tvshows,return) + SetProperty(first_load_done, done, 10000) + $NUMBER[4000] + icons/sidemenu/tv.png + emby_tvshows + + + + + ActivateWindow(Videos,videodb://movies/titles/,return) + ActivateWindow(Videos,sources://video/,return) + $NUMBER[5000] + icons/sidemenu/movies.png + movies + !Skin.HasSetting(HomeMenuNoMovieButton) + + + + ActivateWindow(Videos,videodb://tvshows/titles/,return) + ActivateWindow(Videos,sources://video/,return) + $NUMBER[6000] + icons/sidemenu/tv.png + tvshows + !Skin.HasSetting(HomeMenuNoTVShowButton) + + + + ActivateWindow(Music,root,return) + $NUMBER[7000] + icons/sidemenu/music.png + music + !Skin.HasSetting(HomeMenuNoMusicButton) + + + + PlayDisc + $NUMBER[21000] + icons/sidemenu/disc.png + disc + System.HasMediaDVD + + + + $NUMBER[16000] + ActivateWindow(Videos,musicvideos,return) + icons/sidemenu/musicvideos.png + musicvideos + !Skin.HasSetting(HomeMenuNoMusicVideoButton) + + + + $NUMBER[12000] + ActivateWindow(TVChannels) + icons/sidemenu/livetv.png + livetv + !Skin.HasSetting(HomeMenuNoTVButton) + + + + $NUMBER[13000] + ActivateWindow(RadioChannels) + icons/sidemenu/radio.png + radio + !Skin.HasSetting(HomeMenuNoRadioButton) + + + + $NUMBER[8000] + ActivateWindow(1100) + icons/sidemenu/addons.png + addons + !Skin.HasSetting(HomeMenuNoProgramsButton) + + + + ActivateWindow(Pictures) + $NUMBER[4000] + icons/sidemenu/pictures.png + pictures + !Skin.HasSetting(HomeMenuNoPicturesButton) + + + + ActivateWindow(Videos,root) + $NUMBER[11000] + icons/sidemenu/videos.png + video + !Skin.HasSetting(HomeMenuNoVideosButton) + + + + ActivateWindow(favourites) + $NUMBER[14000] + icons/sidemenu/favourites.png + favorites + !Skin.HasSetting(HomeMenuNoFavButton) + + + + ActivateWindow(Weather) + ReplaceWindow(servicesettings,weather) + $NUMBER[15000] + icons/sidemenu/weather.png + weather + !Skin.HasSetting(HomeMenuNoWeatherButton) + + + + ActivateWindow(settings) + $NUMBER[-1] + icons/sidemenu/manage.png + power + + + + ActivateWindow(shutdownmenu) + $NUMBER[-1] + icons/sidemenu/programs.png + power + + + + + + BottomBar + + + + + DepthBars + conditional + WindowOpen + WindowClose + 30 + 90 + + keep + 56 + 56 + icons/logo.png + + + 40 + 10 + keep + 192 + 36 + icons/logo-text.png + + + + conditional + WindowOpen + WindowClose + 0 + 0 + 39 + 100% + font12 + 1 + + button_focus + button_focus + text_shadow + FFC0C0C0 + + + + diff --git a/plugin.video.embycon/service.py b/plugin.video.embycon/service.py index 60b1575..71b5320 100644 --- a/plugin.video.embycon/service.py +++ b/plugin.video.embycon/service.py @@ -1,19 +1,21 @@ # coding=utf-8 # Gnu General Public License - see LICENSE.TXT -import xbmc -import xbmcaddon -import xbmcgui import time import json import traceback +import binascii + +import xbmc +import xbmcaddon +import xbmcgui from resources.lib.downloadutils import DownloadUtils from resources.lib.simple_logging import SimpleLogging -from resources.lib.play_utils import playFile +from resources.lib.play_utils import Service, PlaybackService, sendProgress from resources.lib.kodi_utils import HomeWindow -from resources.lib.translation import i18n from resources.lib.widgets import checkForNewContent +from resources.lib.websocket_client import WebSocketClient # clear user and token when logging in home_window = HomeWindow() @@ -28,282 +30,46 @@ download_utils = DownloadUtils() try: download_utils.authenticate() except Exception as error: - log.error("Error with initial service auth: " + str(error)) - - -def hasData(data): - if data is None or len(data) == 0 or data == "None": - return False - else: - return True - - -def sendProgress(): - playing_file = xbmc.Player().getPlayingFile() - play_data = monitor.played_information.get(playing_file) - - if play_data is None: - return - - log.debug("Sending Progress Update") - - play_time = xbmc.Player().getTime() - play_data["currentPossition"] = play_time - - item_id = play_data.get("item_id") - if item_id is None: - return - - ticks = int(play_time * 10000000) - paused = play_data.get("paused", False) - playback_type = play_data.get("playback_type") - play_session_id = play_data.get("play_session_id") - - postdata = { - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': item_id, - 'MediaSourceId': item_id, - 'PositionTicks': ticks, - 'IsPaused': paused, - 'IsMuted': False, - 'PlayMethod': playback_type, - 'PlaySessionId': play_session_id - } - - log.debug("Sending POST progress started: %s." % postdata) - - url = "{server}/emby/Sessions/Playing/Progress" - download_utils.downloadUrl(url, postBody=postdata, method="POST") - -def promptForStopActions(item_id, current_possition): - - settings = xbmcaddon.Addon(id='plugin.video.embycon') - - prompt_next_percentage = int(settings.getSetting('promptPlayNextEpisodePercentage')) - play_prompt = settings.getSetting('promptPlayNextEpisodePercentage_prompt') == "true" - prompt_delete_episode_percentage = int(settings.getSetting('promptDeleteEpisodePercentage')) - prompt_delete_movie_percentage = int(settings.getSetting('promptDeleteMoviePercentage')) - - # everything is off so return - if prompt_next_percentage == 100 and prompt_delete_episode_percentage == 100 and prompt_delete_movie_percentage == 100: - return - - jsonData = download_utils.downloadUrl("{server}/emby/Users/{userid}/Items/" + - item_id + "?format=json", - suppress=False, popup=1) - result = json.loads(jsonData) - prompt_to_delete = False - runtime = result.get("RunTimeTicks", 0) - - # if no runtime we cant calculate perceantge so just return - if runtime == 0: - log.debug("No runtime so returing") - return - - # item percentage complete - percenatge_complete = int(((current_possition * 10000000) / runtime) * 100) - log.debug("Episode Percentage Complete: %s" % percenatge_complete) - - if (prompt_delete_episode_percentage < 100 and - result.get("Type", "na") == "Episode" and - percenatge_complete > prompt_delete_episode_percentage): - prompt_to_delete = True - - if (prompt_delete_movie_percentage < 100 and - result.get("Type", "na") == "Movie" and - percenatge_complete > prompt_delete_movie_percentage): - prompt_to_delete = True - - if prompt_to_delete: - log.debug("Prompting for delete") - resp = xbmcgui.Dialog().yesno(i18n('confirm_file_delete'), i18n('file_delete_confirm'), autoclose=10000) - if resp: - log.debug("Deleting item: %s" % item_id) - url = "{server}/emby/Items/%s?format=json" % item_id - download_utils.downloadUrl(url, method="DELETE") - xbmc.executebuiltin("Container.Refresh") - - # prompt for next episode - if (prompt_next_percentage < 100 and - result.get("Type", "na") == "Episode" and - percenatge_complete > prompt_next_percentage): - parendId = result.get("ParentId", "na") - item_index = result.get("IndexNumber", -1) - - if parendId == "na": - log.debug("No parent id, can not prompt for next episode") - return - - if item_index == -1: - log.debug("No episode number, can not prompt for next episode") - return - - jsonData = download_utils.downloadUrl('{server}/emby/Users/{userid}/Items?' + - '?Recursive=true' + - '&ParentId=' + parendId + - #'&Filters=IsUnplayed,IsNotFolder' + - '&IsVirtualUnaired=false' + - '&IsMissing=False' + - '&IncludeItemTypes=Episode' + - '&ImageTypeLimit=1' + - '&format=json', - suppress=False, popup=1) - - items_result = json.loads(jsonData) - log.debug("Prompt Next Item Details: %s" % items_result) - # find next episode - item_list = items_result.get("Items", []) - for item in item_list: - index = item.get("IndexNumber", -1) - if index == item_index + 1: # find the very next episode in the season - - resp = True - if play_prompt: - #next_epp_name = str(index) + " of " + str(item_list[-1].get("IndexNumber", -1)) + " - " + item.get("Name", "n/a") - next_epp_name = ("%02d - " % (index,)) + item.get("Name", "n/a") - resp = xbmcgui.Dialog().yesno(i18n("play_next_title"), i18n("play_next_question"), next_epp_name, autoclose=10000) + log.error("Error with initial service auth: {0}", error) - if resp: - next_item_id = item.get("Id") - log.debug("Playing Next Episode: %s" % next_item_id) - - play_info = {} - play_info["item_id"] = next_item_id - play_info["auto_resume"] = "-1" - play_info["force_transcode"] = False - play_data = json.dumps(play_info) - - home_window = HomeWindow() - home_window.setProperty("item_id", next_item_id) - home_window.setProperty("play_item_message", play_data) - - break - - -def stopAll(played_information): - if len(played_information) == 0: - return - - log.debug("played_information : " + str(played_information)) - - for item_url in played_information: - data = played_information.get(item_url) - if data is not None: - log.debug("item_url : " + item_url) - log.debug("item_data : " + str(data)) - - current_possition = data.get("currentPossition", 0) - emby_item_id = data.get("item_id") - - if hasData(emby_item_id): - log.debug("Playback Stopped at: " + str(int(current_possition * 10000000))) - - url = "{server}/emby/Sessions/Playing/Stopped" - postdata = { - 'ItemId': emby_item_id, - 'MediaSourceId': emby_item_id, - 'PositionTicks': int(current_possition * 10000000) - } - download_utils.downloadUrl(url, postBody=postdata, method="POST") - - promptForStopActions(emby_item_id, current_possition) - - played_information.clear() - - -class Service(xbmc.Player): - played_information = {} - - def __init__(self, *args): - log.debug("Starting monitor service: " + str(args)) - self.played_information = {} - - def onPlayBackStarted(self): - # Will be called when xbmc starts playing a file - stopAll(self.played_information) - - current_playing_file = xbmc.Player().getPlayingFile() - log.debug("onPlayBackStarted: " + current_playing_file) - - home_window = HomeWindow() - emby_item_id = home_window.getProperty("item_id") - playback_type = home_window.getProperty("PlaybackType_" + emby_item_id) - play_session_id = home_window.getProperty("PlaySessionId_" + emby_item_id) - - # if we could not find the ID of the current item then return - if emby_item_id is None or len(emby_item_id) == 0: - return - - log.debug("Sending Playback Started") - postdata = { - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': emby_item_id, - 'MediaSourceId': emby_item_id, - 'PlayMethod': playback_type, - 'PlaySessionId': play_session_id - } - - log.debug("Sending POST play started: %s." % postdata) - - url = "{server}/emby/Sessions/Playing" - download_utils.downloadUrl(url, postBody=postdata, method="POST") - - data = {} - data["item_id"] = emby_item_id - data["paused"] = False - data["playback_type"] = playback_type - data["play_session_id"] = play_session_id - self.played_information[current_playing_file] = data - - log.debug("ADDING_FILE : " + current_playing_file) - log.debug("ADDING_FILE : " + str(self.played_information)) +# set up all the services +monitor = Service() +playback_service = PlaybackService(monitor) - def onPlayBackEnded(self): - # Will be called when kodi stops playing a file - log.debug("EmbyCon Service -> onPlayBackEnded") - home_window = HomeWindow() - home_window.clearProperty("item_id") - stopAll(self.played_information) +home_window = HomeWindow() +last_progress_update = time.time() +last_content_check = time.time() +websocket_client = WebSocketClient() - def onPlayBackStopped(self): - # Will be called when user stops kodi playing a file - log.debug("onPlayBackStopped") - home_window = HomeWindow() - home_window.clearProperty("item_id") - stopAll(self.played_information) +# start the WebSocket Client running +settings = xbmcaddon.Addon(id='plugin.video.embycon') +remote_control = settings.getSetting('remoteControl') == "true" +if remote_control: + websocket_client.start() - def onPlayBackPaused(self): - # Will be called when kodi pauses the video - log.debug("onPlayBackPaused") - current_file = xbmc.Player().getPlayingFile() - play_data = monitor.played_information.get(current_file) - if play_data is not None: - play_data['paused'] = True - sendProgress() +def get_now_playing(): - def onPlayBackResumed(self): - # Will be called when kodi resumes the video - log.debug("onPlayBackResumed") - current_file = xbmc.Player().getPlayingFile() - play_data = monitor.played_information.get(current_file) + # Get the active player + result = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "id": 1, "method": "Player.GetActivePlayers"}') + result = unicode(result, 'utf-8', errors='ignore') + log.debug("Got active player: {0}", result) + result = json.loads(result) - if play_data is not None: - play_data['paused'] = False - sendProgress() + if 'result' in result and len(result["result"]) > 0: + playerid = result["result"][0]["playerid"] - def onPlayBackSeek(self, time, seekOffset): - # Will be called when kodi seeks in video - log.debug("onPlayBackSeek") - sendProgress() + # Get details of the playing media + log.debug("Getting details of now playing media") + result = xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "id": 1, "method": "Player.GetItem", "params": {"playerid": ' + str( + playerid) + ', "properties": ["showtitle", "tvshowid", "episode", "season", "playcount", "genre", "plotoutline", "uniqueid"] } }') + result = unicode(result, 'utf-8', errors='ignore') + log.debug("playing_item_details: {0}", result) + result = json.loads(result) + return result -monitor = Service() -home_window = HomeWindow() -last_progress_update = time.time() -last_content_check = time.time() # monitor.abortRequested() is causes issues, it currently triggers for all addon cancelations which causes # the service to exit when a user cancels an addon load action. This is a bug in Kodi. @@ -316,29 +82,27 @@ while not xbmc.abortRequested: # if playing every 10 seconds updated the server with progress if (time.time() - last_progress_update) > 10: last_progress_update = time.time() - sendProgress() + sendProgress(monitor) else: - # if we have a play item them trigger playback - play_data = home_window.getProperty("play_item_message") - if play_data: - home_window.clearProperty("play_item_message") - play_info = json.loads(play_data) - playFile(play_info) - # if not playing every 60 seonds check for new widget content if (time.time() - last_content_check) > 60: last_content_check = time.time() checkForNewContent() + #get_now_playing() + except Exception as error: - log.error("Exception in Playback Monitor : " + str(error)) - log.error(traceback.format_exc()) + log.error("Exception in Playback Monitor: {0}", error) + log.error("{0}", traceback.format_exc()) xbmc.sleep(1000) +# stop the WebSocket Client +websocket_client.stop_client() + # clear user and token when loggin off home_window.clearProperty("userid") home_window.clearProperty("AccessToken") home_window.clearProperty("Params") -log.error("Service shutting down") +log.debug("Service shutting down") -- cgit v1.2.3