diff --git a/contextmenu.py b/contextmenu.py index d462807a..bc640ef0 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -147,7 +147,7 @@ if __name__ == '__main__': doUtils = downloadutils.DownloadUtils() url = "{server}/emby/Items/%s?format=json" % embyid logMsg("Deleting request: %s" % embyid, 0) - doUtils.downloadUrl(url, type="DELETE") + doUtils.downloadUrl(url, action_type="DELETE") '''if utils.settings('skipContextMenu') != "true": if xbmcgui.Dialog().yesno( @@ -156,8 +156,7 @@ if __name__ == '__main__': "also delete the file(s) from disk!")): import downloadutils doUtils = downloadutils.DownloadUtils() - url = "{server}/emby/Items/%s?format=json" % embyid - doUtils.downloadUrl(url, type="DELETE")''' + doUtils.downloadUrl("{server}/emby/Items/%s?format=json" % embyid, action_type="DELETE")''' xbmc.sleep(500) xbmc.executebuiltin("Container.Update") \ No newline at end of file diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 003324f8..348a9994 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -328,7 +328,7 @@ Gathering tv shows from: Gathering: Detected the database needs to be recreated for this version of Emby for Kodi. Proceed? - Emby for Kod may not work correctly until the database is reset. + Emby for Kodi may not work correctly until the database is reset. Cancelling the database syncing process. The current Kodi version is unsupported. completed in: Comparing movies from: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 10113064..7cff323c 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -28,7 +28,7 @@ class Artwork(): xbmc_port = None xbmc_username = None xbmc_password = None - + imageCacheThreads = [] imageCacheLimitThreads = 0 @@ -36,9 +36,9 @@ class Artwork(): self.enableTextureCache = utils.settings('enableTextureCache') == "true" self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit")) - self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5); + self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5) utils.logMsg("Using Image Cache Thread Count: " + str(self.imageCacheLimitThreads), 1) - + if not self.xbmc_port and self.enableTextureCache: self.setKodiWebServerDetails() @@ -48,11 +48,11 @@ class Artwork(): def double_urlencode(self, text): text = self.single_urlencode(text) text = self.single_urlencode(text) - + return text def single_urlencode(self, text): - + text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) #urlencode needs a utf- string text = text[13:] @@ -74,9 +74,9 @@ class Artwork(): result = json.loads(result) try: xbmc_webserver_enabled = result['result']['value'] - except KeyError, TypeError: + except (KeyError, TypeError): xbmc_webserver_enabled = False - + if not xbmc_webserver_enabled: # Enable the webserver, it is disabled web_port = { @@ -159,7 +159,7 @@ class Artwork(): self.xbmc_password = result['result']['value'] except TypeError: pass - + def FullTextureCacheSync(self): # This method will sync all Kodi artwork to textures13.db # and cache them locally. This takes diskspace! @@ -169,12 +169,12 @@ class Artwork(): if not xbmcgui.Dialog().yesno( "Image Texture Cache", string(39250).encode('utf-8')): return - + self.logMsg("Doing Image Cache Sync", 1) - + dialog = xbmcgui.DialogProgress() dialog.create("Emby for Kodi", "Image Cache Sync") - + # ask to rest all existing or not if xbmcgui.Dialog().yesno( "Image Texture Cache", string(39251).encode('utf-8'), ""): @@ -190,7 +190,7 @@ class Artwork(): xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8'))) else: xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file)) - + # remove all existing data from texture DB textureconnection = utils.kodiSQL('texture') texturecursor = textureconnection.cursor() @@ -209,8 +209,8 @@ class Artwork(): cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors result = cursor.fetchall() total = len(result) - count = 1 - percentage = 0 + count = 1 + percentage = 0 self.logMsg("Image cache sync about to process " + str(total) + " images", 1) for url in result: if dialog.iscanceled(): @@ -221,26 +221,26 @@ class Artwork(): self.CacheTexture(url[0]) count += 1 cursor.close() - + # Cache all entries in music DB connection = utils.kodiSQL('music') cursor = connection.cursor() cursor.execute("SELECT url FROM art") result = cursor.fetchall() total = len(result) - count = 1 + count = 1 percentage = 0 self.logMsg("Image cache sync about to process " + str(total) + " images", 1) for url in result: if dialog.iscanceled(): - break + break percentage = int((float(count) / float(total))*100) textMessage = str(count) + " of " + str(total) dialog.update(percentage, "Updating Image Cache: " + textMessage) self.CacheTexture(url[0]) count += 1 cursor.close() - + dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads))) self.logMsg("Waiting for all threads to exit", 1) while len(self.imageCacheThreads) > 0: @@ -250,16 +250,16 @@ class Artwork(): dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads))) self.logMsg("Waiting for all threads to exit: " + str(len(self.imageCacheThreads)), 1) xbmc.sleep(500) - + dialog.close() def addWorkerImageCacheThread(self, urlToAdd): - + while(True): # removed finished for thread in self.imageCacheThreads: if thread.isFinished: - self.imageCacheThreads.remove(thread) + self.imageCacheThreads.remove(thread) # add a new thread or wait and retry if we hit our limit if(len(self.imageCacheThreads) < self.imageCacheLimitThreads): @@ -273,14 +273,14 @@ class Artwork(): else: self.logMsg("Waiting for empty queue spot: " + str(len(self.imageCacheThreads)), 2) xbmc.sleep(50) - - + + def CacheTexture(self, url): # Cache a single image url to the texture cache if url and self.enableTextureCache: if(self.imageCacheLimitThreads == 0 or self.imageCacheLimitThreads == None): #Add image to texture cache by simply calling it at the http endpoint - + url = self.double_urlencode(url) try: # Extreme short timeouts so we will have a exception. response = requests.head( @@ -291,7 +291,7 @@ class Artwork(): timeout=(0.01, 0.01)) # We don't need the result except: pass - + else: self.addWorkerImageCacheThread(url) @@ -349,13 +349,13 @@ class Artwork(): mediaType=mediaType, imageType="%s%s" % ("fanart", index), cursor=cursor) - + if backdropsNumber > 1: try: # Will only fail on the first try, str to int. index += 1 except TypeError: index = 1 - + elif art == "Primary": # Primary art is processed as thumb and poster for Kodi. for artType in kodiart[art]: @@ -365,7 +365,7 @@ class Artwork(): mediaType=mediaType, imageType=artType, cursor=cursor) - + elif kodiart.get(art): # Process the rest artwork type that Kodi can use self.addOrUpdateArt( @@ -391,9 +391,11 @@ class Artwork(): cursor.execute(query, (kodiId, mediaType, imageType,)) try: # Update the artwork url = cursor.fetchone()[0] - + except TypeError: # Add the artwork cacheimage = True + self.logMsg("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl), 2) + query = ( ''' INSERT INTO art(media_id, media_type, type, url) @@ -402,17 +404,21 @@ class Artwork(): ''' ) cursor.execute(query, (kodiId, mediaType, imageType, imageUrl)) - + else: # Only cache artwork if it changed if url != imageUrl: cacheimage = True - + # Only for the main backdrop, poster if (utils.window('emby_initialScan') != "true" and imageType in ("fanart", "poster")): # Delete current entry before updating with the new one self.deleteCachedArtwork(url) + self.logMsg( + "Updating Art url for %s kodiId: %s (%s) -> (%s)" + % (imageType, kodiId, url, imageUrl), 1) + query = ' '.join(( "UPDATE art", @@ -422,7 +428,7 @@ class Artwork(): "AND type = ?" )) cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) - + # Cache fanart and poster in Kodi texture cache if cacheimage and imageType in ("fanart", "poster", "thumb"): self.CacheTexture(imageUrl) @@ -453,24 +459,24 @@ class Artwork(): try: cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) cachedurl = cursor.fetchone()[0] - + except TypeError: self.logMsg("Could not find cached url.", 1) except OperationalError: self.logMsg("Database is locked. Skip deletion process.", 1) - + else: # Delete thumbnail as well as the entry thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8') self.logMsg("Deleting cached thumbnail: %s" % thumbnails, 1) xbmcvfs.delete(thumbnails) - + try: cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() except OperationalError: self.logMsg("Issue deleting url from cache. Skipping.", 2) - + finally: cursor.close() @@ -487,7 +493,7 @@ class Artwork(): "%s/emby/Items/%s/Images/Primary?" "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" % (self.server, personId, tag)) - + person['imageurl'] = image return people @@ -501,8 +507,6 @@ class Artwork(): def getAllArtwork(self, item, parentInfo=False): - server = self.server - itemid = item['Id'] artworks = item['ImageTags'] backdrops = item.get('BackdropImageTags',[]) @@ -527,13 +531,13 @@ def getAllArtwork(self, item, parentInfo=False): 'Disc': "", 'Backdrop': [] } - + # Process backdrops for index, tag in enumerate(backdrops): artwork = ( "%s/emby/Items/%s/Images/Backdrop/%s?" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, itemid, index, maxWidth, maxHeight, tag, customquery)) + % (self.server, itemid, index, maxWidth, maxHeight, tag, customquery)) allartworks['Backdrop'].append(artwork) # Process the rest of the artwork @@ -544,15 +548,15 @@ def getAllArtwork(self, item, parentInfo=False): artwork = ( "%s/emby/Items/%s/Images/%s/0?" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, itemid, art, maxWidth, maxHeight, tag, customquery)) + % (self.server, itemid, art, maxWidth, maxHeight, tag, customquery)) allartworks[art] = artwork # Process parent items if the main item is missing artwork if parentInfo: - + # Process parent backdrops if not allartworks['Backdrop']: - + parentId = item.get('ParentBackdropItemId') if parentId: # If there is a parentId, go through the parent backdrop list @@ -562,7 +566,7 @@ def getAllArtwork(self, item, parentInfo=False): artwork = ( "%s/emby/Items/%s/Images/Backdrop/%s?" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, parentId, index, maxWidth, maxHeight, tag, customquery)) + % (self.server, parentId, index, maxWidth, maxHeight, tag, customquery)) allartworks['Backdrop'].append(artwork) # Process the rest of the artwork @@ -570,15 +574,15 @@ def getAllArtwork(self, item, parentInfo=False): for parentart in parentartwork: if not allartworks[parentart]: - + parentId = item.get('Parent%sItemId' % parentart) if parentId: - + parentTag = item['Parent%sImageTag' % parentart] artwork = ( "%s/emby/Items/%s/Images/%s/0?" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, parentId, parentart, + % (self.server, parentId, parentart, maxWidth, maxHeight, parentTag, customquery)) allartworks[parentart] = artwork @@ -587,12 +591,12 @@ def getAllArtwork(self, item, parentInfo=False): parentId = item.get('AlbumId') if parentId and item.get('AlbumPrimaryImageTag'): - + parentTag = item['AlbumPrimaryImageTag'] artwork = ( "%s/emby/Items/%s/Images/Primary/0?" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, parentId, maxWidth, maxHeight, parentTag, customquery)) + % (self.server, parentId, maxWidth, maxHeight, parentTag, customquery)) allartworks['Primary'] = artwork - return allartworks \ No newline at end of file + return allartworks diff --git a/resources/lib/connect.py b/resources/lib/connect.py index 05b563a3..2bd5c05d 100644 --- a/resources/lib/connect.py +++ b/resources/lib/connect.py @@ -21,7 +21,7 @@ requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) class ConnectUtils(): - + # Borg - multiple instances, shared state _shared_state = {} clientInfo = clientinfo.ClientInfo() @@ -60,8 +60,6 @@ class ConnectUtils(): def startSession(self): - log = self.logMsg - self.deviceId = self.clientInfo.getDeviceId() # User is identified from this point @@ -75,8 +73,8 @@ class ConnectUtils(): if self.sslclient is not None: verify = self.sslclient except: - log("Could not load SSL settings.", 1) - + self.logMsg("Could not load SSL settings.", 1) + # Start session self.c = requests.Session() self.c.headers = header @@ -85,7 +83,7 @@ class ConnectUtils(): self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - log("Requests session started on: %s" % self.server, 1) + self.logMsg("Requests session started on: %s" % self.server, 1) def stopSession(self): try: @@ -95,8 +93,7 @@ class ConnectUtils(): def getHeader(self, authenticate=True): - clientInfo = self.clientInfo - version = clientInfo.getVersion() + version = self.clientInfo.getVersion() if not authenticate: # If user is not authenticated @@ -105,9 +102,9 @@ class ConnectUtils(): 'X-Application': "Kodi/%s" % version, 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Accept': "application/json" - } + } self.logMsg("Header: %s" % header, 1) - + else: token = self.token # Attached to the requests session @@ -117,18 +114,17 @@ class ConnectUtils(): 'Accept': "application/json", 'X-Application': "Kodi/%s" % version, 'X-Connect-UserToken': token - } + } self.logMsg("Header: %s" % header, 1) - + return header def doUrl(self, url, data=None, postBody=None, rtype="GET", parameters=None, authenticate=True, timeout=None): - log = self.logMsg window = utils.window - log("=== ENTER connectUrl ===", 2) + self.logMsg("=== ENTER connectUrl ===", 2) default_link = "" if timeout is None: timeout = self.timeout @@ -137,7 +133,7 @@ class ConnectUtils(): try: # If connect user is authenticated if authenticate: - try: + try: c = self.c # Replace for the real values url = url.replace("{server}", self.server) @@ -167,7 +163,7 @@ class ConnectUtils(): verifyssl = self.sslclient except AttributeError: pass - + # Prepare request if rtype == "GET": r = requests.get(url, @@ -195,7 +191,7 @@ class ConnectUtils(): verifyssl = self.sslclient except AttributeError: pass - + # Prepare request if rtype == "GET": r = requests.get(url, @@ -213,28 +209,28 @@ class ConnectUtils(): verify=verifyssl) ##### THE RESPONSE ##### - log(r.url, 1) - log(r, 1) + self.logMsg(r.url, 1) + self.logMsg(r, 1) if r.status_code == 204: # No body in the response - log("====== 204 Success ======", 1) + self.logMsg("====== 204 Success ======", 1) elif r.status_code == requests.codes.ok: - - try: + + try: # UNICODE - JSON object r = r.json() - log("====== 200 Success ======", 1) - log("Response: %s" % r, 1) + self.logMsg("====== 200 Success ======", 1) + self.logMsg("Response: %s" % r, 1) return r except: if r.headers.get('content-type') != "text/html": - log("Unable to convert the response for: %s" % url, 1) + self.logMsg("Unable to convert the response for: %s" % url, 1) else: r.raise_for_status() - + ##### EXCEPTIONS ##### except requests.exceptions.ConnectionError as e: @@ -242,8 +238,8 @@ class ConnectUtils(): pass except requests.exceptions.ConnectTimeout as e: - log("Server timeout at: %s" % url, 0) - log(e, 1) + self.logMsg("Server timeout at: %s" % url, 0) + self.logMsg(e, 1) except requests.exceptions.HTTPError as e: @@ -259,11 +255,11 @@ class ConnectUtils(): pass except requests.exceptions.SSLError as e: - log("Invalid SSL certificate for: %s" % url, 0) - log(e, 1) + self.logMsg("Invalid SSL certificate for: %s" % url, 0) + self.logMsg(e, 1) except requests.exceptions.RequestException as e: - log("Unknown error connecting to: %s" % url, 0) - log(e, 1) + self.logMsg("Unknown error connecting to: %s" % url, 0) + self.logMsg(e, 1) return default_link diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py index d6485fe3..b73030df 100644 --- a/resources/lib/embydb_functions.py +++ b/resources/lib/embydb_functions.py @@ -34,7 +34,6 @@ class Embydb_Functions(): def getViews(self): - embycursor = self.embycursor views = [] query = ' '.join(( @@ -42,8 +41,8 @@ class Embydb_Functions(): "SELECT view_id", "FROM view" )) - embycursor.execute(query) - rows = embycursor.fetchall() + self.embycursor.execute(query) + rows = self.embycursor.fetchall() for row in rows: views.append(row[0]) return views @@ -68,7 +67,6 @@ class Embydb_Functions(): def getView_byId(self, viewid): - embycursor = self.embycursor query = ' '.join(( @@ -76,13 +74,13 @@ class Embydb_Functions(): "FROM view", "WHERE view_id = ?" )) - embycursor.execute(query, (viewid,)) - view = embycursor.fetchone() + self.embycursor.execute(query, (viewid,)) + view = self.embycursor.fetchone() + return view def getView_byType(self, mediatype): - embycursor = self.embycursor views = [] query = ' '.join(( @@ -91,8 +89,8 @@ class Embydb_Functions(): "FROM view", "WHERE media_type = ?" )) - embycursor.execute(query, (mediatype,)) - rows = embycursor.fetchall() + self.embycursor.execute(query, (mediatype,)) + rows = self.embycursor.fetchall() for row in rows: views.append({ @@ -105,17 +103,16 @@ class Embydb_Functions(): def getView_byName(self, tagname): - embycursor = self.embycursor - query = ' '.join(( "SELECT view_id", "FROM view", "WHERE view_name = ?" )) - embycursor.execute(query, (tagname,)) + self.embycursor.execute(query, (tagname,)) try: - view = embycursor.fetchone()[0] + view = self.embycursor.fetchone()[0] + except TypeError: view = None @@ -190,8 +187,6 @@ class Embydb_Functions(): def getItem_byId(self, embyid): - embycursor = self.embycursor - query = ' '.join(( "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type", @@ -199,40 +194,32 @@ class Embydb_Functions(): "WHERE emby_id = ?" )) try: - embycursor.execute(query, (embyid,)) - item = embycursor.fetchone() + self.embycursor.execute(query, (embyid,)) + item = self.embycursor.fetchone() return item except: return None def getItem_byWildId(self, embyid): - embycursor = self.embycursor - query = ' '.join(( "SELECT kodi_id, media_type", "FROM emby", "WHERE emby_id LIKE ?" )) - embycursor.execute(query, (embyid+"%",)) - items = embycursor.fetchall() - - return items + self.embycursor.execute(query, (embyid+"%",)) + return self.embycursor.fetchall() def getItem_byView(self, mediafolderid): - embycursor = self.embycursor - query = ' '.join(( "SELECT kodi_id", "FROM emby", "WHERE media_folder = ?" )) - embycursor.execute(query, (mediafolderid,)) - items = embycursor.fetchall() - - return items + self.embycursor.execute(query, (mediafolderid,)) + return self.embycursor.fetchall() def getPlexId(self, kodiid, mediatype): """ @@ -253,8 +240,6 @@ class Embydb_Functions(): def getItem_byKodiId(self, kodiid, mediatype): - embycursor = self.embycursor - query = ' '.join(( "SELECT emby_id, parent_id", @@ -262,15 +247,11 @@ class Embydb_Functions(): "WHERE kodi_id = ?", "AND media_type = ?" )) - embycursor.execute(query, (kodiid, mediatype,)) - item = embycursor.fetchone() - - return item + self.embycursor.execute(query, (kodiid, mediatype,)) + return self.embycursor.fetchone() def getItem_byParentId(self, parentid, mediatype): - embycursor = self.embycursor - query = ' '.join(( "SELECT emby_id, kodi_id, kodi_fileid", @@ -278,15 +259,11 @@ class Embydb_Functions(): "WHERE parent_id = ?", "AND media_type = ?" )) - embycursor.execute(query, (parentid, mediatype,)) - items = embycursor.fetchall() - - return items + self.embycursor.execute(query, (parentid, mediatype,)) + return self.embycursor.fetchall() def getItemId_byParentId(self, parentid, mediatype): - embycursor = self.embycursor - query = ' '.join(( "SELECT emby_id, kodi_id", @@ -294,39 +271,32 @@ class Embydb_Functions(): "WHERE parent_id = ?", "AND media_type = ?" )) - embycursor.execute(query, (parentid, mediatype,)) - items = embycursor.fetchall() - - return items + self.embycursor.execute(query, (parentid, mediatype,)) + return self.embycursor.fetchall() def getChecksum(self, mediatype): - embycursor = self.embycursor - query = ' '.join(( "SELECT emby_id, checksum", "FROM emby", "WHERE emby_type = ?" )) - embycursor.execute(query, (mediatype,)) - items = embycursor.fetchall() - - return items + self.embycursor.execute(query, (mediatype,)) + return self.embycursor.fetchall() def getMediaType_byId(self, embyid): - embycursor = self.embycursor - query = ' '.join(( "SELECT emby_type", "FROM emby", "WHERE emby_id = ?" )) - embycursor.execute(query, (embyid,)) + self.embycursor.execute(query, (embyid,)) try: - itemtype = embycursor.fetchone()[0] + itemtype = self.embycursor.fetchone()[0] + except TypeError: itemtype = None diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 2e5bd59e..8e42492b 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -328,12 +328,12 @@ def doMainListing(): if not path: path = utils.window('Emby.nodes.%s.content' % i) label = utils.window('Emby.nodes.%s.title' % i) - type = utils.window('Emby.nodes.%s.type' % i) + node_type = utils.window('Emby.nodes.%s.type' % i) #because we do not use seperate entrypoints for each content type, we need to figure out which items to show in each listing. #for now we just only show picture nodes in the picture library video nodes in the video library and all nodes in any other window - if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and type == "photos": + if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and node_type == "photos": addDirectoryItem(label, path) - elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and type != "photos": + elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and node_type != "photos": addDirectoryItem(label, path) elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"): addDirectoryItem(label, path) @@ -431,7 +431,7 @@ def deleteItem(): doUtils = downloadutils.DownloadUtils() url = "{server}/emby/Items/%s?format=json" % embyid utils.logMsg("EMBY delete", "Deleting request: %s" % embyid, 0) - doUtils.downloadUrl(url, type="DELETE") + doUtils.downloadUrl(url, action_type="DELETE") ##### ADD ADDITIONAL USERS ##### def addUser(): @@ -486,7 +486,7 @@ def addUser(): selected = additionalUsername[resp] selected_userId = additionalUserlist[selected] url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="DELETE") + doUtils.downloadUrl(url, postBody={}, action_type="DELETE") dialog.notification( heading="Success!", message="%s removed from viewing session" % selected, @@ -519,7 +519,7 @@ def addUser(): selected = users[resp] selected_userId = userlist[selected] url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="POST") + doUtils.downloadUrl(url, postBody={}, action_type="POST") dialog.notification( heading="Success!", message="%s added to viewing session" % selected, @@ -759,22 +759,22 @@ def GetSubFolders(nodeindex): title = utils.window('Emby.nodes.%s%s.title' %(nodeindex,node)) if title: path = utils.window('Emby.nodes.%s%s.content' %(nodeindex,node)) - type = utils.window('Emby.nodes.%s%s.type' %(nodeindex,node)) addDirectoryItem(title, path) xbmcplugin.endOfDirectory(int(sys.argv[1])) ##### BROWSE EMBY NODES DIRECTLY ##### -def BrowseContent(viewname, type="", folderid=""): +def BrowseContent(viewname, browse_type="", folderid=""): + emby = embyserver.Read_EmbyServer() art = artwork.Artwork() doUtils = downloadutils.DownloadUtils() #folderid used as filter ? if folderid in ["recent","recentepisodes","inprogress","inprogressepisodes","unwatched","nextepisodes","sets","genres","random","recommended"]: - filter = folderid + filter_type = folderid folderid = "" else: - filter = "" + filter_type = "" xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname) #get views for root level @@ -783,33 +783,35 @@ def BrowseContent(viewname, type="", folderid=""): for view in views: if view.get("name") == viewname.decode('utf-8'): folderid = view.get("id") + break - utils.logMsg("BrowseContent","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname.decode('utf-8'), type.decode('utf-8'), folderid.decode('utf-8'), filter.decode('utf-8'))) + if viewname is not None: + utils.logMsg("BrowseContent","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname.decode('utf-8'), browse_type.decode('utf-8'), folderid.decode('utf-8'), filter_type.decode('utf-8'))) #set the correct params for the content type #only proceed if we have a folderid if folderid: - if type.lower() == "homevideos": + if browse_type.lower() == "homevideos": xbmcplugin.setContent(int(sys.argv[1]), 'episodes') itemtype = "Video,Folder,PhotoAlbum" - elif type.lower() == "photos": + elif browse_type.lower() == "photos": xbmcplugin.setContent(int(sys.argv[1]), 'files') itemtype = "Photo,PhotoAlbum,Folder" else: itemtype = "" #get the actual listing - if type == "recordings": + if browse_type == "recordings": listing = emby.getTvRecordings(folderid) - elif type == "tvchannels": + elif browse_type == "tvchannels": listing = emby.getTvChannels() - elif filter == "recent": + elif filter_type == "recent": listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending") - elif filter == "random": + elif filter_type == "random": listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") - elif filter == "recommended": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite") - elif filter == "sets": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite") + elif filter_type == "recommended": + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") + elif filter_type == "sets": + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") else: listing = emby.getFilteredSection(folderid, itemtype=itemtype, recursive=False) @@ -819,14 +821,14 @@ def BrowseContent(viewname, type="", folderid=""): li = createListItemFromEmbyItem(item,art,doUtils) if item.get("IsFolder") == True: #for folders we add an additional browse request, passing the folderId - path = "%s?id=%s&mode=browsecontent&type=%s&folderid=%s" % (sys.argv[0].decode('utf-8'), viewname.decode('utf-8'), type.decode('utf-8'), item.get("Id").decode('utf-8')) + path = "%s?id=%s&mode=browsecontent&type=%s&folderid=%s" % (sys.argv[0].decode('utf-8'), viewname.decode('utf-8'), browse_type.decode('utf-8'), item.get("Id").decode('utf-8')) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True) else: #playable item, set plugin path and mediastreams xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=li.getProperty("path"), listitem=li) - if filter == "recent": + if filter_type == "recent": xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) else: xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 8683f106..0f0bbc2f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -309,10 +309,9 @@ class Movies(Items): count = 0 for boxset in items: - title = boxset['Name'] if pdialog: percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) + pdialog.update(percentage, message=boxset['Name']) count += 1 self.add_updateBoxset(boxset) @@ -333,7 +332,6 @@ class Movies(Items): # Process single movie kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -517,23 +515,23 @@ class Movies(Items): kodi_db.addCountries(movieid, countries, "movie") # Process cast people = API.getPeopleList() - kodi_db.addPeople(movieid, people, "movie") + self.kodi_db.addPeople(movieid, people, "movie") # Process genres - kodi_db.addGenres(movieid, genres, "movie") + self.kodi_db.addGenres(movieid, genres, "movie") # Process artwork allartworks = API.getAllArtwork() artwork.addArtwork(allartworks, movieid, "movie", kodicursor) # Process stream details streams = API.getMediaStreams() - kodi_db.addStreams(fileid, streams, runtime) + self.kodi_db.addStreams(fileid, streams, runtime) # Process studios - kodi_db.addStudios(movieid, studios, "movie") + self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags tags = [viewtag] tags.extend(collections) if userdata['Favorite']: tags.append("Favorite movies") - kodi_db.addTags(movieid, tags, "movie") + self.kodi_db.addTags(movieid, tags, "movie") # Process playstates kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) @@ -603,7 +601,6 @@ class MusicVideos(Items): # Process single music video kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = api.API(item) @@ -794,32 +791,31 @@ class MusicVideos(Items): artist['Type'] = "Artist" people.extend(artists) people = artwork.getPeopleArtwork(people) - kodi_db.addPeople(mvideoid, people, "musicvideo") + self.kodi_db.addPeople(mvideoid, people, "musicvideo") # Process genres - kodi_db.addGenres(mvideoid, genres, "musicvideo") + self.kodi_db.addGenres(mvideoid, genres, "musicvideo") # Process artwork artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor) # Process stream details streams = API.getMediaStreams() - kodi_db.addStreams(fileid, streams, runtime) + self.kodi_db.addStreams(fileid, streams, runtime) # Process studios - kodi_db.addStudios(mvideoid, studios, "musicvideo") + self.kodi_db.addStudios(mvideoid, studios, "musicvideo") # Process tags: view, emby tags tags = [viewtag] tags.extend(item['Tags']) if userdata['Favorite']: tags.append("Favorite musicvideos") - kodi_db.addTags(mvideoid, tags, "musicvideo") + self.kodi_db.addTags(mvideoid, tags, "musicvideo") # Process playstates resume = API.adjustResume(userdata['Resume']) total = round(float(runtime), 6) - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) def updateUserdata(self, item): # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks # Poster with progress bar emby_db = self.emby_db - kodi_db = self.kodi_db API = api.API(item) # Get emby information @@ -841,9 +837,9 @@ class MusicVideos(Items): # Process favorite tags if userdata['Favorite']: - kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") + self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") else: - kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") + self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") # Process playstates playcount = userdata['PlayCount'] @@ -851,7 +847,7 @@ class MusicVideos(Items): resume = API.adjustResume(userdata['Resume']) total = round(float(runtime), 6) - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) emby_db.updateReference(itemid, checksum) def remove(self, itemid): @@ -878,8 +874,7 @@ class MusicVideos(Items): "AND media_type = 'musicvideo'" )) kodicursor.execute(query, (mvideoid,)) - rows = kodicursor.fetchall() - for row in rows: + for row in kodicursor.fetchall(): url = row[0] imagetype = row[1] @@ -959,7 +954,6 @@ class TVShows(Items): # Process single tvshow kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -1079,7 +1073,7 @@ class TVShows(Items): kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid)) # Add path - pathid = kodi_db.addPath(path) + pathid = self.kodi_db.addPath(path) # Create the tvshow entry query = ( @@ -1112,18 +1106,18 @@ class TVShows(Items): # Process cast people = API.getPeopleList() - kodi_db.addPeople(showid, people, "tvshow") + self.kodi_db.addPeople(showid, people, "tvshow") # Process genres - kodi_db.addGenres(showid, genres, "tvshow") + self.kodi_db.addGenres(showid, genres, "tvshow") # Process artwork allartworks = API.getAllArtwork() artwork.addArtwork(allartworks, showid, "tvshow", kodicursor) # Process studios - kodi_db.addStudios(showid, studios, "tvshow") + self.kodi_db.addStudios(showid, studios, "tvshow") # Process tags: view, PMS collection tags tags = [viewtag] tags.extend(collections) - kodi_db.addTags(showid, tags, "tvshow") + self.kodi_db.addTags(showid, tags, "tvshow") if force_episodes: # We needed to recreate the show entry. Re-add episodes now. @@ -1154,7 +1148,6 @@ class TVShows(Items): return kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork seasonnum = API.getIndex() # Get parent tv show Plex id @@ -1210,10 +1203,8 @@ class TVShows(Items): viewtag and viewid are irrelevant! """ # Process single episode - kodiversion = self.kodiversion kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -1310,7 +1301,7 @@ class TVShows(Items): # self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1) self.logMsg("Parent tvshow now found, skip item", 2) return False - seasonid = kodi_db.addSeason(showid, season) + seasonid = self.kodi_db.addSeason(showid, season) # GET THE FILE AND PATH ##### doIndirect = not self.directpath @@ -1368,7 +1359,7 @@ class TVShows(Items): self.logMsg("UPDATE episode itemid: %s" % (itemid), 1) # Update the movie entry - if kodiversion in (16, 17): + if self.kodiversion in (16, 17): # Kodi Jarvis, Krypton query = ' '.join(( @@ -1400,10 +1391,9 @@ class TVShows(Items): ##### OR ADD THE EPISODE ##### else: - self.logMsg("ADD episode itemid: %s" % (itemid), 1) - + self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1) # Create the episode entry - if kodiversion in (16, 17): + if self.kodiversion in (16, 17): # Kodi Jarvis, Krypton query = ( ''' @@ -1454,7 +1444,7 @@ class TVShows(Items): kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process cast people = API.getPeopleList() - kodi_db.addPeople(episodeid, people, "episode") + self.kodi_db.addPeople(episodeid, people, "episode") # Process artwork # Wide "screenshot" of particular episode poster = item.attrib.get('thumb') @@ -1473,13 +1463,13 @@ class TVShows(Items): # Process stream details streams = API.getMediaStreams() - kodi_db.addStreams(fileid, streams, runtime) + self.kodi_db.addStreams(fileid, streams, runtime) # Process playstates - kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) + self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) if not self.directpath and resume: # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/") - tempfileid = kodi_db.addFile(filename, temppathid) + temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/") + tempfileid = self.kodi_db.addFile(filename, temppathid) query = ' '.join(( "UPDATE files", @@ -1487,7 +1477,7 @@ class TVShows(Items): "WHERE idFile = ?" )) kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) - kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed) + self.kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed) self.kodiconn.commit() self.embyconn.commit() @@ -1600,27 +1590,23 @@ class TVShows(Items): def removeShow(self, kodiid): kodicursor = self.kodicursor - artwork = self.artwork - - artwork.deleteArtwork(kodiid, "tvshow", kodicursor) + self.artwork.deleteArtwork(kodiid, "tvshow", kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,)) self.logMsg("Removed tvshow: %s." % kodiid, 2) def removeSeason(self, kodiid): kodicursor = self.kodicursor - artwork = self.artwork - artwork.deleteArtwork(kodiid, "season", kodicursor) + self.artwork.deleteArtwork(kodiid, "season", kodicursor) kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,)) self.logMsg("Removed season: %s." % kodiid, 2) def removeEpisode(self, kodiid, fileid): kodicursor = self.kodicursor - artwork = self.artwork - artwork.deleteArtwork(kodiid, "episode", kodicursor) + self.artwork.deleteArtwork(kodiid, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,)) kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) self.logMsg("Removed episode: %s." % kodiid, 2) @@ -1656,10 +1642,9 @@ class Music(Items): count = 0 for artist in items: - title = artist['Name'] if pdialog: percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) + pdialog.update(percentage, message=artist['Name']) count += 1 self.add_updateArtist(artist) # Add albums @@ -1672,10 +1657,9 @@ class Music(Items): count = 0 for album in items: - title = album['Name'] if pdialog: percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) + pdialog.update(percentage, message=album['Name']) count += 1 self.add_updateAlbum(album) # Add songs @@ -1688,14 +1672,13 @@ class Music(Items): count = 0 for song in items: - title = song['Name'] if pdialog: percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) + pdialog.update(percentage, message=song['Name']) count += 1 self.add_updateSong(song) if not pdialog and self.contentmsg: - self.contentPop(title, self.newmusic_time) + self.contentPop(song['Name'], self.newmusic_time) def add_updateArtist(self, item, viewtag=None, viewid=None, artisttype="MusicArtist"): try: @@ -1715,7 +1698,6 @@ class Music(Items): artisttype="MusicArtist"): kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -1764,7 +1746,7 @@ class Music(Items): # multiple times. # Kodi doesn't allow that. In case that happens we just merge the # artist entries. - artistid = kodi_db.addArtist(name, musicBrainzId) + artistid = self.kodi_db.addArtist(name, musicBrainzId) # Create the reference in emby table emby_db.addReference( itemid, artistid, artisttype, "artist", checksum=checksum) @@ -1811,10 +1793,8 @@ class Music(Items): return def run_add_updateAlbum(self, item, viewtag=None, viewid=None): - kodiversion = self.kodiversion kodicursor = self.kodicursor emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -1875,13 +1855,13 @@ class Music(Items): # multiple times. # Kodi doesn't allow that. In case that happens we just merge the # artist entries. - albumid = kodi_db.addAlbum(name, musicBrainzId) + albumid = self.kodi_db.addAlbum(name, musicBrainzId) # Create the reference in emby table emby_db.addReference( itemid, albumid, "MusicAlbum", "album", checksum=checksum) # Process the album info - if kodiversion == 17: + if self.kodiversion == 17: # Kodi Krypton query = ' '.join(( @@ -1894,7 +1874,7 @@ class Music(Items): kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, "album", studio, albumid)) - elif kodiversion == 16: + elif self.kodiversion == 16: # Kodi Jarvis query = ' '.join(( @@ -1907,7 +1887,7 @@ class Music(Items): kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, "album", studio, albumid)) - elif kodiversion == 15: + elif self.kodiversion == 15: # Kodi Isengard query = ' '.join(( @@ -1998,7 +1978,7 @@ class Music(Items): # Update emby reference with parentid emby_db.updateParentId(artistId, albumid) # Add genres - kodi_db.addMusicGenres(albumid, genres, "album") + self.kodi_db.addMusicGenres(albumid, genres, "album") # Update artwork artwork.addArtwork(artworks, albumid, "album", kodicursor) self.embyconn.commit() @@ -2020,11 +2000,9 @@ class Music(Items): def run_add_updateSong(self, item, viewtag=None, viewid=None): # Process single song - kodiversion = self.kodiversion kodicursor = self.kodicursor emby = self.emby emby_db = self.emby_db - kodi_db = self.kodi_db artwork = self.artwork API = PlexAPI.API(item) @@ -2136,7 +2114,7 @@ class Music(Items): self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1) # Add path - pathid = kodi_db.addPath(path, strHash="123") + pathid = self.kodi_db.addPath(path, strHash="123") try: # Get the album @@ -2148,7 +2126,7 @@ class Music(Items): album_name = item.get('parentTitle') if album_name: self.logMsg("Creating virtual music album for song: %s." % itemid, 1) - albumid = kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) + albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album") else: # No album Id associated to the song. @@ -2173,7 +2151,7 @@ class Music(Items): self.logMsg("Failed to add album. Creating singles.", 1) kodicursor.execute("select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 - if kodiversion == 16: + if self.kodiversion == 16: # Kodi Jarvis query = ( ''' @@ -2183,7 +2161,7 @@ class Music(Items): ''' ) kodicursor.execute(query, (albumid, genre, year, "single")) - elif kodiversion == 15: + elif self.kodiversion == 15: # Kodi Isengard query = ( ''' @@ -2316,11 +2294,11 @@ class Music(Items): result = kodicursor.fetchone() if result and result[0] != album_artists: # Field is empty - if kodiversion in (16, 17): + if self.kodiversion in (16, 17): # Kodi Jarvis, Krypton query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" kodicursor.execute(query, (album_artists, albumid)) - elif kodiversion == 15: + elif self.kodiversion == 15: # Kodi Isengard query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" kodicursor.execute(query, (album_artists, albumid)) @@ -2330,7 +2308,7 @@ class Music(Items): kodicursor.execute(query, (album_artists, albumid)) # Add genres - kodi_db.addMusicGenres(songid, genres, "song") + self.kodi_db.addMusicGenres(songid, genres, "song") # Update artwork allart = API.getAllArtwork(parentInfo=True) @@ -2372,10 +2350,9 @@ class Music(Items): self.removeSong(kodiid) # This should only address single song scenario, where server doesn't actually # create an album for the song. - customitems = emby_db.getItem_byWildId(itemid) emby_db.removeWildItem(itemid) - for item in customitems: + for item in emby_db.getItem_byWildId(itemid): item_kid = item[0] item_mediatype = item[1] @@ -2431,23 +2408,16 @@ class Music(Items): def removeSong(self, kodiid): kodicursor = self.kodicursor - artwork = self.artwork - artwork.deleteArtwork(kodiid, "song", kodicursor) - kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) + self.artwork.deleteArtwork(kodiid, "song", self.kodicursor) + self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) def removeAlbum(self, kodiid): - kodicursor = self.kodicursor - artwork = self.artwork - - artwork.deleteArtwork(kodiid, "album", kodicursor) - kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) + self.artwork.deleteArtwork(kodiid, "album", self.kodicursor) + self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) def removeArtist(self, kodiid): - kodicursor = self.kodicursor - artwork = self.artwork - - artwork.deleteArtwork(kodiid, "artist", kodicursor) - kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,)) + self.artwork.deleteArtwork(kodiid, "artist", self.kodicursor) + self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 4ee18755..29eee017 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -100,19 +100,18 @@ class Kodidb_Functions(): # SQL won't return existing paths otherwise if path is None: path = "" - cursor = self.cursor query = ' '.join(( "SELECT idPath", "FROM path", "WHERE strPath = ?" )) - cursor.execute(query, (path,)) + self.cursor.execute(query, (path,)) try: - pathid = cursor.fetchone()[0] + pathid = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idPath),0) from path") - pathid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idPath),0) from path") + pathid = self.cursor.fetchone()[0] + 1 if strHash is None: query = ( ''' @@ -122,7 +121,7 @@ class Kodidb_Functions(): VALUES (?, ?) ''' ) - cursor.execute(query, (pathid, path)) + self.cursor.execute(query, (pathid, path)) else: query = ( ''' @@ -132,23 +131,21 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (pathid, path, strHash)) + self.cursor.execute(query, (pathid, path, strHash)) return pathid def getPath(self, path): - cursor = self.cursor - query = ' '.join(( "SELECT idPath", "FROM path", "WHERE strPath = ?" )) - cursor.execute(query, (path,)) + self.cursor.execute(query, (path,)) try: - pathid = cursor.fetchone()[0] + pathid = self.cursor.fetchone()[0] except TypeError: pathid = None @@ -156,8 +153,6 @@ class Kodidb_Functions(): def addFile(self, filename, pathid): - cursor = self.cursor - query = ' '.join(( "SELECT idFile", @@ -165,12 +160,12 @@ class Kodidb_Functions(): "WHERE strFilename = ?", "AND idPath = ?" )) - cursor.execute(query, (filename, pathid,)) + self.cursor.execute(query, (filename, pathid,)) try: - fileid = cursor.fetchone()[0] + fileid = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idFile),0) from files") - fileid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idFile),0) from files") + fileid = self.cursor.fetchone()[0] + 1 query = ( ''' INSERT INTO files( @@ -179,23 +174,21 @@ class Kodidb_Functions(): VALUES (?, ?) ''' ) - cursor.execute(query, (fileid, filename)) + self.cursor.execute(query, (fileid, filename)) return fileid def getFile(self, fileid): - cursor = self.cursor - query = ' '.join(( "SELECT strFilename", "FROM files", "WHERE idFile = ?" )) - cursor.execute(query, (fileid,)) + self.cursor.execute(query, (fileid,)) try: - filename = cursor.fetchone()[0] + filename = self.cursor.fetchone()[0] except TypeError: filename = "" @@ -216,8 +209,6 @@ class Kodidb_Functions(): def addCountries(self, kodiid, countries, mediatype): - cursor = self.cursor - if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton for country in countries: @@ -228,18 +219,18 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (country,)) + self.cursor.execute(query, (country,)) try: - country_id = cursor.fetchone()[0] + country_id = self.cursor.fetchone()[0] except TypeError: # Country entry does not exists - cursor.execute("select coalesce(max(country_id),0) from country") - country_id = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(country_id),0) from country") + country_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO country(country_id, name) values(?, ?)" - cursor.execute(query, (country_id, country)) + self.cursor.execute(query, (country_id, country)) self.logMsg("Add country to media, processing: %s" % country, 2) finally: # Assign country to content @@ -251,7 +242,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (country_id, kodiid, mediatype)) + self.cursor.execute(query, (country_id, kodiid, mediatype)) else: # Kodi Helix for country in countries: @@ -262,18 +253,18 @@ class Kodidb_Functions(): "WHERE strCountry = ?", "COLLATE NOCASE" )) - cursor.execute(query, (country,)) + self.cursor.execute(query, (country,)) try: - idCountry = cursor.fetchone()[0] + idCountry = self.cursor.fetchone()[0] except TypeError: # Country entry does not exists - cursor.execute("select coalesce(max(idCountry),0) from country") - idCountry = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idCountry),0) from country") + idCountry = self.cursor.fetchone()[0] + 1 query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" - cursor.execute(query, (idCountry, country)) + self.cursor.execute(query, (idCountry, country)) self.logMsg("Add country to media, processing: %s" % country, 2) finally: @@ -287,23 +278,19 @@ class Kodidb_Functions(): VALUES (?, ?) ''' ) - cursor.execute(query, (idCountry, kodiid)) + self.cursor.execute(query, (idCountry, kodiid)) def addPeople(self, kodiid, people, mediatype): - cursor = self.cursor - artwork = self.artwork - kodiversion = self.kodiversion - castorder = 1 for person in people: name = person['Name'] - type = person['Type'] + person_type = person['Type'] thumb = person['imageurl'] # Kodi Isengard, Jarvis, Krypton - if kodiversion in (15, 16, 17): + if self.kodiversion in (15, 16, 17): query = ' '.join(( "SELECT actor_id", @@ -311,22 +298,23 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (name,)) + self.cursor.execute(query, (name,)) try: - actorid = cursor.fetchone()[0] + actorid = self.cursor.fetchone()[0] except TypeError: # Cast entry does not exists - cursor.execute("select coalesce(max(actor_id),0) from actor") - actorid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(actor_id),0) from actor") + actorid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO actor(actor_id, name) values(?, ?)" - cursor.execute(query, (actorid, name)) + self.cursor.execute(query, (actorid, name)) + self.logMsg("Add people to media, processing: %s" % name, 2) finally: # Link person to content - if "Actor" in type: + if "Actor" in person_type: role = person.get('Role') query = ( ''' @@ -336,10 +324,10 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?, ?) ''' ) - cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) + self.cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) castorder += 1 - elif "Director" in type: + elif "Director" in person_type: query = ( ''' INSERT OR REPLACE INTO director_link( @@ -348,9 +336,9 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (actorid, kodiid, mediatype)) + self.cursor.execute(query, (actorid, kodiid, mediatype)) - elif type in ("Writing", "Writer"): + elif person_type in ("Writing", "Writer"): query = ( ''' INSERT OR REPLACE INTO writer_link( @@ -359,9 +347,9 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (actorid, kodiid, mediatype)) + self.cursor.execute(query, (actorid, kodiid, mediatype)) - elif "Artist" in type: + elif "Artist" in person_type: query = ( ''' INSERT OR REPLACE INTO actor_link( @@ -370,7 +358,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (actorid, kodiid, mediatype)) + self.cursor.execute(query, (actorid, kodiid, mediatype)) # Kodi Helix else: query = ' '.join(( @@ -380,22 +368,23 @@ class Kodidb_Functions(): "WHERE strActor = ?", "COLLATE NOCASE" )) - cursor.execute(query, (name,)) + self.cursor.execute(query, (name,)) try: - actorid = cursor.fetchone()[0] + actorid = self.cursor.fetchone()[0] except TypeError: # Cast entry does not exists - cursor.execute("select coalesce(max(idActor),0) from actors") - actorid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idActor),0) from actors") + actorid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO actors(idActor, strActor) values(?, ?)" - cursor.execute(query, (actorid, name)) + self.cursor.execute(query, (actorid, name)) + self.logMsg("Add people to media, processing: %s" % name, 2) finally: # Link person to content - if "Actor" in type: + if "Actor" in person_type: role = person.get('Role') if "movie" in mediatype: @@ -427,10 +416,10 @@ class Kodidb_Functions(): ) else: return # Item is invalid - cursor.execute(query, (actorid, kodiid, role, castorder)) + self.cursor.execute(query, (actorid, kodiid, role, castorder)) castorder += 1 - elif "Director" in type: + elif "Director" in person_type: if "movie" in mediatype: query = ( ''' @@ -470,9 +459,9 @@ class Kodidb_Functions(): ) else: return # Item is invalid - cursor.execute(query, (actorid, kodiid)) + self.cursor.execute(query, (actorid, kodiid)) - elif type in ("Writing", "Writer"): + elif person_type in ("Writing", "Writer"): if "movie" in mediatype: query = ( ''' @@ -493,9 +482,9 @@ class Kodidb_Functions(): ) else: return # Item is invalid - cursor.execute(query, (actorid, kodiid)) + self.cursor.execute(query, (actorid, kodiid)) - elif "Artist" in type: + elif "Artist" in person_type: query = ( ''' INSERT OR REPLACE INTO artistlinkmusicvideo( @@ -504,20 +493,19 @@ class Kodidb_Functions(): VALUES (?, ?) ''' ) - cursor.execute(query, (actorid, kodiid)) + self.cursor.execute(query, (actorid, kodiid)) # Add person image to art table if thumb: - arttype = type.lower() + arttype = person_type.lower() if "writing" in arttype: arttype = "writer" - artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", cursor) + self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor) def addGenres(self, kodiid, genres, mediatype): - cursor = self.cursor # Kodi Isengard, Jarvis, Krypton if self.kodiversion in (15, 16, 17): @@ -528,7 +516,7 @@ class Kodidb_Functions(): "WHERE media_id = ?", "AND media_type = ?" )) - cursor.execute(query, (kodiid, mediatype,)) + self.cursor.execute(query, (kodiid, mediatype,)) # Add genres for genre in genres: @@ -540,19 +528,20 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (genre,)) + self.cursor.execute(query, (genre,)) try: - genre_id = cursor.fetchone()[0] + genre_id = self.cursor.fetchone()[0] except TypeError: # Create genre in database - cursor.execute("select coalesce(max(genre_id),0) from genre") - genre_id = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(genre_id),0) from genre") + genre_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(genre_id, name) values(?, ?)" - cursor.execute(query, (genre_id, genre)) - + self.cursor.execute(query, (genre_id, genre)) + self.logMsg("Add Genres to media, processing: %s" % genre, 2) + finally: # Assign genre to item query = ( @@ -563,16 +552,16 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (genre_id, kodiid, mediatype)) + self.cursor.execute(query, (genre_id, kodiid, mediatype)) else: # Kodi Helix # Delete current genres for clean slate if "movie" in mediatype: - cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,)) + self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,)) elif "tvshow" in mediatype: - cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) + self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) elif "musicvideo" in mediatype: - cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) + self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) # Add genres for genre in genres: @@ -584,19 +573,20 @@ class Kodidb_Functions(): "WHERE strGenre = ?", "COLLATE NOCASE" )) - cursor.execute(query, (genre,)) + self.cursor.execute(query, (genre,)) try: - idGenre = cursor.fetchone()[0] + idGenre = self.cursor.fetchone()[0] except TypeError: # Create genre in database - cursor.execute("select coalesce(max(idGenre),0) from genre") - idGenre = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + idGenre = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - cursor.execute(query, (idGenre, genre)) - + self.cursor.execute(query, (idGenre, genre)) + self.logMsg("Add Genres to media, processing: %s" % genre, 2) + finally: # Assign genre to item if "movie" in mediatype: @@ -628,16 +618,13 @@ class Kodidb_Functions(): ) else: return # Item is invalid - cursor.execute(query, (idGenre, kodiid)) + self.cursor.execute(query, (idGenre, kodiid)) def addStudios(self, kodiid, studios, mediatype): - cursor = self.cursor - kodiversion = self.kodiversion - for studio in studios: - if kodiversion in (15, 16, 17): + if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -646,17 +633,18 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (studio,)) + self.cursor.execute(query, (studio,)) try: - studioid = cursor.fetchone()[0] + studioid = self.cursor.fetchone()[0] except TypeError: # Studio does not exists. - cursor.execute("select coalesce(max(studio_id),0) from studio") - studioid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(studio_id),0) from studio") + studioid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO studio(studio_id, name) values(?, ?)" - cursor.execute(query, (studioid, studio)) + self.cursor.execute(query, (studioid, studio)) + self.logMsg("Add Studios to media, processing: %s" % studio, 2) finally: # Assign studio to item query = ( @@ -666,7 +654,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''') - cursor.execute(query, (studioid, kodiid, mediatype)) + self.cursor.execute(query, (studioid, kodiid, mediatype)) else: # Kodi Helix query = ' '.join(( @@ -676,17 +664,18 @@ class Kodidb_Functions(): "WHERE strstudio = ?", "COLLATE NOCASE" )) - cursor.execute(query, (studio,)) + self.cursor.execute(query, (studio,)) try: - studioid = cursor.fetchone()[0] + studioid = self.cursor.fetchone()[0] except TypeError: # Studio does not exists. - cursor.execute("select coalesce(max(idstudio),0) from studio") - studioid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idstudio),0) from studio") + studioid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" - cursor.execute(query, (studioid, studio)) + self.cursor.execute(query, (studioid, studio)) + self.logMsg("Add Studios to media, processing: %s" % studio, 2) finally: # Assign studio to item if "movie" in mediatype: @@ -713,14 +702,12 @@ class Kodidb_Functions(): INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) VALUES (?, ?) ''') - cursor.execute(query, (studioid, kodiid)) + self.cursor.execute(query, (studioid, kodiid)) def addStreams(self, fileid, streamdetails, runtime): - cursor = self.cursor - # First remove any existing entries - cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) + self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) if streamdetails: # Video details for videotrack in streamdetails['video']: @@ -733,7 +720,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''' ) - cursor.execute(query, (fileid, 0, videotrack['codec'], + self.cursor.execute(query, (fileid, 0, videotrack['codec'], videotrack['aspect'], videotrack['width'], videotrack['height'], runtime ,videotrack['video3DFormat'])) @@ -747,7 +734,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?, ?) ''' ) - cursor.execute(query, (fileid, 1, audiotrack['codec'], + self.cursor.execute(query, (fileid, 1, audiotrack['codec'], audiotrack['channels'], audiotrack['language'])) # Subtitles details @@ -760,7 +747,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (fileid, 2, subtitletrack)) + self.cursor.execute(query, (fileid, 2, subtitletrack)) def getResumes(self): """ @@ -926,35 +913,27 @@ class Kodidb_Functions(): return int(runtime) def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): - cursor = self.cursor # Delete existing resume point query = ' '.join(( "DELETE FROM bookmark", "WHERE idFile = ?" )) - cursor.execute(query, (fileid,)) + self.cursor.execute(query, (fileid,)) # Set watched count - if playcount is None: - query = ' '.join(( - "UPDATE files", - "SET lastPlayed = ?", - "WHERE idFile = ?" - )) - cursor.execute(query, (dateplayed, fileid)) - else: - query = ' '.join(( - "UPDATE files", - "SET playCount = ?, lastPlayed = ?", - "WHERE idFile = ?" - )) - cursor.execute(query, (playcount, dateplayed, fileid)) + query = ' '.join(( + + "UPDATE files", + "SET playCount = ?, lastPlayed = ?", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (playcount, dateplayed, fileid)) # Set the resume bookmark if resume_seconds: - cursor.execute("select coalesce(max(idBookmark),0) from bookmark") - bookmarkId = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") + bookmarkId = self.cursor.fetchone()[0] + 1 query = ( ''' INSERT INTO bookmark( @@ -963,12 +942,10 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?, ?, ?) ''' ) - cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, + self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, "DVDPlayer", 1)) def addTags(self, kodiid, tags, mediatype): - - cursor = self.cursor # First, delete any existing tags associated to the id if self.kodiversion in (15, 16, 17): @@ -979,7 +956,7 @@ class Kodidb_Functions(): "WHERE media_id = ?", "AND media_type = ?" )) - cursor.execute(query, (kodiid, mediatype)) + self.cursor.execute(query, (kodiid, mediatype)) else: # Kodi Helix query = ' '.join(( @@ -988,16 +965,15 @@ class Kodidb_Functions(): "WHERE idMedia = ?", "AND media_type = ?" )) - cursor.execute(query, (kodiid, mediatype)) + self.cursor.execute(query, (kodiid, mediatype)) # Add tags + self.logMsg("Adding Tags: %s" % tags, 2) for tag in tags: self.addTag(kodiid, tag, mediatype) def addTag(self, kodiid, tag, mediatype): - cursor = self.cursor - if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1007,9 +983,9 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (tag,)) + self.cursor.execute(query, (tag,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: # Create the tag, because it does not exist @@ -1026,7 +1002,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (tag_id, kodiid, mediatype)) + self.cursor.execute(query, (tag_id, kodiid, mediatype)) else: # Kodi Helix query = ' '.join(( @@ -1036,9 +1012,9 @@ class Kodidb_Functions(): "WHERE strTag = ?", "COLLATE NOCASE" )) - cursor.execute(query, (tag,)) + self.cursor.execute(query, (tag,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: # Create the tag @@ -1055,12 +1031,10 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (tag_id, kodiid, mediatype)) + self.cursor.execute(query, (tag_id, kodiid, mediatype)) def createTag(self, name): - cursor = self.cursor - # This will create and return the tag_id if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton @@ -1071,16 +1045,16 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (name,)) + self.cursor.execute(query, (name,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(tag_id),0) from tag") - tag_id = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(tag_id),0) from tag") + tag_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO tag(tag_id, name) values(?, ?)" - cursor.execute(query, (tag_id, name)) + self.cursor.execute(query, (tag_id, name)) self.logMsg("Create tag_id: %s name: %s" % (tag_id, name), 2) else: # Kodi Helix @@ -1091,23 +1065,22 @@ class Kodidb_Functions(): "WHERE strTag = ?", "COLLATE NOCASE" )) - cursor.execute(query, (name,)) + self.cursor.execute(query, (name,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idTag),0) from tag") - tag_id = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idTag),0) from tag") + tag_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO tag(idTag, strTag) values(?, ?)" - cursor.execute(query, (tag_id, name)) + self.cursor.execute(query, (tag_id, name)) self.logMsg("Create idTag: %s name: %s" % (tag_id, name), 2) return tag_id def updateTag(self, oldtag, newtag, kodiid, mediatype): - cursor = self.cursor self.logMsg("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid), 2) if self.kodiversion in (15, 16, 17): @@ -1121,7 +1094,7 @@ class Kodidb_Functions(): "AND media_type = ?", "AND tag_id = ?" )) - cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) + self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) except Exception as e: # The new tag we are going to apply already exists for this item # delete current tag instead @@ -1133,7 +1106,7 @@ class Kodidb_Functions(): "AND media_type = ?", "AND tag_id = ?" )) - cursor.execute(query, (kodiid, mediatype, oldtag,)) + self.cursor.execute(query, (kodiid, mediatype, oldtag,)) else: # Kodi Helix try: @@ -1145,7 +1118,7 @@ class Kodidb_Functions(): "AND media_type = ?", "AND idTag = ?" )) - cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) + self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) except Exception as e: # The new tag we are going to apply already exists for this item # delete current tag instead @@ -1157,12 +1130,10 @@ class Kodidb_Functions(): "AND media_type = ?", "AND idTag = ?" )) - cursor.execute(query, (kodiid, mediatype, oldtag,)) + self.cursor.execute(query, (kodiid, mediatype, oldtag,)) def removeTag(self, kodiid, tagname, mediatype): - cursor = self.cursor - if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1172,9 +1143,9 @@ class Kodidb_Functions(): "WHERE name = ?", "COLLATE NOCASE" )) - cursor.execute(query, (tagname,)) + self.cursor.execute(query, (tagname,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: return else: @@ -1185,7 +1156,7 @@ class Kodidb_Functions(): "AND media_type = ?", "AND tag_id = ?" )) - cursor.execute(query, (kodiid, mediatype, tag_id,)) + self.cursor.execute(query, (kodiid, mediatype, tag_id,)) else: # Kodi Helix query = ' '.join(( @@ -1195,9 +1166,9 @@ class Kodidb_Functions(): "WHERE strTag = ?", "COLLATE NOCASE" )) - cursor.execute(query, (tagname,)) + self.cursor.execute(query, (tagname,)) try: - tag_id = cursor.fetchone()[0] + tag_id = self.cursor.fetchone()[0] except TypeError: return else: @@ -1208,11 +1179,10 @@ class Kodidb_Functions(): "AND media_type = ?", "AND idTag = ?" )) - cursor.execute(query, (kodiid, mediatype, tag_id,)) + self.cursor.execute(query, (kodiid, mediatype, tag_id,)) def createBoxset(self, boxsetname): - cursor = self.cursor self.logMsg("Adding boxset: %s" % boxsetname, 2) query = ' '.join(( @@ -1221,16 +1191,16 @@ class Kodidb_Functions(): "WHERE strSet = ?", "COLLATE NOCASE" )) - cursor.execute(query, (boxsetname,)) + self.cursor.execute(query, (boxsetname,)) try: - setid = cursor.fetchone()[0] + setid = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idSet),0) from sets") - setid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idSet),0) from sets") + setid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO sets(idSet, strSet) values(?, ?)" - cursor.execute(query, (setid, boxsetname)) + self.cursor.execute(query, (setid, boxsetname)) return setid @@ -1256,8 +1226,6 @@ class Kodidb_Functions(): def addSeason(self, showid, seasonnumber): - cursor = self.cursor - query = ' '.join(( "SELECT idSeason", @@ -1265,30 +1233,28 @@ class Kodidb_Functions(): "WHERE idShow = ?", "AND season = ?" )) - cursor.execute(query, (showid, seasonnumber,)) + self.cursor.execute(query, (showid, seasonnumber,)) try: - seasonid = cursor.fetchone()[0] + seasonid = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idSeason),0) from seasons") - seasonid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idSeason),0) from seasons") + seasonid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" - cursor.execute(query, (seasonid, showid, seasonnumber)) + self.cursor.execute(query, (seasonid, showid, seasonnumber)) return seasonid def addArtist(self, name, musicbrainz): - cursor = self.cursor - query = ' '.join(( "SELECT idArtist, strArtist", "FROM artist", "WHERE strMusicBrainzArtistID = ?" )) - cursor.execute(query, (musicbrainz,)) + self.cursor.execute(query, (musicbrainz,)) try: - result = cursor.fetchone() + result = self.cursor.fetchone() artistid = result[0] artistname = result[1] @@ -1301,12 +1267,12 @@ class Kodidb_Functions(): "WHERE strArtist = ?", "COLLATE NOCASE" )) - cursor.execute(query, (name,)) + self.cursor.execute(query, (name,)) try: - artistid = cursor.fetchone()[0] + artistid = self.cursor.fetchone()[0] except TypeError: - cursor.execute("select coalesce(max(idArtist),0) from artist") - artistid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idArtist),0) from artist") + artistid = self.cursor.fetchone()[0] + 1 query = ( ''' INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) @@ -1314,33 +1280,30 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (artistid, name, musicbrainz)) + self.cursor.execute(query, (artistid, name, musicbrainz)) else: if artistname != name: query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - cursor.execute(query, (name, artistid,)) + self.cursor.execute(query, (name, artistid,)) return artistid def addAlbum(self, name, musicbrainz): - kodiversion = self.kodiversion - cursor = self.cursor - query = ' '.join(( "SELECT idAlbum", "FROM album", "WHERE strMusicBrainzAlbumID = ?" )) - cursor.execute(query, (musicbrainz,)) + self.cursor.execute(query, (musicbrainz,)) try: - albumid = cursor.fetchone()[0] + albumid = self.cursor.fetchone()[0] except TypeError: # Create the album - cursor.execute("select coalesce(max(idAlbum),0) from album") - albumid = cursor.fetchone()[0] + 1 - if kodiversion in (15, 16, 17): + self.cursor.execute("select coalesce(max(idAlbum),0) from album") + albumid = self.cursor.fetchone()[0] + 1 + if self.kodiversion in (15, 16, 17): query = ( ''' INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) @@ -1348,7 +1311,7 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?) ''' ) - cursor.execute(query, (albumid, name, musicbrainz, "album")) + self.cursor.execute(query, (albumid, name, musicbrainz, "album")) else: # Helix query = ( ''' @@ -1357,14 +1320,12 @@ class Kodidb_Functions(): VALUES (?, ?, ?) ''' ) - cursor.execute(query, (albumid, name, musicbrainz)) + self.cursor.execute(query, (albumid, name, musicbrainz)) return albumid def addMusicGenres(self, kodiid, genres, mediatype): - cursor = self.cursor - if mediatype == "album": # Delete current genres for clean slate @@ -1373,7 +1334,7 @@ class Kodidb_Functions(): "DELETE FROM album_genre", "WHERE idAlbum = ?" )) - cursor.execute(query, (kodiid,)) + self.cursor.execute(query, (kodiid,)) for genre in genres: query = ' '.join(( @@ -1383,18 +1344,18 @@ class Kodidb_Functions(): "WHERE strGenre = ?", "COLLATE NOCASE" )) - cursor.execute(query, (genre,)) + self.cursor.execute(query, (genre,)) try: - genreid = cursor.fetchone()[0] + genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - cursor.execute(query, (genreid, genre)) + self.cursor.execute(query, (genreid, genre)) query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" - cursor.execute(query, (genreid, kodiid)) + self.cursor.execute(query, (genreid, kodiid)) elif mediatype == "song": @@ -1404,7 +1365,7 @@ class Kodidb_Functions(): "DELETE FROM song_genre", "WHERE idSong = ?" )) - cursor.execute(query, (kodiid,)) + self.cursor.execute(query, (kodiid,)) for genre in genres: query = ' '.join(( @@ -1414,15 +1375,15 @@ class Kodidb_Functions(): "WHERE strGenre = ?", "COLLATE NOCASE" )) - cursor.execute(query, (genre,)) + self.cursor.execute(query, (genre,)) try: - genreid = cursor.fetchone()[0] + genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = cursor.fetchone()[0] + 1 + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - cursor.execute(query, (genreid, genre)) + self.cursor.execute(query, (genreid, genre)) query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" - cursor.execute(query, (genreid, kodiid)) \ No newline at end of file + self.cursor.execute(query, (genreid, kodiid)) \ No newline at end of file diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 768c3b27..3ea4953c 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -82,13 +82,13 @@ class KodiMonitor(xbmc.Monitor): item = data.get('item') try: kodiid = item['id'] - type = item['type'] + item_type = item['type'] except (KeyError, TypeError): self.logMsg("Item is invalid for playstate update.", 1) else: # Send notification to the server. with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) + emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) try: itemid = emby_dbitem[0] except TypeError: @@ -137,7 +137,7 @@ class KodiMonitor(xbmc.Monitor): url = "{server}/emby/Items/%s?format=json" % itemid self.logMsg("Deleting request: %s" % itemid) - doUtils.downloadUrl(url, type="DELETE") + doUtils.downloadUrl(url, action_type="DELETE") finally: embycursor.close()''' diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py index 1f977781..3a7dc294 100644 --- a/resources/lib/musicutils.py +++ b/resources/lib/musicutils.py @@ -193,6 +193,7 @@ def getSongTags(file): if pic.type == 3 and pic.data: #the file has an embedded cover hasEmbeddedCover = True + break if audio.get("rating"): rating = float(audio.get("rating")[0]) #flac rating is 0-100 and needs to be converted to 0-5 range diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index f96d2aaa..c691cebf 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -44,7 +44,6 @@ class PlaybackUtils(): def play(self, itemid, dbid=None): - log = self.logMsg window = utils.window settings = utils.settings @@ -56,7 +55,7 @@ class PlaybackUtils(): listitem = xbmcgui.ListItem() playutils = putils.PlayUtils(item[0]) - log("Play called.", 1) + self.logMsg("Play called.", 1) playurl = playutils.getPlayUrl() if not playurl: return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) @@ -101,9 +100,9 @@ class PlaybackUtils(): introsPlaylist = False dummyPlaylist = False - log("Playlist start position: %s" % startPos, 1) - log("Playlist plugin position: %s" % self.currentPosition, 1) - log("Playlist size: %s" % sizePlaylist, 1) + self.logMsg("Playlist start position: %s" % startPos, 2) + self.logMsg("Playlist plugin position: %s" % currentPosition, 2) + self.logMsg("Playlist size: %s" % sizePlaylist, 2) ############### RESUME POINT ################ @@ -114,11 +113,11 @@ class PlaybackUtils(): if not propertiesPlayback: window('emby_playbackProps', value="true") - log("Setting up properties in playlist.", 1) + self.logMsg("Setting up properties in playlist.", 1) if (not homeScreen and not seektime and window('emby_customPlaylist') != "true"): - log("Adding dummy file to playlist.", 2) + self.logMsg("Adding dummy file to playlist.", 2) dummyPlaylist = True playlist.add(playurl, listitem, index=startPos) # Remove the original item from playlist @@ -181,13 +180,13 @@ class PlaybackUtils(): if dummyPlaylist: # Added a dummy file to the playlist, # because the first item is going to fail automatically. - log("Processed as a playlist. First item is skipped.", 1) + self.logMsg("Processed as a playlist. First item is skipped.", 1) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: - log("Resetting properties playback flag.", 2) + self.logMsg("Resetting properties playback flag.", 2) window('emby_playbackProps', clear=True) #self.pl.verifyPlaylist() @@ -206,18 +205,18 @@ class PlaybackUtils(): ############### PLAYBACK ################ if homeScreen and seektime and window('emby_customPlaylist') != "true": - log("Play as a widget item.", 1) + self.logMsg("Play as a widget item.", 1) API.CreateListItemFromPlexItem(listitem) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) elif ((introsPlaylist and window('emby_customPlaylist') == "true") or (homeScreen and not sizePlaylist)): # Playlist was created just now, play it. - log("Play playlist.", 1) + self.logMsg("Play playlist.", 1) xbmc.Player().play(playlist, startpos=startPos) else: - log("Play as a regular item.", 1) + self.logMsg("Play as a regular item.", 1) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) def AddTrailers(self, xml): diff --git a/resources/lib/player.py b/resources/lib/player.py index 4f0f5483..d6304675 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -51,15 +51,13 @@ class Player(xbmc.Player): """ Window values need to have been set in Kodimonitor.py """ - log = self.logMsg window = utils.window # Will be called when xbmc starts playing a file - xbmcplayer = self.xbmcplayer self.stopAll() # Get current file (in utf-8!) try: - currentFile = xbmcplayer.getPlayingFile() + currentFile = self.xbmcplayer.getPlayingFile() xbmc.sleep(300) except: currentFile = "" @@ -67,11 +65,11 @@ class Player(xbmc.Player): while not currentFile: xbmc.sleep(100) try: - currentFile = xbmcplayer.getPlayingFile() + currentFile = self.xbmcplayer.getPlayingFile() except: pass if count == 20: - log("Cancelling playback report...", 1) + self.logMsg("Cancelling playback report...", 1) break else: count += 1 @@ -220,6 +218,8 @@ class Player(xbmc.Player): except ValueError: runtime = xbmcplayer.getTotalTime() log("Runtime is missing, Kodi runtime: %s" % runtime, 1) + runtime = self.xbmcplayer.getTotalTime() + self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1) playQueueVersion = window('playQueueVersion') playQueueID = window('playQueueID') @@ -263,7 +263,7 @@ class Player(xbmc.Player): if not self.doNotify: return - log = self.logMsg + self.logMsg("reportPlayback Called", 2) log("reportPlayback Called", 2) @@ -382,7 +382,7 @@ class Player(xbmc.Player): if mapping: # Set in PlaybackUtils.py - log("Mapping for external subtitles index: %s" % mapping, 2) + self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) externalIndex = json.loads(mapping) if externalIndex.get(str(indexSubs)): @@ -404,7 +404,7 @@ class Player(xbmc.Player): # postdata = json.dumps(postdata) # self.ws.sendProgressUpdate(postdata) self.doUtils( - "{server}/:/timeline?" + urlencode(postdata), type="GET") + "{server}/:/timeline?" + urlencode(postdata), action_type="GET") def onPlayBackPaused(self): @@ -440,7 +440,6 @@ class Player(xbmc.Player): def onPlayBackStopped(self): # Will be called when user stops xbmc playing a file - log = self.logMsg window = utils.window log("ONPLAYBACK_STOPPED", 1) @@ -459,31 +458,28 @@ class Player(xbmc.Player): def stopAll(self): - log = self.logMsg lang = utils.language settings = utils.settings - doUtils = self.doUtils - if not self.played_info: return - log("Played_information: %s" % self.played_info, 1) + self.logMsg("Played_information: %s" % self.played_info, 1) # Process each items for item in self.played_info: data = self.played_info.get(item) if data: - log("Item path: %s" % item, 2) - log("Item data: %s" % data, 2) + self.logMsg("Item path: %s" % item, 2) + self.logMsg("Item data: %s" % data, 2) runtime = data['runtime'] currentPosition = data['currentPosition'] itemid = data['item_id'] refresh_id = data['refresh_id'] currentFile = data['currentfile'] - type = data['Type'] + media_type = data['Type'] playMethod = data['playmethod'] # Prevent manually mark as watched in Kodi monitor @@ -497,15 +493,15 @@ class Player(xbmc.Player): percentComplete = 0 markPlayedAt = float(settings('markPlayed')) / 100 - log("Percent complete: %s Mark played at: %s" + self.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt), 1) # Send the delete action to the server. offerDelete = False - if type == "Episode" and settings('deleteTV') == "true": + if media_type == "Episode" and settings('deleteTV') == "true": offerDelete = True - elif type == "Movie" and settings('deleteMovies') == "true": + elif media_type == "Movie" and settings('deleteMovies') == "true": offerDelete = True if settings('offerDelete') != "true": @@ -520,7 +516,7 @@ class Player(xbmc.Player): lang(33015), autoclose=120000) if not resp: - log("User skipped deletion.", 1) + self.logMsg("User skipped deletion.", 1) continue url = "{server}/emby/Items/%s?format=json" % itemid @@ -567,4 +563,4 @@ class Player(xbmc.Player): 'duration': int(duration) } url = url + urlencode(args) - self.doUtils(url, type="GET") + self.doUtils(url, action_type="GET") diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index b3f9c154..7376d55e 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -27,7 +27,6 @@ class Playlist(): self.emby = embyserver.Read_EmbyServer() def playAll(self, itemids, startat): - log = self.logMsg window = utils.window embyconn = utils.kodiSQL('emby') @@ -38,8 +37,8 @@ class Playlist(): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist.clear() - log("---*** PLAY ALL ***---", 1) - log("Items: %s and start at: %s" % (itemids, startat), 1) + self.logMsg("---*** PLAY ALL ***---", 1) + self.logMsg("Items: %s and start at: %s" % (itemids, startat), 1) started = False window('emby_customplaylist', value="true") @@ -76,14 +75,12 @@ class Playlist(): def modifyPlaylist(self, itemids): - log = self.logMsg - embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() emby_db = embydb.Embydb_Functions(embycursor) - log("---*** ADD TO PLAYLIST ***---", 1) - log("Items: %s" % itemids, 1) + self.logMsg("---*** ADD TO PLAYLIST ***---", 1) + self.logMsg("Items: %s" % itemids, 1) # player = xbmc.Player() playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) @@ -101,7 +98,7 @@ class Playlist(): # Add to playlist self.addtoPlaylist(dbid, mediatype) - log("Adding %s to playlist." % itemid, 1) + self.logMsg("Adding %s to playlist." % itemid, 1) self.verifyPlaylist() embycursor.close() @@ -124,8 +121,7 @@ class Playlist(): else: pl['params']['item'] = {'file': url} - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) def addtoPlaylist_xbmc(self, playlist, item): path = "plugin://plugin.video.plexkodiconnect.movies/" @@ -160,8 +156,7 @@ class Playlist(): else: pl['params']['item'] = {'file': url} - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) def verifyPlaylist(self): @@ -176,8 +171,7 @@ class Playlist(): 'properties': ['title', 'file'] } } - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) def removefromPlaylist(self, position): @@ -192,5 +186,4 @@ class Playlist(): 'position': position } } - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) \ No newline at end of file + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index f5e5dd34..09da276f 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -56,7 +56,7 @@ class PlayUtils(): utils.window('emby_%s.playmethod' % playurl, "DirectStream") elif self.isTranscoding(): - log("File is transcoding.", 1) + self.logMsg("File is transcoding.", 1) quality = { 'maxVideoBitrate': self.getBitrate(), 'videoResolution': self.getResolution(), @@ -73,16 +73,14 @@ class PlayUtils(): def httpPlay(self): # Audio, Video, Photo - item = self.item - server = self.server - itemid = item['Id'] - mediatype = item['MediaType'] + itemid = self.item['Id'] + mediatype = self.item['MediaType'] if mediatype == "Audio": - playurl = "%s/emby/Audio/%s/stream" % (server, itemid) + playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid) else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) + playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid) return playurl @@ -117,20 +115,16 @@ class PlayUtils(): def directPlay(self): - item = self.item - try: - playurl = item['MediaSources'][0]['Path'] + playurl = self.item['MediaSources'][0]['Path'] except (IndexError, KeyError): - playurl = item['Path'] + playurl = self.item['Path'] - if item.get('VideoType'): + if self.item.get('VideoType'): # Specific format modification - type = item['VideoType'] - - if type == "Dvd": + if self.item['VideoType'] == "Dvd": playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif type == "BluRay": + elif self.item['VideoType'] == "BluRay": playurl = "%s/BDMV/index.bdmv" % playurl # Assign network protocol @@ -146,26 +140,24 @@ class PlayUtils(): def fileExists(self): - log = self.logMsg - if 'Path' not in self.item: # File has no path defined in server return False # Convert path to direct play path = self.directPlay() - log("Verifying path: %s" % path, 1) + self.logMsg("Verifying path: %s" % path, 1) if xbmcvfs.exists(path): - log("Path exists.", 1) + self.logMsg("Path exists.", 1) return True elif ":" not in path: - log("Can't verify path, assumed linux. Still try to direct play.", 1) + self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1) return True else: - log("Failed to find file.", 1) + self.logMsg("Failed to find file.", 1) return False def h265enabled(self): @@ -197,6 +189,8 @@ class PlayUtils(): if utils.settings('playType') == "2": # User forcing to play via HTTP self.logMsg("User chose to transcode", 1) + self.logMsg("Resolution is: %sP, transcode for resolution: %sP+" + canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream'] return False if self.h265enabled(): return False @@ -244,26 +238,19 @@ class PlayUtils(): return True def isTranscoding(self): - # I hope Plex transcodes everything - return True - item = self.item - - canTranscode = item['MediaSources'][0]['SupportsTranscoding'] # Make sure the server supports it - if not canTranscode: + if not self.item['MediaSources'][0]['SupportsTranscoding']: return False return True def transcoding(self): - item = self.item - - if 'Path' in item and item['Path'].endswith('.strm'): + if 'Path' in self.item and self.item['Path'].endswith('.strm'): # Allow strm loading when transcoding playurl = self.directPlay() else: - itemid = item['Id'] + itemid = self.item['Id'] deviceId = self.clientInfo.getDeviceId() playurl = ( "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index 775fe600..1b58c025 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -31,8 +31,7 @@ class Read_EmbyServer(): # This will return the full item item = {} - url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid - result = self.doUtils(url) + result = self.doUtils("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid) if result: item = result @@ -45,13 +44,12 @@ class Read_EmbyServer(): itemlists = self.split_list(itemlist, 50) for itemlist in itemlists: # Will return basic information - url = "{server}/emby/Users/{UserId}/Items?&format=json" params = { 'Ids': ",".join(itemlist), 'Fields': "Etag" } - result = self.doUtils(url, parameters=params) + result = self.doUtils("{server}/emby/Users/{UserId}/Items?&format=json", parameters=params) if result: items.extend(result['Items']) @@ -64,7 +62,6 @@ class Read_EmbyServer(): itemlists = self.split_list(itemlist, 50) for itemlist in itemlists: - url = "{server}/emby/Users/{UserId}/Items?format=json" params = { "Ids": ",".join(itemlist), @@ -75,10 +72,10 @@ class Read_EmbyServer(): "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources" + "MediaSources,VoteCount" ) } - result = self.doUtils(url, parameters=params) + result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) if result: items.extend(result['Items']) @@ -87,13 +84,10 @@ class Read_EmbyServer(): def getView_embyId(self, itemid): # Returns ancestors using embyId viewId = None - url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid - result = self.doUtils(url) - for view in result: + for view in self.doUtils("{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid): - viewtype = view['Type'] - if viewtype == "CollectionFolder": + if view['Type'] == "CollectionFolder": # Found view viewId = view['Id'] @@ -120,8 +114,6 @@ class Read_EmbyServer(): return [viewName, viewId, mediatype] def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""): - doUtils = self.doUtils - url = "{server}/emby/Users/{UserId}/Items?format=json" params = { 'ParentId': parentid, @@ -140,11 +132,9 @@ class Read_EmbyServer(): "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") } - return doUtils(url, parameters=params) + return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) def getTvChannels(self): - doUtils = self.doUtils - url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" params = { 'EnableImages': True, @@ -154,11 +144,9 @@ class Read_EmbyServer(): "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") } - return doUtils(url, parameters=params) + return self.doUtils("{server}/emby/LiveTv/Channels/?userid={UserId}&format=json", parameters=params) def getTvRecordings(self, groupid): - doUtils = self.doUtils - url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" if groupid == "root": groupid = "" params = { @@ -170,13 +158,10 @@ class Read_EmbyServer(): "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") } - return doUtils(url, parameters=params) + return self.doUtils("{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json", parameters=params) def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None): - log = self.logMsg - - doUtils = self.doUtils items = { 'Items': [], @@ -195,13 +180,13 @@ class Read_EmbyServer(): 'Recursive': True, 'Limit': 1 } - result = doUtils(url, parameters=params) + result = self.doUtils(url, parameters=params) try: total = result['TotalRecordCount'] items['TotalRecordCount'] = total except TypeError: # Failed to retrieve - log("%s:%s Failed to retrieve the server response." % (url, params), 2) + self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2) else: index = 0 @@ -234,36 +219,36 @@ class Read_EmbyServer(): "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources" + "MediaSources,VoteCount" ) - result = doUtils(url, parameters=params) + result = self.doUtils(url, parameters=params) try: items['Items'].extend(result['Items']) except TypeError: # Something happened to the connection if not throttled: throttled = True - log("Throttle activated.", 1) + self.logMsg("Throttle activated.", 1) if jump == highestjump: # We already tried with the highestjump, but it failed. Reset value. - log("Reset highest value.", 1) + self.logMsg("Reset highest value.", 1) highestjump = 0 # Lower the number by half if highestjump: throttled = False jump = highestjump - log("Throttle deactivated.", 1) + self.logMsg("Throttle deactivated.", 1) else: jump = int(jump/4) - log("Set jump limit to recover: %s" % jump, 2) + self.logMsg("Set jump limit to recover: %s" % jump, 2) retry = 0 while utils.window('emby_online') != "true": # Wait server to come back online if retry == 5: - log("Unable to reconnect to server. Abort process.", 1) + self.logMsg("Unable to reconnect to server. Abort process.", 1) return items retry += 1 @@ -291,12 +276,11 @@ class Read_EmbyServer(): increment = 10 jump += increment - log("Increase jump limit to: %s" % jump, 1) + self.logMsg("Increase jump limit to: %s" % jump, 1) return items def getViews(self, mediatype="", root=False, sortedlist=False): # Build a list of user views - doUtils = self.doUtils views = [] mediatype = mediatype.lower() @@ -305,7 +289,7 @@ class Read_EmbyServer(): else: # Views ungrouped url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" - result = doUtils(url) + result = self.doUtils(url) try: items = result['Items'] except TypeError: @@ -313,11 +297,8 @@ class Read_EmbyServer(): else: for item in items: - name = item['Name'] - itemId = item['Id'] - viewtype = item['Type'] - - if viewtype == "Channel": + item['Name'] = item['Name'] + if item['Type'] == "Channel": # Filter view types continue @@ -328,20 +309,20 @@ class Read_EmbyServer(): # Assumed missing is mixed then. '''if itemtype is None: url = "{server}/emby/Library/MediaFolders?format=json" - result = doUtils(url) + result = self.doUtils(url) for folder in result['Items']: - if itemId == folder['Id']: + if item['Id'] == folder['Id']: itemtype = folder.get('CollectionType', "mixed")''' - if name not in ('Collections', 'Trailers'): + if item['Name'] not in ('Collections', 'Trailers'): if sortedlist: views.append({ - 'name': name, + 'name': item['Name'], 'type': itemtype, - 'id': itemId + 'id': item['Id'] }) elif (itemtype == mediatype or @@ -349,9 +330,9 @@ class Read_EmbyServer(): views.append({ - 'name': name, + 'name': item['Name'], 'type': itemtype, - 'id': itemId + 'id': item['Id'] }) return views @@ -359,8 +340,6 @@ class Read_EmbyServer(): def verifyView(self, parentid, itemid): belongs = False - - url = "{server}/emby/Users/{UserId}/Items?format=json" params = { 'ParentId': parentid, @@ -370,7 +349,7 @@ class Read_EmbyServer(): 'Recursive': True, 'Ids': itemid } - result = self.doUtils(url, parameters=params) + result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) try: total = result['TotalRecordCount'] except TypeError: @@ -383,40 +362,23 @@ class Read_EmbyServer(): return belongs def getMovies(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog) - - return items + return self.getSection(parentId, "Movie", basic=basic, dialog=dialog) def getBoxset(self, dialog=None): - - items = self.getSection(None, "BoxSet", dialog=dialog) - - return items + return self.getSection(None, "BoxSet", dialog=dialog) def getMovies_byBoxset(self, boxsetid): - - items = self.getSection(boxsetid, "Movie") - - return items + return self.getSection(boxsetid, "Movie") def getMusicVideos(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) - - return items + return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) def getHomeVideos(self, parentId): - items = self.getSection(parentId, "Video") - - return items + return self.getSection(parentId, "Video") def getShows(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "Series", basic=basic, dialog=dialog) - - return items + return self.getSection(parentId, "Series", basic=basic, dialog=dialog) def getSeasons(self, showId): @@ -426,13 +388,12 @@ class Read_EmbyServer(): 'TotalRecordCount': 0 } - url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId params = { 'IsVirtualUnaired': False, 'Fields': "Etag" } - result = self.doUtils(url, parameters=params) + result = self.doUtils("{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId, parameters=params) if result: items = result @@ -440,25 +401,19 @@ class Read_EmbyServer(): def getEpisodes(self, parentId, basic=False, dialog=None): - items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog) - - return items + return self.getSection(parentId, "Episode", basic=basic, dialog=dialog) def getEpisodesbyShow(self, showId): - items = self.getSection(showId, "Episode") - - return items + return self.getSection(showId, "Episode") def getEpisodesbySeason(self, seasonId): - items = self.getSection(seasonId, "Episode") + return self.getSection(seasonId, "Episode") - return items def getArtists(self, dialog=None): - doUtils = self.doUtils items = { 'Items': [], @@ -472,7 +427,7 @@ class Read_EmbyServer(): 'Recursive': True, 'Limit': 1 } - result = doUtils(url, parameters=params) + result = self.doUtils(url, parameters=params) try: total = result['TotalRecordCount'] items['TotalRecordCount'] = total @@ -502,7 +457,7 @@ class Read_EmbyServer(): "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" ) } - result = doUtils(url, parameters=params) + result = self.doUtils(url, parameters=params) items['Items'].extend(result['Items']) index += jump @@ -512,28 +467,17 @@ class Read_EmbyServer(): return items def getAlbums(self, basic=False, dialog=None): - - items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) - - return items + return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) def getAlbumsbyArtist(self, artistId): - - items = self.getSection(artistId, "MusicAlbum", sortby="DateCreated") - - return items + return self.getSection(artistId, "MusicAlbum", sortby="DateCreated") def getSongs(self, basic=False, dialog=None): - - items = self.getSection(None, "Audio", basic=basic, dialog=dialog) - - return items + return self.getSection(None, "Audio", basic=basic, dialog=dialog) def getSongsbyAlbum(self, albumId): + return self.getSection(albumId, "Audio") - items = self.getSection(albumId, "Audio") - - return items def getAdditionalParts(self, itemId): @@ -543,8 +487,7 @@ class Read_EmbyServer(): 'TotalRecordCount': 0 } - url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId - result = self.doUtils(url) + result = self.doUtils("{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId) if result: items = result @@ -566,25 +509,21 @@ class Read_EmbyServer(): def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False): # Updates the user rating to Emby - doUtils = self.doUtils if favourite: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, type="POST") + self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="POST") elif favourite == False: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, type="DELETE") + self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="DELETE") if not deletelike and like: - url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid - doUtils(url, type="POST") - elif not deletelike and like == False: - url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid - doUtil(url, type="POST") + self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid, action_type="POST") + elif not deletelike and like is False: + self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid, action_type="POST") elif deletelike: - url = "{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid - doUtils(url, type="DELETE") + self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid, action_type="DELETE") + else: + self.logMsg("Error processing user rating.", 1) self.logMsg("Update user rating to emby for itemid: %s " "| like: %s | favourite: %s | deletelike: %s" - % (itemid, like, favourite, deletelike), 1) \ No newline at end of file + % (itemid, like, favourite, deletelike), 1) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 091a044b..1e4a9bd9 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -299,7 +299,7 @@ def window(property, value=None, clear=False, windowid=10000): Property needs to be string; value may be string or unicode """ WINDOW = xbmcgui.Window(windowid) - + #setproperty accepts both string and unicode but utf-8 strings are adviced by kodi devs because some unicode can give issues '''if isinstance(property, unicode): property = property.encode("utf-8") @@ -334,23 +334,22 @@ def language(stringid): string = addon.getLocalizedString(stringid) #returns unicode object return string -def kodiSQL(type="video"): - - if type == "emby": +def kodiSQL(media_type="video"): + + if media_type == "emby": dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8') - elif type == "music": + elif media_type == "music": dbPath = getKodiMusicDBPath() - elif type == "texture": + elif media_type == "texture": dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') else: dbPath = getKodiVideoDBPath() - + connection = sqlite3.connect(dbPath) return connection def getKodiVideoDBPath(): - kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2] dbVersion = { "13": 78, # Gotham @@ -358,16 +357,16 @@ def getKodiVideoDBPath(): "15": 93, # Isengard "16": 99, # Jarvis "17":104 # Krypton + "17": 104 # Krypton } dbPath = xbmc.translatePath( "special://database/MyVideos%s.db" - % dbVersion.get(kodibuild, "")).decode('utf-8') + % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') return dbPath def getKodiMusicDBPath(): - kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2] dbVersion = { "13": 46, # Gotham @@ -379,7 +378,7 @@ def getKodiMusicDBPath(): dbPath = xbmc.translatePath( "special://database/MyMusic%s.db" - % dbVersion.get(kodibuild, "")).decode('utf-8') + % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') return dbPath def getScreensaver(): @@ -394,11 +393,7 @@ def getScreensaver(): 'setting': "screensaver.mode" } } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - screensaver = result['result']['value'] - - return screensaver + return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value'] def setScreensaver(value): # Toggle the screensaver @@ -413,15 +408,13 @@ def setScreensaver(value): 'value': value } } - result = xbmc.executeJSONRPC(json.dumps(query)) - logMsg("PLEX", "Toggling screensaver: %s %s" % (value, result), 1) + logMsg("PLEX", "Toggling screensaver: %s %s" % (value, xbmc.executeJSONRPC(json.dumps(query))), 1) def reset(): dialog = xbmcgui.Dialog() - resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") - if resp == 0: + if dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") == 0: return # first stop any db sync @@ -483,7 +476,7 @@ def reset(): cursor.close() # Offer to wipe cached thumbnails - resp = dialog.yesno("Warning", "Removed all cached artwork?") + resp = dialog.yesno("Warning", "Remove all cached artwork?") if resp: logMsg("EMBY", "Resetting all cached artwork.", 0) # Remove all existing textures first @@ -497,7 +490,7 @@ def reset(): xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8'))) else: xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file)) - + # remove all existing data from texture DB connection = kodiSQL('texture') cursor = connection.cursor() @@ -509,8 +502,8 @@ def reset(): cursor.execute("DELETE FROM " + tableName) connection.commit() cursor.close() - - # reset the install run flag + + # reset the install run flag settings('SyncInstallRunDone', value="false") # Remove emby info @@ -532,7 +525,7 @@ def profiling(sortby="cumulative"): # Will print results to Kodi log def decorator(func): def wrapper(*args, **kwargs): - + pr = cProfile.Profile() pr.enable() @@ -576,7 +569,7 @@ def normalize_nodes(text): # with dots at the end text = text.rstrip('.') text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') - + return text def normalize_string(text): @@ -717,7 +710,7 @@ def sourcesXML(): root = etree.Element('sources') else: root = xmlparse.getroot() - + video = root.find('video') if video is None: @@ -729,7 +722,7 @@ def sourcesXML(): for source in root.findall('.//path'): if source.text == "smb://": count -= 1 - + if count == 0: # sources already set break @@ -775,9 +768,7 @@ def passwordsXML(): elif option == 1: # User selected remove - iterator = root.getiterator('passwords') - - for paths in iterator: + for paths in root.getiterator('passwords'): for path in paths: if path.find('.//from').text == "smb://%s/" % credentials: paths.remove(path) @@ -788,7 +779,7 @@ def passwordsXML(): break else: logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1) - + settings('networkCreds', value="") xbmcgui.Dialog().notification( heading='PlexKodiConnect', @@ -842,7 +833,7 @@ def passwordsXML(): # Force Kodi to see the credentials without restarting xbmcvfs.exists(topath) - # Add credentials + # Add credentials settings('networkCreds', value="%s" % server) logMsg("PLEX", "Added server: %s to passwords.xml" % server, 1) # Prettify and write to file @@ -850,7 +841,7 @@ def passwordsXML(): indent(root) except: pass etree.ElementTree(root).write(xmlpath) - + # dialog.notification( # heading="PlexKodiConnect", # message="Added to passwords.xml", @@ -881,7 +872,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): if delete: xbmcvfs.delete(xsppath.encode('utf-8')) logMsg("PLEX", "Successfully removed playlist: %s." % tagname, 1) - + return # Using write process since there's no guarantee the xml declaration works with etree @@ -949,4 +940,4 @@ def try_decode(text, encoding="utf-8"): try: return text.decode(encoding,"ignore") except: - return text \ No newline at end of file + return text diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 253223a5..fce5c169 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -54,7 +54,6 @@ class VideoNodes(object): mediatype = mediatypes[mediatype] window = utils.window - kodiversion = self.kodiversion if viewtype == "mixed": dirname = "%s-%s" % (viewid, mediatype) @@ -231,7 +230,7 @@ class VideoNodes(object): # Custom query path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s" % (viewid, mediatype, tagname, limit)) - elif kodiversion == 14 and nodetype == "inprogressepisodes": + elif self.kodiversion == 14 and nodetype == "inprogressepisodes": # Custom query path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit) elif nodetype == 'ondeck': diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py index 3d777a97..e35d1966 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/websocket.py @@ -1,912 +1,911 @@ -""" -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 _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 = 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 << 7 | length) - elif length < ABNF.LENGTH_16: - frame_header += chr(self.mask << 7 | 0x7e) - frame_header += struct.pack("!H", length) - else: - frame_header += chr(self.mask << 7 | 0x7f) - frame_header += struct.pack("!Q", length) - - if not self.mask: - 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 - 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): - sock = self.sock - 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() +""" +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 _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 = 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 << 7 | length) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header += struct.pack("!Q", length) + + if not self.mask: + 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 + 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()