diff --git a/contextmenu.py b/contextmenu.py index 3b367ab2..c91da6a1 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -1,159 +1,158 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import os -import sys -import urlparse - -import xbmc -import xbmcaddon -import xbmcgui - -addon_ = xbmcaddon.Addon(id='plugin.video.emby') -addon_path = addon_.getAddonInfo('path').decode('utf-8') -base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8') -sys.path.append(base_resource) - -import artwork -import utils -import clientinfo -import downloadutils -import librarysync -import read_embyserver as embyserver -import embydb_functions as embydb -import kodidb_functions as kodidb -import musicutils as musicutils -import api - -def logMsg(msg, lvl=1): - utils.logMsg("%s %s" % ("EMBY", "Contextmenu"), msg, lvl) - - -#Kodi contextmenu item to configure the emby settings -#for now used to set ratings but can later be used to sync individual items etc. -if __name__ == '__main__': - itemid = xbmc.getInfoLabel("ListItem.DBID").decode("utf-8") - itemtype = xbmc.getInfoLabel("ListItem.DBTYPE").decode("utf-8") - - emby = embyserver.Read_EmbyServer() - - embyid = "" - if not itemtype and xbmc.getCondVisibility("Container.Content(albums)"): itemtype = "album" - if not itemtype and xbmc.getCondVisibility("Container.Content(artists)"): itemtype = "artist" - if not itemtype and xbmc.getCondVisibility("Container.Content(songs)"): itemtype = "song" - if not itemtype and xbmc.getCondVisibility("Container.Content(pictures)"): itemtype = "picture" - - if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"): - embyid = xbmc.getInfoLabel("ListItem.Property(embyid)") - else: - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(itemid, itemtype) - embycursor.close() - if item: embyid = item[0] - - logMsg("Contextmenu opened for embyid: %s - itemtype: %s" %(embyid,itemtype)) - - if embyid: - item = emby.getItem(embyid) - API = api.API(item) - userdata = API.getUserData() - likes = userdata['Likes'] - favourite = userdata['Favorite'] - - options=[] - if likes == True: - #clear like for the item - options.append(utils.language(30402)) - if likes == False or likes == None: - #Like the item - options.append(utils.language(30403)) - if likes == True or likes == None: - #Dislike the item - options.append(utils.language(30404)) - if favourite == False: - #Add to emby favourites - options.append(utils.language(30405)) - if favourite == True: - #Remove from emby favourites - options.append(utils.language(30406)) - if itemtype == "song": - #Set custom song rating - options.append(utils.language(30407)) - - #delete item - options.append(utils.language(30409)) - - #addon settings - options.append(utils.language(30408)) - - #display select dialog and process results - header = utils.language(30401) - ret = xbmcgui.Dialog().select(header, options) - if ret != -1: - if options[ret] == utils.language(30402): - emby.updateUserRating(embyid, deletelike=True) - if options[ret] == utils.language(30403): - emby.updateUserRating(embyid, like=True) - if options[ret] == utils.language(30404): - emby.updateUserRating(embyid, like=False) - if options[ret] == utils.language(30405): - emby.updateUserRating(embyid, favourite=True) - if options[ret] == utils.language(30406): - emby.updateUserRating(embyid, favourite=False) - if options[ret] == utils.language(30407): - kodiconn = utils.kodiSQL('music') - kodicursor = kodiconn.cursor() - query = ' '.join(("SELECT rating", "FROM song", "WHERE idSong = ?" )) - kodicursor.execute(query, (itemid,)) - currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) - newvalue = xbmcgui.Dialog().numeric(0, "Set custom song rating (0-5)", str(currentvalue)) - if newvalue: - newvalue = int(newvalue) - if newvalue > 5: newvalue = "5" - if utils.settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(newvalue, API.getFilePath()) - if utils.settings('enableExportSongRating') == "true": - like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue) - emby.updateUserRating(embyid, like, favourite, deletelike) - query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" )) - kodicursor.execute(query, (newvalue,itemid,)) - kodiconn.commit() - - if options[ret] == utils.language(30408): - #Open addon settings - xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)") - - if options[ret] == utils.language(30409): - #delete item from the server - delete = True - if utils.settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1=("Delete file from Emby Server? This will " - "also delete the file(s) from disk!")) - if not resp: - logMsg("User skipped deletion for: %s." % embyid, 1) - delete = False - - if delete: - import downloadutils - doUtils = downloadutils.DownloadUtils() - url = "{server}/emby/Items/%s?format=json" % embyid - logMsg("Deleting request: %s" % embyid, 0) - doUtils.downloadUrl(url, type="DELETE") - - '''if utils.settings('skipContextMenu') != "true": - if xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1=("Delete file on Emby Server? This will " - "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")''' - - xbmc.sleep(500) +# -*- coding: utf-8 -*- + +################################################################################################# + +import os +import sys +import urlparse + +import xbmc +import xbmcaddon +import xbmcgui + +addon_ = xbmcaddon.Addon(id='plugin.video.emby') +addon_path = addon_.getAddonInfo('path').decode('utf-8') +base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8') +sys.path.append(base_resource) + +import artwork +import utils +import clientinfo +import downloadutils +import librarysync +import read_embyserver as embyserver +import embydb_functions as embydb +import kodidb_functions as kodidb +import musicutils as musicutils +import api + +def logMsg(msg, lvl=1): + utils.logMsg("%s %s" % ("EMBY", "Contextmenu"), msg, lvl) + + +#Kodi contextmenu item to configure the emby settings +#for now used to set ratings but can later be used to sync individual items etc. +if __name__ == '__main__': + itemid = xbmc.getInfoLabel("ListItem.DBID").decode("utf-8") + itemtype = xbmc.getInfoLabel("ListItem.DBTYPE").decode("utf-8") + + emby = embyserver.Read_EmbyServer() + + embyid = "" + if not itemtype and xbmc.getCondVisibility("Container.Content(albums)"): itemtype = "album" + if not itemtype and xbmc.getCondVisibility("Container.Content(artists)"): itemtype = "artist" + if not itemtype and xbmc.getCondVisibility("Container.Content(songs)"): itemtype = "song" + if not itemtype and xbmc.getCondVisibility("Container.Content(pictures)"): itemtype = "picture" + + if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"): + embyid = xbmc.getInfoLabel("ListItem.Property(embyid)") + else: + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + item = emby_db.getItem_byKodiId(itemid, itemtype) + embycursor.close() + if item: embyid = item[0] + + logMsg("Contextmenu opened for embyid: %s - itemtype: %s" %(embyid,itemtype)) + + if embyid: + item = emby.getItem(embyid) + API = api.API(item) + userdata = API.getUserData() + likes = userdata['Likes'] + favourite = userdata['Favorite'] + + options=[] + if likes == True: + #clear like for the item + options.append(utils.language(30402)) + if likes == False or likes == None: + #Like the item + options.append(utils.language(30403)) + if likes == True or likes == None: + #Dislike the item + options.append(utils.language(30404)) + if favourite == False: + #Add to emby favourites + options.append(utils.language(30405)) + if favourite == True: + #Remove from emby favourites + options.append(utils.language(30406)) + if itemtype == "song": + #Set custom song rating + options.append(utils.language(30407)) + + #delete item + options.append(utils.language(30409)) + + #addon settings + options.append(utils.language(30408)) + + #display select dialog and process results + header = utils.language(30401) + ret = xbmcgui.Dialog().select(header, options) + if ret != -1: + if options[ret] == utils.language(30402): + emby.updateUserRating(embyid, deletelike=True) + if options[ret] == utils.language(30403): + emby.updateUserRating(embyid, like=True) + if options[ret] == utils.language(30404): + emby.updateUserRating(embyid, like=False) + if options[ret] == utils.language(30405): + emby.updateUserRating(embyid, favourite=True) + if options[ret] == utils.language(30406): + emby.updateUserRating(embyid, favourite=False) + if options[ret] == utils.language(30407): + kodiconn = utils.kodiSQL('music') + kodicursor = kodiconn.cursor() + query = ' '.join(("SELECT rating", "FROM song", "WHERE idSong = ?" )) + kodicursor.execute(query, (itemid,)) + currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) + newvalue = xbmcgui.Dialog().numeric(0, "Set custom song rating (0-5)", str(currentvalue)) + if newvalue: + newvalue = int(newvalue) + if newvalue > 5: newvalue = "5" + if utils.settings('enableUpdateSongRating') == "true": + musicutils.updateRatingToFile(newvalue, API.getFilePath()) + if utils.settings('enableExportSongRating') == "true": + like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue) + emby.updateUserRating(embyid, like, favourite, deletelike) + query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" )) + kodicursor.execute(query, (newvalue,itemid,)) + kodiconn.commit() + + if options[ret] == utils.language(30408): + #Open addon settings + xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)") + + if options[ret] == utils.language(30409): + #delete item from the server + delete = True + if utils.settings('skipContextMenu') != "true": + resp = xbmcgui.Dialog().yesno( + heading="Confirm delete", + line1=("Delete file from Emby Server? This will " + "also delete the file(s) from disk!")) + if not resp: + logMsg("User skipped deletion for: %s." % embyid, 1) + delete = False + + if delete: + import downloadutils + doUtils = downloadutils.DownloadUtils() + url = "{server}/emby/Items/%s?format=json" % embyid + logMsg("Deleting request: %s" % embyid, 0) + doUtils.downloadUrl(url, action_type="DELETE") + + '''if utils.settings('skipContextMenu') != "true": + if xbmcgui.Dialog().yesno( + heading="Confirm delete", + line1=("Delete file on Emby Server? This will " + "also delete the file(s) from disk!")): + import downloadutils + doUtils = downloadutils.DownloadUtils() + 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 776e06a1..78a94417 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -323,7 +323,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/api.py b/resources/lib/api.py index af993adb..97ad4178 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -37,7 +37,7 @@ class API(): try: userdata = self.item['UserData'] - + except KeyError: # No userdata found. pass @@ -57,7 +57,7 @@ class API(): lastPlayedDate = userdata.get('LastPlayedDate') if lastPlayedDate: lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ") - + if userdata['Played']: # Playcount is tied to the watch status played = True @@ -91,10 +91,10 @@ class API(): try: people = self.item['People'] - + except KeyError: pass - + else: for person in people: @@ -116,17 +116,16 @@ class API(): } def getMediaStreams(self): - item = self.item videotracks = [] audiotracks = [] subtitlelanguages = [] try: - media_streams = item['MediaSources'][0]['MediaStreams'] + media_streams = self.item['MediaSources'][0]['MediaStreams'] except KeyError: - if not item.get("MediaStreams"): return None - media_streams = item['MediaStreams'] + if not self.item.get("MediaStreams"): return None + media_streams = self.item['MediaStreams'] for media_stream in media_streams: # Sort through Video, Audio, Subtitle @@ -141,12 +140,12 @@ class API(): 'codec': codec, 'height': media_stream.get('Height'), 'width': media_stream.get('Width'), - 'video3DFormat': item.get('Video3DFormat'), + 'video3DFormat': self.item.get('Video3DFormat'), 'aspect': 1.85 } try: - container = item['MediaSources'][0]['Container'].lower() + container = self.item['MediaSources'][0]['Container'].lower() except: container = "" @@ -161,16 +160,16 @@ class API(): track['codec'] = "avc1" # Aspect ratio - if item.get('AspectRatio'): + if self.item.get('AspectRatio'): # Metadata AR - aspect = item['AspectRatio'] + aspect = self.item['AspectRatio'] else: # File AR aspect = media_stream.get('AspectRatio', "0") try: aspectwidth, aspectheight = aspect.split(':') track['aspect'] = round(float(aspectwidth) / float(aspectheight), 6) - + except (ValueError, ZeroDivisionError): width = track.get('width') height = track.get('height') @@ -179,16 +178,16 @@ class API(): track['aspect'] = round(float(width / height), 6) else: track['aspect'] = 1.85 - - if item.get("RunTimeTicks"): - track['duration'] = item.get("RunTimeTicks") / 10000000.0 - + + if self.item.get("RunTimeTicks"): + track['duration'] = self.item.get("RunTimeTicks") / 10000000.0 + videotracks.append(track) elif stream_type == "Audio": # Codec, Channels, language track = { - + 'codec': codec, 'channels': media_stream.get('Channels'), 'language': media_stream.get('Language') @@ -205,18 +204,17 @@ class API(): return { - 'video': videotracks, + 'video': videotracks, 'audio': audiotracks, 'subtitle': subtitlelanguages } def getRuntime(self): - item = self.item try: - runtime = item['RunTimeTicks'] / 10000000.0 - + runtime = self.item['RunTimeTicks'] / 10000000.0 + except KeyError: - runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0 + runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0 return runtime @@ -234,20 +232,19 @@ class API(): def getStudios(self): # Process Studios - item = self.item studios = [] try: - studio = item['SeriesStudio'] + studio = self.item['SeriesStudio'] studios.append(self.verifyStudio(studio)) - + except KeyError: - studioList = item['Studios'] + studioList = self.item['Studios'] for studio in studioList: name = studio['Name'] studios.append(self.verifyStudio(name)) - + return studios def verifyStudio(self, studioName): @@ -265,12 +262,11 @@ class API(): def getChecksum(self): # Use the etags checksum and userdata - item = self.item - userdata = item['UserData'] + userdata = self.item['UserData'] checksum = "%s%s%s%s%s%s%s" % ( - - item['Etag'], + + self.item['Etag'], userdata['Played'], userdata['IsFavorite'], userdata.get('Likes',''), @@ -282,9 +278,8 @@ class API(): return checksum def getGenres(self): - item = self.item all_genres = "" - genres = item.get('Genres', item.get('SeriesGenres')) + genres = self.item.get('Genres', self.item.get('SeriesGenres')) if genres: all_genres = " / ".join(genres) @@ -344,7 +339,7 @@ class API(): def getMpaa(self): # Convert more complex cases mpaa = self.item.get('OfficialRating', "") - + if mpaa in ("NR", "UR"): # Kodi seems to not like NR, but will accept Not Rated mpaa = "Not Rated" @@ -362,9 +357,8 @@ class API(): def getFilePath(self): - item = self.item try: - filepath = item['Path'] + filepath = self.item['Path'] except KeyError: filepath = "" @@ -375,17 +369,16 @@ class API(): filepath = filepath.replace("\\\\", "smb://") filepath = filepath.replace("\\", "/") - if item.get('VideoType'): - videotype = item['VideoType'] + if self.item.get('VideoType'): + videotype = self.item['VideoType'] # Specific format modification if 'Dvd'in videotype: filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath elif 'BluRay' in videotype: filepath = "%s/BDMV/index.bdmv" % filepath - + if "\\" in filepath: # Local path scenario, with special videotype filepath = filepath.replace("/", "\\") return filepath - \ No newline at end of file diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 2740f09c..79fb617c 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -1,606 +1,604 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import requests -import os -import urllib -from sqlite3 import OperationalError - -import xbmc -import xbmcgui -import xbmcvfs - -import utils -import clientinfo -import image_cache_thread - -################################################################################################# - - -class Artwork(): - - xbmc_host = 'localhost' - xbmc_port = None - xbmc_username = None - xbmc_password = None - - imageCacheThreads = [] - imageCacheLimitThreads = 0 - - def __init__(self): - self.clientinfo = clientinfo.ClientInfo() - self.addonName = self.clientinfo.getAddonName() - - self.enableTextureCache = utils.settings('enableTextureCache') == "true" - self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit")) - 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() - - self.userId = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userId) - - def logMsg(self, msg, lvl=1): - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - 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:] - - return text.decode("utf-8") #return the result again as unicode - - def setKodiWebServerDetails(self): - # Get the Kodi webserver details - used to set the texture cache - web_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserver" - } - } - result = xbmc.executeJSONRPC(json.dumps(web_query)) - result = json.loads(result) - try: - xbmc_webserver_enabled = result['result']['value'] - except TypeError: - xbmc_webserver_enabled = False - - if not xbmc_webserver_enabled: - # Enable the webserver, it is disabled - web_port = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - - "setting": "services.webserverport", - "value": 8080 - } - } - result = xbmc.executeJSONRPC(json.dumps(web_port)) - self.xbmc_port = 8080 - - web_user = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - - "setting": "services.webserver", - "value": True - } - } - result = xbmc.executeJSONRPC(json.dumps(web_user)) - self.xbmc_username = "kodi" - - - # Webserver already enabled - web_port = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverport" - } - } - result = xbmc.executeJSONRPC(json.dumps(web_port)) - result = json.loads(result) - try: - self.xbmc_port = result['result']['value'] - except TypeError: - pass - - web_user = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverusername" - } - } - result = xbmc.executeJSONRPC(json.dumps(web_user)) - result = json.loads(result) - try: - self.xbmc_username = result['result']['value'] - except TypeError: - pass - - web_pass = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverpassword" - } - } - result = xbmc.executeJSONRPC(json.dumps(web_pass)) - result = json.loads(result) - try: - 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! - - if not xbmcgui.Dialog().yesno("Image Texture Cache", "Running the image cache process can take some time.", "Are you sure you want continue?"): - 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", "Reset all existing cache data first?", ""): - self.logMsg("Resetting all cache data first", 1) - # Remove all existing textures first - path = xbmc.translatePath("special://thumbnails/").decode('utf-8') - if xbmcvfs.exists(path): - allDirs, allFiles = xbmcvfs.listdir(path) - for dir in allDirs: - allDirs, allFiles = xbmcvfs.listdir(path+dir) - for file in allFiles: - if os.path.supports_unicode_filenames: - 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() - texturecursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = texturecursor.fetchall() - for row in rows: - tableName = row[0] - if(tableName != "version"): - texturecursor.execute("DELETE FROM " + tableName) - textureconnection.commit() - texturecursor.close() - - # Cache all entries in video DB - connection = utils.kodiSQL('video') - cursor = connection.cursor() - cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors - result = cursor.fetchall() - total = len(result) - count = 1 - percentage = 0 - self.logMsg("Image cache sync about to process " + str(total) + " images", 1) - for url in result: - if dialog.iscanceled(): - break - percentage = int((float(count) / float(total))*100) - textMessage = str(count) + " of " + str(total) + " (" + str(len(self.imageCacheThreads)) + ")" - dialog.update(percentage, "Updating Image Cache: " + textMessage) - 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 - percentage = 0 - self.logMsg("Image cache sync about to process " + str(total) + " images", 1) - for url in result: - if dialog.iscanceled(): - 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: - for thread in self.imageCacheThreads: - if thread.isFinished: - self.imageCacheThreads.remove(thread) - 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) - - # add a new thread or wait and retry if we hit our limit - if(len(self.imageCacheThreads) < self.imageCacheLimitThreads): - newThread = image_cache_thread.image_cache_thread() - newThread.setUrl(self.double_urlencode(urlToAdd)) - newThread.setHost(self.xbmc_host, self.xbmc_port) - newThread.setAuth(self.xbmc_username, self.xbmc_password) - newThread.start() - self.imageCacheThreads.append(newThread) - return - 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: - self.logMsg("Processing: %s" % url, 2) - - 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( - url=( - "http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, url)), - auth=(self.xbmc_username, self.xbmc_password), - timeout=(0.01, 0.01)) - # We don't need the result - except: pass - - else: - self.addWorkerImageCacheThread(url) - - - def addArtwork(self, artwork, kodiId, mediaType, cursor): - # Kodi conversion table - kodiart = { - - 'Primary': ["thumb", "poster"], - 'Banner': "banner", - 'Logo': "clearlogo", - 'Art': "clearart", - 'Thumb': "landscape", - 'Disc': "discart", - 'Backdrop': "fanart", - 'BoxRear': "poster" - } - - # Artwork is a dictionary - for art in artwork: - - if art == "Backdrop": - # Backdrop entry is a list - # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...) - backdrops = artwork[art] - backdropsNumber = len(backdrops) - - query = ' '.join(( - - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodiId, mediaType, "fanart%",)) - rows = cursor.fetchall() - - if len(rows) > backdropsNumber: - # More backdrops in database. Delete extra fanart. - query = ' '.join(( - - "DELETE FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodiId, mediaType, "fanart_",)) - - # Process backdrops and extra fanart - index = "" - for backdrop in backdrops: - self.addOrUpdateArt( - imageUrl=backdrop, - kodiId=kodiId, - 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]: - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=artType, - cursor=cursor) - - elif kodiart.get(art): - # Process the rest artwork type that Kodi can use - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=kodiart[art], - cursor=cursor) - - def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): - # Possible that the imageurl is an empty string - if imageUrl: - cacheimage = False - - query = ' '.join(( - - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - 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) - - VALUES (?, ?, ?, ?) - ''' - ) - 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", - "SET url = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) - - # Cache fanart and poster in Kodi texture cache - if cacheimage and imageType in ("fanart", "poster"): - self.CacheTexture(imageUrl) - - def deleteArtwork(self, kodiid, mediatype, cursor): - - query = ' '.join(( - - "SELECT url, type", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?" - )) - cursor.execute(query, (kodiid, mediatype,)) - rows = cursor.fetchall() - for row in rows: - - url = row[0] - imagetype = row[1] - if imagetype in ("poster", "fanart"): - self.deleteCachedArtwork(url) - - def deleteCachedArtwork(self, url): - # Only necessary to remove and apply a new backdrop or poster - connection = utils.kodiSQL('texture') - cursor = connection.cursor() - - 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() - - def getPeopleArtwork(self, people): - # append imageurl if existing - for person in people: - - personId = person['Id'] - tag = person.get('PrimaryImageTag') - - image = "" - if tag: - image = ( - "%s/emby/Items/%s/Images/Primary?" - "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" - % (self.server, personId, tag)) - - person['imageurl'] = image - - return people - - def getUserArtwork(self, itemid, itemtype): - # Load user information set by UserClient - image = ("%s/emby/Users/%s/Images/%s?Format=original" - % (self.server, itemid, itemtype)) - return image - - def getAllArtwork(self, item, parentInfo=False): - - server = self.server - - itemid = item['Id'] - artworks = item['ImageTags'] - backdrops = item.get('BackdropImageTags',[]) - - maxHeight = 10000 - maxWidth = 10000 - customquery = "" - - if utils.settings('compressArt') == "true": - customquery = "&Quality=90" - - if utils.settings('enableCoverArt') == "false": - customquery += "&EnableImageEnhancers=false" - - allartworks = { - - 'Primary': "", - 'Art': "", - 'Banner': "", - 'Logo': "", - 'Thumb': "", - '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)) - allartworks['Backdrop'].append(artwork) - - # Process the rest of the artwork - for art in artworks: - # Filter backcover - if art != "BoxRear": - tag = artworks[art] - artwork = ( - "%s/emby/Items/%s/Images/%s/0?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (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 - parentbackdrops = item['ParentBackdropImageTags'] - - for index, tag in enumerate(parentbackdrops): - artwork = ( - "%s/emby/Items/%s/Images/Backdrop/%s?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (server, parentId, index, maxWidth, maxHeight, tag, customquery)) - allartworks['Backdrop'].append(artwork) - - # Process the rest of the artwork - parentartwork = ['Logo', 'Art', 'Thumb'] - 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, - maxWidth, maxHeight, parentTag, customquery)) - allartworks[parentart] = artwork - - # Parent album works a bit differently - if not allartworks['Primary']: - - 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)) - allartworks['Primary'] = artwork - +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import requests +import os +import urllib +from sqlite3 import OperationalError + +import xbmc +import xbmcgui +import xbmcvfs + +import utils +import clientinfo +import image_cache_thread + +################################################################################################# + + +class Artwork(): + + xbmc_host = 'localhost' + xbmc_port = None + xbmc_username = None + xbmc_password = None + + imageCacheThreads = [] + imageCacheLimitThreads = 0 + + def __init__(self): + self.clientinfo = clientinfo.ClientInfo() + self.addonName = self.clientinfo.getAddonName() + + self.enableTextureCache = utils.settings('enableTextureCache') == "true" + self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit")) + 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() + + self.userId = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userId) + + def logMsg(self, msg, lvl=1): + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + 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:] + + return text.decode("utf-8") #return the result again as unicode + + def setKodiWebServerDetails(self): + # Get the Kodi webserver details - used to set the texture cache + web_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.GetSettingValue", + "params": { + + "setting": "services.webserver" + } + } + result = xbmc.executeJSONRPC(json.dumps(web_query)) + result = json.loads(result) + try: + xbmc_webserver_enabled = result['result']['value'] + except TypeError: + xbmc_webserver_enabled = False + + if not xbmc_webserver_enabled: + # Enable the webserver, it is disabled + web_port = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.SetSettingValue", + "params": { + + "setting": "services.webserverport", + "value": 8080 + } + } + result = xbmc.executeJSONRPC(json.dumps(web_port)) + self.xbmc_port = 8080 + + web_user = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.SetSettingValue", + "params": { + + "setting": "services.webserver", + "value": True + } + } + result = xbmc.executeJSONRPC(json.dumps(web_user)) + self.xbmc_username = "kodi" + + + # Webserver already enabled + web_port = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.GetSettingValue", + "params": { + + "setting": "services.webserverport" + } + } + result = xbmc.executeJSONRPC(json.dumps(web_port)) + result = json.loads(result) + try: + self.xbmc_port = result['result']['value'] + except TypeError: + pass + + web_user = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.GetSettingValue", + "params": { + + "setting": "services.webserverusername" + } + } + result = xbmc.executeJSONRPC(json.dumps(web_user)) + result = json.loads(result) + try: + self.xbmc_username = result['result']['value'] + except TypeError: + pass + + web_pass = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Settings.GetSettingValue", + "params": { + + "setting": "services.webserverpassword" + } + } + result = xbmc.executeJSONRPC(json.dumps(web_pass)) + result = json.loads(result) + try: + 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! + + if not xbmcgui.Dialog().yesno("Image Texture Cache", "Running the image cache process can take some time.", "Are you sure you want continue?"): + 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", "Reset all existing cache data first?", ""): + self.logMsg("Resetting all cache data first", 1) + # Remove all existing textures first + path = xbmc.translatePath("special://thumbnails/").decode('utf-8') + if xbmcvfs.exists(path): + allDirs, allFiles = xbmcvfs.listdir(path) + for dir in allDirs: + allDirs, allFiles = xbmcvfs.listdir(path+dir) + for file in allFiles: + if os.path.supports_unicode_filenames: + 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() + texturecursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = texturecursor.fetchall() + for row in rows: + tableName = row[0] + if(tableName != "version"): + texturecursor.execute("DELETE FROM " + tableName) + textureconnection.commit() + texturecursor.close() + + # Cache all entries in video DB + connection = utils.kodiSQL('video') + cursor = connection.cursor() + cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors + result = cursor.fetchall() + total = len(result) + count = 1 + percentage = 0 + self.logMsg("Image cache sync about to process " + str(total) + " images", 1) + for url in result: + if dialog.iscanceled(): + break + percentage = int((float(count) / float(total))*100) + textMessage = str(count) + " of " + str(total) + " (" + str(len(self.imageCacheThreads)) + ")" + dialog.update(percentage, "Updating Image Cache: " + textMessage) + 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 + percentage = 0 + self.logMsg("Image cache sync about to process " + str(total) + " images", 1) + for url in result: + if dialog.iscanceled(): + 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: + for thread in self.imageCacheThreads: + if thread.isFinished: + self.imageCacheThreads.remove(thread) + 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) + + # add a new thread or wait and retry if we hit our limit + if(len(self.imageCacheThreads) < self.imageCacheLimitThreads): + newThread = image_cache_thread.image_cache_thread() + newThread.setUrl(self.double_urlencode(urlToAdd)) + newThread.setHost(self.xbmc_host, self.xbmc_port) + newThread.setAuth(self.xbmc_username, self.xbmc_password) + newThread.start() + self.imageCacheThreads.append(newThread) + return + 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: + self.logMsg("Processing: %s" % url, 2) + + 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( + url=( + "http://%s:%s/image/image://%s" + % (self.xbmc_host, self.xbmc_port, url)), + auth=(self.xbmc_username, self.xbmc_password), + timeout=(0.01, 0.01)) + # We don't need the result + except: pass + + else: + self.addWorkerImageCacheThread(url) + + + def addArtwork(self, artwork, kodiId, mediaType, cursor): + # Kodi conversion table + kodiart = { + + 'Primary': ["thumb", "poster"], + 'Banner': "banner", + 'Logo': "clearlogo", + 'Art': "clearart", + 'Thumb': "landscape", + 'Disc': "discart", + 'Backdrop': "fanart", + 'BoxRear': "poster" + } + + # Artwork is a dictionary + for art in artwork: + + if art == "Backdrop": + # Backdrop entry is a list + # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...) + backdrops = artwork[art] + backdropsNumber = len(backdrops) + + query = ' '.join(( + + "SELECT url", + "FROM art", + "WHERE media_id = ?", + "AND media_type = ?", + "AND type LIKE ?" + )) + cursor.execute(query, (kodiId, mediaType, "fanart%",)) + rows = cursor.fetchall() + + if len(rows) > backdropsNumber: + # More backdrops in database. Delete extra fanart. + query = ' '.join(( + + "DELETE FROM art", + "WHERE media_id = ?", + "AND media_type = ?", + "AND type LIKE ?" + )) + cursor.execute(query, (kodiId, mediaType, "fanart_",)) + + # Process backdrops and extra fanart + index = "" + for backdrop in backdrops: + self.addOrUpdateArt( + imageUrl=backdrop, + kodiId=kodiId, + 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]: + self.addOrUpdateArt( + imageUrl=artwork[art], + kodiId=kodiId, + mediaType=mediaType, + imageType=artType, + cursor=cursor) + + elif kodiart.get(art): + # Process the rest artwork type that Kodi can use + self.addOrUpdateArt( + imageUrl=artwork[art], + kodiId=kodiId, + mediaType=mediaType, + imageType=kodiart[art], + cursor=cursor) + + def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): + # Possible that the imageurl is an empty string + if imageUrl: + cacheimage = False + + query = ' '.join(( + + "SELECT url", + "FROM art", + "WHERE media_id = ?", + "AND media_type = ?", + "AND type = ?" + )) + 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) + + VALUES (?, ?, ?, ?) + ''' + ) + 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", + "SET url = ?", + "WHERE media_id = ?", + "AND media_type = ?", + "AND type = ?" + )) + cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) + + # Cache fanart and poster in Kodi texture cache + if cacheimage and imageType in ("fanart", "poster"): + self.CacheTexture(imageUrl) + + def deleteArtwork(self, kodiid, mediatype, cursor): + + query = ' '.join(( + + "SELECT url, type", + "FROM art", + "WHERE media_id = ?", + "AND media_type = ?" + )) + cursor.execute(query, (kodiid, mediatype,)) + rows = cursor.fetchall() + for row in rows: + + url = row[0] + imagetype = row[1] + if imagetype in ("poster", "fanart"): + self.deleteCachedArtwork(url) + + def deleteCachedArtwork(self, url): + # Only necessary to remove and apply a new backdrop or poster + connection = utils.kodiSQL('texture') + cursor = connection.cursor() + + 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() + + def getPeopleArtwork(self, people): + # append imageurl if existing + for person in people: + + personId = person['Id'] + tag = person.get('PrimaryImageTag') + + image = "" + if tag: + image = ( + "%s/emby/Items/%s/Images/Primary?" + "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" + % (self.server, personId, tag)) + + person['imageurl'] = image + + return people + + def getUserArtwork(self, itemid, itemtype): + # Load user information set by UserClient + image = ("%s/emby/Users/%s/Images/%s?Format=original" + % (self.server, itemid, itemtype)) + return image + + def getAllArtwork(self, item, parentInfo=False): + + itemid = item['Id'] + artworks = item['ImageTags'] + backdrops = item.get('BackdropImageTags',[]) + + maxHeight = 10000 + maxWidth = 10000 + customquery = "" + + if utils.settings('compressArt') == "true": + customquery = "&Quality=90" + + if utils.settings('enableCoverArt') == "false": + customquery += "&EnableImageEnhancers=false" + + allartworks = { + + 'Primary': "", + 'Art': "", + 'Banner': "", + 'Logo': "", + 'Thumb': "", + '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" + % (self.server, itemid, index, maxWidth, maxHeight, tag, customquery)) + allartworks['Backdrop'].append(artwork) + + # Process the rest of the artwork + for art in artworks: + # Filter backcover + if art != "BoxRear": + tag = artworks[art] + artwork = ( + "%s/emby/Items/%s/Images/%s/0?" + "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" + % (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 + parentbackdrops = item['ParentBackdropImageTags'] + + for index, tag in enumerate(parentbackdrops): + artwork = ( + "%s/emby/Items/%s/Images/Backdrop/%s?" + "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" + % (self.server, parentId, index, maxWidth, maxHeight, tag, customquery)) + allartworks['Backdrop'].append(artwork) + + # Process the rest of the artwork + parentartwork = ['Logo', 'Art', 'Thumb'] + 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" + % (self.server, parentId, parentart, + maxWidth, maxHeight, parentTag, customquery)) + allartworks[parentart] = artwork + + # Parent album works a bit differently + if not allartworks['Primary']: + + 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" + % (self.server, parentId, maxWidth, maxHeight, parentTag, customquery)) + allartworks['Primary'] = artwork + return allartworks \ No newline at end of file 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/downloadutils.py b/resources/lib/downloadutils.py index de340f07..a74ee6f2 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -23,7 +23,7 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning) class DownloadUtils(): - + # Borg - multiple instances, shared state _shared_state = {} clientInfo = clientinfo.ClientInfo() @@ -77,11 +77,11 @@ class DownloadUtils(): # Post settings to session url = "{server}/emby/Sessions/Capabilities/Full?format=json" data = { - + 'PlayableMediaTypes': "Audio,Video", 'SupportsMediaControl': True, 'SupportedCommands': ( - + "MoveUp,MoveDown,MoveLeft,MoveRight,Select," "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," "GoHome,PageUp,NextLetter,GoToSearch," @@ -97,7 +97,7 @@ class DownloadUtils(): self.logMsg("Capabilities URL: %s" % url, 2) self.logMsg("Postdata: %s" % data, 2) - self.downloadUrl(url, postBody=data, type="POST") + self.downloadUrl(url, postBody=data, action_type="POST") self.logMsg("Posted capabilities to %s" % self.server, 2) # Attempt at getting sessionId @@ -105,19 +105,19 @@ class DownloadUtils(): result = self.downloadUrl(url) try: sessionId = result[0]['Id'] - + except (KeyError, TypeError): self.logMsg("Failed to retrieve sessionId.", 1) - + else: self.logMsg("Session: %s" % result, 2) self.logMsg("SessionId: %s" % sessionId, 1) utils.window('emby_sessionId', value=sessionId) - + # Post any permanent additional users additionalUsers = utils.settings('additionalUsers') if additionalUsers: - + additionalUsers = additionalUsers.split(',') self.logMsg( "List of permanent users added to the session: %s" @@ -140,13 +140,11 @@ class DownloadUtils(): "{server}/emby/Sessions/%s/Users/%s?format=json" % (sessionId, userId) ) - self.downloadUrl(url, postBody={}, type="POST") + self.downloadUrl(url, postBody={}, action_type="POST") def startSession(self): - log = self.logMsg - self.deviceId = self.clientInfo.getDeviceId() # User is identified from this point @@ -160,8 +158,8 @@ class DownloadUtils(): 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.s = requests.Session() self.s.headers = header @@ -170,7 +168,7 @@ class DownloadUtils(): self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.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: @@ -180,12 +178,10 @@ class DownloadUtils(): def getHeader(self, authenticate=True): - clientInfo = self.clientInfo - - deviceName = clientInfo.getDeviceName() + deviceName = self.clientInfo.getDeviceName() deviceName = utils.normalize_string(deviceName.encode('utf-8')) - deviceId = clientInfo.getDeviceId() - version = clientInfo.getVersion() + deviceId = self.clientInfo.getDeviceId() + version = self.clientInfo.getVersion() if not authenticate: # If user is not authenticated @@ -198,9 +194,9 @@ class DownloadUtils(): 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth - } + } self.logMsg("Header: %s" % header, 2) - + else: userId = self.userId token = self.token @@ -215,36 +211,35 @@ class DownloadUtils(): 'Accept-Charset': 'UTF-8,*', 'Authorization': auth, 'X-MediaBrowser-Token': token - } + } self.logMsg("Header: %s" % header, 2) - + return header - def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True): - + def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None, authenticate=True): + self.logMsg("=== ENTER downloadUrl ===", 2) - timeout = self.timeout default_link = "" try: # If user is authenticated if (authenticate): # Get requests session - try: + try: s = self.s # Replace for the real values url = url.replace("{server}", self.server) url = url.replace("{UserId}", self.userId) # Prepare request - if type == "GET": - r = s.get(url, json=postBody, params=parameters, timeout=timeout) - elif type == "POST": - r = s.post(url, json=postBody, timeout=timeout) - elif type == "DELETE": - r = s.delete(url, json=postBody, timeout=timeout) - + if action_type == "GET": + r = s.get(url, json=postBody, params=parameters, timeout=self.timeout) + elif action_type == "POST": + r = s.post(url, json=postBody, timeout=self.timeout) + elif action_type == "DELETE": + r = s.delete(url, json=postBody, timeout=self.timeout) + except AttributeError: # request session does not exists # Get user information @@ -266,26 +261,26 @@ class DownloadUtils(): url = url.replace("{UserId}", self.userId) # Prepare request - if type == "GET": + if action_type == "GET": r = requests.get(url, json=postBody, params=parameters, headers=header, - timeout=timeout, + timeout=self.timeout, verify=verifyssl) - elif type == "POST": + elif action_type == "POST": r = requests.post(url, json=postBody, headers=header, - timeout=timeout, + timeout=self.timeout, verify=verifyssl) - elif type == "DELETE": + elif action_type == "DELETE": r = requests.delete(url, json=postBody, headers=header, - timeout=timeout, + timeout=self.timeout, verify=verifyssl) # If user is not authenticated @@ -301,23 +296,23 @@ class DownloadUtils(): verifyssl = self.sslclient except AttributeError: pass - + # Prepare request - if type == "GET": + if action_type == "GET": r = requests.get(url, json=postBody, params=parameters, headers=header, - timeout=timeout, + timeout=self.timeout, verify=verifyssl) - elif type == "POST": + elif action_type == "POST": r = requests.post(url, json=postBody, headers=header, - timeout=timeout, + timeout=self.timeout, verify=verifyssl) - + ##### THE RESPONSE ##### self.logMsg(r.url, 2) if r.status_code == 204: @@ -325,8 +320,8 @@ class DownloadUtils(): self.logMsg("====== 204 Success ======", 2) elif r.status_code == requests.codes.ok: - - try: + + try: # UNICODE - JSON object r = r.json() self.logMsg("====== 200 Success ======", 2) @@ -338,7 +333,7 @@ class DownloadUtils(): self.logMsg("Unable to convert the response for: %s" % url, 1) else: r.raise_for_status() - + ##### EXCEPTIONS ##### except requests.exceptions.ConnectionError as e: @@ -369,7 +364,7 @@ class DownloadUtils(): icon=xbmcgui.NOTIFICATION_ERROR, time=5000) return False - + elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException": # User tried to do something his emby account doesn't allow pass diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py index 518498f5..cfebd5ff 100644 --- a/resources/lib/embydb_functions.py +++ b/resources/lib/embydb_functions.py @@ -1,325 +1,292 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import utils -import clientinfo - -################################################################################################# - - -class Embydb_Functions(): - - - def __init__(self, embycursor): - - self.embycursor = embycursor - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - def getViews(self): - - embycursor = self.embycursor - views = [] - - query = ' '.join(( - - "SELECT view_id", - "FROM view" - )) - embycursor.execute(query) - rows = embycursor.fetchall() - for row in rows: - views.append(row[0]) - - return views - - def getView_byId(self, viewid): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT view_name, media_type, kodi_tagid", - "FROM view", - "WHERE view_id = ?" - )) - embycursor.execute(query, (viewid,)) - view = embycursor.fetchone() - - return view - - def getView_byType(self, mediatype): - - embycursor = self.embycursor - views = [] - - query = ' '.join(( - - "SELECT view_id, view_name", - "FROM view", - "WHERE media_type = ?" - )) - embycursor.execute(query, (mediatype,)) - rows = embycursor.fetchall() - for row in rows: - views.append({ - - 'id': row[0], - 'name': row[1] - }) - - return views - - def getView_byName(self, tagname): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT view_id", - "FROM view", - "WHERE view_name = ?" - )) - embycursor.execute(query, (tagname,)) - try: - view = embycursor.fetchone()[0] - - except TypeError: - view = None - - return view - - def addView(self, embyid, name, mediatype, tagid): - - query = ( - ''' - INSERT INTO view( - view_id, view_name, media_type, kodi_tagid) - - VALUES (?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (embyid, name, mediatype, tagid)) - - def updateView(self, name, tagid, mediafolderid): - - query = ' '.join(( - - "UPDATE view", - "SET view_name = ?, kodi_tagid = ?", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (name, tagid, mediafolderid)) - - def removeView(self, viewid): - - query = ' '.join(( - - "DELETE FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (viewid,)) - - def getItem_byId(self, embyid): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - try: - embycursor.execute(query, (embyid,)) - item = 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 - - 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 - - def getItem_byKodiId(self, kodiid, mediatype): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT emby_id, parent_id", - "FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - embycursor.execute(query, (kodiid, mediatype,)) - item = embycursor.fetchone() - - return item - - def getItem_byParentId(self, parentid, mediatype): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT emby_id, kodi_id, kodi_fileid", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - embycursor.execute(query, (parentid, mediatype,)) - items = embycursor.fetchall() - - return items - - def getItemId_byParentId(self, parentid, mediatype): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT emby_id, kodi_id", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - embycursor.execute(query, (parentid, mediatype,)) - items = embycursor.fetchall() - - return items - - 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 - - def getMediaType_byId(self, embyid): - - embycursor = self.embycursor - - query = ' '.join(( - - "SELECT emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - embycursor.execute(query, (embyid,)) - try: - itemtype = embycursor.fetchone()[0] - - except TypeError: - itemtype = None - - return itemtype - - def sortby_mediaType(self, itemids, unsorted=True): - - sorted_items = {} - - for itemid in itemids: - - mediatype = self.getMediaType_byId(itemid) - if mediatype: - sorted_items.setdefault(mediatype, []).append(itemid) - elif unsorted: - sorted_items.setdefault('Unsorted', []).append(itemid) - - return sorted_items - - def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None, - parentid=None, checksum=None, mediafolderid=None): - query = ( - ''' - INSERT OR REPLACE INTO emby( - emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id, - checksum, media_folder) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype, - parentid, checksum, mediafolderid)) - - def updateReference(self, embyid, checksum): - - query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" - self.embycursor.execute(query, (checksum, embyid)) - - def updateParentId(self, embyid, parent_kodiid): - - query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?" - self.embycursor.execute(query, (parent_kodiid, embyid)) - - def removeItems_byParentId(self, parent_kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parent_kodiid, mediatype,)) - - def removeItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (kodiid, mediatype,)) - - def removeItem(self, embyid): - - query = "DELETE FROM emby WHERE emby_id = ?" - self.embycursor.execute(query, (embyid,)) - - def removeWildItem(self, embyid): - - query = "DELETE FROM emby WHERE emby_id LIKE ?" - self.embycursor.execute(query, (embyid+"%",)) +# -*- coding: utf-8 -*- + +################################################################################################# + +import utils +import clientinfo + +################################################################################################# + + +class Embydb_Functions(): + + + def __init__(self, embycursor): + + self.embycursor = embycursor + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + def getViews(self): + + views = [] + + query = ' '.join(( + + "SELECT view_id", + "FROM view" + )) + self.embycursor.execute(query) + rows = self.embycursor.fetchall() + for row in rows: + views.append(row[0]) + + return views + + def getView_byId(self, viewid): + + + query = ' '.join(( + + "SELECT view_name, media_type, kodi_tagid", + "FROM view", + "WHERE view_id = ?" + )) + self.embycursor.execute(query, (viewid,)) + view = self.embycursor.fetchone() + + return view + + def getView_byType(self, mediatype): + + views = [] + + query = ' '.join(( + + "SELECT view_id, view_name", + "FROM view", + "WHERE media_type = ?" + )) + self.embycursor.execute(query, (mediatype,)) + rows = self.embycursor.fetchall() + for row in rows: + views.append({ + + 'id': row[0], + 'name': row[1] + }) + + return views + + def getView_byName(self, tagname): + + query = ' '.join(( + + "SELECT view_id", + "FROM view", + "WHERE view_name = ?" + )) + self.embycursor.execute(query, (tagname,)) + try: + view = self.embycursor.fetchone()[0] + + except TypeError: + view = None + + return view + + def addView(self, embyid, name, mediatype, tagid): + + query = ( + ''' + INSERT INTO view( + view_id, view_name, media_type, kodi_tagid) + + VALUES (?, ?, ?, ?) + ''' + ) + self.embycursor.execute(query, (embyid, name, mediatype, tagid)) + + def updateView(self, name, tagid, mediafolderid): + + query = ' '.join(( + + "UPDATE view", + "SET view_name = ?, kodi_tagid = ?", + "WHERE view_id = ?" + )) + self.embycursor.execute(query, (name, tagid, mediafolderid)) + + def removeView(self, viewid): + + query = ' '.join(( + + "DELETE FROM view", + "WHERE view_id = ?" + )) + self.embycursor.execute(query, (viewid,)) + + def getItem_byId(self, embyid): + + query = ' '.join(( + + "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type", + "FROM emby", + "WHERE emby_id = ?" + )) + try: + self.embycursor.execute(query, (embyid,)) + item = self.embycursor.fetchone() + return item + except: return None + + def getItem_byWildId(self, embyid): + + query = ' '.join(( + + "SELECT kodi_id, media_type", + "FROM emby", + "WHERE emby_id LIKE ?" + )) + self.embycursor.execute(query, (embyid+"%",)) + return self.embycursor.fetchall() + + def getItem_byView(self, mediafolderid): + + query = ' '.join(( + + "SELECT kodi_id", + "FROM emby", + "WHERE media_folder = ?" + )) + self.embycursor.execute(query, (mediafolderid,)) + return self.embycursor.fetchall() + + def getItem_byKodiId(self, kodiid, mediatype): + + query = ' '.join(( + + "SELECT emby_id, parent_id", + "FROM emby", + "WHERE kodi_id = ?", + "AND media_type = ?" + )) + self.embycursor.execute(query, (kodiid, mediatype,)) + return self.embycursor.fetchone() + + def getItem_byParentId(self, parentid, mediatype): + + query = ' '.join(( + + "SELECT emby_id, kodi_id, kodi_fileid", + "FROM emby", + "WHERE parent_id = ?", + "AND media_type = ?" + )) + self.embycursor.execute(query, (parentid, mediatype,)) + return self.embycursor.fetchall() + + def getItemId_byParentId(self, parentid, mediatype): + + query = ' '.join(( + + "SELECT emby_id, kodi_id", + "FROM emby", + "WHERE parent_id = ?", + "AND media_type = ?" + )) + self.embycursor.execute(query, (parentid, mediatype,)) + return self.embycursor.fetchall() + + def getChecksum(self, mediatype): + + query = ' '.join(( + + "SELECT emby_id, checksum", + "FROM emby", + "WHERE emby_type = ?" + )) + self.embycursor.execute(query, (mediatype,)) + return self.embycursor.fetchall() + + def getMediaType_byId(self, embyid): + + query = ' '.join(( + + "SELECT emby_type", + "FROM emby", + "WHERE emby_id = ?" + )) + self.embycursor.execute(query, (embyid,)) + try: + itemtype = self.embycursor.fetchone()[0] + + except TypeError: + itemtype = None + + return itemtype + + def sortby_mediaType(self, itemids, unsorted=True): + + sorted_items = {} + + for itemid in itemids: + + mediatype = self.getMediaType_byId(itemid) + if mediatype: + sorted_items.setdefault(mediatype, []).append(itemid) + elif unsorted: + sorted_items.setdefault('Unsorted', []).append(itemid) + + return sorted_items + + def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None, + parentid=None, checksum=None, mediafolderid=None): + query = ( + ''' + INSERT OR REPLACE INTO emby( + emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id, + checksum, media_folder) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype, + parentid, checksum, mediafolderid)) + + def updateReference(self, embyid, checksum): + + query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" + self.embycursor.execute(query, (checksum, embyid)) + + def updateParentId(self, embyid, parent_kodiid): + + query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?" + self.embycursor.execute(query, (parent_kodiid, embyid)) + + def removeItems_byParentId(self, parent_kodiid, mediatype): + + query = ' '.join(( + + "DELETE FROM emby", + "WHERE parent_id = ?", + "AND media_type = ?" + )) + self.embycursor.execute(query, (parent_kodiid, mediatype,)) + + def removeItem_byKodiId(self, kodiid, mediatype): + + query = ' '.join(( + + "DELETE FROM emby", + "WHERE kodi_id = ?", + "AND media_type = ?" + )) + self.embycursor.execute(query, (kodiid, mediatype,)) + + def removeItem(self, embyid): + + query = "DELETE FROM emby WHERE emby_id = ?" + self.embycursor.execute(query, (embyid,)) + + def removeWildItem(self, embyid): + + query = "DELETE FROM emby WHERE emby_id LIKE ?" + self.embycursor.execute(query, (embyid+"%",)) \ No newline at end of file diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 11c66f5d..bc81ad0a 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -1,1089 +1,1089 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import os -import sys -import urlparse - -import xbmc -import xbmcaddon -import xbmcgui -import xbmcvfs -import xbmcplugin - -import artwork -import utils -import clientinfo -import downloadutils -import librarysync -import read_embyserver as embyserver -import embydb_functions as embydb -import playlist -import playbackutils as pbutils -import playutils -import api - - -################################################################################################# - - -def doPlayback(itemid, dbid): - - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemid) - pbutils.PlaybackUtils(item).play(itemid, dbid) - -##### DO RESET AUTH ##### -def resetAuth(): - # User tried login and failed too many times - resp = xbmcgui.Dialog().yesno( - heading="Warning", - line1=( - "Emby might lock your account if you fail to log in too many times. " - "Proceed anyway?")) - if resp == 1: - utils.logMsg("EMBY", "Reset login attempts.", 1) - utils.window('emby_serverStatus', value="Auth") - else: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') - -def addDirectoryItem(label, path, folder=True): - li = xbmcgui.ListItem(label, path=path) - li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png") - li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"}) - li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"}) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder) - -def doMainListing(): - xbmcplugin.setContent(int(sys.argv[1]), 'files') - # Get emby nodes from the window props - embyprops = utils.window('Emby.nodes.total') - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - path = utils.window('Emby.nodes.%s.index' % i) - 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) - #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": - addDirectoryItem(label, path) - elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and type != "photos": - addDirectoryItem(label, path) - elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"): - addDirectoryItem(label, path) - - #experimental live tv nodes - addDirectoryItem("Live Tv Channels (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=tvchannels&folderid=root") - addDirectoryItem("Live Tv Recordings (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root") - - # some extra entries for settings and stuff. TODO --> localize the labels - addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords") - addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings") - addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser") - addDirectoryItem("Refresh Emby playlists/nodes", "plugin://plugin.video.emby/?mode=refreshplaylist") - addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync") - addDirectoryItem("Repair local database (force update all content)", "plugin://plugin.video.emby/?mode=repair") - addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset") - addDirectoryItem("Cache all images to Kodi texture cache", "plugin://plugin.video.emby/?mode=texturecache") - addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia") - - xbmcplugin.endOfDirectory(int(sys.argv[1])) - - -##### Generate a new deviceId -def resetDeviceId(): - - dialog = xbmcgui.Dialog() - language = utils.language - - deviceId_old = utils.window('emby_deviceId') - try: - utils.window('emby_deviceId', clear=True) - deviceId = clientinfo.ClientInfo().getDeviceId(reset=True) - except Exception as e: - utils.logMsg("EMBY", "Failed to generate a new device Id: %s" % e, 1) - dialog.ok( - heading="Emby for Kodi", - line1=language(33032)) - else: - utils.logMsg("EMBY", "Successfully removed old deviceId: %s New deviceId: %s" - % (deviceId_old, deviceId), 1) - dialog.ok( - heading="Emby for Kodi", - line1=language(33033)) - xbmc.executebuiltin('RestartApp') - -##### Delete Item -def deleteItem(): - - # Serves as a keymap action - if xbmc.getInfoLabel('ListItem.Property(embyid)'): # If we already have the embyid - embyid = xbmc.getInfoLabel('ListItem.Property(embyid)') - else: - dbid = xbmc.getInfoLabel('ListItem.DBID') - itemtype = xbmc.getInfoLabel('ListItem.DBTYPE') - - if not itemtype: - - if xbmc.getCondVisibility('Container.Content(albums)'): - itemtype = "album" - elif xbmc.getCondVisibility('Container.Content(artists)'): - itemtype = "artist" - elif xbmc.getCondVisibility('Container.Content(songs)'): - itemtype = "song" - elif xbmc.getCondVisibility('Container.Content(pictures)'): - itemtype = "picture" - else: - utils.logMsg("EMBY delete", "Unknown type, unable to proceed.", 1) - return - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(dbid, itemtype) - embycursor.close() - - try: - embyid = item[0] - except TypeError: - utils.logMsg("EMBY delete", "Unknown embyId, unable to proceed.", 1) - return - - if utils.settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1=("Delete file from Emby Server? This will " - "also delete the file(s) from disk!")) - if not resp: - utils.logMsg("EMBY delete", "User skipped deletion for: %s." % embyid, 1) - return - - 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") - -##### ADD ADDITIONAL USERS ##### -def addUser(): - - doUtils = downloadutils.DownloadUtils() - art = artwork.Artwork() - clientInfo = clientinfo.ClientInfo() - deviceId = clientInfo.getDeviceId() - deviceName = clientInfo.getDeviceName() - userid = utils.window('emby_currUser') - dialog = xbmcgui.Dialog() - - # Get session - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId - result = doUtils.downloadUrl(url) - - try: - sessionId = result[0]['Id'] - additionalUsers = result[0]['AdditionalUsers'] - # Add user to session - userlist = {} - users = [] - url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json" - result = doUtils.downloadUrl(url) - - # pull the list of users - for user in result: - name = user['Name'] - userId = user['Id'] - if userid != userId: - userlist[name] = userId - users.append(name) - - # Display dialog if there's additional users - if additionalUsers: - - option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"]) - # Users currently in the session - additionalUserlist = {} - additionalUsername = [] - # Users currently in the session - for user in additionalUsers: - name = user['UserName'] - userId = user['UserId'] - additionalUserlist[name] = userId - additionalUsername.append(name) - - if option == 1: - # User selected Remove user - resp = dialog.select("Remove user from the session", additionalUsername) - if resp > -1: - selected = additionalUsername[resp] - selected_userId = additionalUserlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="DELETE") - dialog.notification( - heading="Success!", - message="%s removed from viewing session" % selected, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) - - # clear picture - position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId) - utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) - return - else: - return - - elif option == 0: - # User selected Add user - for adduser in additionalUsername: - try: # Remove from selected already added users. It is possible they are hidden. - users.remove(adduser) - except: pass - - elif option < 0: - # User cancelled - return - - # Subtract any additional users - utils.logMsg("EMBY", "Displaying list of users: %s" % users) - resp = dialog.select("Add user to the session", users) - # post additional user - if resp > -1: - selected = users[resp] - selected_userId = userlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, type="POST") - dialog.notification( - heading="Success!", - message="%s added to viewing session" % selected, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) - - except: - utils.logMsg("EMBY", "Failed to add user to session.") - dialog.notification( - heading="Error", - message="Unable to add/remove user from the session.", - icon=xbmcgui.NOTIFICATION_ERROR) - - # Add additional user images - # always clear the individual items first - totalNodes = 10 - for i in range(totalNodes): - if not utils.window('EmbyAdditionalUserImage.%s' % i): - break - utils.window('EmbyAdditionalUserImage.%s' % i, clear=True) - - url = "{server}/emby/Sessions?DeviceId=%s" % deviceId - result = doUtils.downloadUrl(url) - additionalUsers = result[0]['AdditionalUsers'] - count = 0 - for additionaluser in additionalUsers: - userid = additionaluser['UserId'] - url = "{server}/emby/Users/%s?format=json" % userid - result = doUtils.downloadUrl(url) - utils.window('EmbyAdditionalUserImage.%s' % count, - value=art.getUserArtwork(result['Id'], 'Primary')) - utils.window('EmbyAdditionalUserPosition.%s' % userid, value=str(count)) - count +=1 - -##### THEME MUSIC/VIDEOS ##### -def getThemeMedia(): - - doUtils = downloadutils.DownloadUtils() - dialog = xbmcgui.Dialog() - playback = None - - # Choose playback method - resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"]) - if resp == 0: - playback = "DirectPlay" - elif resp == 1: - playback = "DirectStream" - else: - return - - library = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') - # Create library directory - if not xbmcvfs.exists(library): - xbmcvfs.mkdir(library) - - # Set custom path for user - tvtunes_path = xbmc.translatePath( - "special://profile/addon_data/script.tvtunes/").decode('utf-8') - if xbmcvfs.exists(tvtunes_path): - tvtunes = xbmcaddon.Addon(id="script.tvtunes") - tvtunes.setSetting('custom_path_enable', "true") - tvtunes.setSetting('custom_path', library) - utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1) - else: - # if it does not exist this will not work so warn user - # often they need to edit the settings first for it to be created. - dialog.ok( - heading="Warning", - line1=( - "The settings file does not exist in tvtunes. ", - "Go to the tvtunes addon and change a setting, then come back and re-run.")) - xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)') - return - - # Get every user view Id - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - viewids = emby_db.getViews() - embycursor.close() - - # Get Ids with Theme Videos - itemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view - result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] - folderName = utils.normalize_string(folderName.encode('utf-8')) - itemIds[itemId] = folderName - - # Get paths for theme videos - for itemId in itemIds: - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId]) - # Create folders for each content - if not xbmcvfs.exists(nfo_path): - xbmcvfs.mkdir(nfo_path) - # Where to put the nfos - nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - - url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId - result = doUtils.downloadUrl(url) - - # Create nfo and write themes to it - nfo_file = xbmcvfs.File(nfo_path, 'w') - pathstowrite = "" - # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) - if playback == "DirectPlay": - playurl = putils.directPlay() - else: - playurl = putils.directStream() - pathstowrite += ('%s' % playurl.encode('utf-8')) - - # Check if the item has theme songs and add them - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId - result = doUtils.downloadUrl(url) - - # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) - if playback == "DirectPlay": - playurl = putils.directPlay() - else: - playurl = putils.directStream() - pathstowrite += ('%s' % playurl.encode('utf-8')) - - nfo_file.write( - '%s' % pathstowrite - ) - # Close nfo file - nfo_file.close() - - # Get Ids with Theme songs - musicitemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view - result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] - folderName = utils.normalize_string(folderName.encode('utf-8')) - musicitemIds[itemId] = folderName - - # Get paths - for itemId in musicitemIds: - - # if the item was already processed with video themes back out - if itemId in itemIds: - continue - - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId]) - # Create folders for each content - if not xbmcvfs.exists(nfo_path): - xbmcvfs.mkdir(nfo_path) - # Where to put the nfos - nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId - result = doUtils.downloadUrl(url) - - # Create nfo and write themes to it - nfo_file = xbmcvfs.File(nfo_path, 'w') - pathstowrite = "" - # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) - if playback == "DirectPlay": - playurl = putils.directPlay() - else: - playurl = putils.directStream() - pathstowrite += ('%s' % playurl.encode('utf-8')) - - nfo_file.write( - '%s' % pathstowrite - ) - # Close nfo file - nfo_file.close() - - dialog.notification( - heading="Emby for Kodi", - message="Themes added!", - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - -##### REFRESH EMBY PLAYLISTS ##### -def refreshPlaylist(): - - lib = librarysync.LibrarySync() - dialog = xbmcgui.Dialog() - try: - # First remove playlists - utils.deletePlaylists() - # Remove video nodes - utils.deleteNodes() - # Refresh views - lib.refreshViews() - dialog.notification( - heading="Emby for Kodi", - message="Emby playlists/nodes refreshed", - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - - except Exception as e: - utils.logMsg("EMBY", "Refresh playlists/nodes failed: %s" % e, 1) - dialog.notification( - heading="Emby for Kodi", - message="Emby playlists/nodes refresh failed", - icon=xbmcgui.NOTIFICATION_ERROR, - time=1000, - sound=False) - -#### SHOW SUBFOLDERS FOR NODE ##### -def GetSubFolders(nodeindex): - nodetypes = ["",".recent",".recentepisodes",".inprogress",".inprogressepisodes",".unwatched",".nextepisodes",".sets",".genres",".random",".recommended"] - for node in nodetypes: - 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=""): - - 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 - folderid = "" - else: - filter = "" - - xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname) - #get views for root level - if not folderid: - views = emby.getViews(type) - for view in views: - if view.get("name") == viewname.decode('utf-8'): - folderid = view.get("id") - - if viewname is not None: - 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'))) - #set the correct params for the content type - #only proceed if we have a folderid - if folderid: - if type.lower() == "homevideos": - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - itemtype = "Video,Folder,PhotoAlbum" - elif type.lower() == "photos": - xbmcplugin.setContent(int(sys.argv[1]), 'files') - itemtype = "Photo,PhotoAlbum,Folder" - else: - itemtype = "" - - #get the actual listing - if type == "recordings": - listing = emby.getTvRecordings(folderid) - elif type == "tvchannels": - listing = emby.getTvChannels() - elif filter == "recent": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending") - elif filter == "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") - else: - listing = emby.getFilteredSection(folderid, itemtype=itemtype, recursive=False) - - #process the listing - if listing: - for item in listing.get("Items"): - 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')) - 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": - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) - else: - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### CREATE LISTITEM FROM EMBY METADATA ##### -def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()): - API = api.API(item) - itemid = item['Id'] - - title = item.get('Name') - li = xbmcgui.ListItem(title) - - premieredate = item.get('PremiereDate',"") - if not premieredate: premieredate = item.get('DateCreated',"") - if premieredate: - premieredatelst = premieredate.split('T')[0].split("-") - premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0]) - - li.setProperty("embyid",itemid) - - allart = art.getAllArtwork(item) - - if item["Type"] == "Photo": - #listitem setup for pictures... - img_path = allart.get('Primary') - li.setProperty("path",img_path) - picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid) - if picture: - picture = picture[0] - if picture.get("Width") > picture.get("Height"): - li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title}) - li.setThumbnailImage(img_path) - li.setProperty("plot",API.getOverview()) - li.setIconImage('DefaultPicture.png') - else: - #normal video items - li.setProperty('IsPlayable', 'true') - path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id")) - li.setProperty("path",path) - genre = API.getGenres() - overlay = 0 - userdata = API.getUserData() - runtime = item.get("RunTimeTicks",0)/ 10000000.0 - seektime = userdata['Resume'] - if seektime: - li.setProperty("resumetime", str(seektime)) - li.setProperty("totaltime", str(runtime)) - - played = userdata['Played'] - if played: overlay = 7 - else: overlay = 6 - playcount = userdata['PlayCount'] - if playcount is None: - playcount = 0 - - rating = item.get('CommunityRating') - if not rating: rating = userdata['UserRating'] - - # Populate the extradata list and artwork - extradata = { - 'id': itemid, - 'rating': rating, - 'year': item.get('ProductionYear'), - 'genre': genre, - 'playcount': str(playcount), - 'title': title, - 'plot': API.getOverview(), - 'Overlay': str(overlay), - 'duration': runtime - } - if premieredate: - extradata["premieredate"] = premieredate - extradata["date"] = premieredate - li.setInfo('video', infoLabels=extradata) - if allart.get('Primary'): - li.setThumbnailImage(allart.get('Primary')) - else: li.setThumbnailImage('DefaultTVShows.png') - li.setIconImage('DefaultTVShows.png') - if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setArt( {"fanart": allart.get('Primary') } ) - else: - pbutils.PlaybackUtils(item).setArtwork(li) - - mediastreams = API.getMediaStreams() - videostreamFound = False - if mediastreams: - for key, value in mediastreams.iteritems(): - if key == "video" and value: videostreamFound = True - if value: li.addStreamInfo(key, value[0]) - if not videostreamFound: - #just set empty streamdetails to prevent errors in the logs - li.addStreamInfo("video", {'duration': runtime}) - - return li - -##### BROWSE EMBY CHANNELS ##### -def BrowseChannels(itemid, folderid=None): - - _addon_id = int(sys.argv[1]) - _addon_url = sys.argv[0] - doUtils = downloadutils.DownloadUtils() - art = artwork.Artwork() - - xbmcplugin.setContent(int(sys.argv[1]), 'files') - if folderid: - url = ( - "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json" - % (itemid, folderid)) - elif itemid == "0": - # id 0 is the root channels folder - url = "{server}/emby/Channels?{UserId}&format=json" - else: - url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid - - result = doUtils.downloadUrl(url) - if result and result.get("Items"): - for item in result.get("Items"): - itemid = item['Id'] - itemtype = item['Type'] - li = createListItemFromEmbyItem(item,art,doUtils) - - isFolder = item.get('IsFolder', False) - - channelId = item.get('ChannelId', "") - channelName = item.get('ChannelName', "") - if itemtype == "Channel": - path = "%s?id=%s&mode=channels" % (_addon_url, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) - elif isFolder: - path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) - else: - path = "%s?id=%s&mode=play" % (_addon_url, itemid) - li.setProperty('IsPlayable', 'true') - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li) - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### LISTITEM SETUP FOR VIDEONODES ##### -def createListItem(item): - - title = item['title'] - li = xbmcgui.ListItem(title) - li.setProperty('IsPlayable', "true") - - metadata = { - - 'Title': title, - 'duration': str(item['runtime']/60), - 'Plot': item['plot'], - 'Playcount': item['playcount'] - } - - if "episode" in item: - episode = item['episode'] - metadata['Episode'] = episode - - if "season" in item: - season = item['season'] - metadata['Season'] = season - - if season and episode: - li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) - - if "firstaired" in item: - metadata['Premiered'] = item['firstaired'] - - if "showtitle" in item: - metadata['TVshowTitle'] = item['showtitle'] - - if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']),1)) - - if "director" in item: - metadata['Director'] = " / ".join(item['director']) - - if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) - - if "cast" in item: - cast = [] - castandrole = [] - for person in item['cast']: - name = person['name'] - cast.append(name) - castandrole.append((name, person['role'])) - metadata['Cast'] = cast - metadata['CastAndRole'] = castandrole - - li.setInfo(type="Video", infoLabels=metadata) - li.setProperty('resumetime', str(item['resume']['position'])) - li.setProperty('totaltime', str(item['resume']['total'])) - li.setArt(item['art']) - li.setThumbnailImage(item['art'].get('thumb','')) - li.setIconImage('DefaultTVShows.png') - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) - for key, value in item['streamdetails'].iteritems(): - for stream in value: - li.addStreamInfo(key, stream) - - return li - -##### GET NEXTUP EPISODES FOR TAGNAME ##### -def getNextUpEpisodes(tagname, limit): - - count = 0 - # if the addon is called with nextup parameter, - # we return the nextepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - if utils.settings('ignoreSpecialsNextEpisodes') == "true": - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - {'operator': "greaterthan", 'field': "season", 'value': "0"} - ]}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - else: - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET INPROGRESS EPISODES FOR TAGNAME ##### -def getInProgressEpisodes(tagname, limit): - - count = 0 - # if the addon is called with inprogressepisodes parameter, - # we return the inprogressepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the in-progress TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "true", 'field': "inprogress", 'value': ""}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "cast", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET RECENT EPISODES FOR TAGNAME ##### -def getRecentEpisodes(tagname, limit): - - count = 0 - # if the addon is called with recentepisodes parameter, - # we return the recentepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname}, - 'properties': ["title","sorttitle"] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - allshowsIds = set() - for item in items: - allshowsIds.add(item['tvshowid']) - - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "streamdetails", - "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" - ], - "limits": {"end": limit} - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - if episode['tvshowid'] in allshowsIds: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET VIDEO EXTRAS FOR LISTITEM ##### -def getVideoFiles(embyId,embyPath): - #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc. - emby = embyserver.Read_EmbyServer() - if not embyId: - if "plugin.video.emby" in embyPath: - embyId = embyPath.split("/")[-2] - if embyId: - item = emby.getItem(embyId) - putils = playutils.PlayUtils(item) - if putils.isDirectPlay(): - #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside - filelocation = putils.directPlay() - if not filelocation.endswith("/"): - filelocation = filelocation.rpartition("/")[0] - dirs, files = xbmcvfs.listdir(filelocation) - for file in files: - file = filelocation + file - li = xbmcgui.ListItem(file, path=file) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li) - for dir in dirs: - dir = filelocation + dir - li = xbmcgui.ListItem(dir, path=dir) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True) - xbmcplugin.endOfDirectory(int(sys.argv[1])) - -##### GET EXTRAFANART FOR LISTITEM ##### -def getExtraFanArt(embyId,embyPath): - - emby = embyserver.Read_EmbyServer() - art = artwork.Artwork() - - # Get extrafanart for listitem - # will be called by skinhelper script to get the extrafanart - try: - # for tvshows we get the embyid just from the path - if not embyId: - if "plugin.video.emby" in embyPath: - embyId = embyPath.split("/")[-2] - - if embyId: - #only proceed if we actually have a emby id - utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 0) - - # We need to store the images locally for this to work - # because of the caching system in xbmc - fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8') - - if not xbmcvfs.exists(fanartDir): - # Download the images to the cache directory - xbmcvfs.mkdirs(fanartDir) - item = emby.getItem(embyId) - if item: - backdrops = art.getAllArtwork(item)['Backdrop'] - tags = item['BackdropImageTags'] - count = 0 - for backdrop in backdrops: - # Same ordering as in artwork - tag = tags[count] - if os.path.supports_unicode_filenames: - fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag) - else: - fanartFile = os.path.join(fanartDir.encode("utf-8"), "fanart%s.jpg" % tag.encode("utf-8")) - li = xbmcgui.ListItem(tag, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) - xbmcvfs.copy(backdrop, fanartFile) - count += 1 - else: - utils.logMsg("EMBY", "Found cached backdrop.", 2) - # Use existing cached images - dirs, files = xbmcvfs.listdir(fanartDir) - for file in files: - fanartFile = os.path.join(fanartDir, file.decode('utf-8')) - li = xbmcgui.ListItem(file, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) - except Exception as e: - utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 0) - - # Always do endofdirectory to prevent errors in the logs +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import os +import sys +import urlparse + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcvfs +import xbmcplugin + +import artwork +import utils +import clientinfo +import downloadutils +import librarysync +import read_embyserver as embyserver +import embydb_functions as embydb +import playlist +import playbackutils as pbutils +import playutils +import api + + +################################################################################################# + + +def doPlayback(itemid, dbid): + + emby = embyserver.Read_EmbyServer() + item = emby.getItem(itemid) + pbutils.PlaybackUtils(item).play(itemid, dbid) + +##### DO RESET AUTH ##### +def resetAuth(): + # User tried login and failed too many times + resp = xbmcgui.Dialog().yesno( + heading="Warning", + line1=( + "Emby might lock your account if you fail to log in too many times. " + "Proceed anyway?")) + if resp == 1: + utils.logMsg("EMBY", "Reset login attempts.", 1) + utils.window('emby_serverStatus', value="Auth") + else: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + +def addDirectoryItem(label, path, folder=True): + li = xbmcgui.ListItem(label, path=path) + li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png") + li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"}) + li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"}) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder) + +def doMainListing(): + xbmcplugin.setContent(int(sys.argv[1]), 'files') + # Get emby nodes from the window props + embyprops = utils.window('Emby.nodes.total') + if embyprops: + totalnodes = int(embyprops) + for i in range(totalnodes): + path = utils.window('Emby.nodes.%s.index' % i) + if not path: + path = utils.window('Emby.nodes.%s.content' % i) + label = utils.window('Emby.nodes.%s.title' % 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 node_type == "photos": + addDirectoryItem(label, path) + 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) + + #experimental live tv nodes + addDirectoryItem("Live Tv Channels (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=tvchannels&folderid=root") + addDirectoryItem("Live Tv Recordings (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root") + + # some extra entries for settings and stuff. TODO --> localize the labels + addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords") + addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings") + addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser") + addDirectoryItem("Refresh Emby playlists/nodes", "plugin://plugin.video.emby/?mode=refreshplaylist") + addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync") + addDirectoryItem("Repair local database (force update all content)", "plugin://plugin.video.emby/?mode=repair") + addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset") + addDirectoryItem("Cache all images to Kodi texture cache", "plugin://plugin.video.emby/?mode=texturecache") + addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia") + + xbmcplugin.endOfDirectory(int(sys.argv[1])) + + +##### Generate a new deviceId +def resetDeviceId(): + + dialog = xbmcgui.Dialog() + language = utils.language + + deviceId_old = utils.window('emby_deviceId') + try: + utils.window('emby_deviceId', clear=True) + deviceId = clientinfo.ClientInfo().getDeviceId(reset=True) + except Exception as e: + utils.logMsg("EMBY", "Failed to generate a new device Id: %s" % e, 1) + dialog.ok( + heading="Emby for Kodi", + line1=language(33032)) + else: + utils.logMsg("EMBY", "Successfully removed old deviceId: %s New deviceId: %s" + % (deviceId_old, deviceId), 1) + dialog.ok( + heading="Emby for Kodi", + line1=language(33033)) + xbmc.executebuiltin('RestartApp') + +##### Delete Item +def deleteItem(): + + # Serves as a keymap action + if xbmc.getInfoLabel('ListItem.Property(embyid)'): # If we already have the embyid + embyid = xbmc.getInfoLabel('ListItem.Property(embyid)') + else: + dbid = xbmc.getInfoLabel('ListItem.DBID') + itemtype = xbmc.getInfoLabel('ListItem.DBTYPE') + + if not itemtype: + + if xbmc.getCondVisibility('Container.Content(albums)'): + itemtype = "album" + elif xbmc.getCondVisibility('Container.Content(artists)'): + itemtype = "artist" + elif xbmc.getCondVisibility('Container.Content(songs)'): + itemtype = "song" + elif xbmc.getCondVisibility('Container.Content(pictures)'): + itemtype = "picture" + else: + utils.logMsg("EMBY delete", "Unknown type, unable to proceed.", 1) + return + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + item = emby_db.getItem_byKodiId(dbid, itemtype) + embycursor.close() + + try: + embyid = item[0] + except TypeError: + utils.logMsg("EMBY delete", "Unknown embyId, unable to proceed.", 1) + return + + if utils.settings('skipContextMenu') != "true": + resp = xbmcgui.Dialog().yesno( + heading="Confirm delete", + line1=("Delete file from Emby Server? This will " + "also delete the file(s) from disk!")) + if not resp: + utils.logMsg("EMBY delete", "User skipped deletion for: %s." % embyid, 1) + return + + doUtils = downloadutils.DownloadUtils() + url = "{server}/emby/Items/%s?format=json" % embyid + utils.logMsg("EMBY delete", "Deleting request: %s" % embyid, 0) + doUtils.downloadUrl(url, action_type="DELETE") + +##### ADD ADDITIONAL USERS ##### +def addUser(): + + doUtils = downloadutils.DownloadUtils() + art = artwork.Artwork() + clientInfo = clientinfo.ClientInfo() + deviceId = clientInfo.getDeviceId() + deviceName = clientInfo.getDeviceName() + userid = utils.window('emby_currUser') + dialog = xbmcgui.Dialog() + + # Get session + url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId + result = doUtils.downloadUrl(url) + + try: + sessionId = result[0]['Id'] + additionalUsers = result[0]['AdditionalUsers'] + # Add user to session + userlist = {} + users = [] + url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json" + result = doUtils.downloadUrl(url) + + # pull the list of users + for user in result: + name = user['Name'] + userId = user['Id'] + if userid != userId: + userlist[name] = userId + users.append(name) + + # Display dialog if there's additional users + if additionalUsers: + + option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"]) + # Users currently in the session + additionalUserlist = {} + additionalUsername = [] + # Users currently in the session + for user in additionalUsers: + name = user['UserName'] + userId = user['UserId'] + additionalUserlist[name] = userId + additionalUsername.append(name) + + if option == 1: + # User selected Remove user + resp = dialog.select("Remove user from the session", additionalUsername) + if resp > -1: + selected = additionalUsername[resp] + selected_userId = additionalUserlist[selected] + url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) + doUtils.downloadUrl(url, postBody={}, action_type="DELETE") + dialog.notification( + heading="Success!", + message="%s removed from viewing session" % selected, + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000) + + # clear picture + position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId) + utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) + return + else: + return + + elif option == 0: + # User selected Add user + for adduser in additionalUsername: + try: # Remove from selected already added users. It is possible they are hidden. + users.remove(adduser) + except: pass + + elif option < 0: + # User cancelled + return + + # Subtract any additional users + utils.logMsg("EMBY", "Displaying list of users: %s" % users) + resp = dialog.select("Add user to the session", users) + # post additional user + if resp > -1: + selected = users[resp] + selected_userId = userlist[selected] + url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) + doUtils.downloadUrl(url, postBody={}, action_type="POST") + dialog.notification( + heading="Success!", + message="%s added to viewing session" % selected, + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000) + + except: + utils.logMsg("EMBY", "Failed to add user to session.") + dialog.notification( + heading="Error", + message="Unable to add/remove user from the session.", + icon=xbmcgui.NOTIFICATION_ERROR) + + # Add additional user images + # always clear the individual items first + totalNodes = 10 + for i in range(totalNodes): + if not utils.window('EmbyAdditionalUserImage.%s' % i): + break + utils.window('EmbyAdditionalUserImage.%s' % i, clear=True) + + url = "{server}/emby/Sessions?DeviceId=%s" % deviceId + result = doUtils.downloadUrl(url) + additionalUsers = result[0]['AdditionalUsers'] + count = 0 + for additionaluser in additionalUsers: + userid = additionaluser['UserId'] + url = "{server}/emby/Users/%s?format=json" % userid + result = doUtils.downloadUrl(url) + utils.window('EmbyAdditionalUserImage.%s' % count, + value=art.getUserArtwork(result['Id'], 'Primary')) + utils.window('EmbyAdditionalUserPosition.%s' % userid, value=str(count)) + count +=1 + +##### THEME MUSIC/VIDEOS ##### +def getThemeMedia(): + + doUtils = downloadutils.DownloadUtils() + dialog = xbmcgui.Dialog() + playback = None + + # Choose playback method + resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"]) + if resp == 0: + playback = "DirectPlay" + elif resp == 1: + playback = "DirectStream" + else: + return + + library = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') + # Create library directory + if not xbmcvfs.exists(library): + xbmcvfs.mkdir(library) + + # Set custom path for user + tvtunes_path = xbmc.translatePath( + "special://profile/addon_data/script.tvtunes/").decode('utf-8') + if xbmcvfs.exists(tvtunes_path): + tvtunes = xbmcaddon.Addon(id="script.tvtunes") + tvtunes.setSetting('custom_path_enable', "true") + tvtunes.setSetting('custom_path', library) + utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1) + else: + # if it does not exist this will not work so warn user + # often they need to edit the settings first for it to be created. + dialog.ok( + heading="Warning", + line1=( + "The settings file does not exist in tvtunes. ", + "Go to the tvtunes addon and change a setting, then come back and re-run.")) + xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)') + return + + # Get every user view Id + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + viewids = emby_db.getViews() + embycursor.close() + + # Get Ids with Theme Videos + itemIds = {} + for view in viewids: + url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view + result = doUtils.downloadUrl(url) + if result['TotalRecordCount'] != 0: + for item in result['Items']: + itemId = item['Id'] + folderName = item['Name'] + folderName = utils.normalize_string(folderName.encode('utf-8')) + itemIds[itemId] = folderName + + # Get paths for theme videos + for itemId in itemIds: + nfo_path = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId]) + # Create folders for each content + if not xbmcvfs.exists(nfo_path): + xbmcvfs.mkdir(nfo_path) + # Where to put the nfos + nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") + + url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId + result = doUtils.downloadUrl(url) + + # Create nfo and write themes to it + nfo_file = xbmcvfs.File(nfo_path, 'w') + pathstowrite = "" + # May be more than one theme + for theme in result['Items']: + putils = playutils.PlayUtils(theme) + if playback == "DirectPlay": + playurl = putils.directPlay() + else: + playurl = putils.directStream() + pathstowrite += ('%s' % playurl.encode('utf-8')) + + # Check if the item has theme songs and add them + url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId + result = doUtils.downloadUrl(url) + + # May be more than one theme + for theme in result['Items']: + putils = playutils.PlayUtils(theme) + if playback == "DirectPlay": + playurl = putils.directPlay() + else: + playurl = putils.directStream() + pathstowrite += ('%s' % playurl.encode('utf-8')) + + nfo_file.write( + '%s' % pathstowrite + ) + # Close nfo file + nfo_file.close() + + # Get Ids with Theme songs + musicitemIds = {} + for view in viewids: + url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view + result = doUtils.downloadUrl(url) + if result['TotalRecordCount'] != 0: + for item in result['Items']: + itemId = item['Id'] + folderName = item['Name'] + folderName = utils.normalize_string(folderName.encode('utf-8')) + musicitemIds[itemId] = folderName + + # Get paths + for itemId in musicitemIds: + + # if the item was already processed with video themes back out + if itemId in itemIds: + continue + + nfo_path = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId]) + # Create folders for each content + if not xbmcvfs.exists(nfo_path): + xbmcvfs.mkdir(nfo_path) + # Where to put the nfos + nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") + + url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId + result = doUtils.downloadUrl(url) + + # Create nfo and write themes to it + nfo_file = xbmcvfs.File(nfo_path, 'w') + pathstowrite = "" + # May be more than one theme + for theme in result['Items']: + putils = playutils.PlayUtils(theme) + if playback == "DirectPlay": + playurl = putils.directPlay() + else: + playurl = putils.directStream() + pathstowrite += ('%s' % playurl.encode('utf-8')) + + nfo_file.write( + '%s' % pathstowrite + ) + # Close nfo file + nfo_file.close() + + dialog.notification( + heading="Emby for Kodi", + message="Themes added!", + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000, + sound=False) + +##### REFRESH EMBY PLAYLISTS ##### +def refreshPlaylist(): + + lib = librarysync.LibrarySync() + dialog = xbmcgui.Dialog() + try: + # First remove playlists + utils.deletePlaylists() + # Remove video nodes + utils.deleteNodes() + # Refresh views + lib.refreshViews() + dialog.notification( + heading="Emby for Kodi", + message="Emby playlists/nodes refreshed", + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000, + sound=False) + + except Exception as e: + utils.logMsg("EMBY", "Refresh playlists/nodes failed: %s" % e, 1) + dialog.notification( + heading="Emby for Kodi", + message="Emby playlists/nodes refresh failed", + icon=xbmcgui.NOTIFICATION_ERROR, + time=1000, + sound=False) + +#### SHOW SUBFOLDERS FOR NODE ##### +def GetSubFolders(nodeindex): + nodetypes = ["",".recent",".recentepisodes",".inprogress",".inprogressepisodes",".unwatched",".nextepisodes",".sets",".genres",".random",".recommended"] + for node in nodetypes: + title = utils.window('Emby.nodes.%s%s.title' %(nodeindex,node)) + if title: + path = utils.window('Emby.nodes.%s%s.content' %(nodeindex,node)) + addDirectoryItem(title, path) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +##### BROWSE EMBY NODES DIRECTLY ##### +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_type = folderid + folderid = "" + else: + filter_type = "" + + xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname) + #get views for root level + if not folderid: + views = emby.getViews(browse_type) + for view in views: + if view.get("name") == viewname.decode('utf-8'): + folderid = view.get("id") + break + + 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 browse_type.lower() == "homevideos": + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + itemtype = "Video,Folder,PhotoAlbum" + elif browse_type.lower() == "photos": + xbmcplugin.setContent(int(sys.argv[1]), 'files') + itemtype = "Photo,PhotoAlbum,Folder" + else: + itemtype = "" + + #get the actual listing + if browse_type == "recordings": + listing = emby.getTvRecordings(folderid) + elif browse_type == "tvchannels": + listing = emby.getTvChannels() + elif filter_type == "recent": + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending") + elif filter_type == "random": + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") + 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) + + #process the listing + if listing: + for item in listing.get("Items"): + 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'), 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_type == "recent": + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) + else: + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +##### CREATE LISTITEM FROM EMBY METADATA ##### +def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()): + API = api.API(item) + itemid = item['Id'] + + title = item.get('Name') + li = xbmcgui.ListItem(title) + + premieredate = item.get('PremiereDate',"") + if not premieredate: premieredate = item.get('DateCreated',"") + if premieredate: + premieredatelst = premieredate.split('T')[0].split("-") + premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0]) + + li.setProperty("embyid",itemid) + + allart = art.getAllArtwork(item) + + if item["Type"] == "Photo": + #listitem setup for pictures... + img_path = allart.get('Primary') + li.setProperty("path",img_path) + picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid) + if picture: + picture = picture[0] + if picture.get("Width") > picture.get("Height"): + li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation + li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title}) + li.setThumbnailImage(img_path) + li.setProperty("plot",API.getOverview()) + li.setIconImage('DefaultPicture.png') + else: + #normal video items + li.setProperty('IsPlayable', 'true') + path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id")) + li.setProperty("path",path) + genre = API.getGenres() + overlay = 0 + userdata = API.getUserData() + runtime = item.get("RunTimeTicks",0)/ 10000000.0 + seektime = userdata['Resume'] + if seektime: + li.setProperty("resumetime", str(seektime)) + li.setProperty("totaltime", str(runtime)) + + played = userdata['Played'] + if played: overlay = 7 + else: overlay = 6 + playcount = userdata['PlayCount'] + if playcount is None: + playcount = 0 + + rating = item.get('CommunityRating') + if not rating: rating = userdata['UserRating'] + + # Populate the extradata list and artwork + extradata = { + 'id': itemid, + 'rating': rating, + 'year': item.get('ProductionYear'), + 'genre': genre, + 'playcount': str(playcount), + 'title': title, + 'plot': API.getOverview(), + 'Overlay': str(overlay), + 'duration': runtime + } + if premieredate: + extradata["premieredate"] = premieredate + extradata["date"] = premieredate + li.setInfo('video', infoLabels=extradata) + if allart.get('Primary'): + li.setThumbnailImage(allart.get('Primary')) + else: li.setThumbnailImage('DefaultTVShows.png') + li.setIconImage('DefaultTVShows.png') + if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation + li.setArt( {"fanart": allart.get('Primary') } ) + else: + pbutils.PlaybackUtils(item).setArtwork(li) + + mediastreams = API.getMediaStreams() + videostreamFound = False + if mediastreams: + for key, value in mediastreams.iteritems(): + if key == "video" and value: videostreamFound = True + if value: li.addStreamInfo(key, value[0]) + if not videostreamFound: + #just set empty streamdetails to prevent errors in the logs + li.addStreamInfo("video", {'duration': runtime}) + + return li + +##### BROWSE EMBY CHANNELS ##### +def BrowseChannels(itemid, folderid=None): + + _addon_id = int(sys.argv[1]) + _addon_url = sys.argv[0] + doUtils = downloadutils.DownloadUtils() + art = artwork.Artwork() + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + if folderid: + url = ( + "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json" + % (itemid, folderid)) + elif itemid == "0": + # id 0 is the root channels folder + url = "{server}/emby/Channels?{UserId}&format=json" + else: + url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid + + result = doUtils.downloadUrl(url) + if result and result.get("Items"): + for item in result.get("Items"): + itemid = item['Id'] + itemtype = item['Type'] + li = createListItemFromEmbyItem(item,art,doUtils) + + isFolder = item.get('IsFolder', False) + + channelId = item.get('ChannelId', "") + channelName = item.get('ChannelName', "") + if itemtype == "Channel": + path = "%s?id=%s&mode=channels" % (_addon_url, itemid) + xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) + elif isFolder: + path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid) + xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) + else: + path = "%s?id=%s&mode=play" % (_addon_url, itemid) + li.setProperty('IsPlayable', 'true') + xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li) + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +##### LISTITEM SETUP FOR VIDEONODES ##### +def createListItem(item): + + title = item['title'] + li = xbmcgui.ListItem(title) + li.setProperty('IsPlayable', "true") + + metadata = { + + 'Title': title, + 'duration': str(item['runtime']/60), + 'Plot': item['plot'], + 'Playcount': item['playcount'] + } + + if "episode" in item: + episode = item['episode'] + metadata['Episode'] = episode + + if "season" in item: + season = item['season'] + metadata['Season'] = season + + if season and episode: + li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) + + if "firstaired" in item: + metadata['Premiered'] = item['firstaired'] + + if "showtitle" in item: + metadata['TVshowTitle'] = item['showtitle'] + + if "rating" in item: + metadata['Rating'] = str(round(float(item['rating']),1)) + + if "director" in item: + metadata['Director'] = " / ".join(item['director']) + + if "writer" in item: + metadata['Writer'] = " / ".join(item['writer']) + + if "cast" in item: + cast = [] + castandrole = [] + for person in item['cast']: + name = person['name'] + cast.append(name) + castandrole.append((name, person['role'])) + metadata['Cast'] = cast + metadata['CastAndRole'] = castandrole + + li.setInfo(type="Video", infoLabels=metadata) + li.setProperty('resumetime', str(item['resume']['position'])) + li.setProperty('totaltime', str(item['resume']['total'])) + li.setArt(item['art']) + li.setThumbnailImage(item['art'].get('thumb','')) + li.setIconImage('DefaultTVShows.png') + li.setProperty('dbid', str(item['episodeid'])) + li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) + for key, value in item['streamdetails'].iteritems(): + for stream in value: + li.addStreamInfo(key, stream) + + return li + +##### GET NEXTUP EPISODES FOR TAGNAME ##### +def getNextUpEpisodes(tagname, limit): + + count = 0 + # if the addon is called with nextup parameter, + # we return the nextepisodes list of the given tagname + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + # First we get a list of all the TV shows - filtered by tag + query = { + + 'jsonrpc': "2.0", + 'id': "libTvShows", + 'method': "VideoLibrary.GetTVShows", + 'params': { + + 'sort': {'order': "descending", 'method': "lastplayed"}, + 'filter': { + 'and': [ + {'operator': "true", 'field': "inprogress", 'value': ""}, + {'operator': "is", 'field': "tag", 'value': "%s" % tagname} + ]}, + 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] + } + } + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + # If we found any, find the oldest unwatched show for each one. + try: + items = result['result']['tvshows'] + except (KeyError, TypeError): + pass + else: + for item in items: + if utils.settings('ignoreSpecialsNextEpisodes') == "true": + query = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "VideoLibrary.GetEpisodes", + 'params': { + + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'and': [ + {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + {'operator': "greaterthan", 'field': "season", 'value': "0"} + ]}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + } + else: + query = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "VideoLibrary.GetEpisodes", + 'params': { + + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + } + + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + try: + episodes = result['result']['episodes'] + except (KeyError, TypeError): + pass + else: + for episode in episodes: + li = createListItem(episode) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=episode['file'], + listitem=li) + count += 1 + + if count == limit: + break + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +##### GET INPROGRESS EPISODES FOR TAGNAME ##### +def getInProgressEpisodes(tagname, limit): + + count = 0 + # if the addon is called with inprogressepisodes parameter, + # we return the inprogressepisodes list of the given tagname + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + # First we get a list of all the in-progress TV shows - filtered by tag + query = { + + 'jsonrpc': "2.0", + 'id': "libTvShows", + 'method': "VideoLibrary.GetTVShows", + 'params': { + + 'sort': {'order': "descending", 'method': "lastplayed"}, + 'filter': { + 'and': [ + {'operator': "true", 'field': "inprogress", 'value': ""}, + {'operator': "is", 'field': "tag", 'value': "%s" % tagname} + ]}, + 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] + } + } + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + # If we found any, find the oldest unwatched show for each one. + try: + items = result['result']['tvshows'] + except (KeyError, TypeError): + pass + else: + for item in items: + query = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "VideoLibrary.GetEpisodes", + 'params': { + + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': {'operator': "true", 'field': "inprogress", 'value': ""}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", "plot", + "file", "rating", "resume", "tvshowid", "art", "cast", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ] + } + } + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + try: + episodes = result['result']['episodes'] + except (KeyError, TypeError): + pass + else: + for episode in episodes: + li = createListItem(episode) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=episode['file'], + listitem=li) + count += 1 + + if count == limit: + break + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +##### GET RECENT EPISODES FOR TAGNAME ##### +def getRecentEpisodes(tagname, limit): + + count = 0 + # if the addon is called with recentepisodes parameter, + # we return the recentepisodes list of the given tagname + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + # First we get a list of all the TV shows - filtered by tag + query = { + + 'jsonrpc': "2.0", + 'id': "libTvShows", + 'method': "VideoLibrary.GetTVShows", + 'params': { + + 'sort': {'order': "descending", 'method': "dateadded"}, + 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname}, + 'properties': ["title","sorttitle"] + } + } + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + # If we found any, find the oldest unwatched show for each one. + try: + items = result['result']['tvshows'] + except (KeyError, TypeError): + pass + else: + allshowsIds = set() + for item in items: + allshowsIds.add(item['tvshowid']) + + query = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "VideoLibrary.GetEpisodes", + 'params': { + + 'sort': {'order': "descending", 'method': "dateadded"}, + 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", "plot", + "file", "rating", "resume", "tvshowid", "art", "streamdetails", + "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" + ], + "limits": {"end": limit} + } + } + result = xbmc.executeJSONRPC(json.dumps(query)) + result = json.loads(result) + try: + episodes = result['result']['episodes'] + except (KeyError, TypeError): + pass + else: + for episode in episodes: + if episode['tvshowid'] in allshowsIds: + li = createListItem(episode) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=episode['file'], + listitem=li) + count += 1 + + if count == limit: + break + + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + +##### GET VIDEO EXTRAS FOR LISTITEM ##### +def getVideoFiles(embyId,embyPath): + #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc. + emby = embyserver.Read_EmbyServer() + if not embyId: + if "plugin.video.emby" in embyPath: + embyId = embyPath.split("/")[-2] + if embyId: + item = emby.getItem(embyId) + putils = playutils.PlayUtils(item) + if putils.isDirectPlay(): + #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside + filelocation = putils.directPlay() + if not filelocation.endswith("/"): + filelocation = filelocation.rpartition("/")[0] + dirs, files = xbmcvfs.listdir(filelocation) + for file in files: + file = filelocation + file + li = xbmcgui.ListItem(file, path=file) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li) + for dir in dirs: + dir = filelocation + dir + li = xbmcgui.ListItem(dir, path=dir) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +##### GET EXTRAFANART FOR LISTITEM ##### +def getExtraFanArt(embyId,embyPath): + + emby = embyserver.Read_EmbyServer() + art = artwork.Artwork() + + # Get extrafanart for listitem + # will be called by skinhelper script to get the extrafanart + try: + # for tvshows we get the embyid just from the path + if not embyId: + if "plugin.video.emby" in embyPath: + embyId = embyPath.split("/")[-2] + + if embyId: + #only proceed if we actually have a emby id + utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 0) + + # We need to store the images locally for this to work + # because of the caching system in xbmc + fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8') + + if not xbmcvfs.exists(fanartDir): + # Download the images to the cache directory + xbmcvfs.mkdirs(fanartDir) + item = emby.getItem(embyId) + if item: + backdrops = art.getAllArtwork(item)['Backdrop'] + tags = item['BackdropImageTags'] + count = 0 + for backdrop in backdrops: + # Same ordering as in artwork + tag = tags[count] + if os.path.supports_unicode_filenames: + fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag) + else: + fanartFile = os.path.join(fanartDir.encode("utf-8"), "fanart%s.jpg" % tag.encode("utf-8")) + li = xbmcgui.ListItem(tag, path=fanartFile) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=fanartFile, + listitem=li) + xbmcvfs.copy(backdrop, fanartFile) + count += 1 + else: + utils.logMsg("EMBY", "Found cached backdrop.", 2) + # Use existing cached images + dirs, files = xbmcvfs.listdir(fanartDir) + for file in files: + fanartFile = os.path.join(fanartDir, file.decode('utf-8')) + li = xbmcgui.ListItem(file, path=fanartFile) + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=fanartFile, + listitem=li) + except Exception as e: + utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 0) + + # Always do endofdirectory to prevent errors in the logs xbmcplugin.endOfDirectory(int(sys.argv[1])) \ No newline at end of file diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index e23c9001..7bf0dbb9 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -176,8 +176,8 @@ class InitialSetup(): sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - self.logMsg("MultiGroup : %s" % str(MULTI_GROUP), 2); - self.logMsg("Sending UDP Data: %s" % MESSAGE, 2); + self.logMsg("MultiGroup : %s" % str(MULTI_GROUP), 2) + self.logMsg("Sending UDP Data: %s" % MESSAGE, 2) sock.sendto(MESSAGE, MULTI_GROUP) try: diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f3f03ca6..18c90517 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1,2470 +1,2433 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import urllib -from ntpath import dirname -from datetime import datetime - -import xbmc -import xbmcgui -import xbmcvfs - -import api -import artwork -import clientinfo -import downloadutils -import utils -import embydb_functions as embydb -import kodidb_functions as kodidb -import read_embyserver as embyserver -import musicutils - -################################################################################################## - - -class Items(object): - - - def __init__(self, embycursor, kodicursor): - - self.embycursor = embycursor - self.kodicursor = kodicursor - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils() - - self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) - self.directpath = utils.settings('useDirectPaths') == "1" - self.music_enabled = utils.settings('enableMusic') == "true" - self.contentmsg = utils.settings('newContent') == "true" - self.newvideo_time = int(utils.settings('newvideotime'))*1000 - self.newmusic_time = int(utils.settings('newmusictime'))*1000 - - self.artwork = artwork.Artwork() - self.emby = embyserver.Read_EmbyServer() - self.emby_db = embydb.Embydb_Functions(embycursor) - self.kodi_db = kodidb.Kodidb_Functions(kodicursor) - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def itemsbyId(self, items, process, pdialog=None): - # Process items by itemid. Process can be added, update, userdata, remove - emby = self.emby - embycursor = self.embycursor - kodicursor = self.kodicursor - music_enabled = self.music_enabled - - itemtypes = { - - 'Movie': Movies, - 'BoxSet': Movies, - 'Series': TVShows, - 'Season': TVShows, - 'Episode': TVShows, - 'MusicAlbum': Music, - 'MusicArtist': Music, - 'AlbumArtist': Music, - 'Audio': Music - } - - update_videolibrary = False - total = 0 - for item in items: - total += len(items[item]) - - if total == 0: - return False - - self.logMsg("Processing %s: %s" % (process, items), 1) - if pdialog: - pdialog.update(heading="Processing %s: %s items" % (process, total)) - - count = 0 - for itemtype in items: - - # Safety check - if not itemtypes.get(itemtype): - # We don't process this type of item - continue - - itemlist = items[itemtype] - if not itemlist: - # The list to process is empty - continue - - musicconn = None - - if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): - if music_enabled: - musicconn = utils.kodiSQL('music') - musiccursor = musicconn.cursor() - items_process = itemtypes[itemtype](embycursor, musiccursor) - else: - # Music is not enabled, do not proceed with itemtype - continue - else: - update_videolibrary = True - items_process = itemtypes[itemtype](embycursor, kodicursor) - - if itemtype == "Movie": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "BoxSet": - actions = { - 'added': items_process.added_boxset, - 'update': items_process.add_updateBoxset, - 'remove': items_process.remove - } - elif itemtype == "MusicVideo": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Series": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Season": - actions = { - 'added': items_process.added_season, - 'update': items_process.add_updateSeason, - 'remove': items_process.remove - } - elif itemtype == "Episode": - actions = { - 'added': items_process.added_episode, - 'update': items_process.add_updateEpisode, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "MusicAlbum": - actions = { - 'added': items_process.added_album, - 'update': items_process.add_updateAlbum, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype in ("MusicArtist", "AlbumArtist"): - actions = { - 'added': items_process.added, - 'update': items_process.add_updateArtist, - 'remove': items_process.remove - } - elif itemtype == "Audio": - actions = { - 'added': items_process.added_song, - 'update': items_process.add_updateSong, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - else: - self.logMsg("Unsupported itemtype: %s." % itemtype, 1) - actions = {} - - if actions.get(process): - - if process == "remove": - for item in itemlist: - actions[process](item) - - elif process == "added": - actions[process](itemlist, pdialog) - - else: - processItems = emby.getFullItems(itemlist) - for item in processItems: - - title = item['Name'] - - if itemtype == "Episode": - title = "%s - %s" % (item['SeriesName'], title) - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - actions[process](item) - - - if musicconn is not None: - # close connection for special types - self.logMsg("Updating music database.", 1) - musicconn.commit() - musiccursor.close() - - return (True, update_videolibrary) - - def contentPop(self, name, time=5000): - - if time: - # It's possible for the time to be 0. It should be considered disabled in this case. - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message="Added: %s" % name, - icon="special://home/addons/plugin.video.emby/icon.png", - time=time, - sound=False) - - -class Movies(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for movie in items: - - title = movie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(movie) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - def added_boxset(self, items, pdialog): - - total = len(items) - count = 0 - for boxset in items: - - title = boxset['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateBoxset(boxset) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single movie - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - self.logMsg("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid), 1) - - except TypeError: - update_item = False - self.logMsg("movieid: %s not found." % itemid, 2) - # movieid - kodicursor.execute("select coalesce(max(idMovie),0) from movie") - movieid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM movie WHERE idMovie = ?" - kodicursor.execute(query, (movieid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1) - - if not viewtag or not viewid: - # Get view tag from emby - viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) - self.logMsg("View tag found: %s" % viewtag, 2) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - people = API.getPeople() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - genres = item['Genres'] - title = item['Name'] - plot = API.getOverview() - shortplot = item.get('ShortOverview') - tagline = API.getTagline() - votecount = item.get('VoteCount') - rating = item.get('CommunityRating') - year = item.get('ProductionYear') - imdb = API.getProvider('Imdb') - sorttitle = item['SortName'] - runtime = API.getRuntime() - mpaa = API.getMpaa() - genre = " / ".join(genres) - country = API.getCountry() - studios = API.getStudios() - try: - studio = studios[0] - except IndexError: - studio = None - - if item.get('LocalTrailerCount'): - # There's a local trailer - url = ( - "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json" - % itemid - ) - result = self.doUtils.downloadUrl(url) - try: - trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id'] - except IndexError: - self.logMsg("Failed to process local trailer.", 1) - trailer = None - else: - # Try to get the youtube trailer - try: - trailer = item['RemoteTrailers'][0]['Url'] - except (KeyError, IndexError): - trailer = None - else: - try: - trailerId = trailer.rsplit('=', 1)[1] - except IndexError: - self.logMsg("Failed to process trailer: %s" % trailer, 1) - trailer = None - else: - trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): - # Validate the path is correct with user intervention - resp = xbmcgui.Dialog().yesno( - heading="Can't validate path", - line1=( - "Kodi can't locate file: %s. Verify the path. " - "You may to verify your network credentials in the " - "add-on settings or use the emby path substitution " - "to format your path correctly. Stop syncing?" - % playurl)) - if resp: - utils.window('emby_shouldStop', value="true") - return False - - path = playurl.replace(filename, "") - utils.window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.movies/" - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': movieid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE MOVIE ##### - if update_item: - self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1) - - # Update the movie entry - query = ' '.join(( - - "UPDATE movie", - "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", - "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", - "c16 = ?, c18 = ?, c19 = ?, c21 = ?", - "WHERE idMovie = ?" - )) - kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating, writer, - year, imdb, sorttitle, runtime, mpaa, genre, director, title, studio, trailer, - country, movieid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MOVIE ##### - else: - self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1) - - # Add path - pathid = kodi_db.addPath(path) - # Add the file - fileid = kodi_db.addFile(filename, pathid) - - # Create the movie entry - query = ( - ''' - INSERT INTO movie( - idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, - c09, c10, c11, c12, c14, c15, c16, c18, c19, c21) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount, - rating, writer, year, imdb, sorttitle, runtime, mpaa, genre, director, title, - studio, trailer, country)) - - # Create the reference in emby table - emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid)) - - # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - - # Process countries - kodi_db.addCountries(movieid, item['ProductionLocations'], "movie") - # Process cast - people = artwork.getPeopleArtwork(item['People']) - kodi_db.addPeople(movieid, people, "movie") - # Process genres - kodi_db.addGenres(movieid, genres, "movie") - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), movieid, "movie", kodicursor) - # Process stream details - streams = API.getMediaStreams() - kodi_db.addStreams(fileid, streams, runtime) - # Process studios - kodi_db.addStudios(movieid, studios, "movie") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite movies") - kodi_db.addTags(movieid, tags, "movie") - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - - def add_updateBoxset(self, boxset): - - emby = self.emby - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - - boxsetid = boxset['Id'] - title = boxset['Name'] - checksum = boxset['Etag'] - emby_dbitem = emby_db.getItem_byId(boxsetid) - try: - setid = emby_dbitem[0] - - except TypeError: - setid = kodi_db.createBoxset(title) - - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(boxset), setid, "set", self.kodicursor) - - # Process movies inside boxset - current_movies = emby_db.getItemId_byParentId(setid, "movie") - process = [] - try: - # Try to convert tuple to dictionary - current = dict(current_movies) - except ValueError: - current = {} - - # Sort current titles - for current_movie in current: - process.append(current_movie) - - # New list to compare - boxsetMovies = emby.getMovies_byBoxset(boxsetid) - for movie in boxsetMovies['Items']: - - itemid = movie['Id'] - - if not current.get(itemid): - # Assign boxset to movie - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - except TypeError: - self.logMsg("Failed to add: %s to boxset." % movie['Name'], 1) - continue - - self.logMsg("New addition to boxset %s: %s" % (title, movie['Name']), 1) - kodi_db.assignBoxset(setid, movieid) - # Update emby reference - emby_db.updateParentId(itemid, setid) - else: - # Remove from process, because the item still belongs - process.remove(itemid) - - # Process removals from boxset - for movie in process: - movieid = current[movie] - self.logMsg("Remove from boxset %s: %s" % (title, movieid)) - kodi_db.removefromBoxset(movieid) - # Update emby reference - emby_db.updateParentId(movie, None) - - # Update the reference in the emby table - emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum) - - 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 - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - self.logMsg( - "Update playstate for movie: %s fileid: %s" - % (item['Name'], fileid), 1) - except TypeError: - return - - # Process favorite tags - if userdata['Favorite']: - kodi_db.addTag(movieid, "Favorite movies", "movie") - else: - kodi_db.removeTag(movieid, "Favorite movies", "movie") - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - self.logMsg("%s New resume point: %s" % (itemid, resume)) - - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove movieid, fileid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - self.logMsg("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid), 1) - except TypeError: - return - - # Remove the emby reference - emby_db.removeItem(itemid) - # Remove artwork - artwork.deleteArtwork(kodiid, mediatype, kodicursor) - - if mediatype == "movie": - # Delete kodi movie and file - kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodiid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - - elif mediatype == "set": - # Delete kodi boxset - boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") - for movie in boxset_movies: - embyid = movie[0] - movieid = movie[1] - self.kodi_db.removefromBoxset(movieid) - # Update emby reference - emby_db.updateParentId(embyid, None) - - kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,)) - - self.logMsg("Deleted %s %s from kodi database" % (mediatype, itemid), 1) - -class MusicVideos(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for mvideo in items: - - title = mvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(mvideo) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single music video - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - self.logMsg("mvideoid: %s fileid: %s pathid: %s" % (mvideoid, fileid, pathid), 1) - - except TypeError: - update_item = False - self.logMsg("mvideoid: %s not found." % itemid, 2) - # mvideoid - kodicursor.execute("select coalesce(max(idMVideo),0) from musicvideo") - mvideoid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM musicvideo WHERE idMVideo = ?" - kodicursor.execute(query, (mvideoid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - self.logMsg("mvideoid: %s missing from Kodi, repairing the entry." % mvideoid, 1) - - if not viewtag or not viewid: - # Get view tag from emby - viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) - self.logMsg("View tag found: %s" % viewtag, 2) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - runtime = API.getRuntime() - plot = API.getOverview() - title = item['Name'] - year = item.get('ProductionYear') - genres = item['Genres'] - genre = " / ".join(genres) - studios = API.getStudios() - studio = " / ".join(studios) - artist = " / ".join(item.get('Artists')) - album = item.get('Album') - track = item.get('Track') - people = API.getPeople() - director = " / ".join(people['Director']) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): - # Validate the path is correct with user intervention - resp = xbmcgui.Dialog().yesno( - heading="Can't validate path", - line1=( - "Kodi can't locate file: %s. Verify the path. " - "You may to verify your network credentials in the " - "add-on settings or use the emby path substitution " - "to format your path correctly. Stop syncing?" - % playurl)) - if resp: - utils.window('emby_shouldStop', value="true") - return False - - path = playurl.replace(filename, "") - utils.window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.musicvideos/" - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': mvideoid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE MUSIC VIDEO ##### - if update_item: - self.logMsg("UPDATE mvideo itemid: %s - Title: %s" % (itemid, title), 1) - - # Update path - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - kodicursor.execute(query, (path, pathid)) - - # Update the filename - query = "UPDATE files SET strFilename = ?, dateAdded = ? WHERE idFile = ?" - kodicursor.execute(query, (filename, dateadded, fileid)) - - # Update the music video entry - query = ' '.join(( - - "UPDATE musicvideo", - "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,", - "c11 = ?, c12 = ?" - "WHERE idMVideo = ?" - )) - kodicursor.execute(query, (title, runtime, director, studio, year, plot, album, - artist, genre, track, mvideoid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MUSIC VIDEO ##### - else: - self.logMsg("ADD mvideo itemid: %s - Title: %s" % (itemid, title), 1) - - # Add path - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - kodicursor.execute(query, (path,)) - try: - pathid = kodicursor.fetchone()[0] - except TypeError: - kodicursor.execute("select coalesce(max(idPath),0) from path") - pathid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT OR REPLACE INTO path( - idPath, strPath, strContent, strScraper, noUpdate) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (pathid, path, "musicvideos", "metadata.local", 1)) - - # Add the file - kodicursor.execute("select coalesce(max(idFile),0) from files") - fileid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO files( - idFile, idPath, strFilename, dateAdded) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (fileid, pathid, filename, dateadded)) - - # Create the musicvideo entry - query = ( - ''' - INSERT INTO musicvideo( - idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (mvideoid, fileid, title, runtime, director, studio, - year, plot, album, artist, genre, track)) - - # Create the reference in emby table - emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid, - checksum=checksum, mediafolderid=viewid) - - - # Process cast - people = item['People'] - artists = item['ArtistItems'] - for artist in artists: - artist['Type'] = "Artist" - people.extend(artists) - people = artwork.getPeopleArtwork(people) - kodi_db.addPeople(mvideoid, people, "musicvideo") - # Process genres - 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) - # Process studios - 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") - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - 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 - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - self.logMsg( - "Update playstate for musicvideo: %s fileid: %s" - % (item['Name'], fileid), 1) - except TypeError: - return - - # Process favorite tags - if userdata['Favorite']: - kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") - else: - kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove mvideoid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - self.logMsg("Removing mvideoid: %s fileid: %s" % (mvideoid, fileid, pathid), 1) - except TypeError: - return - - # Remove artwork - query = ' '.join(( - - "SELECT url, type", - "FROM art", - "WHERE media_id = ?", - "AND media_type = 'musicvideo'" - )) - kodicursor.execute(query, (mvideoid,)) - rows = kodicursor.fetchall() - for row in rows: - - url = row[0] - imagetype = row[1] - if imagetype in ("poster", "fanart"): - artwork.deleteCachedArtwork(url) - - kodicursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (mvideoid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - if self.directpath: - kodicursor.execute("DELETE FROM path WHERE idPath = ?", (pathid,)) - self.embycursor.execute("DELETE FROM emby WHERE emby_id = ?", (itemid,)) - - self.logMsg("Deleted musicvideo %s from kodi database" % itemid, 1) - -class TVShows(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for tvshow in items: - - title = tvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(tvshow) - # Add episodes - all_episodes = self.emby.getEpisodesbyShow(tvshow['Id']) - self.added_episode(all_episodes['Items'], pdialog) - - def added_season(self, items, pdialog): - - total = len(items) - count = 0 - for season in items: - - title = "%s - %s" % (season.get('SeriesName', "Unknown"), season['Name']) - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateSeason(season) - # Add episodes - all_episodes = self.emby.getEpisodesbySeason(season['Id']) - self.added_episode(all_episodes['Items'], pdialog) - - def added_episode(self, items, pdialog): - - total = len(items) - count = 0 - for episode in items: - title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateEpisode(episode) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single tvshow - kodicursor = self.kodicursor - emby = self.emby - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - if utils.settings('syncEmptyShows') == "false" and not item['RecursiveItemCount']: - self.logMsg("Skipping empty show: %s" % item['Name'], 1) - return - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - force_episodes = False - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - showid = emby_dbitem[0] - pathid = emby_dbitem[2] - self.logMsg("showid: %s pathid: %s" % (showid, pathid), 1) - - except TypeError: - update_item = False - self.logMsg("showid: %s not found." % itemid, 2) - kodicursor.execute("select coalesce(max(idShow),0) from tvshow") - showid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM tvshow WHERE idShow = ?" - kodicursor.execute(query, (showid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - self.logMsg("showid: %s missing from Kodi, repairing the entry." % showid, 1) - # Force re-add episodes after the show is re-created. - force_episodes = True - - - if viewtag is None or viewid is None: - # Get view tag from emby - viewtag, viewid, mediatype = emby.getView_embyId(itemid) - self.logMsg("View tag found: %s" % viewtag, 2) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - genres = item['Genres'] - title = item['Name'] - plot = API.getOverview() - rating = item.get('CommunityRating') - premieredate = API.getPremiereDate() - tvdb = API.getProvider('Tvdb') - sorttitle = item['SortName'] - mpaa = API.getMpaa() - genre = " / ".join(genres) - studios = API.getStudios() - studio = " / ".join(studios) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if self.directpath: - # Direct paths is set the Kodi way - if "\\" in playurl: - # Local path - path = "%s\\" % playurl - toplevelpath = "%s\\" % dirname(dirname(path)) - else: - # Network path - path = "%s/" % playurl - toplevelpath = "%s/" % dirname(dirname(path)) - - if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(path): - # Validate the path is correct with user intervention - resp = xbmcgui.Dialog().yesno( - heading="Can't validate path", - line1=( - "Kodi can't locate file: %s. Verify the path. " - "You may to verify your network credentials in the " - "add-on settings or use the emby path substitution " - "to format your path correctly. Stop syncing?" - % playurl)) - if resp: - utils.window('emby_shouldStop', value="true") - return False - - utils.window('emby_pathverified', value="true") - else: - # Set plugin path - toplevelpath = "plugin://plugin.video.emby.tvshows/" - path = "%s%s/" % (toplevelpath, itemid) - - - ##### UPDATE THE TVSHOW ##### - if update_item: - self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1) - - # Update the tvshow entry - query = ' '.join(( - - "UPDATE tvshow", - "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (title, plot, rating, premieredate, genre, title, - tvdb, mpaa, studio, sorttitle, showid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE TVSHOW ##### - else: - self.logMsg("ADD tvshow itemid: %s - Title: %s" % (itemid, title), 1) - - # Add top path - toppathid = kodi_db.addPath(toplevelpath) - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid)) - - # Add path - pathid = kodi_db.addPath(path) - - # Create the tvshow entry - query = ( - ''' - INSERT INTO tvshow( - idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre, - title, tvdb, mpaa, studio, sorttitle)) - - # Link the path - query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)" - kodicursor.execute(query, (showid, pathid)) - - # Create the reference in emby table - emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, None, None, 1, pathid)) - - # Process cast - people = artwork.getPeopleArtwork(item['People']) - kodi_db.addPeople(showid, people, "tvshow") - # Process genres - kodi_db.addGenres(showid, genres, "tvshow") - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), showid, "tvshow", kodicursor) - # Process studios - kodi_db.addStudios(showid, studios, "tvshow") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite tvshows") - kodi_db.addTags(showid, tags, "tvshow") - # Process seasons - all_seasons = emby.getSeasons(itemid) - for season in all_seasons['Items']: - self.add_updateSeason(season, showid=showid) - else: - # Finally, refresh the all season entry - seasonid = kodi_db.addSeason(showid, -1) - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) - - if force_episodes: - # We needed to recreate the show entry. Re-add episodes now. - self.logMsg("Repairing episodes for showid: %s %s" % (showid, title), 1) - all_episodes = emby.getEpisodesbyShow(itemid) - self.added_episode(all_episodes['Items'], None) - - def add_updateSeason(self, item, showid=None): - - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - - seasonnum = item.get('IndexNumber', 1) - itemid = item['Id'] - - if showid is None: - try: - seriesId = item['SeriesId'] - showid = emby_db.getItem_byId(seriesId)[0] - except KeyError: - return - except TypeError: - # Show is missing, update show instead. - show = self.emby.getItem(seriesId) - self.add_update(show) - return - - seasonid = kodi_db.addSeason(showid, seasonnum) - - if item['LocationType'] != "Virtual": - # Create the reference in emby table - emby_db.addReference(itemid, seasonid, "Season", "season", parentid=showid) - - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) - - def add_updateEpisode(self, item): - # Process single episode - kodiversion = self.kodiversion - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - episodeid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - self.logMsg("episodeid: %s fileid: %s pathid: %s" % (episodeid, fileid, pathid), 1) - - except TypeError: - update_item = False - self.logMsg("episodeid: %s not found." % itemid, 2) - # episodeid - kodicursor.execute("select coalesce(max(idEpisode),0) from episode") - episodeid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM episode WHERE idEpisode = ?" - kodicursor.execute(query, (episodeid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - self.logMsg("episodeid: %s missing from Kodi, repairing the entry." % episodeid, 1) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - people = API.getPeople() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - title = item['Name'] - plot = API.getOverview() - rating = item.get('CommunityRating') - runtime = API.getRuntime() - premieredate = API.getPremiereDate() - - # episode details - try: - seriesId = item['SeriesId'] - except KeyError: - # Missing seriesId, skip - self.logMsg("Skipping: %s. SeriesId is missing." % itemid, 1) - return False - - seriesName = item['SeriesName'] - season = item.get('ParentIndexNumber') - episode = item.get('IndexNumber', -1) - - if season is None: - if item.get('AbsoluteEpisodeNumber'): - # Anime scenario - season = 1 - episode = item['AbsoluteEpisodeNumber'] - else: - season = -1 - - # Specials ordering within season - if item.get('AirsAfterSeasonNumber'): - airsBeforeSeason = item['AirsAfterSeasonNumber'] - airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering - else: - airsBeforeSeason = item.get('AirsBeforeSeasonNumber') - airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') - - # Append multi episodes to title - if item.get('IndexNumberEnd'): - title = "| %02d | %s" % (item['IndexNumberEnd'], title) - - # Get season id - show = emby_db.getItem_byId(seriesId) - try: - showid = show[0] - except TypeError: - # Show is missing from database - show = self.emby.getItem(seriesId) - self.add_update(show) - show = emby_db.getItem_byId(seriesId) - try: - showid = show[0] - except TypeError: - self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId)) - return False - - seasonid = kodi_db.addSeason(showid, season) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): - # Validate the path is correct with user intervention - resp = xbmcgui.Dialog().yesno( - heading="Can't validate path", - line1=( - "Kodi can't locate file: %s. Verify the path. " - "You may to verify your network credentials in the " - "add-on settings or use the emby path substitution " - "to format your path correctly. Stop syncing?" - % playurl)) - if resp: - utils.window('emby_shouldStop', value="true") - return False - - path = playurl.replace(filename, "") - utils.window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': episodeid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE EPISODE ##### - if update_item: - self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1) - - # Update the movie entry - if kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?", - "WHERE idEpisode = ?" - )) - kodicursor.execute(query, (title, plot, rating, writer, premieredate, - runtime, director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, seasonid, episodeid)) - else: - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?", - "WHERE idEpisode = ?" - )) - kodicursor.execute(query, (title, plot, rating, writer, premieredate, - runtime, director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, episodeid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - # Update parentid reference - emby_db.updateParentId(itemid, seasonid) - - ##### OR ADD THE EPISODE ##### - else: - self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1) - - # Add path - pathid = kodi_db.addPath(path) - # Add the file - fileid = kodi_db.addFile(filename, pathid) - - # Create the episode entry - if kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16, idSeason) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, showid, - airsBeforeSeason, airsBeforeEpisode, seasonid)) - else: - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, showid, - airsBeforeSeason, airsBeforeEpisode)) - - # Create the reference in emby table - emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, - seasonid, checksum) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, None, None, 1, pathid)) - - # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - - # Process cast - people = artwork.getPeopleArtwork(item['People']) - kodi_db.addPeople(episodeid, people, "episode") - # Process artwork - artworks = artwork.getAllArtwork(item) - artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor) - # Process stream details - streams = API.getMediaStreams() - kodi_db.addStreams(fileid, streams, runtime) - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - kodi_db.addPlaystate(fileid, resume, total, 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.emby.tvshows/") - tempfileid = kodi_db.addFile(filename, temppathid) - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) - kodi_db.addPlaystate(tempfileid, 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 - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - dateadded = API.getDateCreated() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - self.logMsg( - "Update playstate for %s: %s fileid: %s" - % (mediatype, item['Name'], fileid), 1) - except TypeError: - return - - # Process favorite tags - if mediatype == "tvshow": - if userdata['Favorite']: - kodi_db.addTag(kodiid, "Favorite tvshows", "tvshow") - else: - kodi_db.removeTag(kodiid, "Favorite tvshows", "tvshow") - - # Process playstates - if mediatype == "episode": - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - self.logMsg("%s New resume point: %s" % (itemid, resume)) - - kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - if not self.directpath and not resume: - # Make sure there's no other bookmarks created by widget. - filename = kodi_db.getFile(fileid) - kodi_db.removeFile("plugin://plugin.video.emby.tvshows/", filename) - - if not self.directpath and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - filename = kodi_db.getFile(fileid) - temppathid = kodi_db.getPath("plugin://plugin.video.emby.tvshows/") - tempfileid = kodi_db.addFile(filename, temppathid) - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - self.kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) - kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed) - - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove showid, fileid, pathid, emby reference - emby_db = self.emby_db - embycursor = self.embycursor - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - parentid = emby_dbitem[3] - mediatype = emby_dbitem[4] - self.logMsg("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid), 1) - except TypeError: - return - - ##### PROCESS ITEM ##### - - # Remove the emby reference - emby_db.removeItem(itemid) - - - ##### IF EPISODE ##### - - if mediatype == "episode": - # Delete kodi episode and file, verify season and tvshow - self.removeEpisode(kodiid, fileid) - - # Season verification - season = emby_db.getItem_byKodiId(parentid, "season") - try: - showid = season[1] - except TypeError: - return - - season_episodes = emby_db.getItem_byParentId(parentid, "episode") - if not season_episodes: - self.removeSeason(parentid) - emby_db.removeItem(season[0]) - - # Show verification - show = emby_db.getItem_byKodiId(showid, "tvshow") - query = ' '.join(( - - "SELECT totalCount", - "FROM tvshowcounts", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (showid,)) - result = kodicursor.fetchone() - if result and result[0] is None: - # There's no episodes left, delete show and any possible remaining seasons - seasons = emby_db.getItem_byParentId(showid, "season") - for season in seasons: - self.removeSeason(season[1]) - else: - # Delete emby season entries - emby_db.removeItems_byParentId(showid, "season") - self.removeShow(showid) - emby_db.removeItem(show[0]) - - ##### IF TVSHOW ##### - - elif mediatype == "tvshow": - # Remove episodes, seasons, tvshow - seasons = emby_db.getItem_byParentId(kodiid, "season") - for season in seasons: - seasonid = season[1] - season_episodes = emby_db.getItem_byParentId(seasonid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - else: - # Remove emby episodes - emby_db.removeItems_byParentId(seasonid, "episode") - else: - # Remove emby seasons - emby_db.removeItems_byParentId(kodiid, "season") - - # Remove tvshow - self.removeShow(kodiid) - - ##### IF SEASON ##### - - elif mediatype == "season": - # Remove episodes, season, verify tvshow - season_episodes = emby_db.getItem_byParentId(kodiid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - else: - # Remove emby episodes - emby_db.removeItems_byParentId(kodiid, "episode") - - # Remove season - self.removeSeason(kodiid) - - # Show verification - seasons = emby_db.getItem_byParentId(parentid, "season") - if not seasons: - # There's no seasons, delete the show - self.removeShow(parentid) - emby_db.removeItem_byKodiId(parentid, "tvshow") - - self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1) - - def removeShow(self, kodiid): - - kodicursor = self.kodicursor - artwork = self.artwork - - 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) - 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) - kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - self.logMsg("Removed episode: %s." % kodiid, 2) - -class Music(Items): - - - def __init__(self, embycursor, musiccursor): - - Items.__init__(self, embycursor, musiccursor) - - self.directstream = utils.settings('streamMusic') == "true" - self.enableimportsongrating = utils.settings('enableImportSongRating') == "true" - self.enableexportsongrating = utils.settings('enableExportSongRating') == "true" - self.enableupdatesongrating = utils.settings('enableUpdateSongRating') == "true" - self.userid = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userid) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for artist in items: - - title = artist['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateArtist(artist) - # Add albums - all_albums = self.emby.getAlbumsbyArtist(artist['Id']) - self.added_album(all_albums['Items'], pdialog) - - def added_album(self, items, pdialog): - - total = len(items) - count = 0 - for album in items: - - title = album['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateAlbum(album) - # Add songs - all_songs = self.emby.getSongsbyAlbum(album['Id']) - self.added_song(all_songs['Items'], pdialog) - - def added_song(self, items, pdialog): - - total = len(items) - count = 0 - for song in items: - - title = song['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateSong(song) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newmusic_time) - - def add_updateArtist(self, item, artisttype="MusicArtist"): - # Process a single artist - kodiversion = self.kodiversion - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - artistid = emby_dbitem[0] - except TypeError: - update_item = False - self.logMsg("artistid: %s not found." % itemid, 2) - - ##### The artist details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - checksum = API.getChecksum() - - name = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzArtist') - genres = " / ".join(item.get('Genres')) - bio = API.getOverview() - - # Associate artwork - artworks = artwork.getAllArtwork(item, parentInfo=True) - thumb = artworks['Primary'] - backdrops = artworks['Backdrop'] # List - - if thumb: - thumb = "%s" % thumb - if backdrops: - fanart = "%s" % backdrops[0] - else: - fanart = "" - - - ##### UPDATE THE ARTIST ##### - if update_item: - self.logMsg("UPDATE artist itemid: %s - Name: %s" % (itemid, name), 1) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE ARTIST ##### - else: - self.logMsg("ADD artist itemid: %s - Name: %s" % (itemid, name), 1) - # safety checks: It looks like Emby supports the same artist multiple times. - # Kodi doesn't allow that. In case that happens we just merge the artist entries. - artistid = kodi_db.addArtist(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum) - - - # Process the artist - if self.kodiversion in (16, 17): - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?", - "WHERE idArtist = ?" - )) - kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid)) - else: - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?, dateAdded = ?", - "WHERE idArtist = ?" - )) - kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, - dateadded, artistid)) - - - # Update artwork - artwork.addArtwork(artworks, artistid, "artist", kodicursor) - - def add_updateAlbum(self, item): - # Process a single artist - emby = self.emby - kodiversion = self.kodiversion - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - albumid = emby_dbitem[0] - except TypeError: - update_item = False - self.logMsg("albumid: %s not found." % itemid, 2) - - ##### The album details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - userdata = API.getUserData() - checksum = API.getChecksum() - - name = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzAlbum') - year = item.get('ProductionYear') - genres = item.get('Genres') - genre = " / ".join(genres) - bio = API.getOverview() - rating = userdata['UserRating'] - artists = item['AlbumArtists'] - if not artists: - artists = item['ArtistItems'] - artistname = [] - for artist in artists: - artistname.append(artist['Name']) - artistname = " / ".join(artistname) - - # Associate artwork - artworks = artwork.getAllArtwork(item, parentInfo=True) - thumb = artworks['Primary'] - if thumb: - thumb = "%s" % thumb - - ##### UPDATE THE ALBUM ##### - if update_item: - self.logMsg("UPDATE album itemid: %s - Name: %s" % (itemid, name), 1) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE ALBUM ##### - else: - self.logMsg("ADD album itemid: %s - Name: %s" % (itemid, name), 1) - # safety checks: It looks like Emby supports the same artist multiple times. - # Kodi doesn't allow that. In case that happens we just merge the artist entries. - albumid = 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: - # Kodi Krypton - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iUserrating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid)) - elif kodiversion == 16: - # Kodi Jarvis - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid)) - elif kodiversion == 15: - # Kodi Isengard - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, "album", albumid)) - else: - # Kodi Helix - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, albumid)) - - # Associate the parentid for emby reference - parentId = item.get('ParentId') - if parentId is not None: - emby_dbartist = emby_db.getItem_byId(parentId) - try: - artistid = emby_dbartist[0] - except TypeError: - # Artist does not exist in emby database. - artist = emby.getItem(parentId) - # Item may not be an artist, verification necessary. - if artist['Type'] == "MusicArtist": - # Update with the parentId, for remove reference - emby_db.addReference(parentId, parentId, "MusicArtist", "artist") - emby_db.updateParentId(itemid, parentId) - else: - # Update emby reference with the artistid - emby_db.updateParentId(itemid, artistid) - - # Assign main artists to album - for artist in artists: - artistname = artist['Name'] - artistId = artist['Id'] - emby_dbartist = emby_db.getItem_byId(artistId) - try: - artistid = emby_dbartist[0] - except TypeError: - # Artist does not exist in emby database, create the reference - artist = emby.getItem(artistId) - self.add_updateArtist(artist, artisttype="AlbumArtist") - emby_dbartist = emby_db.getItem_byId(artistId) - artistid = emby_dbartist[0] - else: - # Best take this name over anything else. - query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - kodicursor.execute(query, (artistname, artistid,)) - - # Add artist to album - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, albumid, artistname)) - # Update discography - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, name, year)) - # Update emby reference with parentid - emby_db.updateParentId(artistId, albumid) - - # Add genres - kodi_db.addMusicGenres(albumid, genres, "album") - # Update artwork - artwork.addArtwork(artworks, albumid, "album", kodicursor) - - def add_updateSong(self, item): - # 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 = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - songid = emby_dbitem[0] - pathid = emby_dbitem[2] - albumid = emby_dbitem[3] - except TypeError: - update_item = False - self.logMsg("songid: %s not found." % itemid, 2) - - ##### The song details ##### - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - title = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzTrackId') - genres = item.get('Genres') - genre = " / ".join(genres) - artists = " / ".join(item['Artists']) - tracknumber = item.get('IndexNumber', 0) - disc = item.get('ParentIndexNumber', 1) - if disc == 1: - track = tracknumber - else: - track = disc*2**16 + tracknumber - year = item.get('ProductionYear') - duration = API.getRuntime() - rating = userdata['UserRating'] - - #if enabled, try to get the rating from file and/or emby - if not self.directstream: - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - else: - hasEmbeddedCover = False - comment = API.getOverview() - - - ##### GET THE FILE AND PATH ##### - if self.directstream: - path = "%s/emby/Audio/%s/" % (self.server, itemid) - filename = "stream.mp3" - else: - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - # Direct paths is set the Kodi way - if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): - # Validate the path is correct with user intervention - utils.window('emby_directPath', clear=True) - resp = xbmcgui.Dialog().yesno( - heading="Can't validate path", - line1=( - "Kodi can't locate file: %s. Verify the path. " - "You may to verify your network credentials in the " - "add-on settings or use the emby path substitution " - "to format your path correctly. Stop syncing?" - % playurl)) - if resp: - utils.window('emby_shouldStop', value="true") - return False - - path = playurl.replace(filename, "") - utils.window('emby_pathverified', value="true") - - ##### UPDATE THE SONG ##### - if update_item: - self.logMsg("UPDATE song itemid: %s - Title: %s" % (itemid, title), 1) - - # Update path - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - kodicursor.execute(query, (path, pathid)) - - # Update the song entry - query = ' '.join(( - - "UPDATE song", - "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", - "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", - "rating = ?, comment = ?", - "WHERE idSong = ?" - )) - kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year, - filename, playcount, dateplayed, rating, comment, songid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE SONG ##### - else: - self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1) - - # Add path - pathid = kodi_db.addPath(path) - - try: - # Get the album - emby_dbalbum = emby_db.getItem_byId(item['AlbumId']) - albumid = emby_dbalbum[0] - except KeyError: - # Verify if there's an album associated. - album_name = item.get('Album') - if album_name: - self.logMsg("Creating virtual music album for song: %s." % itemid, 1) - albumid = 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. - self.logMsg("Song itemid: %s has no albumId associated." % itemid, 1) - return False - - except TypeError: - # No album found. Let's create it - self.logMsg("Album database entry missing.", 1) - emby_albumId = item['AlbumId'] - album = emby.getItem(emby_albumId) - self.add_updateAlbum(album) - emby_dbalbum = emby_db.getItem_byId(emby_albumId) - try: - albumid = emby_dbalbum[0] - self.logMsg("Found albumid: %s" % albumid, 1) - except TypeError: - # No album found, create a single's album - 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: - # Kodi Jarvis - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, "single")) - elif kodiversion == 15: - # Kodi Isengard - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded, "single")) - else: - # Kodi Helix - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded)) - - # Create the song entry - kodicursor.execute("select coalesce(max(idSong),0) from song") - songid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO song( - idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, - iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, - rating) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (songid, albumid, pathid, artists, genre, title, track, - duration, year, filename, musicBrainzId, playcount, dateplayed, rating)) - - # Create the reference in emby table - emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, - checksum=checksum) - - - # Link song to album - query = ( - ''' - INSERT OR REPLACE INTO albuminfosong( - idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (songid, albumid, track, title, duration)) - - # Link song to artists - for index, artist in enumerate(item['ArtistItems']): - - artist_name = artist['Name'] - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, songid, index, artist_name)) - - # Verify if album artist exists - album_artists = [] - for artist in item['AlbumArtists']: - - artist_name = artist['Name'] - album_artists.append(artist_name) - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, albumid, artist_name)) - # Update discography - if item.get('Album'): - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, item['Album'], 0)) - else: - album_artists = " / ".join(album_artists) - query = ' '.join(( - - "SELECT strArtists", - "FROM album", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (albumid,)) - result = kodicursor.fetchone() - if result and result[0] != album_artists: - # Field is empty - if kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - elif kodiversion == 15: - # Kodi Isengard - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - else: - # Kodi Helix - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - - # Add genres - kodi_db.addMusicGenres(songid, genres, "song") - - # Update artwork - allart = artwork.getAllArtwork(item, parentInfo=True) - if hasEmbeddedCover: - allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl ) - artwork.addArtwork(allart, songid, "song", kodicursor) - - if item.get('AlbumId') is None: - # Update album artwork - artwork.addArtwork(allart, albumid, "album", kodicursor) - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - kodicursor = self.kodicursor - emby_db = self.emby_db - kodi_db = self.kodi_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - rating = userdata['UserRating'] - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - self.logMsg("Update playstate for %s: %s" % (mediatype, item['Name']), 1) - except TypeError: - return - - if mediatype == "song": - - #should we ignore this item ? - #happens when userdata updated by ratings method - if utils.window("ignore-update-%s" %itemid): - utils.window("ignore-update-%s" %itemid,clear=True) - return - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - #process item ratings - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - - query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?" - kodicursor.execute(query, (playcount, dateplayed, rating, kodiid)) - - elif mediatype == "album": - # Process playstates - query = "UPDATE album SET iRating = ? WHERE idAlbum = ?" - kodicursor.execute(query, (rating, kodiid)) - - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove kodiid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - self.logMsg("Removing %s kodiid: %s" % (mediatype, kodiid), 1) - except TypeError: - return - - ##### PROCESS ITEM ##### - - # Remove the emby reference - emby_db.removeItem(itemid) - - - ##### IF SONG ##### - - if mediatype == "song": - # Delete song - 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: - - item_kid = item[0] - item_mediatype = item[1] - - if item_mediatype == "album": - childs = emby_db.getItem_byParentId(item_kid, "song") - if not childs: - # Delete album - self.removeAlbum(item_kid) - - ##### IF ALBUM ##### - - elif mediatype == "album": - # Delete songs, album - album_songs = emby_db.getItem_byParentId(kodiid, "song") - for song in album_songs: - self.removeSong(song[1]) - else: - # Remove emby songs - emby_db.removeItems_byParentId(kodiid, "song") - - # Remove the album - self.removeAlbum(kodiid) - - ##### IF ARTIST ##### - - elif mediatype == "artist": - # Delete songs, album, artist - albums = emby_db.getItem_byParentId(kodiid, "album") - for album in albums: - albumid = album[1] - album_songs = emby_db.getItem_byParentId(albumid, "song") - for song in album_songs: - self.removeSong(song[1]) - else: - # Remove emby song - emby_db.removeItems_byParentId(albumid, "song") - # Remove emby artist - emby_db.removeItems_byParentId(albumid, "artist") - # Remove kodi album - self.removeAlbum(albumid) - else: - # Remove emby albums - emby_db.removeItems_byParentId(kodiid, "album") - - # Remove artist - self.removeArtist(kodiid) - - self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1) - - def removeSong(self, kodiid): - - kodicursor = self.kodicursor - artwork = self.artwork - - artwork.deleteArtwork(kodiid, "song", kodicursor) - 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,)) - - def removeArtist(self, kodiid): - - kodicursor = self.kodicursor - artwork = self.artwork - - artwork.deleteArtwork(kodiid, "artist", kodicursor) - kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,)) \ No newline at end of file +# -*- coding: utf-8 -*- + +################################################################################################## + +import urllib +from ntpath import dirname +from datetime import datetime + +import xbmc +import xbmcgui +import xbmcvfs + +import api +import artwork +import clientinfo +import downloadutils +import utils +import embydb_functions as embydb +import kodidb_functions as kodidb +import read_embyserver as embyserver +import musicutils + +################################################################################################## + + +class Items(object): + + + def __init__(self, embycursor, kodicursor): + + self.embycursor = embycursor + self.kodicursor = kodicursor + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils() + + self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) + self.directpath = utils.settings('useDirectPaths') == "1" + self.music_enabled = utils.settings('enableMusic') == "true" + self.contentmsg = utils.settings('newContent') == "true" + self.newvideo_time = int(utils.settings('newvideotime'))*1000 + self.newmusic_time = int(utils.settings('newmusictime'))*1000 + + self.artwork = artwork.Artwork() + self.emby = embyserver.Read_EmbyServer() + self.emby_db = embydb.Embydb_Functions(embycursor) + self.kodi_db = kodidb.Kodidb_Functions(kodicursor) + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def itemsbyId(self, items, process, pdialog=None): + # Process items by itemid. Process can be added, update, userdata, remove + emby = self.emby + embycursor = self.embycursor + kodicursor = self.kodicursor + music_enabled = self.music_enabled + + itemtypes = { + + 'Movie': Movies, + 'BoxSet': Movies, + 'Series': TVShows, + 'Season': TVShows, + 'Episode': TVShows, + 'MusicAlbum': Music, + 'MusicArtist': Music, + 'AlbumArtist': Music, + 'Audio': Music + } + + update_videolibrary = False + total = 0 + for item in items: + total += len(items[item]) + + if total == 0: + return False + + self.logMsg("Processing %s: %s" % (process, items), 1) + if pdialog: + pdialog.update(heading="Processing %s: %s items" % (process, total)) + + count = 0 + for itemtype in items: + + # Safety check + if not itemtypes.get(itemtype): + # We don't process this type of item + continue + + itemlist = items[itemtype] + if not itemlist: + # The list to process is empty + continue + + musicconn = None + + if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + if music_enabled: + musicconn = utils.kodiSQL('music') + musiccursor = musicconn.cursor() + items_process = itemtypes[itemtype](embycursor, musiccursor) + else: + # Music is not enabled, do not proceed with itemtype + continue + else: + update_videolibrary = True + items_process = itemtypes[itemtype](embycursor, kodicursor) + + if itemtype == "Movie": + actions = { + 'added': items_process.added, + 'update': items_process.add_update, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + elif itemtype == "BoxSet": + actions = { + 'added': items_process.added_boxset, + 'update': items_process.add_updateBoxset, + 'remove': items_process.remove + } + elif itemtype == "MusicVideo": + actions = { + 'added': items_process.added, + 'update': items_process.add_update, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + elif itemtype == "Series": + actions = { + 'added': items_process.added, + 'update': items_process.add_update, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + elif itemtype == "Season": + actions = { + 'added': items_process.added_season, + 'update': items_process.add_updateSeason, + 'remove': items_process.remove + } + elif itemtype == "Episode": + actions = { + 'added': items_process.added_episode, + 'update': items_process.add_updateEpisode, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + elif itemtype == "MusicAlbum": + actions = { + 'added': items_process.added_album, + 'update': items_process.add_updateAlbum, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + elif itemtype in ("MusicArtist", "AlbumArtist"): + actions = { + 'added': items_process.added, + 'update': items_process.add_updateArtist, + 'remove': items_process.remove + } + elif itemtype == "Audio": + actions = { + 'added': items_process.added_song, + 'update': items_process.add_updateSong, + 'userdata': items_process.updateUserdata, + 'remove': items_process.remove + } + else: + self.logMsg("Unsupported itemtype: %s." % itemtype, 1) + actions = {} + + if actions.get(process): + + if process == "remove": + for item in itemlist: + actions[process](item) + + elif process == "added": + actions[process](itemlist, pdialog) + + else: + processItems = emby.getFullItems(itemlist) + for item in processItems: + + title = item['Name'] + + if itemtype == "Episode": + title = "%s - %s" % (item['SeriesName'], title) + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + + actions[process](item) + + + if musicconn is not None: + # close connection for special types + self.logMsg("Updating music database.", 1) + musicconn.commit() + musiccursor.close() + + return (True, update_videolibrary) + + def contentPop(self, name, time=5000): + + if time: + # It's possible for the time to be 0. It should be considered disabled in this case. + xbmcgui.Dialog().notification( + heading="Emby for Kodi", + message="Added: %s" % name, + icon="special://home/addons/plugin.video.emby/icon.png", + time=time, + sound=False) + + +class Movies(Items): + + + def __init__(self, embycursor, kodicursor): + Items.__init__(self, embycursor, kodicursor) + + def added(self, items, pdialog): + + total = len(items) + count = 0 + for movie in items: + + title = movie['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + self.add_update(movie) + if not pdialog and self.contentmsg: + self.contentPop(title, self.newvideo_time) + + def added_boxset(self, items, pdialog): + + total = len(items) + count = 0 + for boxset in items: + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=boxset['Name']) + count += 1 + self.add_updateBoxset(boxset) + + + def add_update(self, item, viewtag=None, viewid=None): + # Process single movie + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + self.logMsg("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid), 1) + + except TypeError: + update_item = False + self.logMsg("movieid: %s not found." % itemid, 2) + # movieid + kodicursor.execute("select coalesce(max(idMovie),0) from movie") + movieid = kodicursor.fetchone()[0] + 1 + + else: + # Verification the item is still in Kodi + query = "SELECT * FROM movie WHERE idMovie = ?" + kodicursor.execute(query, (movieid,)) + try: + kodicursor.fetchone()[0] + except TypeError: + # item is not found, let's recreate it. + update_item = False + self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1) + + if not viewtag or not viewid: + # Get view tag from emby + viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) + self.logMsg("View tag found: %s" % viewtag, 2) + + # fileId information + checksum = API.getChecksum() + dateadded = API.getDateCreated() + userdata = API.getUserData() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + people = API.getPeople() + writer = " / ".join(people['Writer']) + director = " / ".join(people['Director']) + genres = item['Genres'] + title = item['Name'] + plot = API.getOverview() + shortplot = item.get('ShortOverview') + tagline = API.getTagline() + votecount = item.get('VoteCount') + rating = item.get('CommunityRating') + year = item.get('ProductionYear') + imdb = API.getProvider('Imdb') + sorttitle = item['SortName'] + runtime = API.getRuntime() + mpaa = API.getMpaa() + genre = " / ".join(genres) + country = API.getCountry() + studios = API.getStudios() + try: + studio = studios[0] + except IndexError: + studio = None + + if item.get('LocalTrailerCount'): + # There's a local trailer + url = ( + "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json" + % itemid + ) + result = self.doUtils.downloadUrl(url) + try: + trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id'] + except IndexError: + self.logMsg("Failed to process local trailer.", 1) + trailer = None + else: + # Try to get the youtube trailer + try: + trailer = item['RemoteTrailers'][0]['Url'] + except (KeyError, IndexError): + trailer = None + else: + try: + trailerId = trailer.rsplit('=', 1)[1] + except IndexError: + self.logMsg("Failed to process trailer: %s" % trailer, 1) + trailer = None + else: + trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId + + + ##### GET THE FILE AND PATH ##### + playurl = API.getFilePath() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.directpath: + # Direct paths is set the Kodi way + if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): + # Validate the path is correct with user intervention + resp = xbmcgui.Dialog().yesno( + heading="Can't validate path", + line1=( + "Kodi can't locate file: %s. Verify the path. " + "You may to verify your network credentials in the " + "add-on settings or use the emby path substitution " + "to format your path correctly. Stop syncing?" + % playurl)) + if resp: + utils.window('emby_shouldStop', value="true") + return False + + path = playurl.replace(filename, "") + utils.window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.movies/" + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': movieid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE MOVIE ##### + if update_item: + self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1) + + # Update the movie entry + query = ' '.join(( + + "UPDATE movie", + "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", + "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", + "c16 = ?, c18 = ?, c19 = ?, c21 = ?", + "WHERE idMovie = ?" + )) + kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating, writer, + year, imdb, sorttitle, runtime, mpaa, genre, director, title, studio, trailer, + country, movieid)) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE MOVIE ##### + else: + self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1) + + # Add path + pathid = self.kodi_db.addPath(path) + # Add the file + fileid = self.kodi_db.addFile(filename, pathid) + + # Create the movie entry + query = ( + ''' + INSERT INTO movie( + idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, + c09, c10, c11, c12, c14, c15, c16, c18, c19, c21) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount, + rating, writer, year, imdb, sorttitle, runtime, mpaa, genre, director, title, + studio, trailer, country)) + + # Create the reference in emby table + emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid) + + # Update the path + query = ' '.join(( + + "UPDATE path", + "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", + "WHERE idPath = ?" + )) + kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid)) + + # Update the file + query = ' '.join(( + + "UPDATE files", + "SET idPath = ?, strFilename = ?, dateAdded = ?", + "WHERE idFile = ?" + )) + kodicursor.execute(query, (pathid, filename, dateadded, fileid)) + + # Process countries + self.kodi_db.addCountries(movieid, item['ProductionLocations'], "movie") + # Process cast + people = artwork.getPeopleArtwork(item['People']) + self.kodi_db.addPeople(movieid, people, "movie") + # Process genres + self.kodi_db.addGenres(movieid, genres, "movie") + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(item), movieid, "movie", kodicursor) + # Process stream details + streams = API.getMediaStreams() + self.kodi_db.addStreams(fileid, streams, runtime) + # Process studios + self.kodi_db.addStudios(movieid, studios, "movie") + # Process tags: view, emby tags + tags = [viewtag] + tags.extend(item['Tags']) + if userdata['Favorite']: + tags.append("Favorite movies") + self.kodi_db.addTags(movieid, tags, "movie") + # Process playstates + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + + def add_updateBoxset(self, boxset): + + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + + boxsetid = boxset['Id'] + title = boxset['Name'] + checksum = boxset['Etag'] + emby_dbitem = emby_db.getItem_byId(boxsetid) + try: + setid = emby_dbitem[0] + + except TypeError: + setid = self.kodi_db.createBoxset(title) + + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(boxset), setid, "set", self.kodicursor) + + # Process movies inside boxset + current_movies = emby_db.getItemId_byParentId(setid, "movie") + process = [] + try: + # Try to convert tuple to dictionary + current = dict(current_movies) + except ValueError: + current = {} + + # Sort current titles + for current_movie in current: + process.append(current_movie) + + # New list to compare + for movie in emby.getMovies_byBoxset(boxsetid)['Items']: + + itemid = movie['Id'] + + if not current.get(itemid): + # Assign boxset to movie + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + except TypeError: + self.logMsg("Failed to add: %s to boxset." % movie['Name'], 1) + continue + + self.logMsg("New addition to boxset %s: %s" % (title, movie['Name']), 1) + self.kodi_db.assignBoxset(setid, movieid) + # Update emby reference + emby_db.updateParentId(itemid, setid) + else: + # Remove from process, because the item still belongs + process.remove(itemid) + + # Process removals from boxset + for movie in process: + movieid = current[movie] + self.logMsg("Remove from boxset %s: %s" % (title, movieid)) + self.kodi_db.removefromBoxset(movieid) + # Update emby reference + emby_db.updateParentId(movie, None) + + # Update the reference in the emby table + emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum) + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.getChecksum() + userdata = API.getUserData() + runtime = API.getRuntime() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + fileid = emby_dbitem[1] + self.logMsg( + "Update playstate for movie: %s fileid: %s" + % (item['Name'], fileid), 1) + except TypeError: + return + + # Process favorite tags + if userdata['Favorite']: + self.kodi_db.addTag(movieid, "Favorite movies", "movie") + else: + self.kodi_db.removeTag(movieid, "Favorite movies", "movie") + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + + self.logMsg("%s New resume point: %s" % (itemid, resume)) + + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove movieid, fileid, emby reference + emby_db = self.emby_db + kodicursor = self.kodicursor + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + mediatype = emby_dbitem[4] + self.logMsg("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid), 1) + except TypeError: + return + + # Remove the emby reference + emby_db.removeItem(itemid) + # Remove artwork + artwork.deleteArtwork(kodiid, mediatype, kodicursor) + + if mediatype == "movie": + # Delete kodi movie and file + kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodiid,)) + kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) + + elif mediatype == "set": + # Delete kodi boxset + boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") + for movie in boxset_movies: + embyid = movie[0] + movieid = movie[1] + self.kodi_db.removefromBoxset(movieid) + # Update emby reference + emby_db.updateParentId(embyid, None) + + kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,)) + + self.logMsg("Deleted %s %s from kodi database" % (mediatype, itemid), 1) + +class MusicVideos(Items): + + + def __init__(self, embycursor, kodicursor): + Items.__init__(self, embycursor, kodicursor) + + def added(self, items, pdialog): + + total = len(items) + count = 0 + for mvideo in items: + + title = mvideo['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + self.add_update(mvideo) + if not pdialog and self.contentmsg: + self.contentPop(title, self.newvideo_time) + + + def add_update(self, item, viewtag=None, viewid=None): + # Process single music video + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + self.logMsg("mvideoid: %s fileid: %s pathid: %s" % (mvideoid, fileid, pathid), 1) + + except TypeError: + update_item = False + self.logMsg("mvideoid: %s not found." % itemid, 2) + # mvideoid + kodicursor.execute("select coalesce(max(idMVideo),0) from musicvideo") + mvideoid = kodicursor.fetchone()[0] + 1 + + else: + # Verification the item is still in Kodi + query = "SELECT * FROM musicvideo WHERE idMVideo = ?" + kodicursor.execute(query, (mvideoid,)) + try: + kodicursor.fetchone()[0] + except TypeError: + # item is not found, let's recreate it. + update_item = False + self.logMsg("mvideoid: %s missing from Kodi, repairing the entry." % mvideoid, 1) + + if not viewtag or not viewid: + # Get view tag from emby + viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) + self.logMsg("View tag found: %s" % viewtag, 2) + + # fileId information + checksum = API.getChecksum() + dateadded = API.getDateCreated() + userdata = API.getUserData() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + runtime = API.getRuntime() + plot = API.getOverview() + title = item['Name'] + year = item.get('ProductionYear') + genres = item['Genres'] + genre = " / ".join(genres) + studios = API.getStudios() + studio = " / ".join(studios) + artist = " / ".join(item.get('Artists')) + album = item.get('Album') + track = item.get('Track') + people = API.getPeople() + director = " / ".join(people['Director']) + + + ##### GET THE FILE AND PATH ##### + playurl = API.getFilePath() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.directpath: + # Direct paths is set the Kodi way + if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): + # Validate the path is correct with user intervention + resp = xbmcgui.Dialog().yesno( + heading="Can't validate path", + line1=( + "Kodi can't locate file: %s. Verify the path. " + "You may to verify your network credentials in the " + "add-on settings or use the emby path substitution " + "to format your path correctly. Stop syncing?" + % playurl)) + if resp: + utils.window('emby_shouldStop', value="true") + return False + + path = playurl.replace(filename, "") + utils.window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.musicvideos/" + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': mvideoid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE MUSIC VIDEO ##### + if update_item: + self.logMsg("UPDATE mvideo itemid: %s - Title: %s" % (itemid, title), 1) + + # Update path + query = "UPDATE path SET strPath = ? WHERE idPath = ?" + kodicursor.execute(query, (path, pathid)) + + # Update the filename + query = "UPDATE files SET strFilename = ?, dateAdded = ? WHERE idFile = ?" + kodicursor.execute(query, (filename, dateadded, fileid)) + + # Update the music video entry + query = ' '.join(( + + "UPDATE musicvideo", + "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,", + "c11 = ?, c12 = ?" + "WHERE idMVideo = ?" + )) + kodicursor.execute(query, (title, runtime, director, studio, year, plot, album, + artist, genre, track, mvideoid)) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE MUSIC VIDEO ##### + else: + self.logMsg("ADD mvideo itemid: %s - Title: %s" % (itemid, title), 1) + + # Add path + query = ' '.join(( + + "SELECT idPath", + "FROM path", + "WHERE strPath = ?" + )) + kodicursor.execute(query, (path,)) + try: + pathid = kodicursor.fetchone()[0] + except TypeError: + kodicursor.execute("select coalesce(max(idPath),0) from path") + pathid = kodicursor.fetchone()[0] + 1 + query = ( + ''' + INSERT OR REPLACE INTO path( + idPath, strPath, strContent, strScraper, noUpdate) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (pathid, path, "musicvideos", "metadata.local", 1)) + + # Add the file + kodicursor.execute("select coalesce(max(idFile),0) from files") + fileid = kodicursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO files( + idFile, idPath, strFilename, dateAdded) + + VALUES (?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (fileid, pathid, filename, dateadded)) + + # Create the musicvideo entry + query = ( + ''' + INSERT INTO musicvideo( + idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (mvideoid, fileid, title, runtime, director, studio, + year, plot, album, artist, genre, track)) + + # Create the reference in emby table + emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid, + checksum=checksum, mediafolderid=viewid) + + + # Process cast + people = item['People'] + artists = item['ArtistItems'] + for artist in artists: + artist['Type'] = "Artist" + people.extend(artists) + people = artwork.getPeopleArtwork(people) + self.kodi_db.addPeople(mvideoid, people, "musicvideo") + # Process genres + self.kodi_db.addGenres(mvideoid, genres, "musicvideo") + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor) + # Process stream details + streams = API.getMediaStreams() + self.kodi_db.addStreams(fileid, streams, runtime) + # Process studios + 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") + self.kodi_db.addTags(mvideoid, tags, "musicvideo") + # Process playstates + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + 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 + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.getChecksum() + userdata = API.getUserData() + runtime = API.getRuntime() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + self.logMsg( + "Update playstate for musicvideo: %s fileid: %s" + % (item['Name'], fileid), 1) + except TypeError: + return + + # Process favorite tags + if userdata['Favorite']: + self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") + else: + self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove mvideoid, fileid, pathid, emby reference + emby_db = self.emby_db + kodicursor = self.kodicursor + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + self.logMsg("Removing mvideoid: %s fileid: %s" % (mvideoid, fileid, pathid), 1) + except TypeError: + return + + # Remove artwork + query = ' '.join(( + + "SELECT url, type", + "FROM art", + "WHERE media_id = ?", + "AND media_type = 'musicvideo'" + )) + kodicursor.execute(query, (mvideoid,)) + for row in kodicursor.fetchall(): + + url = row[0] + imagetype = row[1] + if imagetype in ("poster", "fanart"): + artwork.deleteCachedArtwork(url) + + kodicursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (mvideoid,)) + kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) + if self.directpath: + kodicursor.execute("DELETE FROM path WHERE idPath = ?", (pathid,)) + self.embycursor.execute("DELETE FROM emby WHERE emby_id = ?", (itemid,)) + + self.logMsg("Deleted musicvideo %s from kodi database" % itemid, 1) + +class TVShows(Items): + + + def __init__(self, embycursor, kodicursor): + Items.__init__(self, embycursor, kodicursor) + + def added(self, items, pdialog): + + total = len(items) + count = 0 + for tvshow in items: + + title = tvshow['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + self.add_update(tvshow) + # Add episodes + all_episodes = self.emby.getEpisodesbyShow(tvshow['Id']) + self.added_episode(all_episodes['Items'], pdialog) + + def added_season(self, items, pdialog): + + total = len(items) + count = 0 + for season in items: + + title = "%s - %s" % (season.get('SeriesName', "Unknown"), season['Name']) + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + self.add_updateSeason(season) + # Add episodes + all_episodes = self.emby.getEpisodesbySeason(season['Id']) + self.added_episode(all_episodes['Items'], pdialog) + + def added_episode(self, items, pdialog): + + total = len(items) + count = 0 + for episode in items: + title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + self.add_updateEpisode(episode) + if not pdialog and self.contentmsg: + self.contentPop(title, self.newvideo_time) + + + def add_update(self, item, viewtag=None, viewid=None): + # Process single tvshow + kodicursor = self.kodicursor + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + if utils.settings('syncEmptyShows') == "false" and not item['RecursiveItemCount']: + self.logMsg("Skipping empty show: %s" % item['Name'], 1) + return + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + force_episodes = False + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + showid = emby_dbitem[0] + pathid = emby_dbitem[2] + self.logMsg("showid: %s pathid: %s" % (showid, pathid), 1) + + except TypeError: + update_item = False + self.logMsg("showid: %s not found." % itemid, 2) + kodicursor.execute("select coalesce(max(idShow),0) from tvshow") + showid = kodicursor.fetchone()[0] + 1 + + else: + # Verification the item is still in Kodi + query = "SELECT * FROM tvshow WHERE idShow = ?" + kodicursor.execute(query, (showid,)) + try: + kodicursor.fetchone()[0] + except TypeError: + # item is not found, let's recreate it. + update_item = False + self.logMsg("showid: %s missing from Kodi, repairing the entry." % showid, 1) + # Force re-add episodes after the show is re-created. + force_episodes = True + + + if viewtag is None or viewid is None: + # Get view tag from emby + viewtag, viewid, mediatype = emby.getView_embyId(itemid) + self.logMsg("View tag found: %s" % viewtag, 2) + + # fileId information + checksum = API.getChecksum() + dateadded = API.getDateCreated() + userdata = API.getUserData() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + genres = item['Genres'] + title = item['Name'] + plot = API.getOverview() + rating = item.get('CommunityRating') + premieredate = API.getPremiereDate() + tvdb = API.getProvider('Tvdb') + sorttitle = item['SortName'] + mpaa = API.getMpaa() + genre = " / ".join(genres) + studios = API.getStudios() + studio = " / ".join(studios) + + + ##### GET THE FILE AND PATH ##### + playurl = API.getFilePath() + + if self.directpath: + # Direct paths is set the Kodi way + if "\\" in playurl: + # Local path + path = "%s\\" % playurl + toplevelpath = "%s\\" % dirname(dirname(path)) + else: + # Network path + path = "%s/" % playurl + toplevelpath = "%s/" % dirname(dirname(path)) + + if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(path): + # Validate the path is correct with user intervention + resp = xbmcgui.Dialog().yesno( + heading="Can't validate path", + line1=( + "Kodi can't locate file: %s. Verify the path. " + "You may to verify your network credentials in the " + "add-on settings or use the emby path substitution " + "to format your path correctly. Stop syncing?" + % playurl)) + if resp: + utils.window('emby_shouldStop', value="true") + return False + + utils.window('emby_pathverified', value="true") + else: + # Set plugin path + toplevelpath = "plugin://plugin.video.emby.tvshows/" + path = "%s%s/" % (toplevelpath, itemid) + + + ##### UPDATE THE TVSHOW ##### + if update_item: + self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1) + + # Update the tvshow entry + query = ' '.join(( + + "UPDATE tvshow", + "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?", + "WHERE idShow = ?" + )) + kodicursor.execute(query, (title, plot, rating, premieredate, genre, title, + tvdb, mpaa, studio, sorttitle, showid)) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE TVSHOW ##### + else: + self.logMsg("ADD tvshow itemid: %s - Title: %s" % (itemid, title), 1) + + # Add top path + toppathid = self.kodi_db.addPath(toplevelpath) + query = ' '.join(( + + "UPDATE path", + "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", + "WHERE idPath = ?" + )) + kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid)) + + # Add path + pathid = self.kodi_db.addPath(path) + + # Create the tvshow entry + query = ( + ''' + INSERT INTO tvshow( + idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre, + title, tvdb, mpaa, studio, sorttitle)) + + # Link the path + query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)" + kodicursor.execute(query, (showid, pathid)) + + # Create the reference in emby table + emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, + checksum=checksum, mediafolderid=viewid) + + # Update the path + query = ' '.join(( + + "UPDATE path", + "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", + "WHERE idPath = ?" + )) + kodicursor.execute(query, (path, None, None, 1, pathid)) + + # Process cast + people = artwork.getPeopleArtwork(item['People']) + self.kodi_db.addPeople(showid, people, "tvshow") + # Process genres + self.kodi_db.addGenres(showid, genres, "tvshow") + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(item), showid, "tvshow", kodicursor) + # Process studios + self.kodi_db.addStudios(showid, studios, "tvshow") + # Process tags: view, emby tags + tags = [viewtag] + tags.extend(item['Tags']) + if userdata['Favorite']: + tags.append("Favorite tvshows") + self.kodi_db.addTags(showid, tags, "tvshow") + # Process seasons + all_seasons = emby.getSeasons(itemid) + for season in all_seasons['Items']: + self.add_updateSeason(season, showid=showid) + else: + # Finally, refresh the all season entry + seasonid = self.kodi_db.addSeason(showid, -1) + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) + + if force_episodes: + # We needed to recreate the show entry. Re-add episodes now. + self.logMsg("Repairing episodes for showid: %s %s" % (showid, title), 1) + all_episodes = emby.getEpisodesbyShow(itemid) + self.added_episode(all_episodes['Items'], None) + + def add_updateSeason(self, item, showid=None): + + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + + seasonnum = item.get('IndexNumber', 1) + + if showid is None: + try: + seriesId = item['SeriesId'] + showid = emby_db.getItem_byId(seriesId)[0] + except KeyError: + return + except TypeError: + # Show is missing, update show instead. + show = self.emby.getItem(seriesId) + self.add_update(show) + return + + seasonid = self.kodi_db.addSeason(showid, seasonnum) + + if item['LocationType'] != "Virtual": + # Create the reference in emby table + emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid) + + # Process artwork + artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) + + def add_updateEpisode(self, item): + # Process single episode + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + episodeid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + self.logMsg("episodeid: %s fileid: %s pathid: %s" % (episodeid, fileid, pathid), 1) + + except TypeError: + update_item = False + self.logMsg("episodeid: %s not found." % itemid, 2) + # episodeid + kodicursor.execute("select coalesce(max(idEpisode),0) from episode") + episodeid = kodicursor.fetchone()[0] + 1 + + else: + # Verification the item is still in Kodi + query = "SELECT * FROM episode WHERE idEpisode = ?" + kodicursor.execute(query, (episodeid,)) + try: + kodicursor.fetchone()[0] + except TypeError: + # item is not found, let's recreate it. + update_item = False + self.logMsg("episodeid: %s missing from Kodi, repairing the entry." % episodeid, 1) + + # fileId information + checksum = API.getChecksum() + dateadded = API.getDateCreated() + userdata = API.getUserData() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + people = API.getPeople() + writer = " / ".join(people['Writer']) + director = " / ".join(people['Director']) + title = item['Name'] + plot = API.getOverview() + rating = item.get('CommunityRating') + runtime = API.getRuntime() + premieredate = API.getPremiereDate() + + # episode details + try: + seriesId = item['SeriesId'] + except KeyError: + # Missing seriesId, skip + self.logMsg("Skipping: %s. SeriesId is missing." % itemid, 1) + return False + + seriesName = item['SeriesName'] + season = item.get('ParentIndexNumber') + episode = item.get('IndexNumber', -1) + + if season is None: + if item.get('AbsoluteEpisodeNumber'): + # Anime scenario + season = 1 + episode = item['AbsoluteEpisodeNumber'] + else: + season = -1 + + # Specials ordering within season + if item.get('AirsAfterSeasonNumber'): + airsBeforeSeason = item['AirsAfterSeasonNumber'] + airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering + else: + airsBeforeSeason = item.get('AirsBeforeSeasonNumber') + airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') + + # Append multi episodes to title + if item.get('IndexNumberEnd'): + title = "| %02d | %s" % (item['IndexNumberEnd'], title) + + # Get season id + show = emby_db.getItem_byId(seriesId) + try: + showid = show[0] + except TypeError: + # Show is missing from database + show = self.emby.getItem(seriesId) + self.add_update(show) + show = emby_db.getItem_byId(seriesId) + try: + showid = show[0] + except TypeError: + self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId)) + return False + + seasonid = self.kodi_db.addSeason(showid, season) + + + ##### GET THE FILE AND PATH ##### + playurl = API.getFilePath() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.directpath: + # Direct paths is set the Kodi way + if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): + # Validate the path is correct with user intervention + resp = xbmcgui.Dialog().yesno( + heading="Can't validate path", + line1=( + "Kodi can't locate file: %s. Verify the path. " + "You may to verify your network credentials in the " + "add-on settings or use the emby path substitution " + "to format your path correctly. Stop syncing?" + % playurl)) + if resp: + utils.window('emby_shouldStop', value="true") + return False + + path = playurl.replace(filename, "") + utils.window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': episodeid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE EPISODE ##### + if update_item: + self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1) + + # Update the movie entry + if self.kodiversion in (16, 17): + # Kodi Jarvis, Krypton + query = ' '.join(( + + "UPDATE episode", + "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?", + "WHERE idEpisode = ?" + )) + kodicursor.execute(query, (title, plot, rating, writer, premieredate, + runtime, director, season, episode, title, airsBeforeSeason, + airsBeforeEpisode, seasonid, episodeid)) + else: + query = ' '.join(( + + "UPDATE episode", + "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?", + "WHERE idEpisode = ?" + )) + kodicursor.execute(query, (title, plot, rating, writer, premieredate, + runtime, director, season, episode, title, airsBeforeSeason, + airsBeforeEpisode, episodeid)) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + # Update parentid reference + emby_db.updateParentId(itemid, seasonid) + + ##### OR ADD THE EPISODE ##### + else: + self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1) + + # Add path + pathid = self.kodi_db.addPath(path) + # Add the file + fileid = self.kodi_db.addFile(filename, pathid) + + # Create the episode entry + if self.kodiversion in (16, 17): + # Kodi Jarvis, Krypton + query = ( + ''' + INSERT INTO episode( + idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, + idShow, c15, c16, idSeason) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, + premieredate, runtime, director, season, episode, title, showid, + airsBeforeSeason, airsBeforeEpisode, seasonid)) + else: + query = ( + ''' + INSERT INTO episode( + idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, + idShow, c15, c16) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, + premieredate, runtime, director, season, episode, title, showid, + airsBeforeSeason, airsBeforeEpisode)) + + # Create the reference in emby table + emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, + seasonid, checksum) + + # Update the path + query = ' '.join(( + + "UPDATE path", + "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", + "WHERE idPath = ?" + )) + kodicursor.execute(query, (path, None, None, 1, pathid)) + + # Update the file + query = ' '.join(( + + "UPDATE files", + "SET idPath = ?, strFilename = ?, dateAdded = ?", + "WHERE idFile = ?" + )) + kodicursor.execute(query, (pathid, filename, dateadded, fileid)) + + # Process cast + people = artwork.getPeopleArtwork(item['People']) + self.kodi_db.addPeople(episodeid, people, "episode") + # Process artwork + artworks = artwork.getAllArtwork(item) + artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor) + # Process stream details + streams = API.getMediaStreams() + self.kodi_db.addStreams(fileid, streams, runtime) + # Process playstates + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + if not self.directpath and resume: + # Create additional entry for widgets. This is only required for plugin/episode. + temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/") + tempfileid = self.kodi_db.addFile(filename, temppathid) + query = ' '.join(( + + "UPDATE files", + "SET idPath = ?, strFilename = ?, dateAdded = ?", + "WHERE idFile = ?" + )) + kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) + self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed) + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.getChecksum() + userdata = API.getUserData() + runtime = API.getRuntime() + dateadded = API.getDateCreated() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + mediatype = emby_dbitem[4] + self.logMsg( + "Update playstate for %s: %s fileid: %s" + % (mediatype, item['Name'], fileid), 1) + except TypeError: + return + + # Process favorite tags + if mediatype == "tvshow": + if userdata['Favorite']: + self.kodi_db.addTag(kodiid, "Favorite tvshows", "tvshow") + else: + self.kodi_db.removeTag(kodiid, "Favorite tvshows", "tvshow") + elif mediatype == "episode": + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjustResume(userdata['Resume']) + total = round(float(runtime), 6) + + self.logMsg("%s New resume point: %s" % (itemid, resume)) + + self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) + if not self.directpath and not resume: + # Make sure there's no other bookmarks created by widget. + filename = self.kodi_db.getFile(fileid) + self.kodi_db.removeFile("plugin://plugin.video.emby.tvshows/", filename) + + if not self.directpath and resume: + # Create additional entry for widgets. This is only required for plugin/episode. + filename = self.kodi_db.getFile(fileid) + temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/") + tempfileid = self.kodi_db.addFile(filename, temppathid) + query = ' '.join(( + + "UPDATE files", + "SET idPath = ?, strFilename = ?, dateAdded = ?", + "WHERE idFile = ?" + )) + self.kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) + self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed) + + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove showid, fileid, pathid, emby reference + emby_db = self.emby_db + embycursor = self.embycursor + kodicursor = self.kodicursor + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + parentid = emby_dbitem[3] + mediatype = emby_dbitem[4] + self.logMsg("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid), 1) + except TypeError: + return + + ##### PROCESS ITEM ##### + + # Remove the emby reference + emby_db.removeItem(itemid) + + + ##### IF EPISODE ##### + + if mediatype == "episode": + # Delete kodi episode and file, verify season and tvshow + self.removeEpisode(kodiid, fileid) + + # Season verification + season = emby_db.getItem_byKodiId(parentid, "season") + try: + showid = season[1] + except TypeError: + return + + season_episodes = emby_db.getItem_byParentId(parentid, "episode") + if not season_episodes: + self.removeSeason(parentid) + emby_db.removeItem(season[0]) + + # Show verification + show = emby_db.getItem_byKodiId(showid, "tvshow") + query = ' '.join(( + + "SELECT totalCount", + "FROM tvshowcounts", + "WHERE idShow = ?" + )) + kodicursor.execute(query, (showid,)) + result = kodicursor.fetchone() + if result and result[0] is None: + # There's no episodes left, delete show and any possible remaining seasons + seasons = emby_db.getItem_byParentId(showid, "season") + for season in seasons: + self.removeSeason(season[1]) + else: + # Delete emby season entries + emby_db.removeItems_byParentId(showid, "season") + self.removeShow(showid) + emby_db.removeItem(show[0]) + + ##### IF TVSHOW ##### + + elif mediatype == "tvshow": + # Remove episodes, seasons, tvshow + seasons = emby_db.getItem_byParentId(kodiid, "season") + for season in seasons: + seasonid = season[1] + season_episodes = emby_db.getItem_byParentId(seasonid, "episode") + for episode in season_episodes: + self.removeEpisode(episode[1], episode[2]) + else: + # Remove emby episodes + emby_db.removeItems_byParentId(seasonid, "episode") + else: + # Remove emby seasons + emby_db.removeItems_byParentId(kodiid, "season") + + # Remove tvshow + self.removeShow(kodiid) + + ##### IF SEASON ##### + + elif mediatype == "season": + # Remove episodes, season, verify tvshow + season_episodes = emby_db.getItem_byParentId(kodiid, "episode") + for episode in season_episodes: + self.removeEpisode(episode[1], episode[2]) + else: + # Remove emby episodes + emby_db.removeItems_byParentId(kodiid, "episode") + + # Remove season + self.removeSeason(kodiid) + + # Show verification + seasons = emby_db.getItem_byParentId(parentid, "season") + if not seasons: + # There's no seasons, delete the show + self.removeShow(parentid) + emby_db.removeItem_byKodiId(parentid, "tvshow") + + self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1) + + def removeShow(self, kodiid): + + kodicursor = self.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 + + 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 + + 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) + +class Music(Items): + + + def __init__(self, embycursor, musiccursor): + + Items.__init__(self, embycursor, musiccursor) + + self.directstream = utils.settings('streamMusic') == "true" + self.enableimportsongrating = utils.settings('enableImportSongRating') == "true" + self.enableexportsongrating = utils.settings('enableExportSongRating') == "true" + self.enableupdatesongrating = utils.settings('enableUpdateSongRating') == "true" + self.userid = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userid) + + def added(self, items, pdialog): + + total = len(items) + count = 0 + for artist in items: + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=artist['Name']) + count += 1 + self.add_updateArtist(artist) + # Add albums + all_albums = self.emby.getAlbumsbyArtist(artist['Id']) + self.added_album(all_albums['Items'], pdialog) + + def added_album(self, items, pdialog): + + total = len(items) + count = 0 + for album in items: + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=album['Name']) + count += 1 + self.add_updateAlbum(album) + # Add songs + all_songs = self.emby.getSongsbyAlbum(album['Id']) + self.added_song(all_songs['Items'], pdialog) + + def added_song(self, items, pdialog): + + total = len(items) + count = 0 + for song in items: + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=song['Name']) + count += 1 + self.add_updateSong(song) + if not pdialog and self.contentmsg: + self.contentPop(song['Name'], self.newmusic_time) + + def add_updateArtist(self, item, artisttype="MusicArtist"): + # Process a single artist + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + artistid = emby_dbitem[0] + except TypeError: + update_item = False + self.logMsg("artistid: %s not found." % itemid, 2) + + ##### The artist details ##### + lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + dateadded = API.getDateCreated() + checksum = API.getChecksum() + + name = item['Name'] + musicBrainzId = API.getProvider('MusicBrainzArtist') + genres = " / ".join(item.get('Genres')) + bio = API.getOverview() + + # Associate artwork + artworks = artwork.getAllArtwork(item, parentInfo=True) + thumb = artworks['Primary'] + backdrops = artworks['Backdrop'] # List + + if thumb: + thumb = "%s" % thumb + if backdrops: + fanart = "%s" % backdrops[0] + else: + fanart = "" + + + ##### UPDATE THE ARTIST ##### + if update_item: + self.logMsg("UPDATE artist itemid: %s - Name: %s" % (itemid, name), 1) + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE ARTIST ##### + else: + self.logMsg("ADD artist itemid: %s - Name: %s" % (itemid, name), 1) + # safety checks: It looks like Emby supports the same artist multiple times. + # Kodi doesn't allow that. In case that happens we just merge the artist entries. + artistid = self.kodi_db.addArtist(name, musicBrainzId) + # Create the reference in emby table + emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum) + + + # Process the artist + if self.kodiversion in (16, 17): + query = ' '.join(( + + "UPDATE artist", + "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", + "lastScraped = ?", + "WHERE idArtist = ?" + )) + kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid)) + else: + query = ' '.join(( + + "UPDATE artist", + "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", + "lastScraped = ?, dateAdded = ?", + "WHERE idArtist = ?" + )) + kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, + dateadded, artistid)) + + + # Update artwork + artwork.addArtwork(artworks, artistid, "artist", kodicursor) + + def add_updateAlbum(self, item): + # Process a single artist + emby = self.emby + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + albumid = emby_dbitem[0] + except TypeError: + update_item = False + self.logMsg("albumid: %s not found." % itemid, 2) + + ##### The album details ##### + lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + dateadded = API.getDateCreated() + userdata = API.getUserData() + checksum = API.getChecksum() + + name = item['Name'] + musicBrainzId = API.getProvider('MusicBrainzAlbum') + year = item.get('ProductionYear') + genres = item.get('Genres') + genre = " / ".join(genres) + bio = API.getOverview() + rating = userdata['UserRating'] + artists = item['AlbumArtists'] + if not artists: + artists = item['ArtistItems'] + artistname = [] + for artist in artists: + artistname.append(artist['Name']) + artistname = " / ".join(artistname) + + # Associate artwork + artworks = artwork.getAllArtwork(item, parentInfo=True) + thumb = artworks['Primary'] + if thumb: + thumb = "%s" % thumb + + ##### UPDATE THE ALBUM ##### + if update_item: + self.logMsg("UPDATE album itemid: %s - Name: %s" % (itemid, name), 1) + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE ALBUM ##### + else: + self.logMsg("ADD album itemid: %s - Name: %s" % (itemid, name), 1) + # safety checks: It looks like Emby supports the same artist multiple times. + # Kodi doesn't allow that. In case that happens we just merge the artist entries. + 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 self.kodiversion == 17: + # Kodi Krypton + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iUserrating = ?, lastScraped = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, + "album", albumid)) + elif self.kodiversion == 16: + # Kodi Jarvis + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, + "album", albumid)) + elif self.kodiversion == 15: + # Kodi Isengard + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, + dateadded, "album", albumid)) + else: + # Kodi Helix + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, dateAdded = ?", + "WHERE idAlbum = ?" + )) + kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, + dateadded, albumid)) + + # Associate the parentid for emby reference + parentId = item.get('ParentId') + if parentId is not None: + emby_dbartist = emby_db.getItem_byId(parentId) + try: + artistid = emby_dbartist[0] + except TypeError: + # Artist does not exist in emby database. + artist = emby.getItem(parentId) + # Item may not be an artist, verification necessary. + if artist['Type'] == "MusicArtist": + # Update with the parentId, for remove reference + emby_db.addReference(parentId, parentId, "MusicArtist", "artist") + emby_db.updateParentId(itemid, parentId) + else: + # Update emby reference with the artistid + emby_db.updateParentId(itemid, artistid) + + # Assign main artists to album + for artist in artists: + artistname = artist['Name'] + artistId = artist['Id'] + emby_dbartist = emby_db.getItem_byId(artistId) + try: + artistid = emby_dbartist[0] + except TypeError: + # Artist does not exist in emby database, create the reference + artist = emby.getItem(artistId) + self.add_updateArtist(artist, artisttype="AlbumArtist") + emby_dbartist = emby_db.getItem_byId(artistId) + artistid = emby_dbartist[0] + else: + # Best take this name over anything else. + query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" + kodicursor.execute(query, (artistname, artistid,)) + + # Add artist to album + query = ( + ''' + INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) + + VALUES (?, ?, ?) + ''' + ) + kodicursor.execute(query, (artistid, albumid, artistname)) + # Update discography + query = ( + ''' + INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) + + VALUES (?, ?, ?) + ''' + ) + kodicursor.execute(query, (artistid, name, year)) + # Update emby reference with parentid + emby_db.updateParentId(artistId, albumid) + + # Add genres + self.kodi_db.addMusicGenres(albumid, genres, "album") + # Update artwork + artwork.addArtwork(artworks, albumid, "album", kodicursor) + + def add_updateSong(self, item): + # Process single song + kodicursor = self.kodicursor + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + songid = emby_dbitem[0] + pathid = emby_dbitem[2] + albumid = emby_dbitem[3] + except TypeError: + update_item = False + self.logMsg("songid: %s not found." % itemid, 2) + + ##### The song details ##### + checksum = API.getChecksum() + dateadded = API.getDateCreated() + userdata = API.getUserData() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + title = item['Name'] + musicBrainzId = API.getProvider('MusicBrainzTrackId') + genres = item.get('Genres') + genre = " / ".join(genres) + artists = " / ".join(item['Artists']) + tracknumber = item.get('IndexNumber', 0) + disc = item.get('ParentIndexNumber', 1) + if disc == 1: + track = tracknumber + else: + track = disc*2**16 + tracknumber + year = item.get('ProductionYear') + duration = API.getRuntime() + rating = userdata['UserRating'] + + #if enabled, try to get the rating from file and/or emby + if not self.directstream: + rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) + else: + hasEmbeddedCover = False + comment = API.getOverview() + + + ##### GET THE FILE AND PATH ##### + if self.directstream: + path = "%s/emby/Audio/%s/" % (self.server, itemid) + filename = "stream.mp3" + else: + playurl = API.getFilePath() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + # Direct paths is set the Kodi way + if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl): + # Validate the path is correct with user intervention + utils.window('emby_directPath', clear=True) + resp = xbmcgui.Dialog().yesno( + heading="Can't validate path", + line1=( + "Kodi can't locate file: %s. Verify the path. " + "You may to verify your network credentials in the " + "add-on settings or use the emby path substitution " + "to format your path correctly. Stop syncing?" + % playurl)) + if resp: + utils.window('emby_shouldStop', value="true") + return False + + path = playurl.replace(filename, "") + utils.window('emby_pathverified', value="true") + + ##### UPDATE THE SONG ##### + if update_item: + self.logMsg("UPDATE song itemid: %s - Title: %s" % (itemid, title), 1) + + # Update path + query = "UPDATE path SET strPath = ? WHERE idPath = ?" + kodicursor.execute(query, (path, pathid)) + + # Update the song entry + query = ' '.join(( + + "UPDATE song", + "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", + "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", + "rating = ?, comment = ?", + "WHERE idSong = ?" + )) + kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year, + filename, playcount, dateplayed, rating, comment, songid)) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE SONG ##### + else: + self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1) + + # Add path + pathid = self.kodi_db.addPath(path) + + try: + # Get the album + emby_dbalbum = emby_db.getItem_byId(item['AlbumId']) + albumid = emby_dbalbum[0] + except KeyError: + # Verify if there's an album associated. + album_name = item.get('Album') + if album_name: + self.logMsg("Creating virtual music album for song: %s." % itemid, 1) + 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. + self.logMsg("Song itemid: %s has no albumId associated." % itemid, 1) + return False + + except TypeError: + # No album found. Let's create it + self.logMsg("Album database entry missing.", 1) + emby_albumId = item['AlbumId'] + album = emby.getItem(emby_albumId) + self.add_updateAlbum(album) + emby_dbalbum = emby_db.getItem_byId(emby_albumId) + try: + albumid = emby_dbalbum[0] + self.logMsg("Found albumid: %s" % albumid, 1) + except TypeError: + # No album found, create a single's album + self.logMsg("Failed to add album. Creating singles.", 1) + kodicursor.execute("select coalesce(max(idAlbum),0) from album") + albumid = kodicursor.fetchone()[0] + 1 + if self.kodiversion == 16: + # Kodi Jarvis + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) + + VALUES (?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (albumid, genre, year, "single")) + elif self.kodiversion == 15: + # Kodi Isengard + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (albumid, genre, year, dateadded, "single")) + else: + # Kodi Helix + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) + + VALUES (?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (albumid, genre, year, dateadded)) + + # Create the song entry + kodicursor.execute("select coalesce(max(idSong),0) from song") + songid = kodicursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO song( + idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, + iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, + rating) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (songid, albumid, pathid, artists, genre, title, track, + duration, year, filename, musicBrainzId, playcount, dateplayed, rating)) + + # Create the reference in emby table + emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, + checksum=checksum) + + + # Link song to album + query = ( + ''' + INSERT OR REPLACE INTO albuminfosong( + idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (songid, albumid, track, title, duration)) + + # Link song to artists + for index, artist in enumerate(item['ArtistItems']): + + artist_name = artist['Name'] + artist_eid = artist['Id'] + artist_edb = emby_db.getItem_byId(artist_eid) + try: + artistid = artist_edb[0] + except TypeError: + # Artist is missing from emby database, add it. + artist_full = emby.getItem(artist_eid) + self.add_updateArtist(artist_full) + artist_edb = emby_db.getItem_byId(artist_eid) + artistid = artist_edb[0] + finally: + query = ( + ''' + INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) + + VALUES (?, ?, ?, ?) + ''' + ) + kodicursor.execute(query, (artistid, songid, index, artist_name)) + + # Verify if album artist exists + album_artists = [] + for artist in item['AlbumArtists']: + + artist_name = artist['Name'] + album_artists.append(artist_name) + artist_eid = artist['Id'] + artist_edb = emby_db.getItem_byId(artist_eid) + try: + artistid = artist_edb[0] + except TypeError: + # Artist is missing from emby database, add it. + artist_full = emby.getItem(artist_eid) + self.add_updateArtist(artist_full) + artist_edb = emby_db.getItem_byId(artist_eid) + artistid = artist_edb[0] + finally: + query = ( + ''' + INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) + + VALUES (?, ?, ?) + ''' + ) + kodicursor.execute(query, (artistid, albumid, artist_name)) + # Update discography + if item.get('Album'): + query = ( + ''' + INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) + + VALUES (?, ?, ?) + ''' + ) + kodicursor.execute(query, (artistid, item['Album'], 0)) + else: + album_artists = " / ".join(album_artists) + query = ' '.join(( + + "SELECT strArtists", + "FROM album", + "WHERE idAlbum = ?" + )) + kodicursor.execute(query, (albumid,)) + result = kodicursor.fetchone() + if result and result[0] != album_artists: + # Field is empty + if self.kodiversion in (16, 17): + # Kodi Jarvis, Krypton + query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" + kodicursor.execute(query, (album_artists, albumid)) + elif self.kodiversion == 15: + # Kodi Isengard + query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" + kodicursor.execute(query, (album_artists, albumid)) + else: + # Kodi Helix + query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" + kodicursor.execute(query, (album_artists, albumid)) + + # Add genres + self.kodi_db.addMusicGenres(songid, genres, "song") + + # Update artwork + allart = artwork.getAllArtwork(item, parentInfo=True) + if hasEmbeddedCover: + allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl ) + artwork.addArtwork(allart, songid, "song", kodicursor) + + if item.get('AlbumId') is None: + # Update album artwork + artwork.addArtwork(allart, albumid, "album", kodicursor) + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + kodicursor = self.kodicursor + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.getChecksum() + userdata = API.getUserData() + runtime = API.getRuntime() + rating = userdata['UserRating'] + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + mediatype = emby_dbitem[4] + self.logMsg("Update playstate for %s: %s" % (mediatype, item['Name']), 1) + except TypeError: + return + + if mediatype == "song": + + #should we ignore this item ? + #happens when userdata updated by ratings method + if utils.window("ignore-update-%s" %itemid): + utils.window("ignore-update-%s" %itemid,clear=True) + return + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + #process item ratings + rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) + + query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?" + kodicursor.execute(query, (playcount, dateplayed, rating, kodiid)) + + elif mediatype == "album": + # Process playstates + query = "UPDATE album SET iRating = ? WHERE idAlbum = ?" + kodicursor.execute(query, (rating, kodiid)) + + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove kodiid, fileid, pathid, emby reference + emby_db = self.emby_db + kodicursor = self.kodicursor + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + mediatype = emby_dbitem[4] + self.logMsg("Removing %s kodiid: %s" % (mediatype, kodiid), 1) + except TypeError: + return + + ##### PROCESS ITEM ##### + + # Remove the emby reference + emby_db.removeItem(itemid) + + + ##### IF SONG ##### + + if mediatype == "song": + # Delete song + self.removeSong(kodiid) + # This should only address single song scenario, where server doesn't actually + # create an album for the song. + emby_db.removeWildItem(itemid) + + for item in emby_db.getItem_byWildId(itemid): + + item_kid = item[0] + item_mediatype = item[1] + + if item_mediatype == "album": + childs = emby_db.getItem_byParentId(item_kid, "song") + if not childs: + # Delete album + self.removeAlbum(item_kid) + + ##### IF ALBUM ##### + + elif mediatype == "album": + # Delete songs, album + album_songs = emby_db.getItem_byParentId(kodiid, "song") + for song in album_songs: + self.removeSong(song[1]) + else: + # Remove emby songs + emby_db.removeItems_byParentId(kodiid, "song") + + # Remove the album + self.removeAlbum(kodiid) + + ##### IF ARTIST ##### + + elif mediatype == "artist": + # Delete songs, album, artist + albums = emby_db.getItem_byParentId(kodiid, "album") + for album in albums: + albumid = album[1] + album_songs = emby_db.getItem_byParentId(albumid, "song") + for song in album_songs: + self.removeSong(song[1]) + else: + # Remove emby song + emby_db.removeItems_byParentId(albumid, "song") + # Remove emby artist + emby_db.removeItems_byParentId(albumid, "artist") + # Remove kodi album + self.removeAlbum(albumid) + else: + # Remove emby albums + emby_db.removeItems_byParentId(kodiid, "album") + + # Remove artist + self.removeArtist(kodiid) + + self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1) + + def removeSong(self, kodiid): + + kodicursor = self.kodicursor + + self.artwork.deleteArtwork(kodiid, "song", self.kodicursor) + self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) + + def removeAlbum(self, kodiid): + + self.artwork.deleteArtwork(kodiid, "album", self.kodicursor) + self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) + + def removeArtist(self, 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 a7d82a3e..6c3dd8b1 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1,1190 +1,1149 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import xbmc - -import api -import artwork -import clientinfo -import utils - -################################################################################################## - - -class Kodidb_Functions(): - - kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) - - - def __init__(self, cursor): - - self.cursor = cursor - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.artwork = artwork.Artwork() - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def addPath(self, path): - - cursor = self.cursor - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - cursor.execute(query, (path,)) - try: - pathid = cursor.fetchone()[0] - except TypeError: - cursor.execute("select coalesce(max(idPath),0) from path") - pathid = cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO path( - idPath, strPath) - - VALUES (?, ?) - ''' - ) - cursor.execute(query, (pathid, path)) - - return pathid - - def getPath(self, path): - - cursor = self.cursor - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - cursor.execute(query, (path,)) - try: - pathid = cursor.fetchone()[0] - except TypeError: - pathid = None - - return pathid - - def addFile(self, filename, pathid): - - cursor = self.cursor - - query = ' '.join(( - - "SELECT idFile", - "FROM files", - "WHERE strFilename = ?", - "AND idPath = ?" - )) - cursor.execute(query, (filename, pathid,)) - try: - fileid = cursor.fetchone()[0] - except TypeError: - cursor.execute("select coalesce(max(idFile),0) from files") - fileid = cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO files( - idFile, strFilename) - - VALUES (?, ?) - ''' - ) - 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,)) - try: - filename = cursor.fetchone()[0] - except TypeError: - filename = "" - - return filename - - def removeFile(self, path, filename): - - pathid = self.getPath(path) - - if pathid is not None: - query = ' '.join(( - - "DELETE FROM files", - "WHERE idPath = ?", - "AND strFilename = ?" - )) - self.cursor.execute(query, (pathid, filename,)) - - def addCountries(self, kodiid, countries, mediatype): - - cursor = self.cursor - - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - for country in countries: - query = ' '.join(( - - "SELECT country_id", - "FROM country", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (country,)) - - try: - country_id = 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 - - query = "INSERT INTO country(country_id, name) values(?, ?)" - cursor.execute(query, (country_id, country)) - self.logMsg("Add country to media, processing: %s" % country, 2) - - finally: # Assign country to content - query = ( - ''' - INSERT OR REPLACE INTO country_link( - country_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (country_id, kodiid, mediatype)) - else: - # Kodi Helix - for country in countries: - query = ' '.join(( - - "SELECT idCountry", - "FROM country", - "WHERE strCountry = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (country,)) - - try: - idCountry = cursor.fetchone()[0] - - except TypeError: - # Country entry does not exists - cursor.execute("select coalesce(max(idCountry),0) from country") - idCountry = cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" - cursor.execute(query, (idCountry, country)) - self.logMsg("Add country to media, processing: %s" % country, 2) - - finally: - # Only movies have a country field - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO countrylinkmovie( - idCountry, idMovie) - - VALUES (?, ?) - ''' - ) - 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'] - thumb = person['imageurl'] - - # Kodi Isengard, Jarvis, Krypton - if kodiversion in (15, 16, 17): - query = ' '.join(( - - "SELECT actor_id", - "FROM actor", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (name,)) - - try: - actorid = 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 - - query = "INSERT INTO actor(actor_id, name) values(?, ?)" - cursor.execute(query, (actorid, name)) - self.logMsg("Add people to media, processing: %s" % name, 2) - - finally: - # Link person to content - if "Actor" in type: - role = person.get('Role') - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type, role, cast_order) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) - castorder += 1 - - elif "Director" in type: - query = ( - ''' - INSERT OR REPLACE INTO director_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (actorid, kodiid, mediatype)) - - elif type in ("Writing", "Writer"): - query = ( - ''' - INSERT OR REPLACE INTO writer_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (actorid, kodiid, mediatype)) - - elif "Artist" in type: - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (actorid, kodiid, mediatype)) - # Kodi Helix - else: - query = ' '.join(( - - "SELECT idActor", - "FROM actors", - "WHERE strActor = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (name,)) - - try: - actorid = cursor.fetchone()[0] - - except TypeError: - # Cast entry does not exists - cursor.execute("select coalesce(max(idActor),0) from actors") - actorid = cursor.fetchone()[0] + 1 - - query = "INSERT INTO actors(idActor, strActor) values(?, ?)" - cursor.execute(query, (actorid, name)) - self.logMsg("Add people to media, processing: %s" % name, 2) - - finally: - # Link person to content - if "Actor" in type: - role = person.get('Role') - - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkmovie( - idActor, idMovie, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinktvshow( - idActor, idShow, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkepisode( - idActor, idEpisode, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - else: return # Item is invalid - - cursor.execute(query, (actorid, kodiid, role, castorder)) - castorder += 1 - - elif "Director" in type: - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmovie( - idDirector, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinktvshow( - idDirector, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmusicvideo( - idDirector, idMVideo) - - VALUES (?, ?) - ''' - ) - - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkepisode( - idDirector, idEpisode) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - cursor.execute(query, (actorid, kodiid)) - - elif type in ("Writing", "Writer"): - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkmovie( - idWriter, idMovie) - - VALUES (?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkepisode( - idWriter, idEpisode) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - cursor.execute(query, (actorid, kodiid)) - - elif "Artist" in type: - query = ( - ''' - INSERT OR REPLACE INTO artistlinkmusicvideo( - idArtist, idMVideo) - - VALUES (?, ?) - ''' - ) - cursor.execute(query, (actorid, kodiid)) - - # Add person image to art table - if thumb: - arttype = type.lower() - - if "writing" in arttype: - arttype = "writer" - - artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", cursor) - - def addGenres(self, kodiid, genres, mediatype): - - cursor = self.cursor - - # Kodi Isengard, Jarvis, Krypton - if self.kodiversion in (15, 16, 17): - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM genre_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - cursor.execute(query, (kodiid, mediatype,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT genre_id", - "FROM genre", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (genre,)) - - try: - genre_id = 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 - - query = "INSERT INTO genre(genre_id, name) values(?, ?)" - cursor.execute(query, (genre_id, genre)) - self.logMsg("Add Genres to media, processing: %s" % genre, 2) - - finally: - # Assign genre to item - query = ( - ''' - INSERT OR REPLACE INTO genre_link( - genre_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - 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,)) - elif "tvshow" in mediatype: - cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) - elif "musicvideo" in mediatype: - cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (genre,)) - - try: - idGenre = cursor.fetchone()[0] - - except TypeError: - # Create genre in database - cursor.execute("select coalesce(max(idGenre),0) from genre") - idGenre = cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - cursor.execute(query, (idGenre, genre)) - self.logMsg("Add Genres to media, processing: %s" % genre, 2) - - finally: - # Assign genre to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmovie( - idGenre, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinktvshow( - idGenre, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmusicvideo( - idGenre, idMVideo) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - 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): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "SELECT studio_id", - "FROM studio", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (studio,)) - try: - studioid = cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - cursor.execute("select coalesce(max(studio_id),0) from studio") - studioid = cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(studio_id, name) values(?, ?)" - cursor.execute(query, (studioid, studio)) - self.logMsg("Add Studios to media, processing: %s" % studio, 2) - - finally: # Assign studio to item - query = ( - ''' - INSERT OR REPLACE INTO studio_link( - studio_id, media_id, media_type) - - VALUES (?, ?, ?) - ''') - cursor.execute(query, (studioid, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idstudio", - "FROM studio", - "WHERE strstudio = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (studio,)) - try: - studioid = cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - cursor.execute("select coalesce(max(idstudio),0) from studio") - studioid = cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" - cursor.execute(query, (studioid, studio)) - self.logMsg("Add Studios to media, processing: %s" % studio, 2) - - finally: # Assign studio to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) - VALUES (?, ?) - ''') - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) - VALUES (?, ?) - ''') - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) - VALUES (?, ?) - ''') - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) - VALUES (?, ?) - ''') - 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,)) - if streamdetails: - # Video details - for videotrack in streamdetails['video']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strVideoCodec, fVideoAspect, - iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - cursor.execute(query, (fileid, 0, videotrack['codec'], - videotrack['aspect'], videotrack['width'], videotrack['height'], - runtime ,videotrack['video3DFormat'])) - - # Audio details - for audiotrack in streamdetails['audio']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - cursor.execute(query, (fileid, 1, audiotrack['codec'], - audiotrack['channels'], audiotrack['language'])) - - # Subtitles details - for subtitletrack in streamdetails['subtitle']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strSubtitleLanguage) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (fileid, 2, subtitletrack)) - - 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,)) - - # Set watched count - query = ' '.join(( - - "UPDATE files", - "SET playCount = ?, lastPlayed = ?", - "WHERE idFile = ?" - )) - 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 - query = ( - ''' - INSERT INTO bookmark( - idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - 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): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - cursor.execute(query, (kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?" - )) - 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(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (tag,)) - try: - tag_id = cursor.fetchone()[0] - - except TypeError: - # Create the tag, because it does not exist - tag_id = self.createTag(tag) - self.logMsg("Adding tag: %s" % tag, 2) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO tag_link( - tag_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (tag_id, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (tag,)) - try: - tag_id = cursor.fetchone()[0] - - except TypeError: - # Create the tag - tag_id = self.createTag(tag) - self.logMsg("Adding tag: %s" % tag, 2) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO taglinks( - idTag, idMedia, media_type) - - VALUES (?, ?, ?) - ''' - ) - 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 - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (name,)) - try: - tag_id = cursor.fetchone()[0] - - except TypeError: - cursor.execute("select coalesce(max(tag_id),0) from tag") - tag_id = cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(tag_id, name) values(?, ?)" - cursor.execute(query, (tag_id, name)) - self.logMsg("Create tag_id: %s name: %s" % (tag_id, name), 2) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (name,)) - try: - tag_id = cursor.fetchone()[0] - - except TypeError: - cursor.execute("select coalesce(max(idTag),0) from tag") - tag_id = cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(idTag, strTag) values(?, ?)" - 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): - # Kodi Isengard, Jarvis, Krypton - try: - query = ' '.join(( - - "UPDATE tag_link", - "SET tag_id = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - 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 - self.logMsg("Exception: %s" % e, 1) - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - cursor.execute(query, (kodiid, mediatype, oldtag,)) - else: - # Kodi Helix - try: - query = ' '.join(( - - "UPDATE taglinks", - "SET idTag = ?", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - 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 - self.logMsg("Exception: %s" % e, 1) - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - 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(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (tagname,)) - try: - tag_id = cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - cursor.execute(query, (kodiid, mediatype, tag_id,)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (tagname,)) - try: - tag_id = cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - cursor.execute(query, (kodiid, mediatype, tag_id,)) - - def createBoxset(self, boxsetname): - - cursor = self.cursor - self.logMsg("Adding boxset: %s" % boxsetname, 2) - query = ' '.join(( - - "SELECT idSet", - "FROM sets", - "WHERE strSet = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (boxsetname,)) - try: - setid = cursor.fetchone()[0] - - except TypeError: - cursor.execute("select coalesce(max(idSet),0) from sets") - setid = cursor.fetchone()[0] + 1 - - query = "INSERT INTO sets(idSet, strSet) values(?, ?)" - cursor.execute(query, (setid, boxsetname)) - - return setid - - def assignBoxset(self, setid, movieid): - - query = ' '.join(( - - "UPDATE movie", - "SET idSet = ?", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (setid, movieid,)) - - def removefromBoxset(self, movieid): - - query = ' '.join(( - - "UPDATE movie", - "SET idSet = null", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (movieid,)) - - def addSeason(self, showid, seasonnumber): - - cursor = self.cursor - - query = ' '.join(( - - "SELECT idSeason", - "FROM seasons", - "WHERE idShow = ?", - "AND season = ?" - )) - cursor.execute(query, (showid, seasonnumber,)) - try: - seasonid = cursor.fetchone()[0] - except TypeError: - cursor.execute("select coalesce(max(idSeason),0) from seasons") - seasonid = cursor.fetchone()[0] + 1 - query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" - 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,)) - try: - result = cursor.fetchone() - artistid = result[0] - artistname = result[1] - - except TypeError: - - query = ' '.join(( - - "SELECT idArtist", - "FROM artist", - "WHERE strArtist = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (name,)) - try: - artistid = cursor.fetchone()[0] - except TypeError: - cursor.execute("select coalesce(max(idArtist),0) from artist") - artistid = cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) - - VALUES (?, ?, ?) - ''' - ) - cursor.execute(query, (artistid, name, musicbrainz)) - else: - if artistname != name: - query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - 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,)) - try: - albumid = 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): - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - cursor.execute(query, (albumid, name, musicbrainz, "album")) - else: # Helix - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) - - VALUES (?, ?, ?) - ''' - ) - 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 - query = ' '.join(( - - "DELETE FROM album_genre", - "WHERE idAlbum = ?" - )) - cursor.execute(query, (kodiid,)) - - for genre in genres: - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (genre,)) - try: - genreid = cursor.fetchone()[0] - except TypeError: - # Create the genre - cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - cursor.execute(query, (genreid, genre)) - - query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" - cursor.execute(query, (genreid, kodiid)) - - elif mediatype == "song": - - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM song_genre", - "WHERE idSong = ?" - )) - cursor.execute(query, (kodiid,)) - - for genre in genres: - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - cursor.execute(query, (genre,)) - try: - genreid = cursor.fetchone()[0] - except TypeError: - # Create the genre - cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - 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 +# -*- coding: utf-8 -*- + +################################################################################################## + +import xbmc + +import api +import artwork +import clientinfo +import utils + +################################################################################################## + + +class Kodidb_Functions(): + + kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) + + + def __init__(self, cursor): + + self.cursor = cursor + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.artwork = artwork.Artwork() + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def addPath(self, path): + + query = ' '.join(( + + "SELECT idPath", + "FROM path", + "WHERE strPath = ?" + )) + self.cursor.execute(query, (path,)) + try: + pathid = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute("select coalesce(max(idPath),0) from path") + pathid = self.cursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO path( + idPath, strPath) + + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (pathid, path)) + + return pathid + + def getPath(self, path): + + query = ' '.join(( + + "SELECT idPath", + "FROM path", + "WHERE strPath = ?" + )) + self.cursor.execute(query, (path,)) + try: + pathid = self.cursor.fetchone()[0] + except TypeError: + pathid = None + + return pathid + + def addFile(self, filename, pathid): + + query = ' '.join(( + + "SELECT idFile", + "FROM files", + "WHERE strFilename = ?", + "AND idPath = ?" + )) + self.cursor.execute(query, (filename, pathid,)) + try: + fileid = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute("select coalesce(max(idFile),0) from files") + fileid = self.cursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO files( + idFile, strFilename) + + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (fileid, filename)) + + return fileid + + def getFile(self, fileid): + + query = ' '.join(( + + "SELECT strFilename", + "FROM files", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (fileid,)) + try: + filename = self.cursor.fetchone()[0] + except TypeError: + filename = "" + + return filename + + def removeFile(self, path, filename): + + pathid = self.getPath(path) + + if pathid is not None: + query = ' '.join(( + + "DELETE FROM files", + "WHERE idPath = ?", + "AND strFilename = ?" + )) + self.cursor.execute(query, (pathid, filename,)) + + def addCountries(self, kodiid, countries, mediatype): + + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + for country in countries: + query = ' '.join(( + + "SELECT country_id", + "FROM country", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (country,)) + + try: + country_id = self.cursor.fetchone()[0] + + except TypeError: + # Country entry does not exists + 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(?, ?)" + self.cursor.execute(query, (country_id, country)) + self.logMsg("Add country to media, processing: %s" % country, 2) + + finally: # Assign country to content + query = ( + ''' + INSERT OR REPLACE INTO country_link( + country_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (country_id, kodiid, mediatype)) + else: + # Kodi Helix + for country in countries: + query = ' '.join(( + + "SELECT idCountry", + "FROM country", + "WHERE strCountry = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (country,)) + + try: + idCountry = self.cursor.fetchone()[0] + + except TypeError: + # Country entry does not exists + self.cursor.execute("select coalesce(max(idCountry),0) from country") + idCountry = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" + self.cursor.execute(query, (idCountry, country)) + self.logMsg("Add country to media, processing: %s" % country, 2) + + finally: + # Only movies have a country field + if "movie" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO countrylinkmovie( + idCountry, idMovie) + + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (idCountry, kodiid)) + + def addPeople(self, kodiid, people, mediatype): + + castorder = 1 + for person in people: + + name = person['Name'] + person_type = person['Type'] + thumb = person['imageurl'] + + # Kodi Isengard, Jarvis, Krypton + if self.kodiversion in (15, 16, 17): + query = ' '.join(( + + "SELECT actor_id", + "FROM actor", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + + try: + actorid = self.cursor.fetchone()[0] + + except TypeError: + # Cast entry does not exists + 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(?, ?)" + self.cursor.execute(query, (actorid, name)) + self.logMsg("Add people to media, processing: %s" % name, 2) + + finally: + # Link person to content + if "Actor" in person_type: + role = person.get('Role') + query = ( + ''' + INSERT OR REPLACE INTO actor_link( + actor_id, media_id, media_type, role, cast_order) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) + castorder += 1 + + elif "Director" in person_type: + query = ( + ''' + INSERT OR REPLACE INTO director_link( + actor_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (actorid, kodiid, mediatype)) + + elif person_type in ("Writing", "Writer"): + query = ( + ''' + INSERT OR REPLACE INTO writer_link( + actor_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (actorid, kodiid, mediatype)) + + elif "Artist" in person_type: + query = ( + ''' + INSERT OR REPLACE INTO actor_link( + actor_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (actorid, kodiid, mediatype)) + # Kodi Helix + else: + query = ' '.join(( + + "SELECT idActor", + "FROM actors", + "WHERE strActor = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + + try: + actorid = self.cursor.fetchone()[0] + + except TypeError: + # Cast entry does not exists + self.cursor.execute("select coalesce(max(idActor),0) from actors") + actorid = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO actors(idActor, strActor) values(?, ?)" + self.cursor.execute(query, (actorid, name)) + self.logMsg("Add people to media, processing: %s" % name, 2) + + finally: + # Link person to content + if "Actor" in person_type: + role = person.get('Role') + + if "movie" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO actorlinkmovie( + idActor, idMovie, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + elif "tvshow" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO actorlinktvshow( + idActor, idShow, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + elif "episode" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO actorlinkepisode( + idActor, idEpisode, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (actorid, kodiid, role, castorder)) + castorder += 1 + + elif "Director" in person_type: + if "movie" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO directorlinkmovie( + idDirector, idMovie) + + VALUES (?, ?) + ''' + ) + elif "tvshow" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO directorlinktvshow( + idDirector, idShow) + + VALUES (?, ?) + ''' + ) + elif "musicvideo" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO directorlinkmusicvideo( + idDirector, idMVideo) + + VALUES (?, ?) + ''' + ) + + elif "episode" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO directorlinkepisode( + idDirector, idEpisode) + + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (actorid, kodiid)) + + elif person_type in ("Writing", "Writer"): + if "movie" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO writerlinkmovie( + idWriter, idMovie) + + VALUES (?, ?) + ''' + ) + elif "episode" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO writerlinkepisode( + idWriter, idEpisode) + + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (actorid, kodiid)) + + elif "Artist" in person_type: + query = ( + ''' + INSERT OR REPLACE INTO artistlinkmusicvideo( + idArtist, idMVideo) + + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (actorid, kodiid)) + + # Add person image to art table + if thumb: + arttype = person_type.lower() + + if "writing" in arttype: + arttype = "writer" + + self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor) + + def addGenres(self, kodiid, genres, mediatype): + + + # Kodi Isengard, Jarvis, Krypton + if self.kodiversion in (15, 16, 17): + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM genre_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodiid, mediatype,)) + + # Add genres + for genre in genres: + + query = ' '.join(( + + "SELECT genre_id", + "FROM genre", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + + try: + genre_id = self.cursor.fetchone()[0] + + except TypeError: + # Create genre in database + 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(?, ?)" + self.cursor.execute(query, (genre_id, genre)) + self.logMsg("Add Genres to media, processing: %s" % genre, 2) + + finally: + # Assign genre to item + query = ( + ''' + INSERT OR REPLACE INTO genre_link( + genre_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (genre_id, kodiid, mediatype)) + else: + # Kodi Helix + # Delete current genres for clean slate + if "movie" in mediatype: + self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,)) + elif "tvshow" in mediatype: + self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) + elif "musicvideo" in mediatype: + self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) + + # Add genres + for genre in genres: + + query = ' '.join(( + + "SELECT idGenre", + "FROM genre", + "WHERE strGenre = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + + try: + idGenre = self.cursor.fetchone()[0] + + except TypeError: + # Create genre in database + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + idGenre = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" + 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: + query = ( + ''' + INSERT OR REPLACE into genrelinkmovie( + idGenre, idMovie) + + VALUES (?, ?) + ''' + ) + elif "tvshow" in mediatype: + query = ( + ''' + INSERT OR REPLACE into genrelinktvshow( + idGenre, idShow) + + VALUES (?, ?) + ''' + ) + elif "musicvideo" in mediatype: + query = ( + ''' + INSERT OR REPLACE into genrelinkmusicvideo( + idGenre, idMVideo) + + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (idGenre, kodiid)) + + def addStudios(self, kodiid, studios, mediatype): + + for studio in studios: + + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "SELECT studio_id", + "FROM studio", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (studio,)) + try: + studioid = self.cursor.fetchone()[0] + + except TypeError: + # Studio does not exists. + 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(?, ?)" + self.cursor.execute(query, (studioid, studio)) + self.logMsg("Add Studios to media, processing: %s" % studio, 2) + + finally: # Assign studio to item + query = ( + ''' + INSERT OR REPLACE INTO studio_link( + studio_id, media_id, media_type) + + VALUES (?, ?, ?) + ''') + self.cursor.execute(query, (studioid, kodiid, mediatype)) + else: + # Kodi Helix + query = ' '.join(( + + "SELECT idstudio", + "FROM studio", + "WHERE strstudio = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (studio,)) + try: + studioid = self.cursor.fetchone()[0] + + except TypeError: + # Studio does not exists. + self.cursor.execute("select coalesce(max(idstudio),0) from studio") + studioid = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" + 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: + query = ( + ''' + INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) + VALUES (?, ?) + ''') + elif "musicvideo" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) + VALUES (?, ?) + ''') + elif "tvshow" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) + VALUES (?, ?) + ''') + elif "episode" in mediatype: + query = ( + ''' + INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) + VALUES (?, ?) + ''') + self.cursor.execute(query, (studioid, kodiid)) + + def addStreams(self, fileid, streamdetails, runtime): + + # First remove any existing entries + self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) + if streamdetails: + # Video details + for videotrack in streamdetails['video']: + query = ( + ''' + INSERT INTO streamdetails( + idFile, iStreamType, strVideoCodec, fVideoAspect, + iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (fileid, 0, videotrack['codec'], + videotrack['aspect'], videotrack['width'], videotrack['height'], + runtime ,videotrack['video3DFormat'])) + + # Audio details + for audiotrack in streamdetails['audio']: + query = ( + ''' + INSERT INTO streamdetails( + idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (fileid, 1, audiotrack['codec'], + audiotrack['channels'], audiotrack['language'])) + + # Subtitles details + for subtitletrack in streamdetails['subtitle']: + query = ( + ''' + INSERT INTO streamdetails( + idFile, iStreamType, strSubtitleLanguage) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (fileid, 2, subtitletrack)) + + def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): + + # Delete existing resume point + query = ' '.join(( + + "DELETE FROM bookmark", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (fileid,)) + + # Set watched count + query = ' '.join(( + + "UPDATE files", + "SET playCount = ?, lastPlayed = ?", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (playcount, dateplayed, fileid)) + + # Set the resume bookmark + if resume_seconds: + self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") + bookmarkId = self.cursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO bookmark( + idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) + + VALUES (?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, + "DVDPlayer", 1)) + + def addTags(self, kodiid, tags, mediatype): + + # First, delete any existing tags associated to the id + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodiid, mediatype)) + else: + # Kodi Helix + query = ' '.join(( + + "DELETE FROM taglinks", + "WHERE idMedia = ?", + "AND media_type = ?" + )) + 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): + + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + # Create the tag, because it does not exist + tag_id = self.createTag(tag) + self.logMsg("Adding tag: %s" % tag, 2) + + finally: + # Assign tag to item + query = ( + ''' + INSERT OR REPLACE INTO tag_link( + tag_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (tag_id, kodiid, mediatype)) + else: + # Kodi Helix + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + # Create the tag + tag_id = self.createTag(tag) + self.logMsg("Adding tag: %s" % tag, 2) + + finally: + # Assign tag to item + query = ( + ''' + INSERT OR REPLACE INTO taglinks( + idTag, idMedia, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (tag_id, kodiid, mediatype)) + + def createTag(self, name): + + # This will create and return the tag_id + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + 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(?, ?)" + self.cursor.execute(query, (tag_id, name)) + self.logMsg("Create tag_id: %s name: %s" % (tag_id, name), 2) + else: + # Kodi Helix + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + self.cursor.execute("select coalesce(max(idTag),0) from tag") + tag_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO tag(idTag, strTag) values(?, ?)" + 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): + + self.logMsg("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid), 2) + + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + try: + query = ' '.join(( + + "UPDATE tag_link", + "SET tag_id = ?", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + 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 + self.logMsg("Exception: %s" % e, 1) + query = ' '.join(( + + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + self.cursor.execute(query, (kodiid, mediatype, oldtag,)) + else: + # Kodi Helix + try: + query = ' '.join(( + + "UPDATE taglinks", + "SET idTag = ?", + "WHERE idMedia = ?", + "AND media_type = ?", + "AND idTag = ?" + )) + 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 + self.logMsg("Exception: %s" % e, 1) + query = ' '.join(( + + "DELETE FROM taglinks", + "WHERE idMedia = ?", + "AND media_type = ?", + "AND idTag = ?" + )) + self.cursor.execute(query, (kodiid, mediatype, oldtag,)) + + def removeTag(self, kodiid, tagname, mediatype): + + if self.kodiversion in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tagname,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + else: + query = ' '.join(( + + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + self.cursor.execute(query, (kodiid, mediatype, tag_id,)) + else: + # Kodi Helix + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tagname,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + else: + query = ' '.join(( + + "DELETE FROM taglinks", + "WHERE idMedia = ?", + "AND media_type = ?", + "AND idTag = ?" + )) + self.cursor.execute(query, (kodiid, mediatype, tag_id,)) + + def createBoxset(self, boxsetname): + + self.logMsg("Adding boxset: %s" % boxsetname, 2) + query = ' '.join(( + + "SELECT idSet", + "FROM sets", + "WHERE strSet = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (boxsetname,)) + try: + setid = self.cursor.fetchone()[0] + + except TypeError: + self.cursor.execute("select coalesce(max(idSet),0) from sets") + setid = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO sets(idSet, strSet) values(?, ?)" + self.cursor.execute(query, (setid, boxsetname)) + + return setid + + def assignBoxset(self, setid, movieid): + + query = ' '.join(( + + "UPDATE movie", + "SET idSet = ?", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (setid, movieid,)) + + def removefromBoxset(self, movieid): + + query = ' '.join(( + + "UPDATE movie", + "SET idSet = null", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (movieid,)) + + def addSeason(self, showid, seasonnumber): + + query = ' '.join(( + + "SELECT idSeason", + "FROM seasons", + "WHERE idShow = ?", + "AND season = ?" + )) + self.cursor.execute(query, (showid, seasonnumber,)) + try: + seasonid = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute("select coalesce(max(idSeason),0) from seasons") + seasonid = self.cursor.fetchone()[0] + 1 + query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" + self.cursor.execute(query, (seasonid, showid, seasonnumber)) + + return seasonid + + def addArtist(self, name, musicbrainz): + + query = ' '.join(( + + "SELECT idArtist, strArtist", + "FROM artist", + "WHERE strMusicBrainzArtistID = ?" + )) + self.cursor.execute(query, (musicbrainz,)) + try: + result = self.cursor.fetchone() + artistid = result[0] + artistname = result[1] + + except TypeError: + + query = ' '.join(( + + "SELECT idArtist", + "FROM artist", + "WHERE strArtist = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + try: + artistid = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute("select coalesce(max(idArtist),0) from artist") + artistid = self.cursor.fetchone()[0] + 1 + query = ( + ''' + INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (artistid, name, musicbrainz)) + else: + if artistname != name: + query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" + self.cursor.execute(query, (name, artistid,)) + + return artistid + + def addAlbum(self, name, musicbrainz): + + query = ' '.join(( + + "SELECT idAlbum", + "FROM album", + "WHERE strMusicBrainzAlbumID = ?" + )) + self.cursor.execute(query, (musicbrainz,)) + try: + albumid = self.cursor.fetchone()[0] + except TypeError: + # Create the album + 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) + + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (albumid, name, musicbrainz, "album")) + else: # Helix + query = ( + ''' + INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (albumid, name, musicbrainz)) + + return albumid + + def addMusicGenres(self, kodiid, genres, mediatype): + + if mediatype == "album": + + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM album_genre", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (kodiid,)) + + for genre in genres: + query = ' '.join(( + + "SELECT idGenre", + "FROM genre", + "WHERE strGenre = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + try: + genreid = self.cursor.fetchone()[0] + except TypeError: + # Create the genre + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + genreid = self.cursor.fetchone()[0] + 1 + query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" + self.cursor.execute(query, (genreid, genre)) + + query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" + self.cursor.execute(query, (genreid, kodiid)) + + elif mediatype == "song": + + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM song_genre", + "WHERE idSong = ?" + )) + self.cursor.execute(query, (kodiid,)) + + for genre in genres: + query = ' '.join(( + + "SELECT idGenre", + "FROM genre", + "WHERE strGenre = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + try: + genreid = self.cursor.fetchone()[0] + except TypeError: + # Create the genre + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + genreid = self.cursor.fetchone()[0] + 1 + query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" + self.cursor.execute(query, (genreid, genre)) + + query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" + 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 f00423c1..f2b5ae86 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -1,209 +1,209 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json - -import xbmc -import xbmcgui - -import clientinfo -import downloadutils -import embydb_functions as embydb -import playbackutils as pbutils -import utils - -################################################################################################# - - -class KodiMonitor(xbmc.Monitor): - - - def __init__(self): - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils() - - self.logMsg("Kodi monitor started.", 1) - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def onScanStarted(self, library): - self.logMsg("Kodi library scan %s running." % library, 2) - if library == "video": - utils.window('emby_kodiScan', value="true") - - def onScanFinished(self, library): - self.logMsg("Kodi library scan %s finished." % library, 2) - if library == "video": - utils.window('emby_kodiScan', clear=True) - - def onSettingsChanged(self): - # Monitor emby settings - # Review reset setting at a later time, need to be adjusted to account for initial setup - # changes. - '''currentPath = utils.settings('useDirectPaths') - if utils.window('emby_pluginpath') != currentPath: - # Plugin path value changed. Offer to reset - self.logMsg("Changed to playback mode detected", 1) - utils.window('emby_pluginpath', value=currentPath) - resp = xbmcgui.Dialog().yesno( - heading="Playback mode change detected", - line1=( - "Detected the playback mode has changed. The database " - "needs to be recreated for the change to be applied. " - "Proceed?")) - if resp: - utils.reset()''' - - currentLog = utils.settings('logLevel') - if utils.window('emby_logLevel') != currentLog: - # The log level changed, set new prop - self.logMsg("New log level: %s" % currentLog, 1) - utils.window('emby_logLevel', value=currentLog) - - def onNotification(self, sender, method, data): - - doUtils = self.doUtils - if method not in ("Playlist.OnAdd"): - self.logMsg("Method: %s Data: %s" % (method, data), 1) - - if data: - data = json.loads(data,'utf-8') - - - if method == "Player.OnPlay": - # Set up report progress for emby playback - item = data.get('item') - try: - kodiid = item['id'] - type = item['type'] - except (KeyError, TypeError): - self.logMsg("Item is invalid for playstate update.", 1) - else: - if ((utils.settings('useDirectPaths') == "1" and not type == "song") or - (type == "song" and utils.settings('enableMusic') == "true")): - # Set up properties for player - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - self.logMsg("No kodiid returned.", 1) - else: - url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid - result = doUtils.downloadUrl(url) - self.logMsg("Item: %s" % result, 2) - - playurl = None - count = 0 - while not playurl and count < 2: - try: - playurl = xbmc.Player().getPlayingFile() - except RuntimeError: - count += 1 - xbmc.sleep(200) - else: - listItem = xbmcgui.ListItem() - playback = pbutils.PlaybackUtils(result) - - if type == "song" and utils.settings('streamMusic') == "true": - utils.window('emby_%s.playmethod' % playurl, - value="DirectStream") - else: - utils.window('emby_%s.playmethod' % playurl, - value="DirectPlay") - # Set properties for player.py - playback.setProperties(playurl, listItem) - finally: - embycursor.close() - - - elif method == "VideoLibrary.OnUpdate": - # Manually marking as watched/unwatched - playcount = data.get('playcount') - item = data.get('item') - try: - kodiid = item['id'] - type = item['type'] - except (KeyError, TypeError): - self.logMsg("Item is invalid for playstate update.", 1) - else: - # Send notification to the server. - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - self.logMsg("Could not find itemid in emby database.", 1) - else: - # Stop from manually marking as watched unwatched, with actual playback. - if utils.window('emby_skipWatched%s' % itemid) == "true": - # property is set in player.py - utils.window('emby_skipWatched%s' % itemid, clear=True) - else: - # notify the server - url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid - if playcount != 0: - doUtils.downloadUrl(url, type="POST") - self.logMsg("Mark as watched for itemid: %s" % itemid, 1) - else: - doUtils.downloadUrl(url, type="DELETE") - self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1) - finally: - embycursor.close() - - - elif method == "VideoLibrary.OnRemove": - # Removed function, because with plugin paths + clean library, it will wipe - # entire library if user has permissions. Instead, use the emby context menu available - # in Isengard and higher version - pass - '''try: - kodiid = data['id'] - type = data['type'] - except (KeyError, TypeError): - self.logMsg("Item is invalid for emby deletion.", 1) - else: - # Send the delete action to the server. - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - self.logMsg("Could not find itemid in emby database.", 1) - else: - if utils.settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1="Delete file on Emby Server?") - if not resp: - self.logMsg("User skipped deletion.", 1) - embycursor.close() - return - - url = "{server}/emby/Items/%s?format=json" % itemid - self.logMsg("Deleting request: %s" % itemid) - doUtils.downloadUrl(url, type="DELETE") - finally: - embycursor.close()''' - - - elif method == "System.OnWake": - # Allow network to wake up - xbmc.sleep(10000) - utils.window('emby_onWake', value="true") - - elif method == "Playlist.OnClear": +# -*- coding: utf-8 -*- + +################################################################################################# + +import json + +import xbmc +import xbmcgui + +import clientinfo +import downloadutils +import embydb_functions as embydb +import playbackutils as pbutils +import utils + +################################################################################################# + + +class KodiMonitor(xbmc.Monitor): + + + def __init__(self): + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils() + + self.logMsg("Kodi monitor started.", 1) + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def onScanStarted(self, library): + self.logMsg("Kodi library scan %s running." % library, 2) + if library == "video": + utils.window('emby_kodiScan', value="true") + + def onScanFinished(self, library): + self.logMsg("Kodi library scan %s finished." % library, 2) + if library == "video": + utils.window('emby_kodiScan', clear=True) + + def onSettingsChanged(self): + # Monitor emby settings + # Review reset setting at a later time, need to be adjusted to account for initial setup + # changes. + '''currentPath = utils.settings('useDirectPaths') + if utils.window('emby_pluginpath') != currentPath: + # Plugin path value changed. Offer to reset + self.logMsg("Changed to playback mode detected", 1) + utils.window('emby_pluginpath', value=currentPath) + resp = xbmcgui.Dialog().yesno( + heading="Playback mode change detected", + line1=( + "Detected the playback mode has changed. The database " + "needs to be recreated for the change to be applied. " + "Proceed?")) + if resp: + utils.reset()''' + + currentLog = utils.settings('logLevel') + if utils.window('emby_logLevel') != currentLog: + # The log level changed, set new prop + self.logMsg("New log level: %s" % currentLog, 1) + utils.window('emby_logLevel', value=currentLog) + + def onNotification(self, sender, method, data): + + doUtils = self.doUtils + if method not in ("Playlist.OnAdd"): + self.logMsg("Method: %s Data: %s" % (method, data), 1) + + if data: + data = json.loads(data,'utf-8') + + + if method == "Player.OnPlay": + # Set up report progress for emby playback + item = data.get('item') + try: + kodiid = item['id'] + item_type = item['type'] + except (KeyError, TypeError): + self.logMsg("Item is invalid for playstate update.", 1) + else: + if ((utils.settings('useDirectPaths') == "1" and not item_type == "song") or + (item_type == "song" and utils.settings('enableMusic') == "true")): + # Set up properties for player + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) + try: + itemid = emby_dbitem[0] + except TypeError: + self.logMsg("No kodiid returned.", 1) + else: + url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid + result = doUtils.downloadUrl(url) + self.logMsg("Item: %s" % result, 2) + + playurl = None + count = 0 + while not playurl and count < 2: + try: + playurl = xbmc.Player().getPlayingFile() + except RuntimeError: + count += 1 + xbmc.sleep(200) + else: + listItem = xbmcgui.ListItem() + playback = pbutils.PlaybackUtils(result) + + if item_type == "song" and utils.settings('streamMusic') == "true": + utils.window('emby_%s.playmethod' % playurl, + value="DirectStream") + else: + utils.window('emby_%s.playmethod' % playurl, + value="DirectPlay") + # Set properties for player.py + playback.setProperties(playurl, listItem) + finally: + embycursor.close() + + + elif method == "VideoLibrary.OnUpdate": + # Manually marking as watched/unwatched + playcount = data.get('playcount') + item = data.get('item') + try: + kodiid = item['id'] + item_type = item['type'] + except (KeyError, TypeError): + self.logMsg("Item is invalid for playstate update.", 1) + else: + # Send notification to the server. + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) + try: + itemid = emby_dbitem[0] + except TypeError: + self.logMsg("Could not find itemid in emby database.", 1) + else: + # Stop from manually marking as watched unwatched, with actual playback. + if utils.window('emby_skipWatched%s' % itemid) == "true": + # property is set in player.py + utils.window('emby_skipWatched%s' % itemid, clear=True) + else: + # notify the server + url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid + if playcount != 0: + doUtils.downloadUrl(url, action_type="POST") + self.logMsg("Mark as watched for itemid: %s" % itemid, 1) + else: + doUtils.downloadUrl(url, action_type="DELETE") + self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1) + finally: + embycursor.close() + + + elif method == "VideoLibrary.OnRemove": + # Removed function, because with plugin paths + clean library, it will wipe + # entire library if user has permissions. Instead, use the emby context menu available + # in Isengard and higher version + pass + '''try: + kodiid = data['id'] + type = data['type'] + except (KeyError, TypeError): + self.logMsg("Item is invalid for emby deletion.", 1) + else: + # Send the delete action to the server. + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) + try: + itemid = emby_dbitem[0] + except TypeError: + self.logMsg("Could not find itemid in emby database.", 1) + else: + if utils.settings('skipContextMenu') != "true": + resp = xbmcgui.Dialog().yesno( + heading="Confirm delete", + line1="Delete file on Emby Server?") + if not resp: + self.logMsg("User skipped deletion.", 1) + embycursor.close() + return + + url = "{server}/emby/Items/%s?format=json" % itemid + self.logMsg("Deleting request: %s" % itemid) + doUtils.downloadUrl(url, action_type="DELETE") + finally: + embycursor.close()''' + + + elif method == "System.OnWake": + # Allow network to wake up + xbmc.sleep(10000) + utils.window('emby_onWake', value="true") + + elif method == "Playlist.OnClear": pass \ No newline at end of file diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index dacebbde..d3a441dd 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1,1549 +1,1480 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import sqlite3 -import threading -from datetime import datetime, timedelta, time - -import xbmc -import xbmcgui -import xbmcvfs - -import api -import utils -import clientinfo -import downloadutils -import itemtypes -import embydb_functions as embydb -import kodidb_functions as kodidb -import read_embyserver as embyserver -import userclient -import videonodes - -################################################################################################## - - -class LibrarySync(threading.Thread): - - _shared_state = {} - - stop_thread = False - suspend_thread = False - - # Track websocketclient updates - addedItems = [] - updateItems = [] - userdataItems = [] - removeItems = [] - forceLibraryUpdate = False - refresh_views = False - - - def __init__(self): - - self.__dict__ = self._shared_state - self.monitor = xbmc.Monitor() - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.user = userclient.UserClient() - self.emby = embyserver.Read_EmbyServer() - self.vnodes = videonodes.VideoNodes() - - threading.Thread.__init__(self) - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def progressDialog(self, title, forced=False): - - dialog = None - - if utils.settings('dbSyncIndicator') == "true" or forced: - dialog = xbmcgui.DialogProgressBG() - dialog.create("Emby for Kodi", title) - self.logMsg("Show progress dialog: %s" % title, 2) - - return dialog - - def startSync(self): - - settings = utils.settings - # Run at start up - optional to use the server plugin - if settings('SyncInstallRunDone') == "true": - - # Validate views - self.refreshViews() - completed = False - # Verify if server plugin is installed. - if settings('serverSync') == "true": - # Try to use fast start up - url = "{server}/emby/Plugins?format=json" - result = self.doUtils(url) - - for plugin in result: - if plugin['Name'] == "Emby.Kodi Sync Queue": - self.logMsg("Found server plugin.", 2) - completed = self.fastSync() - - if not completed: - # Fast sync failed or server plugin is not found - completed = ManualSync().sync() - else: - # Install sync is not completed - completed = self.fullSync() - - return completed - - def fastSync(self): - - log = self.logMsg - - doUtils = self.doUtils - - lastSync = utils.settings('LastIncrementalSync') - if not lastSync: - lastSync = "2010-01-01T00:00:00Z" - - lastSyncTime = utils.convertdate(lastSync) - log("Last sync run: %s" % lastSyncTime, 1) - - # get server RetentionDateTime - url = "{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json" - result = doUtils(url) - retention_time = "2010-01-01T00:00:00Z" - if result and result.get('RetentionDateTime'): - retention_time = result['RetentionDateTime'] - - #Try/except equivalent - ''' - try: - retention_time = result['RetentionDateTime'] - except (TypeError, KeyError): - retention_time = "2010-01-01T00:00:00Z" - ''' - - retention_time = utils.convertdate(retention_time) - log("RetentionDateTime: %s" % retention_time, 1) - - # if last sync before retention time do a full sync - if retention_time > lastSyncTime: - log("Fast sync server retention insufficient, fall back to full sync", 1) - return False - - url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json" - params = {'LastUpdateDT': lastSync} - result = doUtils(url, parameters=params) - - try: - processlist = { - - 'added': result['ItemsAdded'], - 'update': result['ItemsUpdated'], - 'userdata': result['UserDataChanged'], - 'remove': result['ItemsRemoved'] - } - - except (KeyError, TypeError): - log("Failed to retrieve latest updates using fast sync.", 1) - return False - - else: - log("Fast sync changes: %s" % result, 1) - for action in processlist: - self.triage_items(action, processlist[action]) - - return True - - def saveLastSync(self): - - log = self.logMsg - # Save last sync time - overlap = 2 - - url = "{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json" - result = self.doUtils(url) - try: # datetime fails when used more than once, TypeError - server_time = result['ServerDateTime'] - server_time = utils.convertdate(server_time) - - except Exception as e: - # If the server plugin is not installed or an error happened. - log("An exception occurred: %s" % e, 1) - time_now = datetime.utcnow()-timedelta(minutes=overlap) - lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') - log("New sync time: client time -%s min: %s" % (overlap, lastSync), 1) - - else: - lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ') - log("New sync time: server time -%s min: %s" % (overlap, lastSync), 1) - - finally: - utils.settings('LastIncrementalSync', value=lastSync) - - def shouldStop(self): - # Checkpoint during the syncing process - if self.monitor.abortRequested(): - return True - elif utils.window('emby_shouldStop') == "true": - return True - else: # Keep going - return False - - def dbCommit(self, connection): - - log = self.logMsg - window = utils.window - # Central commit, verifies if Kodi database update is running - kodidb_scan = window('emby_kodiScan') == "true" - - while kodidb_scan: - - log("Kodi scan is running. Waiting...", 1) - kodidb_scan = window('emby_kodiScan') == "true" - - if self.shouldStop(): - log("Commit unsuccessful. Sync terminated.", 1) - break - - if self.monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - log("Commit unsuccessful.", 1) - break - else: - connection.commit() - log("Commit successful.", 1) - - def fullSync(self, manualrun=False, repair=False, forceddialog=False): - - log = self.logMsg - window = utils.window - settings = utils.settings - # Only run once when first setting up. Can be run manually. - emby = self.emby - music_enabled = utils.settings('enableMusic') == "true" - - xbmc.executebuiltin('InhibitIdleShutdown(true)') - screensaver = utils.getScreensaver() - utils.setScreensaver(value="") - window('emby_dbScan', value="true") - # Add sources - utils.sourcesXML() - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - # Create the tables for the emby database - # emby, view, version - embycursor.execute( - """CREATE TABLE IF NOT EXISTS emby( - emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, - kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""") - embycursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") - embyconn.commit() - - # content sync: movies, tvshows, musicvideos, music - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - - if manualrun: - message = "Manual sync" - elif repair: - message = "Repair sync" - forceddialog = True - else: - message = "Initial sync" - forceddialog = True - window('emby_initialScan', value="true") - - pDialog = self.progressDialog("%s" % message, forced=forceddialog) - starttotal = datetime.now() - - # Set views - self.maintainViews(embycursor, kodicursor) - embyconn.commit() - - # Sync video library - process = { - - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows - } - for itemtype in process: - startTime = datetime.now() - completed = process[itemtype](embycursor, kodicursor, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - kodicursor.close() - return False - else: - self.dbCommit(kodiconn) - embyconn.commit() - elapsedTime = datetime.now() - startTime - log("SyncDatabase (finished %s in: %s)" - % (itemtype, str(elapsedTime).split('.')[0]), 1) - else: - # Close the Kodi cursor - kodicursor.close() - - # sync music - if music_enabled: - - musicconn = utils.kodiSQL('music') - musiccursor = musicconn.cursor() - - startTime = datetime.now() - completed = self.music(embycursor, musiccursor, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - musiccursor.close() - return False - else: - musicconn.commit() - embyconn.commit() - elapsedTime = datetime.now() - startTime - log("SyncDatabase (finished music in: %s)" - % (str(elapsedTime).split('.')[0]), 1) - musiccursor.close() - - if pDialog: - pDialog.close() - - embycursor.close() - - settings('SyncInstallRunDone', value="true") - settings("dbCreatedWithVersion", self.clientInfo.getVersion()) - self.saveLastSync() - xbmc.executebuiltin('UpdateLibrary(video)') - elapsedtotal = datetime.now() - starttotal - - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - window('emby_initialScan', clear=True) - if forceddialog: - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message="%s %s %s" % - (message, utils.language(33025), str(elapsedtotal).split('.')[0]), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - return True - - - def refreshViews(self): - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - - # Compare views, assign correct tags to items - self.maintainViews(embycursor, kodicursor) - - self.dbCommit(kodiconn) - kodicursor.close() - - embyconn.commit() - embycursor.close() - - def maintainViews(self, embycursor, kodicursor): - - log = self.logMsg - # Compare the views to emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - kodi_db = kodidb.Kodidb_Functions(kodicursor) - doUtils = self.doUtils - vnodes = self.vnodes - - # Get views - url = "{server}/emby/Users/{UserId}/Views?format=json" - result = doUtils(url) - grouped_views = result['Items'] - ordered_views = emby.getViews(sortedlist=True) - all_views = [] - sorted_views = [] - for view in ordered_views: - all_views.append(view['name']) - if view['type'] == "music": - continue - - if view['type'] == "mixed": - sorted_views.append(view['name']) - sorted_views.append(view['name']) - log("Sorted views: %s" % sorted_views, 1) - - # total nodes for window properties - vnodes.clearProperties() - totalnodes = len(sorted_views) + 0 - - current_views = emby_db.getViews() - # Set views for supported media type - emby_mediatypes = { - - 'movies': "Movie", - 'tvshows': "Series", - 'musicvideos': "MusicVideo", - 'homevideos': "Video", - 'music': "Audio", - 'photos': "Photo" - } - mediatypes = ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos'] - for mediatype in mediatypes: - - nodes = [] # Prevent duplicate for nodes of the same type - playlists = [] # Prevent duplicate for playlists of the same type - # Get media folders from server - folders = emby.getViews(mediatype, root=True) - for folder in folders: - - folderid = folder['id'] - foldername = folder['name'] - viewtype = folder['type'] - - if foldername not in all_views: - # Media folders are grouped into userview - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - 'ParentId': folderid, - 'Recursive': True, - 'Limit': 1, - 'IncludeItemTypes': emby_mediatypes[mediatype] - } # Get one item from server using the folderid - result = doUtils(url, parameters=params) - try: - verifyitem = result['Items'][0]['Id'] - except (TypeError, IndexError): - # Something is wrong. Keep the same folder name. - # Could be the view is empty or the connection - pass - else: - for grouped_view in grouped_views: - # This is only reserved for the detection of grouped views - if (grouped_view['Type'] == "UserView" and - grouped_view.get('CollectionType') == mediatype): - # Take the userview, and validate the item belong to the view - if emby.verifyView(grouped_view['Id'], verifyitem): - # Take the name of the userview - log("Found corresponding view: %s %s" - % (grouped_view['Name'], grouped_view['Id']), 1) - foldername = grouped_view['Name'] - break - else: - # Unable to find a match, add the name to our sorted_view list - sorted_views.append(foldername) - log("Couldn't find corresponding grouped view: %s" % sorted_views, 1) - - # Failsafe - try: - sorted_views.index(foldername) - except ValueError: - sorted_views.append(foldername) - - # Get current media folders from emby database - view = emby_db.getView_byId(folderid) - try: - current_viewname = view[0] - current_viewtype = view[1] - current_tagid = view[2] - - except TypeError: - log("Creating viewid: %s in Emby database." % folderid, 1) - tagid = kodi_db.createTag(foldername) - # Create playlist for the video library - if (foldername not in playlists and - mediatype in ('movies', 'tvshows', 'musicvideos')): - utils.playlistXSP(mediatype, foldername, folderid, viewtype) - playlists.append(foldername) - # Create the video node - if foldername not in nodes and mediatype not in ("musicvideos", "music"): - vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, - viewtype, folderid) - if viewtype == "mixed": # Change the value - sorted_views[sorted_views.index(foldername)] = "%ss" % foldername - nodes.append(foldername) - totalnodes += 1 - # Add view to emby database - emby_db.addView(folderid, foldername, viewtype, tagid) - - else: - log(' '.join(( - - "Found viewid: %s" % folderid, - "viewname: %s" % current_viewname, - "viewtype: %s" % current_viewtype, - "tagid: %s" % current_tagid)), 2) - - # View is still valid - try: - current_views.remove(folderid) - except ValueError: - # View was just created, nothing to remove - pass - - # View was modified, update with latest info - if current_viewname != foldername: - log("viewid: %s new viewname: %s" % (folderid, foldername), 1) - tagid = kodi_db.createTag(foldername) - - # Update view with new info - emby_db.updateView(foldername, tagid, folderid) - - if mediatype != "music": - if emby_db.getView_byName(current_viewname) is None: - # The tag could be a combined view. Ensure there's no other tags - # with the same name before deleting playlist. - utils.playlistXSP( - mediatype, current_viewname, folderid, current_viewtype, True) - # Delete video node - if mediatype != "musicvideos": - vnodes.viewNode( - indexnumber=None, - tagname=current_viewname, - mediatype=mediatype, - viewtype=current_viewtype, - viewid=folderid, - delete=True) - # Added new playlist - if (foldername not in playlists and - mediatype in ('movies', 'tvshows', 'musicvideos')): - utils.playlistXSP(mediatype, foldername, folderid, viewtype) - playlists.append(foldername) - # Add new video node - if foldername not in nodes and mediatype != "musicvideos": - vnodes.viewNode(sorted_views.index(foldername), foldername, - mediatype, viewtype, folderid) - if viewtype == "mixed": # Change the value - sorted_views[sorted_views.index(foldername)] = "%ss" % foldername - nodes.append(foldername) - totalnodes += 1 - - # Update items with new tag - items = emby_db.getItem_byView(folderid) - for item in items: - # Remove the "s" from viewtype for tags - kodi_db.updateTag( - current_tagid, tagid, item[0], current_viewtype[:-1]) - else: - # Validate the playlist exists or recreate it - if mediatype != "music": - if (foldername not in playlists and - mediatype in ('movies', 'tvshows', 'musicvideos')): - utils.playlistXSP(mediatype, foldername, folderid, viewtype) - playlists.append(foldername) - # Create the video node if not already exists - if foldername not in nodes and mediatype != "musicvideos": - vnodes.viewNode(sorted_views.index(foldername), foldername, - mediatype, viewtype, folderid) - if viewtype == "mixed": # Change the value - sorted_views[sorted_views.index(foldername)] = "%ss" % foldername - nodes.append(foldername) - totalnodes += 1 - else: - # Add video nodes listings - vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites") - totalnodes += 1 - vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites") - totalnodes += 1 - vnodes.singleNode(totalnodes, "channels", "movies", "channels") - totalnodes += 1 - # Save total - utils.window('Emby.nodes.total', str(totalnodes)) - - # Remove any old referenced views - log("Removing views: %s" % current_views, 1) - for view in current_views: - emby_db.removeView(view) - - def movies(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - lang = utils.language - # Get movies from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - movies = itemtypes.Movies(embycursor, kodicursor) - - views = emby_db.getView_byType('movies') - views += emby_db.getView_byType('mixed') - log("Media folders: %s" % views, 1) - - ##### PROCESS MOVIES ##### - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (lang(33017), viewName)) - - # Initial or repair sync - all_embymovies = emby.getMovies(viewId, dialog=pdialog) - total = all_embymovies['TotalRecordCount'] - embymovies = all_embymovies['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymovie in embymovies: - # Process individual movies - if self.shouldStop(): - return False - - title = embymovie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_update(embymovie, viewName, viewId) - else: - log("Movies finished.", 2) - - - ##### PROCESS BOXSETS ##### - if pdialog: - pdialog.update(heading="Emby for Kodi", message=lang(33018)) - - boxsets = emby.getBoxset(dialog=pdialog) - total = boxsets['TotalRecordCount'] - embyboxsets = boxsets['Items'] - - if pdialog: - pdialog.update(heading="Processing Boxsets / %s items" % total) - - count = 0 - for boxset in embyboxsets: - # Process individual boxset - if self.shouldStop(): - return False - - title = boxset['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_updateBoxset(boxset) - else: - log("Boxsets finished.", 2) - - return True - - def musicvideos(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - # Get musicvideos from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - mvideos = itemtypes.MusicVideos(embycursor, kodicursor) - - views = emby_db.getView_byType('musicvideos') - log("Media folders: %s" % views, 1) - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (utils.language(33019), viewName)) - - # Initial or repair sync - all_embymvideos = emby.getMusicVideos(viewId, dialog=pdialog) - total = all_embymvideos['TotalRecordCount'] - embymvideos = all_embymvideos['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymvideo in embymvideos: - # Process individual musicvideo - if self.shouldStop(): - return False - - title = embymvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - mvideos.add_update(embymvideo, viewName, viewId) - else: - log("MusicVideos finished.", 2) - - return True - - def tvshows(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - # Get shows from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - tvshows = itemtypes.TVShows(embycursor, kodicursor) - - views = emby_db.getView_byType('tvshows') - views += emby_db.getView_byType('mixed') - log("Media folders: %s" % views, 1) - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (utils.language(33020), viewName)) - - all_embytvshows = emby.getShows(viewId, dialog=pdialog) - total = all_embytvshows['TotalRecordCount'] - embytvshows = all_embytvshows['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.shouldStop(): - return False - - itemid = embytvshow['Id'] - title = embytvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_update(embytvshow, viewName, viewId) - - # Process episodes - all_episodes = emby.getEpisodesbyShow(itemid) - for episode in all_episodes['Items']: - - # Process individual show - if self.shouldStop(): - return False - - episodetitle = episode['Name'] - if pdialog: - pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) - tvshows.add_updateEpisode(episode) - else: - log("TVShows finished.", 2) - - return True - - def music(self, embycursor, kodicursor, pdialog): - # Get music from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - music = itemtypes.Music(embycursor, kodicursor) - - process = { - - 'artists': [emby.getArtists, music.add_updateArtist], - 'albums': [emby.getAlbums, music.add_updateAlbum], - 'songs': [emby.getSongs, music.add_updateSong] - } - types = ['artists', 'albums', 'songs'] - for itemtype in types: - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (utils.language(33021), itemtype)) - - all_embyitems = process[itemtype][0](dialog=pdialog) - total = all_embyitems['TotalRecordCount'] - embyitems = all_embyitems['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (itemtype, total)) - - count = 0 - for embyitem in embyitems: - # Process individual item - if self.shouldStop(): - return False - - title = embyitem['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - process[itemtype][1](embyitem) - else: - self.logMsg("%s finished." % itemtype, 2) - - return True - - # Reserved for websocket_client.py and fast start - def triage_items(self, process, items): - - processlist = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - if items: - if process == "userdata": - itemids = [] - for item in items: - itemids.append(item['ItemId']) - items = itemids - - self.logMsg("Queue %s: %s" % (process, items), 1) - processlist[process].extend(items) - - def incrementalSync(self): - - log = self.logMsg - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - pDialog = None - update_embydb = False - - if self.refresh_views: - # Received userconfig update - self.refresh_views = False - self.maintainViews(embycursor, kodicursor) - self.forceLibraryUpdate = True - update_embydb = True - - if self.addedItems or self.updateItems or self.userdataItems or self.removeItems: - # Only present dialog if we are going to process items - pDialog = self.progressDialog('Incremental sync') - - - process = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - types = ['added', 'update', 'userdata', 'remove'] - for type in types: - - if process[type] and utils.window('emby_kodiScan') != "true": - - listItems = list(process[type]) - del process[type][:] # Reset class list - - items_process = itemtypes.Items(embycursor, kodicursor) - update = False - - # Prepare items according to process type - if type == "added": - items = emby.sortby_mediatype(listItems) - - elif type in ("userdata", "remove"): - items = emby_db.sortby_mediaType(listItems, unsorted=False) - - else: - items = emby_db.sortby_mediaType(listItems) - if items.get('Unsorted'): - sorted_items = emby.sortby_mediatype(items['Unsorted']) - doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) - if doupdate: - embyupdate, kodiupdate_video = doupdate - if embyupdate: - update_embydb = True - if kodiupdate_video: - self.forceLibraryUpdate = True - del items['Unsorted'] - - doupdate = items_process.itemsbyId(items, type, pDialog) - if doupdate: - embyupdate, kodiupdate_video = doupdate - if embyupdate: - update_embydb = True - if kodiupdate_video: - self.forceLibraryUpdate = True - - if update_embydb: - update_embydb = False - log("Updating emby database.", 1) - embyconn.commit() - self.saveLastSync() - - if self.forceLibraryUpdate: - # Force update the Kodi library - self.forceLibraryUpdate = False - self.dbCommit(kodiconn) - - log("Updating video library.", 1) - utils.window('emby_kodiScan', value="true") - xbmc.executebuiltin('UpdateLibrary(video)') - - if pDialog: - pDialog.close() - - kodicursor.close() - embycursor.close() - - - def compareDBVersion(self, current, minimum): - # It returns True is database is up to date. False otherwise. - self.logMsg("current: %s minimum: %s" % (current, minimum), 1) - currMajor, currMinor, currPatch = current.split(".") - minMajor, minMinor, minPatch = minimum.split(".") - - if currMajor > minMajor: - return True - elif currMajor == minMajor and (currMinor > minMinor or - (currMinor == minMinor and currPatch >= minPatch)): - return True - else: - # Database out of date. - return False - - def run(self): - - try: - self.run_internal() - except Exception as e: - utils.window('emby_dbScan', clear=True) - xbmcgui.Dialog().ok( - heading="Emby for Kodi", - line1=( - "Library sync thread has exited! " - "You should restart Kodi now. " - "Please report this on the forum.")) - raise - - def run_internal(self): - - log = self.logMsg - lang = utils.language - window = utils.window - settings = utils.settings - dialog = xbmcgui.Dialog() - - startupComplete = False - monitor = self.monitor - - log("---===### Starting LibrarySync ###===---", 0) - - while not monitor.abortRequested(): - - # In the event the server goes offline - while self.suspend_thread: - # Set in service.py - if monitor.waitForAbort(5): - # Abort was requested while waiting. We should exit - break - - if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): - # Verify the validity of the database - currentVersion = settings('dbCreatedWithVersion') - minVersion = window('emby_minDBVersion') - uptoDate = self.compareDBVersion(currentVersion, minVersion) - - if not uptoDate: - log("Database version out of date: %s minimum version required: %s" - % (currentVersion, minVersion), 0) - - resp = dialog.yesno("Emby for Kodi", lang(33022)) - if not resp: - log("Database version is out of date! USER IGNORED!", 0) - dialog.ok("Emby for Kodi", lang(33023)) - else: - utils.reset() - - break - - window('emby_dbCheck', value="true") - - - if not startupComplete: - # Verify the video database can be found - videoDb = utils.getKodiVideoDBPath() - if not xbmcvfs.exists(videoDb): - # Database does not exists - log( - "The current Kodi version is incompatible " - "with the Emby for Kodi add-on. Please visit " - "https://github.com/MediaBrowser/Emby.Kodi/wiki " - "to know which Kodi versions are supported.", 0) - - dialog.ok( - heading="Emby for Kodi", - line1=lang(33024)) - break - - # Run start up sync - log("Database version: %s" % settings('dbCreatedWithVersion'), 0) - log("SyncDatabase (started)", 1) - startTime = datetime.now() - librarySync = self.startSync() - elapsedTime = datetime.now() - startTime - log("SyncDatabase (finished in: %s) %s" - % (str(elapsedTime).split('.')[0], librarySync), 1) - # Only try the initial sync once per kodi session regardless - # This will prevent an infinite loop in case something goes wrong. - startupComplete = True - - # Process updates - if window('emby_dbScan') != "true": - self.incrementalSync() - - if window('emby_onWake') == "true" and window('emby_online') == "true": - # Kodi is waking up - # Set in kodimonitor.py - window('emby_onWake', clear=True) - if window('emby_syncRunning') != "true": - log("SyncDatabase onWake (started)", 0) - librarySync = self.startSync() - log("SyncDatabase onWake (finished) %s" % librarySync, 0) - - if self.stop_thread: - # Set in service.py - log("Service terminated thread.", 2) - break - - if monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - log("###===--- LibrarySync Stopped ---===###", 0) - - def stopThread(self): - self.stop_thread = True - self.logMsg("Ending thread...", 2) - - def suspendThread(self): - self.suspend_thread = True - self.logMsg("Pausing thread...", 0) - - def resumeThread(self): - self.suspend_thread = False - self.logMsg("Resuming thread...", 0) - - -class ManualSync(LibrarySync): - - - def __init__(self): - - LibrarySync.__init__(self) - - def sync(self, dialog=False): - - return self.fullSync(manualrun=True, forceddialog=dialog) - - - def movies(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - lang = utils.language - # Get movies from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - movies = itemtypes.Movies(embycursor, kodicursor) - - views = emby_db.getView_byType('movies') - views += emby_db.getView_byType('mixed') - log("Media folders: %s" % views, 1) - - # Pull the list of movies and boxsets in Kodi - try: - all_kodimovies = dict(emby_db.getChecksum('Movie')) - except ValueError: - all_kodimovies = {} - - try: - all_kodisets = dict(emby_db.getChecksum('BoxSet')) - except ValueError: - all_kodisets = {} - - all_embymoviesIds = set() - all_embyboxsetsIds = set() - updatelist = [] - - ##### PROCESS MOVIES ##### - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (lang(33026), viewName)) - - all_embymovies = emby.getMovies(viewId, basic=True, dialog=pdialog) - for embymovie in all_embymovies['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymovie) - itemid = embymovie['Id'] - all_embymoviesIds.add(itemid) - - - if all_kodimovies.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log("Movies to update for %s: %s" % (viewName, updatelist), 1) - embymovies = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymovie in embymovies: - # Process individual movies - if self.shouldStop(): - return False - - title = embymovie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_update(embymovie, viewName, viewId) - - ##### PROCESS BOXSETS ##### - - boxsets = emby.getBoxset(dialog=pdialog) - embyboxsets = [] - - if pdialog: - pdialog.update(heading="Emby for Kodi", message=lang(33027)) - - for boxset in boxsets['Items']: - - if self.shouldStop(): - return False - - # Boxset has no real userdata, so using etag to compare - checksum = boxset['Etag'] - itemid = boxset['Id'] - all_embyboxsetsIds.add(itemid) - - if all_kodisets.get(itemid) != checksum: - # Only update if boxset is not in Kodi or checksum is different - updatelist.append(itemid) - embyboxsets.append(boxset) - - log("Boxsets to update: %s" % updatelist, 1) - total = len(updatelist) - - if pdialog: - pdialog.update(heading="Processing Boxsets / %s items" % total) - - count = 0 - for boxset in embyboxsets: - # Process individual boxset - if self.shouldStop(): - return False - - title = boxset['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_updateBoxset(boxset) - - ##### PROCESS DELETES ##### - - for kodimovie in all_kodimovies: - if kodimovie not in all_embymoviesIds: - movies.remove(kodimovie) - else: - log("Movies compare finished.", 1) - - for boxset in all_kodisets: - if boxset not in all_embyboxsetsIds: - movies.remove(boxset) - else: - log("Boxsets compare finished.", 1) - - return True - - def musicvideos(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - # Get musicvideos from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - mvideos = itemtypes.MusicVideos(embycursor, kodicursor) - - views = emby_db.getView_byType('musicvideos') - log("Media folders: %s" % views, 1) - - # Pull the list of musicvideos in Kodi - try: - all_kodimvideos = dict(emby_db.getChecksum('MusicVideo')) - except ValueError: - all_kodimvideos = {} - - all_embymvideosIds = set() - updatelist = [] - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (utils.language(33028), viewName)) - - all_embymvideos = emby.getMusicVideos(viewId, basic=True, dialog=pdialog) - for embymvideo in all_embymvideos['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymvideo) - itemid = embymvideo['Id'] - all_embymvideosIds.add(itemid) - - - if all_kodimvideos.get(itemid) != API.getChecksum(): - # Only update if musicvideo is not in Kodi or checksum is different - updatelist.append(itemid) - - log("MusicVideos to update for %s: %s" % (viewName, updatelist), 1) - embymvideos = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymvideo in embymvideos: - # Process individual musicvideo - if self.shouldStop(): - return False - - title = embymvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - mvideos.add_update(embymvideo, viewName, viewId) - - ##### PROCESS DELETES ##### - - for kodimvideo in all_kodimvideos: - if kodimvideo not in all_embymvideosIds: - mvideos.remove(kodimvideo) - else: - log("MusicVideos compare finished.", 1) - - return True - - def tvshows(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - lang = utils.language - # Get shows from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - tvshows = itemtypes.TVShows(embycursor, kodicursor) - - views = emby_db.getView_byType('tvshows') - views += emby_db.getView_byType('mixed') - log("Media folders: %s" % views, 1) - - # Pull the list of tvshows and episodes in Kodi - try: - all_koditvshows = dict(emby_db.getChecksum('Series')) - except ValueError: - all_koditvshows = {} - - try: - all_kodiepisodes = dict(emby_db.getChecksum('Episode')) - except ValueError: - all_kodiepisodes = {} - - all_embytvshowsIds = set() - all_embyepisodesIds = set() - updatelist = [] - - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (lang(33029), viewName)) - - all_embytvshows = emby.getShows(viewId, basic=True, dialog=pdialog) - for embytvshow in all_embytvshows['Items']: - - if self.shouldStop(): - return False - - API = api.API(embytvshow) - itemid = embytvshow['Id'] - all_embytvshowsIds.add(itemid) - - - if all_koditvshows.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log("TVShows to update for %s: %s" % (viewName, updatelist), 1) - embytvshows = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.shouldStop(): - return False - - itemid = embytvshow['Id'] - title = embytvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_update(embytvshow, viewName, viewId) - - else: - # Get all episodes in view - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (lang(33030), viewName)) - - all_embyepisodes = emby.getEpisodes(viewId, basic=True, dialog=pdialog) - for embyepisode in all_embyepisodes['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyepisode) - itemid = embyepisode['Id'] - all_embyepisodesIds.add(itemid) - - if all_kodiepisodes.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log("Episodes to update for %s: %s" % (viewName, updatelist), 1) - embyepisodes = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - count = 0 - for episode in embyepisodes: - - # Process individual episode - if self.shouldStop(): - return False - - title = episode['SeriesName'] - episodetitle = episode['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) - count += 1 - tvshows.add_updateEpisode(episode) - - ##### PROCESS DELETES ##### - - for koditvshow in all_koditvshows: - if koditvshow not in all_embytvshowsIds: - tvshows.remove(koditvshow) - else: - log("TVShows compare finished.", 1) - - for kodiepisode in all_kodiepisodes: - if kodiepisode not in all_embyepisodesIds: - tvshows.remove(kodiepisode) - else: - log("Episodes compare finished.", 1) - - return True - - def music(self, embycursor, kodicursor, pdialog): - - log = self.logMsg - # Get music from emby - emby = self.emby - emby_db = embydb.Embydb_Functions(embycursor) - music = itemtypes.Music(embycursor, kodicursor) - - # Pull the list of artists, albums, songs - try: - all_kodiartists = dict(emby_db.getChecksum('MusicArtist')) - except ValueError: - all_kodiartists = {} - - try: - all_kodialbums = dict(emby_db.getChecksum('MusicAlbum')) - except ValueError: - all_kodialbums = {} - - try: - all_kodisongs = dict(emby_db.getChecksum('Audio')) - except ValueError: - all_kodisongs = {} - - all_embyartistsIds = set() - all_embyalbumsIds = set() - all_embysongsIds = set() - updatelist = [] - - process = { - - 'artists': [emby.getArtists, music.add_updateArtist], - 'albums': [emby.getAlbums, music.add_updateAlbum], - 'songs': [emby.getSongs, music.add_updateSong] - } - types = ['artists', 'albums', 'songs'] - for type in types: - - if pdialog: - pdialog.update( - heading="Emby for Kodi", - message="%s %s..." % (utils.language(33031), type)) - - if type != "artists": - all_embyitems = process[type][0](basic=True, dialog=pdialog) - else: - all_embyitems = process[type][0](dialog=pdialog) - for embyitem in all_embyitems['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyitem) - itemid = embyitem['Id'] - if type == "artists": - all_embyartistsIds.add(itemid) - if all_kodiartists.get(itemid) != API.getChecksum(): - # Only update if artist is not in Kodi or checksum is different - updatelist.append(itemid) - elif type == "albums": - all_embyalbumsIds.add(itemid) - if all_kodialbums.get(itemid) != API.getChecksum(): - # Only update if album is not in Kodi or checksum is different - updatelist.append(itemid) - else: - all_embysongsIds.add(itemid) - if all_kodisongs.get(itemid) != API.getChecksum(): - # Only update if songs is not in Kodi or checksum is different - updatelist.append(itemid) - - log("%s to update: %s" % (type, updatelist), 1) - embyitems = emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (type, total)) - - count = 0 - for embyitem in embyitems: - # Process individual item - if self.shouldStop(): - return False - - title = embyitem['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - process[type][1](embyitem) - - ##### PROCESS DELETES ##### - - for kodiartist in all_kodiartists: - if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None: - music.remove(kodiartist) - else: - log("Artist compare finished.", 1) - - for kodialbum in all_kodialbums: - if kodialbum not in all_embyalbumsIds: - music.remove(kodialbum) - else: - log("Albums compare finished.", 1) - - for kodisong in all_kodisongs: - if kodisong not in all_embysongsIds: - music.remove(kodisong) - else: - log("Songs compare finished.", 1) - - return True \ No newline at end of file +# -*- coding: utf-8 -*- + +################################################################################################## + +import sqlite3 +import threading +from datetime import datetime, timedelta, time + +import xbmc +import xbmcgui +import xbmcvfs + +import api +import utils +import clientinfo +import downloadutils +import itemtypes +import embydb_functions as embydb +import kodidb_functions as kodidb +import read_embyserver as embyserver +import userclient +import videonodes + +################################################################################################## + + +class LibrarySync(threading.Thread): + + _shared_state = {} + + stop_thread = False + suspend_thread = False + + # Track websocketclient updates + addedItems = [] + updateItems = [] + userdataItems = [] + removeItems = [] + forceLibraryUpdate = False + refresh_views = False + + + def __init__(self): + + self.__dict__ = self._shared_state + self.monitor = xbmc.Monitor() + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.user = userclient.UserClient() + self.emby = embyserver.Read_EmbyServer() + self.vnodes = videonodes.VideoNodes() + + threading.Thread.__init__(self) + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def progressDialog(self, title, forced=False): + + dialog = None + + if utils.settings('dbSyncIndicator') == "true" or forced: + dialog = xbmcgui.DialogProgressBG() + dialog.create("Emby for Kodi", title) + self.logMsg("Show progress dialog: %s" % title, 2) + + return dialog + + def startSync(self): + + settings = utils.settings + # Run at start up - optional to use the server plugin + if settings('SyncInstallRunDone') == "true": + + # Validate views + self.refreshViews() + completed = False + # Verify if server plugin is installed. + if settings('serverSync') == "true": + # Try to use fast start up + url = "{server}/emby/Plugins?format=json" + result = self.doUtils(url) + + for plugin in result: + if plugin['Name'] == "Emby.Kodi Sync Queue": + self.logMsg("Found server plugin.", 2) + completed = self.fastSync() + break + + if not completed: + # Fast sync failed or server plugin is not found + completed = ManualSync().sync() + else: + # Install sync is not completed + completed = self.fullSync() + + return completed + + def fastSync(self): + + lastSync = utils.settings('LastIncrementalSync') + if not lastSync: + lastSync = "2010-01-01T00:00:00Z" + + lastSyncTime = utils.convertdate(lastSync) + self.logMsg("Last sync run: %s" % lastSyncTime, 1) + + # get server RetentionDateTime + result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") + retention_time = "2010-01-01T00:00:00Z" + if result and result.get('RetentionDateTime'): + retention_time = result['RetentionDateTime'] + + #Try/except equivalent + ''' + try: + retention_time = result['RetentionDateTime'] + except (TypeError, KeyError): + retention_time = "2010-01-01T00:00:00Z" + ''' + + retention_time = utils.convertdate(retention_time) + self.logMsg("RetentionDateTime: %s" % retention_time, 1) + + # if last sync before retention time do a full sync + if retention_time > lastSyncTime: + self.logMsg("Fast sync server retention insufficient, fall back to full sync", 1) + return False + + params = {'LastUpdateDT': lastSync} + result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json", parameters=params) + + try: + processlist = { + + 'added': result['ItemsAdded'], + 'update': result['ItemsUpdated'], + 'userdata': result['UserDataChanged'], + 'remove': result['ItemsRemoved'] + } + + except (KeyError, TypeError): + self.logMsg("Failed to retrieve latest updates using fast sync.", 1) + return False + + else: + self.logMsg("Fast sync changes: %s" % result, 1) + for action in processlist: + self.triage_items(action, processlist[action]) + + return True + + def saveLastSync(self): + + # Save last sync time + overlap = 2 + + result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") + try: # datetime fails when used more than once, TypeError + server_time = result['ServerDateTime'] + server_time = utils.convertdate(server_time) + + except Exception as e: + # If the server plugin is not installed or an error happened. + self.logMsg("An exception occurred: %s" % e, 1) + time_now = datetime.utcnow()-timedelta(minutes=overlap) + lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') + self.logMsg("New sync time: client time -%s min: %s" % (overlap, lastSync), 1) + + else: + lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ') + self.logMsg("New sync time: server time -%s min: %s" % (overlap, lastSync), 1) + + finally: + utils.settings('LastIncrementalSync', value=lastSync) + + def shouldStop(self): + # Checkpoint during the syncing process + if self.monitor.abortRequested(): + return True + elif utils.window('emby_shouldStop') == "true": + return True + else: # Keep going + return False + + def dbCommit(self, connection): + + window = utils.window + # Central commit, verifies if Kodi database update is running + kodidb_scan = window('emby_kodiScan') == "true" + + while kodidb_scan: + + self.logMsg("Kodi scan is running. Waiting...", 1) + kodidb_scan = window('emby_kodiScan') == "true" + + if self.shouldStop(): + self.logMsg("Commit unsuccessful. Sync terminated.", 1) + break + + if self.monitor.waitForAbort(1): + # Abort was requested while waiting. We should exit + self.logMsg("Commit unsuccessful.", 1) + break + else: + connection.commit() + self.logMsg("Commit successful.", 1) + + def fullSync(self, manualrun=False, repair=False, forceddialog=False): + + window = utils.window + settings = utils.settings + # Only run once when first setting up. Can be run manually. + music_enabled = utils.settings('enableMusic') == "true" + + xbmc.executebuiltin('InhibitIdleShutdown(true)') + screensaver = utils.getScreensaver() + utils.setScreensaver(value="") + window('emby_dbScan', value="true") + # Add sources + utils.sourcesXML() + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + # Create the tables for the emby database + # emby, view, version + embycursor.execute( + """CREATE TABLE IF NOT EXISTS emby( + emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, + kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""") + embycursor.execute( + """CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") + embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") + embyconn.commit() + + # content sync: movies, tvshows, musicvideos, music + kodiconn = utils.kodiSQL('video') + kodicursor = kodiconn.cursor() + + if manualrun: + message = "Manual sync" + elif repair: + message = "Repair sync" + forceddialog = True + else: + message = "Initial sync" + forceddialog = True + window('emby_initialScan', value="true") + + pDialog = self.progressDialog("%s" % message, forced=forceddialog) + starttotal = datetime.now() + + # Set views + self.maintainViews(embycursor, kodicursor) + embyconn.commit() + + # Sync video library + process = { + + 'movies': self.movies, + 'musicvideos': self.musicvideos, + 'tvshows': self.tvshows + } + for itemtype in process: + startTime = datetime.now() + completed = process[itemtype](embycursor, kodicursor, pDialog) + if not completed: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() + + embycursor.close() + kodicursor.close() + return False + else: + self.dbCommit(kodiconn) + embyconn.commit() + elapsedTime = datetime.now() - startTime + self.logMsg("SyncDatabase (finished %s in: %s)" + % (itemtype, str(elapsedTime).split('.')[0]), 1) + else: + # Close the Kodi cursor + kodicursor.close() + + # sync music + if music_enabled: + + musicconn = utils.kodiSQL('music') + musiccursor = musicconn.cursor() + + startTime = datetime.now() + completed = self.music(embycursor, musiccursor, pDialog) + if not completed: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() + + embycursor.close() + musiccursor.close() + return False + else: + musicconn.commit() + embyconn.commit() + elapsedTime = datetime.now() - startTime + self.logMsg("SyncDatabase (finished music in: %s)" + % (str(elapsedTime).split('.')[0]), 1) + musiccursor.close() + + if pDialog: + pDialog.close() + + embycursor.close() + + settings('SyncInstallRunDone', value="true") + settings("dbCreatedWithVersion", self.clientInfo.getVersion()) + self.saveLastSync() + xbmc.executebuiltin('UpdateLibrary(video)') + elapsedtotal = datetime.now() - starttotal + + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + window('emby_initialScan', clear=True) + if forceddialog: + xbmcgui.Dialog().notification( + heading="Emby for Kodi", + message="%s %s %s" % + (message, utils.language(33025), str(elapsedtotal).split('.')[0]), + icon="special://home/addons/plugin.video.emby/icon.png", + sound=False) + return True + + + def refreshViews(self): + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + kodiconn = utils.kodiSQL('video') + kodicursor = kodiconn.cursor() + + # Compare views, assign correct tags to items + self.maintainViews(embycursor, kodicursor) + + self.dbCommit(kodiconn) + kodicursor.close() + + embyconn.commit() + embycursor.close() + + def maintainViews(self, embycursor, kodicursor): + + # Compare the views to emby + emby = self.emby + emby_db = embydb.Embydb_Functions(embycursor) + kodi_db = kodidb.Kodidb_Functions(kodicursor) + + # Get views + result = self.doUtils("{server}/emby/Users/{UserId}/Views?format=json") + grouped_views = result['Items'] + ordered_views = self.emby.getViews(sortedlist=True) + all_views = [] + sorted_views = [] + for view in ordered_views: + all_views.append(view['name']) + if view['type'] == "music": + continue + + if view['type'] == "mixed": + sorted_views.append(view['name']) + sorted_views.append(view['name']) + self.logMsg("Sorted views: %s" % sorted_views, 1) + + # total nodes for window properties + self.vnodes.clearProperties() + totalnodes = len(sorted_views) + 0 + + current_views = emby_db.getViews() + # Set views for supported media type + emby_mediatypes = { + + 'movies': "Movie", + 'tvshows': "Series", + 'musicvideos': "MusicVideo", + 'homevideos': "Video", + 'music': "Audio", + 'photos': "Photo" + } + for mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']: + + nodes = [] # Prevent duplicate for nodes of the same type + playlists = [] # Prevent duplicate for playlists of the same type + # Get media folders from server + folders = self.emby.getViews(mediatype, root=True) + for folder in folders: + + folderid = folder['id'] + foldername = folder['name'] + viewtype = folder['type'] + + if foldername not in all_views: + # Media folders are grouped into userview + params = { + 'ParentId': folderid, + 'Recursive': True, + 'Limit': 1, + 'IncludeItemTypes': emby_mediatypes[mediatype] + } # Get one item from server using the folderid + result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) + try: + verifyitem = result['Items'][0]['Id'] + except (TypeError, IndexError): + # Something is wrong. Keep the same folder name. + # Could be the view is empty or the connection + pass + else: + for grouped_view in grouped_views: + # This is only reserved for the detection of grouped views + if (grouped_view['Type'] == "UserView" and + grouped_view.get('CollectionType') == mediatype): + # Take the userview, and validate the item belong to the view + if self.emby.verifyView(grouped_view['Id'], verifyitem): + # Take the name of the userview + self.logMsg("Found corresponding view: %s %s" + % (grouped_view['Name'], grouped_view['Id']), 1) + foldername = grouped_view['Name'] + break + else: + # Unable to find a match, add the name to our sorted_view list + sorted_views.append(foldername) + self.logMsg("Couldn't find corresponding grouped view: %s" % sorted_views, 1) + + # Failsafe + try: + sorted_views.index(foldername) + except ValueError: + sorted_views.append(foldername) + + # Get current media folders from emby database + view = emby_db.getView_byId(folderid) + try: + current_viewname = view[0] + current_viewtype = view[1] + current_tagid = view[2] + + except TypeError: + self.logMsg("Creating viewid: %s in Emby database." % folderid, 1) + tagid = kodi_db.createTag(foldername) + # Create playlist for the video library + if (foldername not in playlists and + mediatype in ('movies', 'tvshows', 'musicvideos')): + utils.playlistXSP(mediatype, foldername, folderid, viewtype) + playlists.append(foldername) + # Create the video node + if foldername not in nodes and mediatype not in ("musicvideos", "music"): + self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, + viewtype, folderid) + if viewtype == "mixed": # Change the value + sorted_views[sorted_views.index(foldername)] = "%ss" % foldername + nodes.append(foldername) + totalnodes += 1 + # Add view to emby database + emby_db.addView(folderid, foldername, viewtype, tagid) + + else: + self.logMsg(' '.join(( + + "Found viewid: %s" % folderid, + "viewname: %s" % current_viewname, + "viewtype: %s" % current_viewtype, + "tagid: %s" % current_tagid)), 2) + + # View is still valid + try: + current_views.remove(folderid) + except ValueError: + # View was just created, nothing to remove + pass + + # View was modified, update with latest info + if current_viewname != foldername: + self.logMsg("viewid: %s new viewname: %s" % (folderid, foldername), 1) + tagid = kodi_db.createTag(foldername) + + # Update view with new info + emby_db.updateView(foldername, tagid, folderid) + + if mediatype != "music": + if emby_db.getView_byName(current_viewname) is None: + # The tag could be a combined view. Ensure there's no other tags + # with the same name before deleting playlist. + utils.playlistXSP( + mediatype, current_viewname, folderid, current_viewtype, True) + # Delete video node + if mediatype != "musicvideos": + self.vnodes.viewNode( + indexnumber=None, + tagname=current_viewname, + mediatype=mediatype, + viewtype=current_viewtype, + viewid=folderid, + delete=True) + # Added new playlist + if (foldername not in playlists and + mediatype in ('movies', 'tvshows', 'musicvideos')): + utils.playlistXSP(mediatype, foldername, folderid, viewtype) + playlists.append(foldername) + # Add new video node + if foldername not in nodes and mediatype != "musicvideos": + self.vnodes.viewNode(sorted_views.index(foldername), foldername, + mediatype, viewtype, folderid) + if viewtype == "mixed": # Change the value + sorted_views[sorted_views.index(foldername)] = "%ss" % foldername + nodes.append(foldername) + totalnodes += 1 + + # Update items with new tag + items = emby_db.getItem_byView(folderid) + for item in items: + # Remove the "s" from viewtype for tags + kodi_db.updateTag( + current_tagid, tagid, item[0], current_viewtype[:-1]) + else: + # Validate the playlist exists or recreate it + if mediatype != "music": + if (foldername not in playlists and + mediatype in ('movies', 'tvshows', 'musicvideos')): + utils.playlistXSP(mediatype, foldername, folderid, viewtype) + playlists.append(foldername) + # Create the video node if not already exists + if foldername not in nodes and mediatype != "musicvideos": + self.vnodes.viewNode(sorted_views.index(foldername), foldername, + mediatype, viewtype, folderid) + if viewtype == "mixed": # Change the value + sorted_views[sorted_views.index(foldername)] = "%ss" % foldername + nodes.append(foldername) + totalnodes += 1 + else: + # Add video nodes listings + self.vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites") + totalnodes += 1 + self.vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites") + totalnodes += 1 + self.vnodes.singleNode(totalnodes, "channels", "movies", "channels") + totalnodes += 1 + # Save total + utils.window('Emby.nodes.total', str(totalnodes)) + + # Remove any old referenced views + self.logMsg("Removing views: %s" % current_views, 1) + for view in current_views: + emby_db.removeView(view) + + def movies(self, embycursor, kodicursor, pdialog): + + lang = utils.language + # Get movies from emby + emby_db = embydb.Embydb_Functions(embycursor) + movies = itemtypes.Movies(embycursor, kodicursor) + + views = emby_db.getView_byType('movies') + views += emby_db.getView_byType('mixed') + self.logMsg("Media folders: %s" % views, 1) + + ##### PROCESS MOVIES ##### + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (lang(33017), view['name'])) + + # Initial or repair sync + all_embymovies = self.emby.getMovies(view['id'], dialog=pdialog) + total = all_embymovies['TotalRecordCount'] + embymovies = all_embymovies['Items'] + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) + + count = 0 + for embymovie in embymovies: + # Process individual movies + if self.shouldStop(): + return False + + title = embymovie['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + movies.add_update(embymovie, view['name'], view['id']) + else: + self.logMsg("Movies finished.", 2) + + + ##### PROCESS BOXSETS ##### + if pdialog: + pdialog.update(heading="Emby for Kodi", message=lang(33018)) + + boxsets = self.emby.getBoxset(dialog=pdialog) + total = boxsets['TotalRecordCount'] + embyboxsets = boxsets['Items'] + + if pdialog: + pdialog.update(heading="Processing Boxsets / %s items" % total) + + count = 0 + for boxset in embyboxsets: + # Process individual boxset + if self.shouldStop(): + return False + + title = boxset['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + movies.add_updateBoxset(boxset) + else: + self.logMsg("Boxsets finished.", 2) + + return True + + def musicvideos(self, embycursor, kodicursor, pdialog): + + # Get musicvideos from emby + emby_db = embydb.Embydb_Functions(embycursor) + mvideos = itemtypes.MusicVideos(embycursor, kodicursor) + + views = emby_db.getView_byType('musicvideos') + self.logMsg("Media folders: %s" % views, 1) + + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + viewId = view['id'] + viewName = view['name'] + + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (utils.language(33019), viewName)) + + # Initial or repair sync + all_embymvideos = self.emby.getMusicVideos(viewId, dialog=pdialog) + total = all_embymvideos['TotalRecordCount'] + embymvideos = all_embymvideos['Items'] + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (viewName, total)) + + count = 0 + for embymvideo in embymvideos: + # Process individual musicvideo + if self.shouldStop(): + return False + + title = embymvideo['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + mvideos.add_update(embymvideo, viewName, viewId) + else: + self.logMsg("MusicVideos finished.", 2) + + return True + + def tvshows(self, embycursor, kodicursor, pdialog): + + # Get shows from emby + emby_db = embydb.Embydb_Functions(embycursor) + tvshows = itemtypes.TVShows(embycursor, kodicursor) + + views = emby_db.getView_byType('tvshows') + views += emby_db.getView_byType('mixed') + self.logMsg("Media folders: %s" % views, 1) + + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (utils.language(33020), view['name'])) + + all_embytvshows = self.emby.getShows(view['id'], dialog=pdialog) + total = all_embytvshows['TotalRecordCount'] + embytvshows = all_embytvshows['Items'] + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) + + count = 0 + for embytvshow in embytvshows: + # Process individual show + if self.shouldStop(): + return False + + title = embytvshow['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + tvshows.add_update(embytvshow, view['name'], view['id']) + + # Process episodes + all_episodes = self.emby.getEpisodesbyShow(embytvshow['Id']) + for episode in all_episodes['Items']: + + # Process individual show + if self.shouldStop(): + return False + + episodetitle = episode['Name'] + if pdialog: + pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) + tvshows.add_updateEpisode(episode) + else: + self.logMsg("TVShows finished.", 2) + + return True + + def music(self, embycursor, kodicursor, pdialog): + # Get music from emby + emby_db = embydb.Embydb_Functions(embycursor) + music = itemtypes.Music(embycursor, kodicursor) + + process = { + + 'artists': [self.emby.getArtists, music.add_updateArtist], + 'albums': [self.emby.getAlbums, music.add_updateAlbum], + 'songs': [self.emby.getSongs, music.add_updateSong] + } + for itemtype in ['artists', 'albums', 'songs']: + + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (utils.language(33021), itemtype)) + + all_embyitems = process[itemtype][0](dialog=pdialog) + total = all_embyitems['TotalRecordCount'] + embyitems = all_embyitems['Items'] + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (itemtype, total)) + + count = 0 + for embyitem in embyitems: + # Process individual item + if self.shouldStop(): + return False + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=embyitem['Name']) + count += 1 + + process[itemtype][1](embyitem) + else: + self.logMsg("%s finished." % itemtype, 2) + + return True + + # Reserved for websocket_client.py and fast start + def triage_items(self, process, items): + + processlist = { + + 'added': self.addedItems, + 'update': self.updateItems, + 'userdata': self.userdataItems, + 'remove': self.removeItems + } + if items: + if process == "userdata": + itemids = [] + for item in items: + itemids.append(item['ItemId']) + items = itemids + + self.logMsg("Queue %s: %s" % (process, items), 1) + processlist[process].extend(items) + + def incrementalSync(self): + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + kodiconn = utils.kodiSQL('video') + kodicursor = kodiconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + pDialog = None + update_embydb = False + + if self.refresh_views: + # Received userconfig update + self.refresh_views = False + self.maintainViews(embycursor, kodicursor) + self.forceLibraryUpdate = True + update_embydb = True + + if self.addedItems or self.updateItems or self.userdataItems or self.removeItems: + # Only present dialog if we are going to process items + pDialog = self.progressDialog('Incremental sync') + + + process = { + + 'added': self.addedItems, + 'update': self.updateItems, + 'userdata': self.userdataItems, + 'remove': self.removeItems + } + for process_type in ['added', 'update', 'userdata', 'remove']: + + if process[process_type] and utils.window('emby_kodiScan') != "true": + + listItems = list(process[process_type]) + del process[process_type][:] # Reset class list + + items_process = itemtypes.Items(embycursor, kodicursor) + update = False + + # Prepare items according to process process_type + if process_type == "added": + items = self.emby.sortby_mediatype(listItems) + + elif process_type in ("userdata", "remove"): + items = emby_db.sortby_mediaType(listItems, unsorted=False) + + else: + items = emby_db.sortby_mediaType(listItems) + if items.get('Unsorted'): + sorted_items = self.emby.sortby_mediatype(items['Unsorted']) + doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) + if doupdate: + embyupdate, kodiupdate_video = doupdate + if embyupdate: + update_embydb = True + if kodiupdate_video: + self.forceLibraryUpdate = True + del items['Unsorted'] + + doupdate = items_process.itemsbyId(items, process_type, pDialog) + if doupdate: + embyupdate, kodiupdate_video = doupdate + if embyupdate: + update_embydb = True + if kodiupdate_video: + self.forceLibraryUpdate = True + + if update_embydb: + update_embydb = False + self.logMsg("Updating emby database.", 1) + embyconn.commit() + self.saveLastSync() + + if self.forceLibraryUpdate: + # Force update the Kodi library + self.forceLibraryUpdate = False + self.dbCommit(kodiconn) + + self.logMsg("Updating video library.", 1) + utils.window('emby_kodiScan', value="true") + xbmc.executebuiltin('UpdateLibrary(video)') + + if pDialog: + pDialog.close() + + kodicursor.close() + embycursor.close() + + + def compareDBVersion(self, current, minimum): + # It returns True is database is up to date. False otherwise. + self.logMsg("current: %s minimum: %s" % (current, minimum), 1) + currMajor, currMinor, currPatch = current.split(".") + minMajor, minMinor, minPatch = minimum.split(".") + + if currMajor > minMajor: + return True + elif currMajor == minMajor and (currMinor > minMinor or + (currMinor == minMinor and currPatch >= minPatch)): + return True + else: + # Database out of date. + return False + + def run(self): + + try: + self.run_internal() + except Exception as e: + utils.window('emby_dbScan', clear=True) + xbmcgui.Dialog().ok( + heading="Emby for Kodi", + line1=( + "Library sync thread has exited! " + "You should restart Kodi now. " + "Please report this on the forum.")) + raise + + def run_internal(self): + + lang = utils.language + window = utils.window + settings = utils.settings + dialog = xbmcgui.Dialog() + + startupComplete = False + + self.logMsg("---===### Starting LibrarySync ###===---", 0) + + while not self.monitor.abortRequested(): + + # In the event the server goes offline + while self.suspend_thread: + # Set in service.py + if self.monitor.waitForAbort(5): + # Abort was requested while waiting. We should exit + break + + if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): + # Verify the validity of the database + currentVersion = settings('dbCreatedWithVersion') + minVersion = window('emby_minDBVersion') + uptoDate = self.compareDBVersion(currentVersion, minVersion) + + if not uptoDate: + self.logMsg("Database version out of date: %s minimum version required: %s" + % (currentVersion, minVersion), 0) + + resp = dialog.yesno("Emby for Kodi", lang(33022)) + if not resp: + self.logMsg("Database version is out of date! USER IGNORED!", 0) + dialog.ok("Emby for Kodi", lang(33023)) + else: + utils.reset() + + break + + window('emby_dbCheck', value="true") + + + if not startupComplete: + # Verify the video database can be found + videoDb = utils.getKodiVideoDBPath() + if not xbmcvfs.exists(videoDb): + # Database does not exists + self.logMsg( + "The current Kodi version is incompatible " + "with the Emby for Kodi add-on. Please visit " + "https://github.com/MediaBrowser/Emby.Kodi/wiki " + "to know which Kodi versions are supported.", 0) + + dialog.ok( + heading="Emby for Kodi", + line1=lang(33024)) + break + + # Run start up sync + self.logMsg("Database version: %s" % settings('dbCreatedWithVersion'), 0) + self.logMsg("SyncDatabase (started)", 1) + startTime = datetime.now() + librarySync = self.startSync() + elapsedTime = datetime.now() - startTime + self.logMsg("SyncDatabase (finished in: %s) %s" + % (str(elapsedTime).split('.')[0], librarySync), 1) + # Only try the initial sync once per kodi session regardless + # This will prevent an infinite loop in case something goes wrong. + startupComplete = True + + # Process updates + if window('emby_dbScan') != "true": + self.incrementalSync() + + if window('emby_onWake') == "true" and window('emby_online') == "true": + # Kodi is waking up + # Set in kodimonitor.py + window('emby_onWake', clear=True) + if window('emby_syncRunning') != "true": + self.logMsg("SyncDatabase onWake (started)", 0) + librarySync = self.startSync() + self.logMsg("SyncDatabase onWake (finished) %s" % librarySync, 0) + + if self.stop_thread: + # Set in service.py + self.logMsg("Service terminated thread.", 2) + break + + if self.monitor.waitForAbort(1): + # Abort was requested while waiting. We should exit + break + + self.logMsg("###===--- LibrarySync Stopped ---===###", 0) + + def stopThread(self): + self.stop_thread = True + self.logMsg("Ending thread...", 2) + + def suspendThread(self): + self.suspend_thread = True + self.logMsg("Pausing thread...", 0) + + def resumeThread(self): + self.suspend_thread = False + self.logMsg("Resuming thread...", 0) + + +class ManualSync(LibrarySync): + + + def __init__(self): + + LibrarySync.__init__(self) + + def sync(self, dialog=False): + + return self.fullSync(manualrun=True, forceddialog=dialog) + + + def movies(self, embycursor, kodicursor, pdialog): + + lang = utils.language + # Get movies from emby + emby_db = embydb.Embydb_Functions(embycursor) + movies = itemtypes.Movies(embycursor, kodicursor) + + views = emby_db.getView_byType('movies') + views += emby_db.getView_byType('mixed') + self.logMsg("Media folders: %s" % views, 1) + + # Pull the list of movies and boxsets in Kodi + try: + all_kodimovies = dict(emby_db.getChecksum('Movie')) + except ValueError: + all_kodimovies = {} + + try: + all_kodisets = dict(emby_db.getChecksum('BoxSet')) + except ValueError: + all_kodisets = {} + + all_embymoviesIds = set() + all_embyboxsetsIds = set() + updatelist = [] + + ##### PROCESS MOVIES ##### + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + viewId = view['id'] + viewName = view['name'] + + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (lang(33026), viewName)) + + all_embymovies = self.emby.getMovies(viewId, basic=True, dialog=pdialog) + for embymovie in all_embymovies['Items']: + + if self.shouldStop(): + return False + + API = api.API(embymovie) + itemid = embymovie['Id'] + all_embymoviesIds.add(itemid) + + + if all_kodimovies.get(itemid) != API.getChecksum(): + # Only update if movie is not in Kodi or checksum is different + updatelist.append(itemid) + + self.logMsg("Movies to update for %s: %s" % (viewName, updatelist), 1) + embymovies = self.emby.getFullItems(updatelist) + total = len(updatelist) + del updatelist[:] + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (viewName, total)) + + count = 0 + for embymovie in embymovies: + # Process individual movies + if self.shouldStop(): + return False + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=embymovie['Name']) + count += 1 + movies.add_update(embymovie, viewName, viewId) + + ##### PROCESS BOXSETS ##### + + boxsets = self.emby.getBoxset(dialog=pdialog) + embyboxsets = [] + + if pdialog: + pdialog.update(heading="Emby for Kodi", message=lang(33027)) + + for boxset in boxsets['Items']: + + if self.shouldStop(): + return False + + # Boxset has no real userdata, so using etag to compare + itemid = boxset['Id'] + all_embyboxsetsIds.add(itemid) + + if all_kodisets.get(itemid) != boxset['Etag']: + # Only update if boxset is not in Kodi or boxset['Etag'] is different + updatelist.append(itemid) + embyboxsets.append(boxset) + + self.logMsg("Boxsets to update: %s" % updatelist, 1) + total = len(updatelist) + + if pdialog: + pdialog.update(heading="Processing Boxsets / %s items" % total) + + count = 0 + for boxset in embyboxsets: + # Process individual boxset + if self.shouldStop(): + return False + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=boxset['Name']) + count += 1 + movies.add_updateBoxset(boxset) + + ##### PROCESS DELETES ##### + + for kodimovie in all_kodimovies: + if kodimovie not in all_embymoviesIds: + movies.remove(kodimovie) + else: + self.logMsg("Movies compare finished.", 1) + + for boxset in all_kodisets: + if boxset not in all_embyboxsetsIds: + movies.remove(boxset) + else: + self.logMsg("Boxsets compare finished.", 1) + + return True + + def musicvideos(self, embycursor, kodicursor, pdialog): + + # Get musicvideos from emby + emby_db = embydb.Embydb_Functions(embycursor) + mvideos = itemtypes.MusicVideos(embycursor, kodicursor) + + views = emby_db.getView_byType('musicvideos') + self.logMsg("Media folders: %s" % views, 1) + + # Pull the list of musicvideos in Kodi + try: + all_kodimvideos = dict(emby_db.getChecksum('MusicVideo')) + except ValueError: + all_kodimvideos = {} + + all_embymvideosIds = set() + updatelist = [] + + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + viewId = view['id'] + viewName = view['name'] + + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (utils.language(33028), viewName)) + + all_embymvideos = self.emby.getMusicVideos(viewId, basic=True, dialog=pdialog) + for embymvideo in all_embymvideos['Items']: + + if self.shouldStop(): + return False + + API = api.API(embymvideo) + itemid = embymvideo['Id'] + all_embymvideosIds.add(itemid) + + + if all_kodimvideos.get(itemid) != API.getChecksum(): + # Only update if musicvideo is not in Kodi or checksum is different + updatelist.append(itemid) + + self.logMsg("MusicVideos to update for %s: %s" % (viewName, updatelist), 1) + embymvideos = self.emby.getFullItems(updatelist) + total = len(updatelist) + del updatelist[:] + + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (viewName, total)) + + count = 0 + for embymvideo in embymvideos: + # Process individual musicvideo + if self.shouldStop(): + return False + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=embymvideo['Name']) + count += 1 + mvideos.add_update(embymvideo, viewName, viewId) + + ##### PROCESS DELETES ##### + + for kodimvideo in all_kodimvideos: + if kodimvideo not in all_embymvideosIds: + mvideos.remove(kodimvideo) + else: + self.logMsg("MusicVideos compare finished.", 1) + + return True + + def tvshows(self, embycursor, kodicursor, pdialog): + + lang = utils.language + # Get shows from emby + emby_db = embydb.Embydb_Functions(embycursor) + tvshows = itemtypes.TVShows(embycursor, kodicursor) + + views = emby_db.getView_byType('tvshows') + views += emby_db.getView_byType('mixed') + self.logMsg("Media folders: %s" % views, 1) + + # Pull the list of tvshows and episodes in Kodi + try: + all_koditvshows = dict(emby_db.getChecksum('Series')) + except ValueError: + all_koditvshows = {} + + try: + all_kodiepisodes = dict(emby_db.getChecksum('Episode')) + except ValueError: + all_kodiepisodes = {} + + all_embytvshowsIds = set() + all_embyepisodesIds = set() + updatelist = [] + + + for view in views: + + if self.shouldStop(): + return False + + # Get items per view + viewId = view['id'] + viewName = view['name'] + + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (lang(33029), viewName)) + + all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog) + for embytvshow in all_embytvshows['Items']: + + if self.shouldStop(): + return False + + API = api.API(embytvshow) + itemid = embytvshow['Id'] + all_embytvshowsIds.add(itemid) + + + if all_koditvshows.get(itemid) != API.getChecksum(): + # Only update if movie is not in Kodi or checksum is different + updatelist.append(itemid) + + self.logMsg("TVShows to update for %s: %s" % (viewName, updatelist), 1) + embytvshows = self.emby.getFullItems(updatelist) + total = len(updatelist) + del updatelist[:] + + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (viewName, total)) + + count = 0 + for embytvshow in embytvshows: + # Process individual show + if self.shouldStop(): + return False + + itemid = embytvshow['Id'] + title = embytvshow['Name'] + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=title) + count += 1 + tvshows.add_update(embytvshow, viewName, viewId) + + else: + # Get all episodes in view + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (lang(33030), viewName)) + + all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog) + for embyepisode in all_embyepisodes['Items']: + + if self.shouldStop(): + return False + + API = api.API(embyepisode) + itemid = embyepisode['Id'] + all_embyepisodesIds.add(itemid) + + if all_kodiepisodes.get(itemid) != API.getChecksum(): + # Only update if movie is not in Kodi or checksum is different + updatelist.append(itemid) + + self.logMsg("Episodes to update for %s: %s" % (viewName, updatelist), 1) + embyepisodes = self.emby.getFullItems(updatelist) + total = len(updatelist) + del updatelist[:] + + count = 0 + for episode in embyepisodes: + + # Process individual episode + if self.shouldStop(): + return False + + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message="%s - %s" % (episode['SeriesName'], episode['Name'])) + count += 1 + tvshows.add_updateEpisode(episode) + + ##### PROCESS DELETES ##### + + for koditvshow in all_koditvshows: + if koditvshow not in all_embytvshowsIds: + tvshows.remove(koditvshow) + else: + self.logMsg("TVShows compare finished.", 1) + + for kodiepisode in all_kodiepisodes: + if kodiepisode not in all_embyepisodesIds: + tvshows.remove(kodiepisode) + else: + self.logMsg("Episodes compare finished.", 1) + + return True + + def music(self, embycursor, kodicursor, pdialog): + + # Get music from emby + emby_db = embydb.Embydb_Functions(embycursor) + music = itemtypes.Music(embycursor, kodicursor) + + # Pull the list of artists, albums, songs + try: + all_kodiartists = dict(emby_db.getChecksum('MusicArtist')) + except ValueError: + all_kodiartists = {} + + try: + all_kodialbums = dict(emby_db.getChecksum('MusicAlbum')) + except ValueError: + all_kodialbums = {} + + try: + all_kodisongs = dict(emby_db.getChecksum('Audio')) + except ValueError: + all_kodisongs = {} + + all_embyartistsIds = set() + all_embyalbumsIds = set() + all_embysongsIds = set() + updatelist = [] + + process = { + + 'artists': [self.emby.getArtists, music.add_updateArtist], + 'albums': [self.emby.getAlbums, music.add_updateAlbum], + 'songs': [self.emby.getSongs, music.add_updateSong] + } + for data_type in ['artists', 'albums', 'songs']: + if pdialog: + pdialog.update( + heading="Emby for Kodi", + message="%s %s..." % (utils.language(33031), data_type)) + if data_type != "artists": + all_embyitems = process[data_type][0](basic=True, dialog=pdialog) + else: + all_embyitems = process[data_type][0](dialog=pdialog) + for embyitem in all_embyitems['Items']: + if self.shouldStop(): + return False + API = api.API(embyitem) + itemid = embyitem['Id'] + if data_type == "artists": + all_embyartistsIds.add(itemid) + if all_kodiartists.get(itemid) != API.getChecksum(): + # Only update if artist is not in Kodi or checksum is different + updatelist.append(itemid) + elif data_type == "albums": + all_embyalbumsIds.add(itemid) + if all_kodialbums.get(itemid) != API.getChecksum(): + # Only update if album is not in Kodi or checksum is different + updatelist.append(itemid) + else: + all_embysongsIds.add(itemid) + if all_kodisongs.get(itemid) != API.getChecksum(): + # Only update if songs is not in Kodi or checksum is different + updatelist.append(itemid) + self.logMsg("%s to update: %s" % (data_type, updatelist), 1) + embyitems = self.emby.getFullItems(updatelist) + total = len(updatelist) + del updatelist[:] + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (data_type, total)) + count = 0 + for embyitem in embyitems: + # Process individual item + if self.shouldStop(): + return False + if pdialog: + percentage = int((float(count) / float(total))*100) + pdialog.update(percentage, message=embyitem['Name']) + count += 1 + process[data_type][1](embyitem) + ##### PROCESS DELETES ##### + for kodiartist in all_kodiartists: + if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None: + music.remove(kodiartist) + else: + self.logMsg("Artist compare finished.", 1) + for kodialbum in all_kodialbums: + if kodialbum not in all_embyalbumsIds: + music.remove(kodialbum) + else: + self.logMsg("Albums compare finished.", 1) + for kodisong in all_kodisongs: + if kodisong not in all_embysongsIds: + music.remove(kodisong) + else: + self.logMsg("Songs compare finished.", 1) + return True diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py index 97db089d..b058c5c5 100644 --- a/resources/lib/musicutils.py +++ b/resources/lib/musicutils.py @@ -1,286 +1,287 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import os - -import xbmc -import xbmcaddon -import xbmcvfs - -from mutagen.flac import FLAC, Picture -from mutagen.id3 import ID3 -from mutagen import id3 -import base64 - -import read_embyserver as embyserver -import utils - -################################################################################################# - -# Helper for the music library, intended to fix missing song ID3 tags on Emby - -def logMsg(msg, lvl=1): - utils.logMsg("%s %s" % ("Emby", "musictools"), msg, lvl) - -def getRealFileName(filename, isTemp=False): - #get the filename path accessible by python if possible... - - if not xbmcvfs.exists(filename): - logMsg( "File does not exist! %s" %(filename), 0) - return (False, "") - - #if we use os.path method on older python versions (sunch as some android builds), we need to pass arguments as string - if os.path.supports_unicode_filenames: - checkfile = filename - else: - checkfile = filename.encode("utf-8") - - # determine if our python module is able to access the file directly... - if os.path.exists(checkfile): - filename = filename - elif os.path.exists(checkfile.replace("smb://","\\\\").replace("/","\\")): - filename = filename.replace("smb://","\\\\").replace("/","\\") - else: - #file can not be accessed by python directly, we copy it for processing... - isTemp = True - if "/" in filename: filepart = filename.split("/")[-1] - else: filepart = filename.split("\\")[-1] - tempfile = "special://temp/"+filepart - xbmcvfs.copy(filename, tempfile) - filename = xbmc.translatePath(tempfile).decode("utf-8") - - return (isTemp,filename) - -def getEmbyRatingFromKodiRating(rating): - # Translation needed between Kodi/ID3 rating and emby likes/favourites: - # 3+ rating in ID3 = emby like - # 5+ rating in ID3 = emby favourite - # rating 0 = emby dislike - # rating 1-2 = emby no likes or dislikes (returns 1 in results) - favourite = False - deletelike = False - like = False - if (rating >= 3): like = True - if (rating == 0): like = False - if (rating == 1 or rating == 2): deletelike = True - if (rating >= 5): favourite = True - return(like, favourite, deletelike) - -def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating): - - emby = embyserver.Read_EmbyServer() - - previous_values = None - filename = API.getFilePath() - rating = 0 - emby_rating = int(round(emby_rating, 0)) - - #get file rating and comment tag from file itself. - if enableimportsongrating: - file_rating, comment, hasEmbeddedCover = getSongTags(filename) - else: - file_rating = 0 - comment = "" - hasEmbeddedCover = False - - - emby_dbitem = emby_db.getItem_byId(embyid) - try: - kodiid = emby_dbitem[0] - except TypeError: - # Item is not in database. - currentvalue = None - else: - query = "SELECT rating FROM song WHERE idSong = ?" - kodicursor.execute(query, (kodiid,)) - try: - currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) - except: currentvalue = None - - # Only proceed if we actually have a rating from the file - if file_rating is None and currentvalue: - return (currentvalue, comment, False) - elif file_rating is None and not currentvalue: - return (emby_rating, comment, False) - - logMsg("getAdditionalSongTags --> embyid: %s - emby_rating: %s - file_rating: %s - current rating in kodidb: %s" %(embyid, emby_rating, file_rating, currentvalue)) - - updateFileRating = False - updateEmbyRating = False - - if currentvalue != None: - # we need to translate the emby values... - if emby_rating == 1 and currentvalue == 2: - emby_rating = 2 - if emby_rating == 3 and currentvalue == 4: - emby_rating = 4 - - #if updating rating into file is disabled, we ignore the rating in the file... - if not enableupdatesongrating: - file_rating = currentvalue - #if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating... - if not enableexportsongrating: - emby_rating = currentvalue - - if (emby_rating == file_rating) and (file_rating != currentvalue): - #the rating has been updated from kodi itself, update change to both emby ands file - rating = currentvalue - updateFileRating = True - updateEmbyRating = True - elif (emby_rating != currentvalue) and (file_rating == currentvalue): - #emby rating changed - update the file - rating = emby_rating - updateFileRating = True - elif (file_rating != currentvalue) and (emby_rating == currentvalue): - #file rating was updated, sync change to emby - rating = file_rating - updateEmbyRating = True - elif (emby_rating != currentvalue) and (file_rating != currentvalue): - #both ratings have changed (corner case) - the highest rating wins... - if emby_rating > file_rating: - rating = emby_rating - updateFileRating = True - else: - rating = file_rating - updateEmbyRating = True - else: - #nothing has changed, just return the current value - rating = currentvalue - else: - # no rating yet in DB - if enableimportsongrating: - #prefer the file rating - rating = file_rating - #determine if we should also send the rating to emby server - if enableexportsongrating: - if emby_rating == 1 and file_rating == 2: - emby_rating = 2 - if emby_rating == 3 and file_rating == 4: - emby_rating = 4 - if emby_rating != file_rating: - updateEmbyRating = True - - elif enableexportsongrating: - #set the initial rating to emby value - rating = emby_rating - - if updateFileRating and enableupdatesongrating: - updateRatingToFile(rating, filename) - - if updateEmbyRating and enableexportsongrating: - # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: - like, favourite, deletelike = getEmbyRatingFromKodiRating(rating) - utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update - emby.updateUserRating(embyid, like, favourite, deletelike) - - return (rating, comment, hasEmbeddedCover) - -def getSongTags(file): - # Get the actual ID3 tags for music songs as the server is lacking that info - rating = 0 - comment = "" - hasEmbeddedCover = False - - isTemp,filename = getRealFileName(file) - logMsg( "getting song ID3 tags for " + filename) - - try: - ###### FLAC FILES ############# - if filename.lower().endswith(".flac"): - audio = FLAC(filename) - if audio.get("comment"): - comment = audio.get("comment")[0] - for pic in audio.pictures: - if pic.type == 3 and pic.data: - #the file has an embedded cover - hasEmbeddedCover = True - if audio.get("rating"): - rating = float(audio.get("rating")[0]) - #flac rating is 0-100 and needs to be converted to 0-5 range - if rating > 5: rating = (rating / 100) * 5 - - ###### MP3 FILES ############# - elif filename.lower().endswith(".mp3"): - audio = ID3(filename) - - if audio.get("APIC:Front Cover"): - if audio.get("APIC:Front Cover").data: - hasEmbeddedCover = True - - if audio.get("comment"): - comment = audio.get("comment")[0] - if audio.get("POPM:Windows Media Player 9 Series"): - if audio.get("POPM:Windows Media Player 9 Series").rating: - rating = float(audio.get("POPM:Windows Media Player 9 Series").rating) - #POPM rating is 0-255 and needs to be converted to 0-5 range - if rating > 5: rating = (rating / 255) * 5 - else: - logMsg( "Not supported fileformat or unable to access file: %s" %(filename)) - - #the rating must be a round value - rating = int(round(rating,0)) - - except Exception as e: - #file in use ? - utils.logMsg("Exception in getSongTags", str(e),0) - rating = None - - #remove tempfile if needed.... - if isTemp: xbmcvfs.delete(filename) - - return (rating, comment, hasEmbeddedCover) - -def updateRatingToFile(rating, file): - #update the rating from Emby to the file - - f = xbmcvfs.File(file) - org_size = f.size() - f.close() - - #create tempfile - if "/" in file: filepart = file.split("/")[-1] - else: filepart = file.split("\\")[-1] - tempfile = "special://temp/"+filepart - xbmcvfs.copy(file, tempfile) - tempfile = xbmc.translatePath(tempfile).decode("utf-8") - - logMsg( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,tempfile)) - - if not tempfile: - return - - try: - if tempfile.lower().endswith(".flac"): - audio = FLAC(tempfile) - calcrating = int(round((float(rating) / 5) * 100, 0)) - audio["rating"] = str(calcrating) - audio.save() - elif tempfile.lower().endswith(".mp3"): - audio = ID3(tempfile) - calcrating = int(round((float(rating) / 5) * 255, 0)) - audio.add(id3.POPM(email="Windows Media Player 9 Series", rating=calcrating, count=1)) - audio.save() - else: - logMsg( "Not supported fileformat: %s" %(tempfile)) - - #once we have succesfully written the flags we move the temp file to destination, otherwise not proceeding and just delete the temp - #safety check: we check the file size of the temp file before proceeding with overwite of original file - f = xbmcvfs.File(tempfile) - checksum_size = f.size() - f.close() - if checksum_size >= org_size: - xbmcvfs.delete(file) - xbmcvfs.copy(tempfile,file) - else: - logMsg( "Checksum mismatch for filename: %s - using tempfile: %s - not proceeding with file overwite!" %(rating,file,tempfile)) - - #always delete the tempfile - xbmcvfs.delete(tempfile) - - except Exception as e: - #file in use ? - logMsg("Exception in updateRatingToFile %s" %e,0) - - +# -*- coding: utf-8 -*- + +################################################################################################# + +import os + +import xbmc +import xbmcaddon +import xbmcvfs + +from mutagen.flac import FLAC, Picture +from mutagen.id3 import ID3 +from mutagen import id3 +import base64 + +import read_embyserver as embyserver +import utils + +################################################################################################# + +# Helper for the music library, intended to fix missing song ID3 tags on Emby + +def logMsg(msg, lvl=1): + utils.logMsg("%s %s" % ("Emby", "musictools"), msg, lvl) + +def getRealFileName(filename, isTemp=False): + #get the filename path accessible by python if possible... + + if not xbmcvfs.exists(filename): + logMsg( "File does not exist! %s" %(filename), 0) + return (False, "") + + #if we use os.path method on older python versions (sunch as some android builds), we need to pass arguments as string + if os.path.supports_unicode_filenames: + checkfile = filename + else: + checkfile = filename.encode("utf-8") + + # determine if our python module is able to access the file directly... + if os.path.exists(checkfile): + filename = filename + elif os.path.exists(checkfile.replace("smb://","\\\\").replace("/","\\")): + filename = filename.replace("smb://","\\\\").replace("/","\\") + else: + #file can not be accessed by python directly, we copy it for processing... + isTemp = True + if "/" in filename: filepart = filename.split("/")[-1] + else: filepart = filename.split("\\")[-1] + tempfile = "special://temp/"+filepart + xbmcvfs.copy(filename, tempfile) + filename = xbmc.translatePath(tempfile).decode("utf-8") + + return (isTemp,filename) + +def getEmbyRatingFromKodiRating(rating): + # Translation needed between Kodi/ID3 rating and emby likes/favourites: + # 3+ rating in ID3 = emby like + # 5+ rating in ID3 = emby favourite + # rating 0 = emby dislike + # rating 1-2 = emby no likes or dislikes (returns 1 in results) + favourite = False + deletelike = False + like = False + if (rating >= 3): like = True + if (rating == 0): like = False + if (rating == 1 or rating == 2): deletelike = True + if (rating >= 5): favourite = True + return(like, favourite, deletelike) + +def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating): + + emby = embyserver.Read_EmbyServer() + + previous_values = None + filename = API.getFilePath() + rating = 0 + emby_rating = int(round(emby_rating, 0)) + + #get file rating and comment tag from file itself. + if enableimportsongrating: + file_rating, comment, hasEmbeddedCover = getSongTags(filename) + else: + file_rating = 0 + comment = "" + hasEmbeddedCover = False + + + emby_dbitem = emby_db.getItem_byId(embyid) + try: + kodiid = emby_dbitem[0] + except TypeError: + # Item is not in database. + currentvalue = None + else: + query = "SELECT rating FROM song WHERE idSong = ?" + kodicursor.execute(query, (kodiid,)) + try: + currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) + except: currentvalue = None + + # Only proceed if we actually have a rating from the file + if file_rating is None and currentvalue: + return (currentvalue, comment, False) + elif file_rating is None and not currentvalue: + return (emby_rating, comment, False) + + logMsg("getAdditionalSongTags --> embyid: %s - emby_rating: %s - file_rating: %s - current rating in kodidb: %s" %(embyid, emby_rating, file_rating, currentvalue)) + + updateFileRating = False + updateEmbyRating = False + + if currentvalue != None: + # we need to translate the emby values... + if emby_rating == 1 and currentvalue == 2: + emby_rating = 2 + if emby_rating == 3 and currentvalue == 4: + emby_rating = 4 + + #if updating rating into file is disabled, we ignore the rating in the file... + if not enableupdatesongrating: + file_rating = currentvalue + #if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating... + if not enableexportsongrating: + emby_rating = currentvalue + + if (emby_rating == file_rating) and (file_rating != currentvalue): + #the rating has been updated from kodi itself, update change to both emby ands file + rating = currentvalue + updateFileRating = True + updateEmbyRating = True + elif (emby_rating != currentvalue) and (file_rating == currentvalue): + #emby rating changed - update the file + rating = emby_rating + updateFileRating = True + elif (file_rating != currentvalue) and (emby_rating == currentvalue): + #file rating was updated, sync change to emby + rating = file_rating + updateEmbyRating = True + elif (emby_rating != currentvalue) and (file_rating != currentvalue): + #both ratings have changed (corner case) - the highest rating wins... + if emby_rating > file_rating: + rating = emby_rating + updateFileRating = True + else: + rating = file_rating + updateEmbyRating = True + else: + #nothing has changed, just return the current value + rating = currentvalue + else: + # no rating yet in DB + if enableimportsongrating: + #prefer the file rating + rating = file_rating + #determine if we should also send the rating to emby server + if enableexportsongrating: + if emby_rating == 1 and file_rating == 2: + emby_rating = 2 + if emby_rating == 3 and file_rating == 4: + emby_rating = 4 + if emby_rating != file_rating: + updateEmbyRating = True + + elif enableexportsongrating: + #set the initial rating to emby value + rating = emby_rating + + if updateFileRating and enableupdatesongrating: + updateRatingToFile(rating, filename) + + if updateEmbyRating and enableexportsongrating: + # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: + like, favourite, deletelike = getEmbyRatingFromKodiRating(rating) + utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update + emby.updateUserRating(embyid, like, favourite, deletelike) + + return (rating, comment, hasEmbeddedCover) + +def getSongTags(file): + # Get the actual ID3 tags for music songs as the server is lacking that info + rating = 0 + comment = "" + hasEmbeddedCover = False + + isTemp,filename = getRealFileName(file) + logMsg( "getting song ID3 tags for " + filename) + + try: + ###### FLAC FILES ############# + if filename.lower().endswith(".flac"): + audio = FLAC(filename) + if audio.get("comment"): + comment = audio.get("comment")[0] + for pic in audio.pictures: + 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 + if rating > 5: rating = (rating / 100) * 5 + + ###### MP3 FILES ############# + elif filename.lower().endswith(".mp3"): + audio = ID3(filename) + + if audio.get("APIC:Front Cover"): + if audio.get("APIC:Front Cover").data: + hasEmbeddedCover = True + + if audio.get("comment"): + comment = audio.get("comment")[0] + if audio.get("POPM:Windows Media Player 9 Series"): + if audio.get("POPM:Windows Media Player 9 Series").rating: + rating = float(audio.get("POPM:Windows Media Player 9 Series").rating) + #POPM rating is 0-255 and needs to be converted to 0-5 range + if rating > 5: rating = (rating / 255) * 5 + else: + logMsg( "Not supported fileformat or unable to access file: %s" %(filename)) + + #the rating must be a round value + rating = int(round(rating,0)) + + except Exception as e: + #file in use ? + utils.logMsg("Exception in getSongTags", str(e),0) + rating = None + + #remove tempfile if needed.... + if isTemp: xbmcvfs.delete(filename) + + return (rating, comment, hasEmbeddedCover) + +def updateRatingToFile(rating, file): + #update the rating from Emby to the file + + f = xbmcvfs.File(file) + org_size = f.size() + f.close() + + #create tempfile + if "/" in file: filepart = file.split("/")[-1] + else: filepart = file.split("\\")[-1] + tempfile = "special://temp/"+filepart + xbmcvfs.copy(file, tempfile) + tempfile = xbmc.translatePath(tempfile).decode("utf-8") + + logMsg( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,tempfile)) + + if not tempfile: + return + + try: + if tempfile.lower().endswith(".flac"): + audio = FLAC(tempfile) + calcrating = int(round((float(rating) / 5) * 100, 0)) + audio["rating"] = str(calcrating) + audio.save() + elif tempfile.lower().endswith(".mp3"): + audio = ID3(tempfile) + calcrating = int(round((float(rating) / 5) * 255, 0)) + audio.add(id3.POPM(email="Windows Media Player 9 Series", rating=calcrating, count=1)) + audio.save() + else: + logMsg( "Not supported fileformat: %s" %(tempfile)) + + #once we have succesfully written the flags we move the temp file to destination, otherwise not proceeding and just delete the temp + #safety check: we check the file size of the temp file before proceeding with overwite of original file + f = xbmcvfs.File(tempfile) + checksum_size = f.size() + f.close() + if checksum_size >= org_size: + xbmcvfs.delete(file) + xbmcvfs.copy(tempfile,file) + else: + logMsg( "Checksum mismatch for filename: %s - using tempfile: %s - not proceeding with file overwite!" %(rating,file,tempfile)) + + #always delete the tempfile + xbmcvfs.delete(tempfile) + + except Exception as e: + #file in use ? + logMsg("Exception in updateRatingToFile %s" %e,0) + + \ No newline at end of file diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index a0c3743c..e77777a3 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -1,358 +1,346 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import sys - -import xbmc -import xbmcgui -import xbmcplugin - -import api -import artwork -import clientinfo -import downloadutils -import playutils as putils -import playlist -import read_embyserver as embyserver -import utils - -################################################################################################# - - -class PlaybackUtils(): - - - def __init__(self, item): - - self.item = item - self.API = api.API(self.item) - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - self.userid = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userid) - - self.artwork = artwork.Artwork() - self.emby = embyserver.Read_EmbyServer() - self.pl = playlist.Playlist() - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def play(self, itemid, dbid=None): - - log = self.logMsg - window = utils.window - settings = utils.settings - - doUtils = self.doUtils - item = self.item - API = self.API - listitem = xbmcgui.ListItem() - playutils = putils.PlayUtils(item) - - log("Play called.", 1) - playurl = playutils.getPlayUrl() - if not playurl: - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - - if dbid is None: - # Item is not in Kodi database - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - ############### ORGANIZE CURRENT PLAYLIST ################ - - homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - startPos = max(playlist.getposition(), 0) # Can return -1 - sizePlaylist = playlist.size() - currentPosition = startPos - - propertiesPlayback = window('emby_playbackProps') == "true" - introsPlaylist = False - dummyPlaylist = False - - log("Playlist start position: %s" % startPos, 2) - log("Playlist plugin position: %s" % currentPosition, 2) - log("Playlist size: %s" % sizePlaylist, 2) - - ############### RESUME POINT ################ - - userdata = API.getUserData() - seektime = API.adjustResume(userdata['Resume']) - - # We need to ensure we add the intro and additional parts only once. - # Otherwise we get a loop. - if not propertiesPlayback: - - window('emby_playbackProps', value="true") - log("Setting up properties in playlist.", 1) - - if (not homeScreen and not seektime and - window('emby_customPlaylist') != "true"): - - log("Adding dummy file to playlist.", 2) - dummyPlaylist = True - playlist.add(playurl, listitem, index=startPos) - # Remove the original item from playlist - self.pl.removefromPlaylist(startPos+1) - # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower()) - currentPosition += 1 - - ############### -- CHECK FOR INTROS ################ - - if settings('enableCinema') == "true" and not seektime: - # if we have any play them when the movie/show is not being resumed - url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid - intros = doUtils(url) - - if intros['TotalRecordCount'] != 0: - getTrailers = True - - if settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno("Emby for Kodi", utils.language(33016)) - if not resp: - # User selected to not play trailers - getTrailers = False - log("Skip trailers.", 1) - - if getTrailers: - for intro in intros['Items']: - # The server randomly returns intros, process them. - introListItem = xbmcgui.ListItem() - introPlayurl = putils.PlayUtils(intro).getPlayUrl() - log("Adding Intro: %s" % introPlayurl, 1) - - # Set listitem and properties for intros - pbutils = PlaybackUtils(intro) - pbutils.setProperties(introPlayurl, introListItem) - - self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) - introsPlaylist = True - currentPosition += 1 - - - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### - - if homeScreen and not seektime and not sizePlaylist: - # Extend our current playlist with the actual item to play - # only if there's no playlist first - log("Adding main item to playlist.", 1) - self.pl.addtoPlaylist(dbid, item['Type'].lower()) - - # Ensure that additional parts are played after the main item - currentPosition += 1 - - ############### -- CHECK FOR ADDITIONAL PARTS ################ - - if item.get('PartCount'): - # Only add to the playlist after intros have played - partcount = item['PartCount'] - url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid - parts = doUtils(url) - for part in parts['Items']: - - additionalListItem = xbmcgui.ListItem() - additionalPlayurl = putils.PlayUtils(part).getPlayUrl() - log("Adding additional part: %s" % partcount, 1) - - # Set listitem and properties for each additional parts - pbutils = PlaybackUtils(part) - pbutils.setProperties(additionalPlayurl, additionalListItem) - pbutils.setArtwork(additionalListItem) - - playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) - self.pl.verifyPlaylist() - currentPosition += 1 - - 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) - 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) - window('emby_playbackProps', clear=True) - - #self.pl.verifyPlaylist() - ########## SETUP MAIN ITEM ########## - - # For transcoding only, ask for audio/subs pref - if window('emby_%s.playmethod' % playurl) == "Transcode": - playurl = playutils.audioSubsPref(playurl, listitem) - window('emby_%s.playmethod' % playurl, value="Transcode") - - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - - ############### PLAYBACK ################ - - if homeScreen and seektime and window('emby_customPlaylist') != "true": - log("Play as a widget item.", 1) - self.setListItem(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) - xbmc.Player().play(playlist, startpos=startPos) - - else: - log("Play as a regular item.", 1) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - def setProperties(self, playurl, listitem): - - window = utils.window - # Set all properties necessary for plugin path playback - item = self.item - itemid = item['Id'] - itemtype = item['Type'] - - embyitem = "emby_%s" % playurl - window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks'))) - window('%s.type' % embyitem, value=itemtype) - window('%s.itemid' % embyitem, value=itemid) - - if itemtype == "Episode": - window('%s.refreshid' % embyitem, value=item.get('SeriesId')) - else: - window('%s.refreshid' % embyitem, value=itemid) - - # Append external subtitles to stream - playmethod = utils.window('%s.playmethod' % embyitem) - # Only for direct stream - if playmethod in ("DirectStream"): - # Direct play automatically appends external - subtitles = self.externalSubs(playurl) - listitem.setSubtitles(subtitles) - - self.setArtwork(listitem) - - def externalSubs(self, playurl): - - externalsubs = [] - mapping = {} - - item = self.item - itemid = item['Id'] - try: - mediastreams = item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - kodiindex = 0 - for stream in mediastreams: - - index = stream['Index'] - # Since Emby returns all possible tracks together, have to pull only external subtitles. - # IsTextSubtitleStream if true, is available to download from emby. - if (stream['Type'] == "Subtitle" and - stream['IsExternal'] and stream['IsTextSubtitleStream']): - - # Direct stream - url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, index)) - - # map external subtitles for mapping - mapping[kodiindex] = index - externalsubs.append(url) - kodiindex += 1 - - mapping = json.dumps(mapping) - utils.window('emby_%s.indexMapping' % playurl, value=mapping) - - return externalsubs - - def setArtwork(self, listItem): - # Set up item and item info - item = self.item - artwork = self.artwork - - allartwork = artwork.getAllArtwork(item, parentInfo=True) - # Set artwork for listitem - arttypes = { - - 'poster': "Primary", - 'tvshow.poster': "Primary", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb" - } - for arttype in arttypes: - - art = arttypes[arttype] - if art == "Backdrop": - try: # Backdrop is a list, grab the first backdrop - self.setArtProp(listItem, arttype, allartwork[art][0]) - except: pass - else: - self.setArtProp(listItem, arttype, allartwork[art]) - - def setArtProp(self, listItem, arttype, path): - - if arttype in ( - 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators'): - - listItem.setProperty(arttype, path) - else: - listItem.setArt({arttype: path}) - - def setListItem(self, listItem): - - item = self.item - itemtype = item['Type'] - API = self.API - people = API.getPeople() - studios = API.getStudios() - - metadata = { - - 'title': item.get('Name', "Missing name"), - 'year': item.get('ProductionYear'), - 'plot': API.getOverview(), - 'director': people.get('Director'), - 'writer': people.get('Writer'), - 'mpaa': API.getMpaa(), - 'genre': " / ".join(item['Genres']), - 'studio': " / ".join(studios), - 'aired': API.getPremiereDate(), - 'rating': item.get('CommunityRating'), - 'votes': item.get('VoteCount') - } - - if "Episode" in itemtype: - # Only for tv shows - thumbId = item.get('SeriesId') - season = item.get('ParentIndexNumber', -1) - episode = item.get('IndexNumber', -1) - show = item.get('SeriesName', "") - - metadata['TVShowTitle'] = show - metadata['season'] = season - metadata['episode'] = episode - - listItem.setProperty('IsPlayable', 'true') - listItem.setProperty('IsFolder', 'false') - listItem.setLabel(metadata['title']) +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import sys + +import xbmc +import xbmcgui +import xbmcplugin + +import api +import artwork +import clientinfo +import downloadutils +import playutils as putils +import playlist +import read_embyserver as embyserver +import utils + +################################################################################################# + + +class PlaybackUtils(): + + + def __init__(self, item): + + self.item = item + self.API = api.API(self.item) + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils().downloadUrl + + self.userid = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userid) + + self.artwork = artwork.Artwork() + self.emby = embyserver.Read_EmbyServer() + self.pl = playlist.Playlist() + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def play(self, itemid, dbid=None): + + window = utils.window + settings = utils.settings + + listitem = xbmcgui.ListItem() + playutils = putils.PlayUtils(self.item) + + self.logMsg("Play called.", 1) + playurl = playutils.getPlayUrl() + if not playurl: + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) + + if dbid is None: + # Item is not in Kodi database + listitem.setPath(playurl) + self.setProperties(playurl, listitem) + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + + ############### ORGANIZE CURRENT PLAYLIST ################ + + homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + startPos = max(playlist.getposition(), 0) # Can return -1 + sizePlaylist = playlist.size() + currentPosition = startPos + + propertiesPlayback = window('emby_playbackProps') == "true" + introsPlaylist = False + dummyPlaylist = False + + 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 ################ + + userdata = self.API.getUserData() + seektime = self.API.adjustResume(userdata['Resume']) + + # We need to ensure we add the intro and additional parts only once. + # Otherwise we get a loop. + if not propertiesPlayback: + + window('emby_playbackProps', value="true") + self.logMsg("Setting up properties in playlist.", 1) + + if (not homeScreen and not seektime and + window('emby_customPlaylist') != "true"): + + self.logMsg("Adding dummy file to playlist.", 2) + dummyPlaylist = True + playlist.add(playurl, listitem, index=startPos) + # Remove the original item from playlist + self.pl.removefromPlaylist(startPos+1) + # Readd the original item to playlist - via jsonrpc so we have full metadata + self.pl.insertintoPlaylist(currentPosition+1, dbid, self.item['Type'].lower()) + currentPosition += 1 + + ############### -- CHECK FOR INTROS ################ + + if settings('enableCinema') == "true" and not seektime: + # if we have any play them when the movie/show is not being resumed + url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid + intros = self.doUtils(url) + + if intros['TotalRecordCount'] != 0: + getTrailers = True + + if settings('askCinema') == "true": + resp = xbmcgui.Dialog().yesno("Emby for Kodi", utils.language(33016)) + if not resp: + # User selected to not play trailers + getTrailers = False + self.logMsg("Skip trailers.", 1) + + if getTrailers: + for intro in intros['Items']: + # The server randomly returns intros, process them. + introListItem = xbmcgui.ListItem() + introPlayurl = putils.PlayUtils(intro).getPlayUrl() + self.logMsg("Adding Intro: %s" % introPlayurl, 1) + + # Set listitem and properties for intros + pbutils = PlaybackUtils(intro) + pbutils.setProperties(introPlayurl, introListItem) + + self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) + introsPlaylist = True + currentPosition += 1 + + + ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### + + if homeScreen and not seektime and not sizePlaylist: + # Extend our current playlist with the actual item to play + # only if there's no playlist first + self.logMsg("Adding main item to playlist.", 1) + self.pl.addtoPlaylist(dbid, self.item['Type'].lower()) + + # Ensure that additional parts are played after the main item + currentPosition += 1 + + ############### -- CHECK FOR ADDITIONAL PARTS ################ + + if self.item.get('PartCount'): + # Only add to the playlist after intros have played + partcount = self.item['PartCount'] + url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid + parts = self.doUtils(url) + for part in parts['Items']: + + additionalListItem = xbmcgui.ListItem() + additionalPlayurl = putils.PlayUtils(part).getPlayUrl() + self.logMsg("Adding additional part: %s" % partcount, 1) + + # Set listitem and properties for each additional parts + pbutils = PlaybackUtils(part) + pbutils.setProperties(additionalPlayurl, additionalListItem) + pbutils.setArtwork(additionalListItem) + + playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) + self.pl.verifyPlaylist() + currentPosition += 1 + + if dummyPlaylist: + # Added a dummy file to the playlist, + # because the first item is going to fail automatically. + 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: + self.logMsg("Resetting properties playback flag.", 2) + window('emby_playbackProps', clear=True) + + #self.pl.verifyPlaylist() + ########## SETUP MAIN ITEM ########## + + # For transcoding only, ask for audio/subs pref + if window('emby_%s.playmethod' % playurl) == "Transcode": + playurl = playutils.audioSubsPref(playurl, listitem) + window('emby_%s.playmethod' % playurl, value="Transcode") + + listitem.setPath(playurl) + self.setProperties(playurl, listitem) + + ############### PLAYBACK ################ + + if homeScreen and seektime and window('emby_customPlaylist') != "true": + self.logMsg("Play as a widget item.", 1) + self.setListItem(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. + self.logMsg("Play playlist.", 1) + xbmc.Player().play(playlist, startpos=startPos) + + else: + self.logMsg("Play as a regular item.", 1) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + + def setProperties(self, playurl, listitem): + + window = utils.window + # Set all properties necessary for plugin path playback + itemid = self.item['Id'] + itemtype = self.item['Type'] + + embyitem = "emby_%s" % playurl + window('%s.runtime' % embyitem, value=str(self.item.get('RunTimeTicks'))) + window('%s.type' % embyitem, value=itemtype) + window('%s.itemid' % embyitem, value=itemid) + + if itemtype == "Episode": + window('%s.refreshid' % embyitem, value=self.item.get('SeriesId')) + else: + window('%s.refreshid' % embyitem, value=itemid) + + # Append external subtitles to stream + playmethod = utils.window('%s.playmethod' % embyitem) + # Only for direct stream + if playmethod in ("DirectStream"): + # Direct play automatically appends external + subtitles = self.externalSubs(playurl) + listitem.setSubtitles(subtitles) + + self.setArtwork(listitem) + + def externalSubs(self, playurl): + + externalsubs = [] + mapping = {} + + itemid = self.item['Id'] + try: + mediastreams = self.item['MediaSources'][0]['MediaStreams'] + except (TypeError, KeyError, IndexError): + return + + kodiindex = 0 + for stream in mediastreams: + + index = stream['Index'] + # Since Emby returns all possible tracks together, have to pull only external subtitles. + # IsTextSubtitleStream if true, is available to download from emby. + if (stream['Type'] == "Subtitle" and + stream['IsExternal'] and stream['IsTextSubtitleStream']): + + # Direct stream + url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (self.server, itemid, itemid, index)) + + # map external subtitles for mapping + mapping[kodiindex] = index + externalsubs.append(url) + kodiindex += 1 + + mapping = json.dumps(mapping) + utils.window('emby_%s.indexMapping' % playurl, value=mapping) + + return externalsubs + + def setArtwork(self, listItem): + # Set up item and item info + allartwork = self.artwork.getAllArtwork(self.item, parentInfo=True) + # Set artwork for listitem + arttypes = { + + 'poster': "Primary", + 'tvshow.poster': "Primary", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb" + } + for arttype in arttypes: + + art = arttypes[arttype] + if art == "Backdrop": + try: # Backdrop is a list, grab the first backdrop + self.setArtProp(listItem, arttype, allartwork[art][0]) + except: pass + else: + self.setArtProp(listItem, arttype, allartwork[art]) + + def setArtProp(self, listItem, arttype, path): + + if arttype in ( + 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', + 'medium_landscape', 'medium_poster', 'small_fanartimage', + 'medium_fanartimage', 'fanart_noindicators'): + + listItem.setProperty(arttype, path) + else: + listItem.setArt({arttype: path}) + + def setListItem(self, listItem): + + people = self.API.getPeople() + studios = self.API.getStudios() + + metadata = { + + 'title': self.item.get('Name', "Missing name"), + 'year': self.item.get('ProductionYear'), + 'plot': self.API.getOverview(), + 'director': people.get('Director'), + 'writer': people.get('Writer'), + 'mpaa': self.API.getMpaa(), + 'genre': " / ".join(self.item['Genres']), + 'studio': " / ".join(studios), + 'aired': self.API.getPremiereDate(), + 'rating': self.item.get('CommunityRating'), + 'votes': self.item.get('VoteCount') + } + + if "Episode" in self.item['Type']: + # Only for tv shows + thumbId = self.item.get('SeriesId') + season = self.item.get('ParentIndexNumber', -1) + episode = self.item.get('IndexNumber', -1) + show = self.item.get('SeriesName', "") + + metadata['TVShowTitle'] = show + metadata['season'] = season + metadata['episode'] = episode + + listItem.setProperty('IsPlayable', 'true') + listItem.setProperty('IsFolder', 'false') + listItem.setLabel(metadata['title']) listItem.setInfo('video', infoLabels=metadata) \ No newline at end of file diff --git a/resources/lib/player.py b/resources/lib/player.py index 2e648180..7f323460 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -1,520 +1,511 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json - -import xbmc -import xbmcgui - -import utils -import clientinfo -import downloadutils -import kodidb_functions as kodidb -import websocket_client as wsc - -################################################################################################# - - -class Player(xbmc.Player): - - # Borg - multiple instances, shared state - _shared_state = {} - - played_info = {} - playStats = {} - currentFile = None - - - def __init__(self): - - self.__dict__ = self._shared_state - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.ws = wsc.WebSocket_Client() - self.xbmcplayer = xbmc.Player() - - self.logMsg("Starting playback monitor.", 2) - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def GetPlayStats(self): - return self.playStats - - def onPlayBackStarted(self): - - log = self.logMsg - window = utils.window - # Will be called when xbmc starts playing a file - xbmcplayer = self.xbmcplayer - self.stopAll() - - # Get current file - try: - currentFile = xbmcplayer.getPlayingFile() - xbmc.sleep(300) - except: - currentFile = "" - count = 0 - while not currentFile: - xbmc.sleep(100) - try: - currentFile = xbmcplayer.getPlayingFile() - except: pass - - if count == 5: # try 5 times - log("Cancelling playback report...", 1) - break - else: count += 1 - - - if currentFile: - - self.currentFile = currentFile - - # We may need to wait for info to be set in kodi monitor - itemId = window("emby_%s.itemid" % currentFile) - tryCount = 0 - while not itemId: - - xbmc.sleep(200) - itemId = window("emby_%s.itemid" % currentFile) - if tryCount == 20: # try 20 times or about 10 seconds - log("Could not find itemId, cancelling playback report...", 1) - break - else: tryCount += 1 - - else: - log("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0) - - # Only proceed if an itemId was found. - embyitem = "emby_%s" % currentFile - runtime = window("%s.runtime" % embyitem) - refresh_id = window("%s.refreshid" % embyitem) - playMethod = window("%s.playmethod" % embyitem) - itemType = window("%s.type" % embyitem) - window('emby_skipWatched%s' % itemId, value="true") - - customseek = window('emby_customPlaylist.seektime') - if window('emby_customPlaylist') == "true" and customseek: - # Start at, when using custom playlist (play to Kodi from webclient) - log("Seeking to: %s" % customseek, 1) - xbmcplayer.seekTime(int(customseek)/10000000.0) - window('emby_customPlaylist.seektime', clear=True) - - seekTime = xbmcplayer.getTime() - - # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') - - # Postdata structure to send to Emby server - url = "{server}/emby/Sessions/Playing" - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'VolumeLevel': volume, - 'PositionTicks': int(seekTime * 10000000), - 'IsMuted': muted - } - - # Get the current audio track and subtitles - if playMethod == "Transcode": - # property set in PlayUtils.py - postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) - postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) - else: - # Get the current kodi audio and subtitles and convert to Emby equivalent - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') - - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - indexSubs = 0 - - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" - - # Postdata for the audio - postdata['AudioStreamIndex'] = indexAudio + 1 - - # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get Emby Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % embyitem) - - if mapping: # Set in playbackutils.py - - log("Mapping for external subtitles index: %s" % mapping, 2) - externalIndex = json.loads(mapping) - - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] - else: - # Internal subtitle currently selected - subindex = indexSubs - len(externalIndex) + audioTracks + 1 - postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 - else: - postdata['SubtitleStreamIndex'] = "" - - - # Post playback to server - log("Sending POST play started: %s." % postdata, 2) - self.doUtils(url, postBody=postdata, type="POST") - - # Ensure we do have a runtime - try: - runtime = int(runtime) - except ValueError: - runtime = xbmcplayer.getTotalTime() - log("Runtime is missing, Kodi runtime: %s" % runtime, 1) - - # Save data map for updates and position calls - data = { - - 'runtime': runtime, - 'item_id': itemId, - 'refresh_id': refresh_id, - 'currentfile': currentFile, - 'AudioStreamIndex': postdata['AudioStreamIndex'], - 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, - 'currentPosition': int(seekTime) - } - - self.played_info[currentFile] = data - log("ADDING_FILE: %s" % self.played_info, 1) - - # log some playback stats - '''if(itemType != None): - if(self.playStats.get(itemType) != None): - count = self.playStats.get(itemType) + 1 - self.playStats[itemType] = count - else: - self.playStats[itemType] = 1 - - if(playMethod != None): - if(self.playStats.get(playMethod) != None): - count = self.playStats.get(playMethod) + 1 - self.playStats[playMethod] = count - else: - self.playStats[playMethod] = 1''' - - def reportPlayback(self): - - log = self.logMsg - - log("reportPlayback Called", 2) - xbmcplayer = self.xbmcplayer - - # Get current file - currentFile = self.currentFile - data = self.played_info.get(currentFile) - - # only report playback if emby has initiated the playback (item_id has value) - if data: - # Get playback information - itemId = data['item_id'] - audioindex = data['AudioStreamIndex'] - subtitleindex = data['SubtitleStreamIndex'] - playTime = data['currentPosition'] - playMethod = data['playmethod'] - paused = data.get('paused', False) - - - # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') - - # Postdata for the websocketclient report - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'PositionTicks': int(playTime * 10000000), - 'IsPaused': paused, - 'VolumeLevel': volume, - 'IsMuted': muted - } - - if playMethod == "Transcode": - # Track can't be changed, keep reporting the same index - postdata['AudioStreamIndex'] = audioindex - postdata['AudioStreamIndex'] = subtitleindex - - else: - # Get current audio and subtitles track - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') - - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - indexSubs = 0 - - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" - - # Postdata for the audio - data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2 - - # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get Emby Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = utils.window("emby_%s.indexMapping" % currentFile) - - if mapping: # Set in PlaybackUtils.py - - log("Mapping for external subtitles index: %s" % mapping, 2) - externalIndex = json.loads(mapping) - - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - subindex = [externalIndex[str(indexSubs)]] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - else: - # Internal subtitle currently selected - subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - subindex = [indexSubs + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - else: - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 - - # Report progress via websocketclient - postdata = json.dumps(postdata) - log("Report: %s" % postdata, 2) - self.ws.sendProgressUpdate(postdata) - - def onPlayBackPaused(self): - - currentFile = self.currentFile - self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = True - - self.reportPlayback() - - def onPlayBackResumed(self): - - currentFile = self.currentFile - self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = False - - self.reportPlayback() - - def onPlayBackSeek(self, time, seekOffset): - # Make position when seeking a bit more accurate - currentFile = self.currentFile - self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) - - if self.played_info.get(currentFile): - position = self.xbmcplayer.getTime() - self.played_info[currentFile]['currentPosition'] = position - - self.reportPlayback() - - def onPlayBackStopped(self): - - log = self.logMsg - window = utils.window - # Will be called when user stops xbmc playing a file - log("ONPLAYBACK_STOPPED", 2) - window('emby_customPlaylist', clear=True) - window('emby_customPlaylist.seektime', clear=True) - window('emby_playbackProps', clear=True) - log("Clear playlist properties.", 1) - self.stopAll() - - def onPlayBackEnded(self): - # Will be called when xbmc stops playing a file - self.logMsg("ONPLAYBACK_ENDED", 2) - utils.window('emby_customPlaylist.seektime', clear=True) - self.stopAll() - - 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) - # 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) - - runtime = data['runtime'] - currentPosition = data['currentPosition'] - itemid = data['item_id'] - refresh_id = data['refresh_id'] - currentFile = data['currentfile'] - type = data['Type'] - playMethod = data['playmethod'] - - # Prevent manually mark as watched in Kodi monitor - utils.window('emby_skipWatched%s' % itemid, value="true") - - if currentPosition and runtime: - try: - percentComplete = (currentPosition * 10000000) / int(runtime) - except ZeroDivisionError: - # Runtime is 0. - percentComplete = 0 - - markPlayedAt = float(settings('markPlayed')) / 100 - log("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": - offerDelete = True - elif type == "Movie" and settings('deleteMovies') == "true": - offerDelete = True - - if settings('offerDelete') != "true": - # Delete could be disabled, even if the subsetting is enabled. - offerDelete = False - - if percentComplete >= markPlayedAt and offerDelete: - resp = xbmcgui.Dialog().yesno(lang(30091), lang(33015), autoclose=120000) - if not resp: - log("User skipped deletion.", 1) - continue - - url = "{server}/emby/Items/%s?format=json" % itemid - log("Deleting request: %s" % itemid, 1) - doUtils(url, type="DELETE") - - self.stopPlayback(data) - - # Stop transcoding - if playMethod == "Transcode": - log("Transcoding for %s terminated." % itemid, 1) - deviceId = self.clientInfo.getDeviceId() - url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId - doUtils(url, type="DELETE") - - self.played_info.clear() - - def stopPlayback(self, data): - - self.logMsg("stopPlayback called", 2) - - itemId = data['item_id'] - currentPosition = data['currentPosition'] - positionTicks = int(currentPosition * 10000000) - - url = "{server}/emby/Sessions/Playing/Stopped" - postdata = { - - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PositionTicks': positionTicks - } - self.doUtils(url, postBody=postdata, type="POST") \ No newline at end of file +# -*- coding: utf-8 -*- + +################################################################################################# + +import json + +import xbmc +import xbmcgui + +import utils +import clientinfo +import downloadutils +import kodidb_functions as kodidb +import websocket_client as wsc + +################################################################################################# + + +class Player(xbmc.Player): + + # Borg - multiple instances, shared state + _shared_state = {} + + played_info = {} + playStats = {} + currentFile = None + + + def __init__(self): + + self.__dict__ = self._shared_state + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.ws = wsc.WebSocket_Client() + self.xbmcplayer = xbmc.Player() + + self.logMsg("Starting playback monitor.", 2) + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def GetPlayStats(self): + return self.playStats + + def onPlayBackStarted(self): + + window = utils.window + # Will be called when xbmc starts playing a file + self.stopAll() + + # Get current file + try: + currentFile = self.xbmcplayer.getPlayingFile() + xbmc.sleep(300) + except: + currentFile = "" + count = 0 + while not currentFile: + xbmc.sleep(100) + try: + currentFile = self.xbmcplayer.getPlayingFile() + except: pass + + if count == 5: # try 5 times + self.logMsg("Cancelling playback report...", 1) + break + else: count += 1 + + + if currentFile: + + self.currentFile = currentFile + + # We may need to wait for info to be set in kodi monitor + itemId = window("emby_%s.itemid" % currentFile) + tryCount = 0 + while not itemId: + + xbmc.sleep(200) + itemId = window("emby_%s.itemid" % currentFile) + if tryCount == 20: # try 20 times or about 10 seconds + self.logMsg("Could not find itemId, cancelling playback report...", 1) + break + else: tryCount += 1 + + else: + self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0) + + # Only proceed if an itemId was found. + embyitem = "emby_%s" % currentFile + runtime = window("%s.runtime" % embyitem) + refresh_id = window("%s.refreshid" % embyitem) + playMethod = window("%s.playmethod" % embyitem) + itemType = window("%s.type" % embyitem) + window('emby_skipWatched%s' % itemId, value="true") + + customseek = window('emby_customPlaylist.seektime') + if window('emby_customPlaylist') == "true" and customseek: + # Start at, when using custom playlist (play to Kodi from webclient) + self.logMsg("Seeking to: %s" % customseek, 1) + self.xbmcplayer.seekTime(int(customseek)/10000000.0) + window('emby_customPlaylist.seektime', clear=True) + + seekTime = self.xbmcplayer.getTime() + + # Get playback volume + volume_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Application.GetProperties", + "params": { + + "properties": ["volume", "muted"] + } + } + result = xbmc.executeJSONRPC(json.dumps(volume_query)) + result = json.loads(result) + result = result.get('result') + + volume = result.get('volume') + muted = result.get('muted') + + # Postdata structure to send to Emby server + url = "{server}/emby/Sessions/Playing" + postdata = { + + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PlayMethod': playMethod, + 'VolumeLevel': volume, + 'PositionTicks': int(seekTime * 10000000), + 'IsMuted': muted + } + + # Get the current audio track and subtitles + if playMethod == "Transcode": + # property set in PlayUtils.py + postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) + postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) + else: + # Get the current kodi audio and subtitles and convert to Emby equivalent + tracks_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Player.GetProperties", + "params": { + + "playerid": 1, + "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + } + result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + result = json.loads(result) + result = result.get('result') + + try: # Audio tracks + indexAudio = result['currentaudiostream']['index'] + except (KeyError, TypeError): + indexAudio = 0 + + try: # Subtitles tracks + indexSubs = result['currentsubtitle']['index'] + except (KeyError, TypeError): + indexSubs = 0 + + try: # If subtitles are enabled + subsEnabled = result['subtitleenabled'] + except (KeyError, TypeError): + subsEnabled = "" + + # Postdata for the audio + postdata['AudioStreamIndex'] = indexAudio + 1 + + # Postdata for the subtitles + if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: + + # Number of audiotracks to help get Emby Index + audioTracks = len(xbmc.Player().getAvailableAudioStreams()) + mapping = window("%s.indexMapping" % embyitem) + + if mapping: # Set in playbackutils.py + + self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) + externalIndex = json.loads(mapping) + + if externalIndex.get(str(indexSubs)): + # If the current subtitle is in the mapping + postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] + else: + # Internal subtitle currently selected + subindex = indexSubs - len(externalIndex) + audioTracks + 1 + postdata['SubtitleStreamIndex'] = subindex + + else: # Direct paths enabled scenario or no external subtitles set + postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 + else: + postdata['SubtitleStreamIndex'] = "" + + + # Post playback to server + self.logMsg("Sending POST play started: %s." % postdata, 2) + self.doUtils(url, postBody=postdata, action_type="POST") + + # Ensure we do have a runtime + try: + runtime = int(runtime) + except ValueError: + runtime = self.xbmcplayer.getTotalTime() + self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1) + + # Save data map for updates and position calls + data = { + + 'runtime': runtime, + 'item_id': itemId, + 'refresh_id': refresh_id, + 'currentfile': currentFile, + 'AudioStreamIndex': postdata['AudioStreamIndex'], + 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], + 'playmethod': playMethod, + 'Type': itemType, + 'currentPosition': int(seekTime) + } + + self.played_info[currentFile] = data + self.logMsg("ADDING_FILE: %s" % self.played_info, 1) + + # log some playback stats + '''if(itemType != None): + if(self.playStats.get(itemType) != None): + count = self.playStats.get(itemType) + 1 + self.playStats[itemType] = count + else: + self.playStats[itemType] = 1 + + if(playMethod != None): + if(self.playStats.get(playMethod) != None): + count = self.playStats.get(playMethod) + 1 + self.playStats[playMethod] = count + else: + self.playStats[playMethod] = 1''' + + def reportPlayback(self): + + self.logMsg("reportPlayback Called", 2) + + # Get current file + currentFile = self.currentFile + data = self.played_info.get(currentFile) + + # only report playback if emby has initiated the playback (item_id has value) + if data: + # Get playback information + itemId = data['item_id'] + audioindex = data['AudioStreamIndex'] + subtitleindex = data['SubtitleStreamIndex'] + playTime = data['currentPosition'] + playMethod = data['playmethod'] + paused = data.get('paused', False) + + + # Get playback volume + volume_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Application.GetProperties", + "params": { + + "properties": ["volume", "muted"] + } + } + result = xbmc.executeJSONRPC(json.dumps(volume_query)) + result = json.loads(result) + result = result.get('result') + + volume = result.get('volume') + muted = result.get('muted') + + # Postdata for the websocketclient report + postdata = { + + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PlayMethod': playMethod, + 'PositionTicks': int(playTime * 10000000), + 'IsPaused': paused, + 'VolumeLevel': volume, + 'IsMuted': muted + } + + if playMethod == "Transcode": + # Track can't be changed, keep reporting the same index + postdata['AudioStreamIndex'] = audioindex + postdata['AudioStreamIndex'] = subtitleindex + + else: + # Get current audio and subtitles track + tracks_query = { + + "jsonrpc": "2.0", + "id": 1, + "method": "Player.GetProperties", + "params": { + + "playerid": 1, + "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + } + result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + result = json.loads(result) + result = result.get('result') + + try: # Audio tracks + indexAudio = result['currentaudiostream']['index'] + except (KeyError, TypeError): + indexAudio = 0 + + try: # Subtitles tracks + indexSubs = result['currentsubtitle']['index'] + except (KeyError, TypeError): + indexSubs = 0 + + try: # If subtitles are enabled + subsEnabled = result['subtitleenabled'] + except (KeyError, TypeError): + subsEnabled = "" + + # Postdata for the audio + data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2 + + # Postdata for the subtitles + if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: + + # Number of audiotracks to help get Emby Index + audioTracks = len(xbmc.Player().getAvailableAudioStreams()) + mapping = utils.window("emby_%s.indexMapping" % currentFile) + + if mapping: # Set in PlaybackUtils.py + + self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) + externalIndex = json.loads(mapping) + + if externalIndex.get(str(indexSubs)): + # If the current subtitle is in the mapping + subindex = [externalIndex[str(indexSubs)]] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + else: + # Internal subtitle currently selected + subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + + else: # Direct paths enabled scenario or no external subtitles set + subindex = [indexSubs + audioTracks + 1] * 2 + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex + else: + data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 + + # Report progress via websocketclient + postdata = json.dumps(postdata) + self.logMsg("Report: %s" % postdata, 2) + self.ws.sendProgressUpdate(postdata) + + def onPlayBackPaused(self): + + currentFile = self.currentFile + self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) + + if self.played_info.get(currentFile): + self.played_info[currentFile]['paused'] = True + + self.reportPlayback() + + def onPlayBackResumed(self): + + currentFile = self.currentFile + self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) + + if self.played_info.get(currentFile): + self.played_info[currentFile]['paused'] = False + + self.reportPlayback() + + def onPlayBackSeek(self, time, seekOffset): + # Make position when seeking a bit more accurate + currentFile = self.currentFile + self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) + + if self.played_info.get(currentFile): + position = self.xbmcplayer.getTime() + self.played_info[currentFile]['currentPosition'] = position + + self.reportPlayback() + + def onPlayBackStopped(self): + + window = utils.window + # Will be called when user stops xbmc playing a file + self.logMsg("ONPLAYBACK_STOPPED", 2) + window('emby_customPlaylist', clear=True) + window('emby_customPlaylist.seektime', clear=True) + window('emby_playbackProps', clear=True) + self.logMsg("Clear playlist properties.", 1) + self.stopAll() + + def onPlayBackEnded(self): + # Will be called when xbmc stops playing a file + self.logMsg("ONPLAYBACK_ENDED", 2) + utils.window('emby_customPlaylist.seektime', clear=True) + self.stopAll() + + def stopAll(self): + + lang = utils.language + settings = utils.settings + + if not self.played_info: + return + + 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: + + 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'] + media_type = data['Type'] + playMethod = data['playmethod'] + + # Prevent manually mark as watched in Kodi monitor + utils.window('emby_skipWatched%s' % itemid, value="true") + + if currentPosition and runtime: + try: + percentComplete = (currentPosition * 10000000) / int(runtime) + except ZeroDivisionError: + # Runtime is 0. + percentComplete = 0 + + markPlayedAt = float(settings('markPlayed')) / 100 + self.logMsg("Percent complete: %s Mark played at: %s" + % (percentComplete, markPlayedAt), 1) + + # Send the delete action to the server. + offerDelete = False + + if media_type == "Episode" and settings('deleteTV') == "true": + offerDelete = True + elif media_type == "Movie" and settings('deleteMovies') == "true": + offerDelete = True + + if settings('offerDelete') != "true": + # Delete could be disabled, even if the subsetting is enabled. + offerDelete = False + + if percentComplete >= markPlayedAt and offerDelete: + resp = xbmcgui.Dialog().yesno(lang(30091), lang(33015), autoclose=120000) + if not resp: + self.logMsg("User skipped deletion.", 1) + continue + + url = "{server}/emby/Items/%s?format=json" % itemid + self.logMsg("Deleting request: %s" % itemid, 1) + self.doUtils(url, action_type="DELETE") + + self.stopPlayback(data) + + # Stop transcoding + if playMethod == "Transcode": + self.logMsg("Transcoding for %s terminated." % itemid, 1) + deviceId = self.clientInfo.getDeviceId() + url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId + self.doUtils(url, action_type="DELETE") + + self.played_info.clear() + + def stopPlayback(self, data): + + self.logMsg("stopPlayback called", 2) + + itemId = data['item_id'] + currentPosition = data['currentPosition'] + positionTicks = int(currentPosition * 10000000) + + url = "{server}/emby/Sessions/Playing/Stopped" + postdata = { + + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PositionTicks': positionTicks + } + self.doUtils(url, postBody=postdata, action_type="POST") \ No newline at end of file diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index 383d34a8..bcd34a46 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -1,204 +1,196 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json - -import xbmc -import xbmcgui -import xbmcplugin - -import clientinfo -import playutils -import playbackutils -import embydb_functions as embydb -import read_embyserver as embyserver -import utils - -################################################################################################# - - -class Playlist(): - - - def __init__(self): - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - - self.userid = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userid) - - self.emby = embyserver.Read_EmbyServer() - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def playAll(self, itemids, startat): - - log = self.logMsg - window = utils.window - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - - player = xbmc.Player() - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - playlist.clear() - - log("---*** PLAY ALL ***---", 1) - log("Items: %s and start at: %s" % (itemids, startat), 1) - - started = False - window('emby_customplaylist', value="true") - - if startat != 0: - # Seek to the starting position - window('emby_customplaylist.seektime', str(startat)) - - for itemid in itemids: - embydb_item = emby_db.getItem_byId(itemid) - try: - dbid = embydb_item[0] - mediatype = embydb_item[4] - except TypeError: - # Item is not found in our database, add item manually - log("Item was not found in the database, manually adding item.", 1) - item = self.emby.getItem(itemid) - self.addtoPlaylist_xbmc(playlist, item) - else: - # Add to playlist - self.addtoPlaylist(dbid, mediatype) - - log("Adding %s to playlist." % itemid, 1) - - if not started: - started = True - player.play(playlist) - - self.verifyPlaylist() - embycursor.close() - - 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) - - player = xbmc.Player() - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - - for itemid in itemids: - embydb_item = emby_db.getItem_byId(itemid) - try: - dbid = embydb_item[0] - mediatype = embydb_item[4] - except TypeError: - # Item is not found in our database, add item manually - item = self.emby.getItem(itemid) - self.addtoPlaylist_xbmc(playlist, item) - else: - # Add to playlist - self.addtoPlaylist(dbid, mediatype) - - log("Adding %s to playlist." % itemid, 1) - - self.verifyPlaylist() - embycursor.close() - return playlist - - def addtoPlaylist(self, dbid=None, mediatype=None, url=None): - - pl = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Add", - 'params': { - - 'playlistid': 1 - } - } - if dbid is not None: - pl['params']['item'] = {'%sid' % mediatype: int(dbid)} - else: - pl['params']['item'] = {'file': url} - - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) - - def addtoPlaylist_xbmc(self, playlist, item): - - itemid = item['Id'] - playurl = playutils.PlayUtils(item).getPlayUrl() - if not playurl: - # Playurl failed - self.logMsg("Failed to retrieve playurl.", 1) - return - - self.logMsg("Playurl: %s" % playurl) - listitem = xbmcgui.ListItem() - playbackutils.PlaybackUtils(item).setProperties(playurl, listitem) - - playlist.add(playurl, listitem) - - def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None): - - pl = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Insert", - 'params': { - - 'playlistid': 1, - 'position': position - } - } - if dbid is not None: - pl['params']['item'] = {'%sid' % mediatype: int(dbid)} - else: - pl['params']['item'] = {'file': url} - - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) - - def verifyPlaylist(self): - - pl = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.GetItems", - 'params': { - - 'playlistid': 1 - } - } - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) - - def removefromPlaylist(self, position): - - pl = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Remove", - 'params': { - - 'playlistid': 1, - 'position': position - } - } - result = xbmc.executeJSONRPC(json.dumps(pl)) - self.logMsg(result, 2) \ No newline at end of file +# -*- coding: utf-8 -*- + +################################################################################################# + +import json + +import xbmc +import xbmcgui +import xbmcplugin + +import clientinfo +import playutils +import playbackutils +import embydb_functions as embydb +import read_embyserver as embyserver +import utils + +################################################################################################# + + +class Playlist(): + + + def __init__(self): + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + + self.userid = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userid) + + self.emby = embyserver.Read_EmbyServer() + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def playAll(self, itemids, startat): + + window = utils.window + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + + player = xbmc.Player() + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() + + self.logMsg("---*** PLAY ALL ***---", 1) + self.logMsg("Items: %s and start at: %s" % (itemids, startat), 1) + + started = False + window('emby_customplaylist', value="true") + + if startat != 0: + # Seek to the starting position + window('emby_customplaylist.seektime', str(startat)) + + for itemid in itemids: + embydb_item = emby_db.getItem_byId(itemid) + try: + dbid = embydb_item[0] + mediatype = embydb_item[4] + except TypeError: + # Item is not found in our database, add item manually + self.logMsg("Item was not found in the database, manually adding item.", 1) + item = self.emby.getItem(itemid) + self.addtoPlaylist_xbmc(playlist, item) + else: + # Add to playlist + self.addtoPlaylist(dbid, mediatype) + + self.logMsg("Adding %s to playlist." % itemid, 1) + + if not started: + started = True + player.play(playlist) + + self.verifyPlaylist() + embycursor.close() + + def modifyPlaylist(self, itemids): + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + + self.logMsg("---*** ADD TO PLAYLIST ***---", 1) + self.logMsg("Items: %s" % itemids, 1) + + player = xbmc.Player() + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + for itemid in itemids: + embydb_item = emby_db.getItem_byId(itemid) + try: + dbid = embydb_item[0] + mediatype = embydb_item[4] + except TypeError: + # Item is not found in our database, add item manually + item = self.emby.getItem(itemid) + self.addtoPlaylist_xbmc(playlist, item) + else: + # Add to playlist + self.addtoPlaylist(dbid, mediatype) + + self.logMsg("Adding %s to playlist." % itemid, 1) + + self.verifyPlaylist() + embycursor.close() + return playlist + + def addtoPlaylist(self, dbid=None, mediatype=None, url=None): + + pl = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "Playlist.Add", + 'params': { + + 'playlistid': 1 + } + } + if dbid is not None: + pl['params']['item'] = {'%sid' % mediatype: int(dbid)} + else: + pl['params']['item'] = {'file': url} + + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) + + def addtoPlaylist_xbmc(self, playlist, item): + + playurl = playutils.PlayUtils(item).getPlayUrl() + if not playurl: + # Playurl failed + self.logMsg("Failed to retrieve playurl.", 1) + return + + self.logMsg("Playurl: %s" % playurl) + listitem = xbmcgui.ListItem() + playbackutils.PlaybackUtils(item).setProperties(playurl, listitem) + + playlist.add(playurl, listitem) + + def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None): + + pl = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "Playlist.Insert", + 'params': { + + 'playlistid': 1, + 'position': position + } + } + if dbid is not None: + pl['params']['item'] = {'%sid' % mediatype: int(dbid)} + else: + pl['params']['item'] = {'file': url} + + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) + + def verifyPlaylist(self): + + pl = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "Playlist.GetItems", + 'params': { + + 'playlistid': 1 + } + } + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) + + def removefromPlaylist(self, position): + + pl = { + + 'jsonrpc': "2.0", + 'id': 1, + 'method': "Playlist.Remove", + 'params': { + + 'playlistid': 1, + 'position': position + } + } + self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 37332b16..622781d2 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -1,458 +1,426 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import xbmc -import xbmcgui -import xbmcvfs - -import clientinfo -import utils - -################################################################################################# - - -class PlayUtils(): - - - def __init__(self, item): - - self.item = item - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - - self.userid = utils.window('emby_currUser') - self.server = utils.window('emby_server%s' % self.userid) - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def getPlayUrl(self): - - log = self.logMsg - window = utils.window - - item = self.item - playurl = None - - if (item.get('Type') in ("Recording", "TvChannel") and - item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http"): - # Play LiveTV or recordings - log("File protocol is http (livetv).", 1) - playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, item['Id']) - window('emby_%s.playmethod' % playurl, value="Transcode") - - elif item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http": - # Only play as http, used for channels, or online hosting of content - log("File protocol is http.", 1) - playurl = self.httpPlay() - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isDirectPlay(): - - log("File is direct playing.", 1) - playurl = self.directPlay() - playurl = playurl.encode('utf-8') - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectPlay") - - elif self.isDirectStream(): - - log("File is direct streaming.", 1) - playurl = self.directStream() - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isTranscoding(): - - log("File is transcoding.", 1) - playurl = self.transcoding() - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="Transcode") - - return playurl - - def httpPlay(self): - # Audio, Video, Photo - item = self.item - server = self.server - - itemid = item['Id'] - mediatype = item['MediaType'] - - if mediatype == "Audio": - playurl = "%s/emby/Audio/%s/stream" % (server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) - - return playurl - - def isDirectPlay(self): - - log = self.logMsg - lang = utils.language - settings = utils.settings - dialog = xbmcgui.Dialog() - - item = self.item - - # Requirement: Filesystem, Accessible path - if settings('playFromStream') == "true": - # User forcing to play via HTTP - log("Can't direct play, play from HTTP enabled.", 1) - return False - - videotrack = item['MediaSources'][0]['Name'] - transcodeH265 = settings('transcodeH265') - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - resolution = int(videotrack.split("P", 1)[0]) - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265]), 1) - if res[transcodeH265] <= resolution: - return False - - canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay'] - # Make sure direct play is supported by the server - if not canDirectPlay: - log("Can't direct play, server doesn't allow/support it.", 1) - return False - - location = item['LocationType'] - if location == "FileSystem": - # Verify the path - if not self.fileExists(): - log("Unable to direct play.") - try: - count = int(settings('failCount')) - except ValueError: - count = 0 - log("Direct play failed: %s times." % count, 1) - - if count < 2: - # Let the user know that direct play failed - settings('failCount', value=str(count+1)) - dialog.notification( - heading="Emby for Kodi", - message=lang(33011), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - elif settings('playFromStream') != "true": - # Permanently set direct stream as true - settings('playFromStream', value="true") - settings('failCount', value="0") - dialog.notification( - heading="Emby for Kodi", - message=lang(33012), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - return False - - return True - - def directPlay(self): - - item = self.item - - try: - playurl = item['MediaSources'][0]['Path'] - except (IndexError, KeyError): - playurl = item['Path'] - - if item.get('VideoType'): - # Specific format modification - type = item['VideoType'] - - if type == "Dvd": - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif type == "BluRay": - playurl = "%s/BDMV/index.bdmv" % playurl - - # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") - - if "apple.com" in playurl: - USER_AGENT = "QuickTime/7.7.4" - playurl += "?|User-Agent=%s" % USER_AGENT - - return playurl - - 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) - - if xbmcvfs.exists(path): - log("Path exists.", 1) - return True - - elif ":" not in path: - log("Can't verify path, assumed linux. Still try to direct play.", 1) - return True - - else: - log("Failed to find file.", 1) - return False - - def isDirectStream(self): - - log = self.logMsg - - item = self.item - - videotrack = item['MediaSources'][0]['Name'] - transcodeH265 = utils.settings('transcodeH265') - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - resolution = int(videotrack.split("P", 1)[0]) - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265]), 1) - if res[transcodeH265] <= resolution: - return False - - # Requirement: BitRate, supported encoding - canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] - # Make sure the server supports it - if not canDirectStream: - return False - - # Verify the bitrate - if not self.isNetworkSufficient(): - log("The network speed is insufficient to direct stream file.", 1) - return False - - return True - - def directStream(self): - - item = self.item - server = self.server - - itemid = item['Id'] - itemtype = item['Type'] - - if 'Path' in item and item['Path'].endswith('.strm'): - # Allow strm loading when direct streaming - playurl = self.directPlay() - elif itemtype == "Audio": - playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) - - return playurl - - def isNetworkSufficient(self): - - log = self.logMsg - - settings = self.getBitrate()*1000 - - try: - sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) - except (KeyError, TypeError): - log("Bitrate value is missing.", 1) - else: - log("The add-on settings bitrate is: %s, the video bitrate required is: %s" - % (settings, sourceBitrate), 1) - if settings < sourceBitrate: - return False - - return True - - def isTranscoding(self): - - item = self.item - - canTranscode = item['MediaSources'][0]['SupportsTranscoding'] - # Make sure the server supports it - if not canTranscode: - return False - - return True - - def transcoding(self): - - item = self.item - - if 'Path' in item and item['Path'].endswith('.strm'): - # Allow strm loading when transcoding - playurl = self.directPlay() - else: - itemid = item['Id'] - deviceId = self.clientInfo.getDeviceId() - playurl = ( - "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" - % (self.server, itemid, itemid) - ) - playurl = ( - "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" - % (playurl, deviceId, self.getBitrate()*1000)) - - return playurl - - def getBitrate(self): - - # get the addon video quality - videoQuality = utils.settings('videoBitrate') - bitrate = { - - '0': 664, - '1': 996, - '2': 1320, - '3': 2000, - '4': 3200, - '5': 4700, - '6': 6200, - '7': 7700, - '8': 9200, - '9': 10700, - '10': 12200, - '11': 13700, - '12': 15200, - '13': 16700, - '14': 18200, - '15': 20000, - '16': 40000, - '17': 100000, - '18': 1000000 - } - - # max bit rate supported by server (max signed 32bit integer) - return bitrate.get(videoQuality, 2147483) - - def audioSubsPref(self, url, listitem): - - log = self.logMsg - lang = utils.language - dialog = xbmcgui.Dialog() - # For transcoding only - # Present the list of audio to select from - audioStreamsList = {} - audioStreams = [] - audioStreamsChannelsList = {} - subtitleStreamsList = {} - subtitleStreams = ['No subtitles'] - downloadableStreams = [] - selectAudioIndex = "" - selectSubsIndex = "" - playurlprefs = "%s" % url - - item = self.item - try: - mediasources = item['MediaSources'][0] - mediastreams = mediasources['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - for stream in mediastreams: - # Since Emby returns all possible tracks together, have to sort them. - index = stream['Index'] - type = stream['Type'] - - if 'Audio' in type: - codec = stream['Codec'] - channelLayout = stream.get('ChannelLayout', "") - - try: - track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) - except: - track = "%s - %s %s" % (index, codec, channelLayout) - - audioStreamsChannelsList[index] = stream['Channels'] - audioStreamsList[track] = index - audioStreams.append(track) - - elif 'Subtitle' in type: - try: - track = "%s - %s" % (index, stream['Language']) - except: - track = "%s - %s" % (index, stream['Codec']) - - default = stream['IsDefault'] - forced = stream['IsForced'] - downloadable = stream['IsTextSubtitleStream'] - - if default: - track = "%s - Default" % track - if forced: - track = "%s - Forced" % track - if downloadable: - downloadableStreams.append(index) - - subtitleStreamsList[track] = index - subtitleStreams.append(track) - - - if len(audioStreams) > 1: - resp = dialog.select(lang(33013), audioStreams) - if resp > -1: - # User selected audio - selected = audioStreams[resp] - selectAudioIndex = audioStreamsList[selected] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] - else: # There's only one audiotrack. - selectAudioIndex = audioStreamsList[audioStreams[0]] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - - if len(subtitleStreams) > 1: - resp = dialog.select(lang(33014), subtitleStreams) - if resp == 0: - # User selected no subtitles - pass - elif resp > -1: - # User selected subtitles - selected = subtitleStreams[resp] - selectSubsIndex = subtitleStreamsList[selected] - - # Load subtitles in the listitem if downloadable - if selectSubsIndex in downloadableStreams: - - itemid = item['Id'] - url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, selectSubsIndex))] - log("Set up subtitles: %s %s" % (selectSubsIndex, url), 1) - listitem.setSubtitles(url) - else: - # Burn subtitles - playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex - - else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") - - # Get number of channels for selected audio track - audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) - if audioChannels > 2: - playurlprefs += "&AudioBitrate=384000" - else: - playurlprefs += "&AudioBitrate=192000" - +# -*- coding: utf-8 -*- + +################################################################################################# + +import xbmc +import xbmcgui +import xbmcvfs + +import clientinfo +import utils + +################################################################################################# + + +class PlayUtils(): + + + def __init__(self, item): + + self.item = item + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + + self.userid = utils.window('emby_currUser') + self.server = utils.window('emby_server%s' % self.userid) + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def getPlayUrl(self): + + window = utils.window + + playurl = None + + if (self.item.get('Type') in ("Recording", "TvChannel") and + self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http"): + # Play LiveTV or recordings + self.logMsg("File protocol is http (livetv).", 1) + playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, self.item['Id']) + window('emby_%s.playmethod' % playurl, value="Transcode") + + elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http": + # Only play as http, used for channels, or online hosting of content + self.logMsg("File protocol is http.", 1) + playurl = self.httpPlay() + window('emby_%s.playmethod' % playurl, value="DirectStream") + + elif self.isDirectPlay(): + + self.logMsg("File is direct playing.", 1) + playurl = self.directPlay() + playurl = playurl.encode('utf-8') + # Set playmethod property + window('emby_%s.playmethod' % playurl, value="DirectPlay") + + elif self.isDirectStream(): + + self.logMsg("File is direct streaming.", 1) + playurl = self.directStream() + # Set playmethod property + window('emby_%s.playmethod' % playurl, value="DirectStream") + + elif self.isTranscoding(): + + self.logMsg("File is transcoding.", 1) + playurl = self.transcoding() + # Set playmethod property + window('emby_%s.playmethod' % playurl, value="Transcode") + + return playurl + + def httpPlay(self): + # Audio, Video, Photo + + itemid = self.item['Id'] + mediatype = self.item['MediaType'] + + if mediatype == "Audio": + playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid) + else: + playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid) + + return playurl + + def isDirectPlay(self): + + lang = utils.language + settings = utils.settings + dialog = xbmcgui.Dialog() + + + # Requirement: Filesystem, Accessible path + if settings('playFromStream') == "true": + # User forcing to play via HTTP + self.logMsg("Can't direct play, play from HTTP enabled.", 1) + return False + + videotrack = self.item['MediaSources'][0]['Name'] + transcodeH265 = settings('transcodeH265') + + if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): + # Avoid H265/HEVC depending on the resolution + resolution = int(videotrack.split("P", 1)[0]) + res = { + + '1': 480, + '2': 720, + '3': 1080 + } + self.logMsg("Resolution is: %sP, transcode for resolution: %sP+" + % (resolution, res[transcodeH265]), 1) + if res[transcodeH265] <= resolution: + return False + + canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay'] + # Make sure direct play is supported by the server + if not canDirectPlay: + self.logMsg("Can't direct play, server doesn't allow/support it.", 1) + return False + + location = self.item['LocationType'] + if location == "FileSystem": + # Verify the path + if not self.fileExists(): + self.logMsg("Unable to direct play.") + try: + count = int(settings('failCount')) + except ValueError: + count = 0 + self.logMsg("Direct play failed: %s times." % count, 1) + + if count < 2: + # Let the user know that direct play failed + settings('failCount', value=str(count+1)) + dialog.notification( + heading="Emby for Kodi", + message=lang(33011), + icon="special://home/addons/plugin.video.emby/icon.png", + sound=False) + elif settings('playFromStream') != "true": + # Permanently set direct stream as true + settings('playFromStream', value="true") + settings('failCount', value="0") + dialog.notification( + heading="Emby for Kodi", + message=lang(33012), + icon="special://home/addons/plugin.video.emby/icon.png", + sound=False) + return False + + return True + + def directPlay(self): + + try: + playurl = self.item['MediaSources'][0]['Path'] + except (IndexError, KeyError): + playurl = self.item['Path'] + + if self.item.get('VideoType'): + # Specific format modification + if self.item['VideoType'] == "Dvd": + playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl + elif self.item['VideoType'] == "BluRay": + playurl = "%s/BDMV/index.bdmv" % playurl + + # Assign network protocol + if playurl.startswith('\\\\'): + playurl = playurl.replace("\\\\", "smb://") + playurl = playurl.replace("\\", "/") + + if "apple.com" in playurl: + USER_AGENT = "QuickTime/7.7.4" + playurl += "?|User-Agent=%s" % USER_AGENT + + return playurl + + def fileExists(self): + + if 'Path' not in self.item: + # File has no path defined in server + return False + + # Convert path to direct play + path = self.directPlay() + self.logMsg("Verifying path: %s" % path, 1) + + if xbmcvfs.exists(path): + self.logMsg("Path exists.", 1) + return True + + elif ":" not in path: + self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1) + return True + + else: + self.logMsg("Failed to find file.", 1) + return False + + def isDirectStream(self): + + + videotrack = self.item['MediaSources'][0]['Name'] + transcodeH265 = utils.settings('transcodeH265') + + if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): + # Avoid H265/HEVC depending on the resolution + resolution = int(videotrack.split("P", 1)[0]) + res = { + + '1': 480, + '2': 720, + '3': 1080 + } + self.logMsg("Resolution is: %sP, transcode for resolution: %sP+" + % (resolution, res[transcodeH265]), 1) + if res[transcodeH265] <= resolution: + return False + + # Requirement: BitRate, supported encoding + canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream'] + # Make sure the server supports it + if not canDirectStream: + return False + + # Verify the bitrate + if not self.isNetworkSufficient(): + self.logMsg("The network speed is insufficient to direct stream file.", 1) + return False + + return True + + def directStream(self): + + if 'Path' in self.item and self.item['Path'].endswith('.strm'): + # Allow strm loading when direct streaming + playurl = self.directPlay() + elif self.item['Type'] == "Audio": + playurl = "%s/emby/Audio/%s/stream.mp3" % (self.server, self.item['Id']) + else: + playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id']) + + return playurl + + def isNetworkSufficient(self): + + + settings = self.getBitrate()*1000 + + try: + sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) + except (KeyError, TypeError): + self.logMsg("Bitrate value is missing.", 1) + else: + self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s" + % (settings, sourceBitrate), 1) + if settings < sourceBitrate: + return False + + return True + + def isTranscoding(self): + # Make sure the server supports it + if not self.item['MediaSources'][0]['SupportsTranscoding']: + return False + + return True + + def transcoding(self): + + if 'Path' in self.item and self.item['Path'].endswith('.strm'): + # Allow strm loading when transcoding + playurl = self.directPlay() + else: + itemid = self.item['Id'] + deviceId = self.clientInfo.getDeviceId() + playurl = ( + "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" + % (self.server, itemid, itemid) + ) + playurl = ( + "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" + % (playurl, deviceId, self.getBitrate()*1000)) + + return playurl + + def getBitrate(self): + + # get the addon video quality + bitrate = { + + '0': 664, + '1': 996, + '2': 1320, + '3': 2000, + '4': 3200, + '5': 4700, + '6': 6200, + '7': 7700, + '8': 9200, + '9': 10700, + '10': 12200, + '11': 13700, + '12': 15200, + '13': 16700, + '14': 18200, + '15': 20000, + '16': 40000, + '17': 100000, + '18': 1000000 + } + + # max bit rate supported by server (max signed 32bit integer) + return bitrate.get(utils.settings('videoBitrate'), 2147483) + + def audioSubsPref(self, url, listitem): + + lang = utils.language + dialog = xbmcgui.Dialog() + # For transcoding only + # Present the list of audio to select from + audioStreamsList = {} + audioStreams = [] + audioStreamsChannelsList = {} + subtitleStreamsList = {} + subtitleStreams = ['No subtitles'] + downloadableStreams = [] + selectAudioIndex = "" + selectSubsIndex = "" + playurlprefs = "%s" % url + + try: + mediasources = self.item['MediaSources'][0] + mediastreams = mediasources['MediaStreams'] + except (TypeError, KeyError, IndexError): + return + + for stream in mediastreams: + # Since Emby returns all possible tracks together, have to sort them. + index = stream['Index'] + + if 'Audio' in stream['Type']: + codec = stream['Codec'] + channelLayout = stream.get('ChannelLayout', "") + + try: + track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) + except: + track = "%s - %s %s" % (index, codec, channelLayout) + + audioStreamsChannelsList[index] = stream['Channels'] + audioStreamsList[track] = index + audioStreams.append(track) + + elif 'Subtitle' in stream['Type']: + try: + track = "%s - %s" % (index, stream['Language']) + except: + track = "%s - %s" % (index, stream['Codec']) + + default = stream['IsDefault'] + forced = stream['IsForced'] + downloadable = stream['IsTextSubtitleStream'] + + if default: + track = "%s - Default" % track + if forced: + track = "%s - Forced" % track + if downloadable: + downloadableStreams.append(index) + + subtitleStreamsList[track] = index + subtitleStreams.append(track) + + + if len(audioStreams) > 1: + resp = dialog.select(lang(33013), audioStreams) + if resp > -1: + # User selected audio + selected = audioStreams[resp] + selectAudioIndex = audioStreamsList[selected] + playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex + else: # User backed out of selection + playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] + else: # There's only one audiotrack. + selectAudioIndex = audioStreamsList[audioStreams[0]] + playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex + + if len(subtitleStreams) > 1: + resp = dialog.select(lang(33014), subtitleStreams) + if resp == 0: + # User selected no subtitles + pass + elif resp > -1: + # User selected subtitles + selected = subtitleStreams[resp] + selectSubsIndex = subtitleStreamsList[selected] + + # Load subtitles in the listitem if downloadable + if selectSubsIndex in downloadableStreams: + + itemid = self.item['Id'] + url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (self.server, itemid, itemid, selectSubsIndex))] + self.logMsg("Set up subtitles: %s %s" % (selectSubsIndex, url), 1) + listitem.setSubtitles(url) + else: + # Burn subtitles + playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex + + else: # User backed out of selection + playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") + + # Get number of channels for selected audio track + audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) + if audioChannels > 2: + playurlprefs += "&AudioBitrate=384000" + else: + playurlprefs += "&AudioBitrate=192000" + return playurlprefs \ No newline at end of file diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index 13c976a1..6eeb5fb6 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -1,601 +1,538 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import xbmc - -import utils -import clientinfo -import downloadutils - -################################################################################################# - - -class Read_EmbyServer(): - - limitIndex = int(utils.settings('limitindex')) - - - def __init__(self): - - window = utils.window - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - self.userId = window('emby_currUser') - self.server = window('emby_server%s' % self.userId) - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def split_list(self, itemlist, size): - # Split up list in pieces of size. Will generate a list of lists - return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] - - - def getItem(self, itemid): - # This will return the full item - item = {} - - url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid - result = self.doUtils(url) - if result: - item = result - - return item - - def getItems(self, itemlist): - - items = [] - - itemlists = self.split_list(itemlist, 50) - for itemlist in itemlists: - # Will return basic information - url = "{server}/emby/Users/{UserId}/Items?&format=json" - params = { - - 'Ids': ",".join(itemlist), - 'Fields': "Etag" - } - result = self.doUtils(url, parameters=params) - if result: - items.extend(result['Items']) - - return items - - def getFullItems(self, itemlist): - - items = [] - - itemlists = self.split_list(itemlist, 50) - for itemlist in itemlists: - - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - "Ids": ",".join(itemlist), - "Fields": ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources" - ) - } - result = self.doUtils(url, parameters=params) - if result: - items.extend(result['Items']) - - return items - - def getView_embyId(self, itemid): - # Returns ancestors using embyId - viewId = None - url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid - result = self.doUtils(url) - - for view in result: - - viewtype = view['Type'] - if viewtype == "CollectionFolder": - # Found view - viewId = view['Id'] - - # Compare to view table in emby database - emby = utils.kodiSQL('emby') - cursor_emby = emby.cursor() - query = ' '.join(( - - "SELECT view_name, media_type", - "FROM view", - "WHERE view_id = ?" - )) - cursor_emby.execute(query, (viewId,)) - result = cursor_emby.fetchone() - try: - viewName = result[0] - mediatype = result[1] - except TypeError: - viewName = None - mediatype = None - - cursor_emby.close() - - return [viewName, viewId, mediatype] - - def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""): - doUtils = self.doUtils - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': recursive, - 'Limit': limit, - 'SortBy': sortby, - 'SortOrder': sortorder, - 'Filters': filter, - 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") - } - return doUtils(url, parameters=params) - - def getTvChannels(self): - doUtils = self.doUtils - url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" - params = { - - 'EnableImages': True, - 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") - } - return doUtils(url, parameters=params) - - def getTvRecordings(self, groupid): - doUtils = self.doUtils - url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" - if groupid == "root": groupid = "" - params = { - - 'GroupId': groupid, - 'EnableImages': True, - 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") - } - return doUtils(url, parameters=params) - - def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None): - - log = self.logMsg - - doUtils = self.doUtils - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - # Get total number of items - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'Limit': 1 - } - result = 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) - - else: - index = 0 - jump = self.limitIndex - throttled = False - highestjump = 0 - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': sortby, - 'SortOrder': "Ascending", - } - if basic: - params['Fields'] = "Etag" - else: - params['Fields'] = ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources" - ) - result = 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) - - if jump == highestjump: - # We already tried with the highestjump, but it failed. Reset value. - log("Reset highest value.", 1) - highestjump = 0 - - # Lower the number by half - if highestjump: - throttled = False - jump = highestjump - log("Throttle deactivated.", 1) - else: - jump = int(jump/4) - log("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) - return items - - retry += 1 - if xbmc.Monitor().waitForAbort(1): - # Abort was requested while waiting. - return items - else: - # Request succeeded - index += jump - - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - - if jump > highestjump: - # Adjust with the latest number, if it's greater - highestjump = jump - - if throttled: - # We needed to adjust the number of item requested. - # keep increasing until the connection times out again - # to find the highest value - increment = int(jump*0.33) - if not increment: # Incase the increment is 0 - increment = 10 - - jump += increment - log("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() - - if not root: - url = "{server}/emby/Users/{UserId}/Views?format=json" - else: # Views ungrouped - url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" - - result = doUtils(url) - try: - items = result['Items'] - except TypeError: - self.logMsg("Error retrieving views for type: %s" % mediatype, 2) - else: - for item in items: - - name = item['Name'] - itemId = item['Id'] - viewtype = item['Type'] - - if viewtype == "Channel": - # Filter view types - continue - - # 3/4/2016 OriginalCollectionType is added - itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) - - # 11/29/2015 Remove this once OriginalCollectionType is added to stable server. - # Assumed missing is mixed then. - '''if itemtype is None: - url = "{server}/emby/Library/MediaFolders?format=json" - result = doUtils(url) - - for folder in result['Items']: - if itemId == folder['Id']: - itemtype = folder.get('CollectionType', "mixed")''' - - if name not in ('Collections', 'Trailers'): - - if sortedlist: - views.append({ - - 'name': name, - 'type': itemtype, - 'id': itemId - }) - - elif (itemtype == mediatype or - (itemtype == "mixed" and mediatype in ("movies", "tvshows"))): - - views.append({ - - 'name': name, - 'type': itemtype, - 'id': itemId - }) - - return views - - def verifyView(self, parentid, itemid): - - belongs = False - - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - 'ParentId': parentid, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'Ids': itemid - } - result = self.doUtils(url, parameters=params) - try: - total = result['TotalRecordCount'] - except TypeError: - # Something happened to the connection - pass - else: - if total: - belongs = True - - return belongs - - def getMovies(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog) - - return items - - def getBoxset(self, dialog=None): - - items = self.getSection(None, "BoxSet", dialog=dialog) - - return items - - def getMovies_byBoxset(self, boxsetid): - - items = self.getSection(boxsetid, "Movie") - - return items - - def getMusicVideos(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) - - return items - - def getHomeVideos(self, parentId): - - items = self.getSection(parentId, "Video") - - return items - - def getShows(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "Series", basic=basic, dialog=dialog) - - return items - - def getSeasons(self, showId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId - params = { - - 'IsVirtualUnaired': False, - 'Fields': "Etag" - } - result = self.doUtils(url, parameters=params) - if result: - items = result - - return items - - def getEpisodes(self, parentId, basic=False, dialog=None): - - items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog) - - return items - - def getEpisodesbyShow(self, showId): - - items = self.getSection(showId, "Episode") - - return items - - def getEpisodesbySeason(self, seasonId): - - items = self.getSection(seasonId, "Episode") - - return items - - def getArtists(self, dialog=None): - - doUtils = self.doUtils - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - # Get total number of items - url = "{server}/emby/Artists?UserId={UserId}&format=json" - params = { - - 'Recursive': True, - 'Limit': 1 - } - result = doUtils(url, parameters=params) - try: - total = result['TotalRecordCount'] - items['TotalRecordCount'] = total - - except TypeError: # Failed to retrieve - self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2) - - else: - index = 1 - jump = self.limitIndex - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'Recursive': True, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': ( - - "Etag,Genres,SortName,Studios,Writer,ProductionYear," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," - "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" - ) - } - result = doUtils(url, parameters=params) - items['Items'].extend(result['Items']) - - index += jump - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - return items - - def getAlbums(self, basic=False, dialog=None): - - items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) - - return items - - def getAlbumsbyArtist(self, artistId): - - items = self.getSection(artistId, "MusicAlbum", sortby="DateCreated") - - return items - - def getSongs(self, basic=False, dialog=None): - - items = self.getSection(None, "Audio", basic=basic, dialog=dialog) - - return items - - def getSongsbyAlbum(self, albumId): - - items = self.getSection(albumId, "Audio") - - return items - - def getAdditionalParts(self, itemId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId - result = self.doUtils(url) - if result: - items = result - - return items - - def sortby_mediatype(self, itemids): - - sorted_items = {} - - # Sort items - items = self.getFullItems(itemids) - for item in items: - - mediatype = item.get('Type') - if mediatype: - sorted_items.setdefault(mediatype, []).append(item) - - return sorted_items - - def updateUserRating(self, itemid, 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") - elif favourite == False: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, 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") - elif deletelike: - url = "{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid - doUtils(url, type="DELETE") - - self.logMsg("Update user rating to emby for itemid: %s " - "| like: %s | favourite: %s | deletelike: %s" +# -*- coding: utf-8 -*- + +################################################################################################# + +import xbmc + +import utils +import clientinfo +import downloadutils + +################################################################################################# + + +class Read_EmbyServer(): + + limitIndex = int(utils.settings('limitindex')) + + + def __init__(self): + + window = utils.window + + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.doUtils = downloadutils.DownloadUtils().downloadUrl + + self.userId = window('emby_currUser') + self.server = window('emby_server%s' % self.userId) + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def split_list(self, itemlist, size): + # Split up list in pieces of size. Will generate a list of lists + return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] + + + def getItem(self, itemid): + # This will return the full item + item = {} + + result = self.doUtils("{server}/metaman/Users/{UserId}/Items/%s?format=json" % itemid) + if result: + item = result + + return item + + def getItems(self, itemlist): + + items = [] + + itemlists = self.split_list(itemlist, 50) + for itemlist in itemlists: + # Will return basic information + params = { + + 'Ids': ",".join(itemlist), + 'Fields': "Etag" + } + result = self.doUtils("{server}/emby/Users/{UserId}/Items?&format=json", parameters=params) + if result: + items.extend(result['Items']) + + return items + + def getFullItems(self, itemlist): + + items = [] + + itemlists = self.split_list(itemlist, 50) + for itemlist in itemlists: + + params = { + + "Ids": ",".join(itemlist), + "Fields": ( + + "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks," + "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," + "MediaSources" + ) + } + result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) + if result: + items.extend(result['Items']) + + return items + + def getView_embyId(self, itemid): + # Returns ancestors using embyId + viewId = None + + for view in self.doUtils("{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid): + + if view['Type'] == "CollectionFolder": + # Found view + viewId = view['Id'] + + # Compare to view table in emby database + emby = utils.kodiSQL('emby') + cursor_emby = emby.cursor() + query = ' '.join(( + + "SELECT view_name, media_type", + "FROM view", + "WHERE view_id = ?" + )) + cursor_emby.execute(query, (viewId,)) + result = cursor_emby.fetchone() + try: + viewName = result[0] + mediatype = result[1] + except TypeError: + viewName = None + mediatype = None + + cursor_emby.close() + + return [viewName, viewId, mediatype] + + def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""): + params = { + + 'ParentId': parentid, + 'IncludeItemTypes': itemtype, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'Recursive': recursive, + 'Limit': limit, + 'SortBy': sortby, + 'SortOrder': sortorder, + 'Filters': filter, + 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks," + "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") + } + return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) + + def getTvChannels(self): + params = { + + 'EnableImages': True, + 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks," + "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") + } + return self.doUtils("{server}/emby/LiveTv/Channels/?userid={UserId}&format=json", parameters=params) + + def getTvRecordings(self, groupid): + if groupid == "root": groupid = "" + params = { + + 'GroupId': groupid, + 'EnableImages': True, + 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks," + "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") + } + 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): + + items = { + + 'Items': [], + 'TotalRecordCount': 0 + } + + # Get total number of items + url = "{server}/emby/Users/{UserId}/Items?format=json" + params = { + + 'ParentId': parentid, + 'IncludeItemTypes': itemtype, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'Recursive': True, + 'Limit': 1 + } + result = self.doUtils(url, parameters=params) + try: + total = result['TotalRecordCount'] + items['TotalRecordCount'] = total + + except TypeError: # Failed to retrieve + self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2) + + else: + index = 0 + jump = self.limitIndex + throttled = False + highestjump = 0 + + while index < total: + # Get items by chunk to increase retrieval speed at scale + params = { + + 'ParentId': parentid, + 'IncludeItemTypes': itemtype, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'Recursive': True, + 'StartIndex': index, + 'Limit': jump, + 'SortBy': sortby, + 'SortOrder': "Ascending", + } + if basic: + params['Fields'] = "Etag" + else: + params['Fields'] = ( + + "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks," + "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," + "MediaSources" + ) + result = self.doUtils(url, parameters=params) + try: + items['Items'].extend(result['Items']) + except TypeError: + # Something happened to the connection + if not throttled: + throttled = True + self.logMsg("Throttle activated.", 1) + + if jump == highestjump: + # We already tried with the highestjump, but it failed. Reset value. + self.logMsg("Reset highest value.", 1) + highestjump = 0 + + # Lower the number by half + if highestjump: + throttled = False + jump = highestjump + self.logMsg("Throttle deactivated.", 1) + else: + jump = int(jump/4) + 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: + self.logMsg("Unable to reconnect to server. Abort process.", 1) + return items + + retry += 1 + if xbmc.Monitor().waitForAbort(1): + # Abort was requested while waiting. + return items + else: + # Request succeeded + index += jump + + if dialog: + percentage = int((float(index) / float(total))*100) + dialog.update(percentage) + + if jump > highestjump: + # Adjust with the latest number, if it's greater + highestjump = jump + + if throttled: + # We needed to adjust the number of item requested. + # keep increasing until the connection times out again + # to find the highest value + increment = int(jump*0.33) + if not increment: # Incase the increment is 0 + increment = 10 + + jump += increment + 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 + views = [] + mediatype = mediatype.lower() + + if not root: + url = "{server}/emby/Users/{UserId}/Views?format=json" + else: # Views ungrouped + url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" + + result = self.doUtils(url) + try: + items = result['Items'] + except TypeError: + self.logMsg("Error retrieving views for type: %s" % mediatype, 2) + else: + for item in items: + + item['Name'] = item['Name'] + if item['Type'] == "Channel": + # Filter view types + continue + + # 3/4/2016 OriginalCollectionType is added + itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) + + # 11/29/2015 Remove this once OriginalCollectionType is added to stable server. + # Assumed missing is mixed then. + '''if itemtype is None: + url = "{server}/emby/Library/MediaFolders?format=json" + result = self.doUtils(url) + + for folder in result['Items']: + if item['Id'] == folder['Id']: + itemtype = folder.get('CollectionType', "mixed")''' + + if item['Name'] not in ('Collections', 'Trailers'): + + if sortedlist: + views.append({ + + 'name': item['Name'], + 'type': itemtype, + 'id': item['Id'] + }) + + elif (itemtype == mediatype or + (itemtype == "mixed" and mediatype in ("movies", "tvshows"))): + + views.append({ + + 'name': item['Name'], + 'type': itemtype, + 'id': item['Id'] + }) + + return views + + def verifyView(self, parentid, itemid): + + belongs = False + params = { + + 'ParentId': parentid, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'Recursive': True, + 'Ids': itemid + } + result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) + try: + total = result['TotalRecordCount'] + except TypeError: + # Something happened to the connection + pass + else: + if total: + belongs = True + + return belongs + + def getMovies(self, parentId, basic=False, dialog=None): + return self.getSection(parentId, "Movie", basic=basic, dialog=dialog) + + def getBoxset(self, dialog=None): + return self.getSection(None, "BoxSet", dialog=dialog) + + def getMovies_byBoxset(self, boxsetid): + return self.getSection(boxsetid, "Movie") + + def getMusicVideos(self, parentId, basic=False, dialog=None): + return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) + + def getHomeVideos(self, parentId): + + return self.getSection(parentId, "Video") + + def getShows(self, parentId, basic=False, dialog=None): + return self.getSection(parentId, "Series", basic=basic, dialog=dialog) + + def getSeasons(self, showId): + + items = { + + 'Items': [], + 'TotalRecordCount': 0 + } + + params = { + + 'IsVirtualUnaired': False, + 'Fields': "Etag" + } + result = self.doUtils("{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId, parameters=params) + if result: + items = result + + return items + + def getEpisodes(self, parentId, basic=False, dialog=None): + + return self.getSection(parentId, "Episode", basic=basic, dialog=dialog) + + def getEpisodesbyShow(self, showId): + + return self.getSection(showId, "Episode") + + def getEpisodesbySeason(self, seasonId): + + return self.getSection(seasonId, "Episode") + + + def getArtists(self, dialog=None): + + items = { + + 'Items': [], + 'TotalRecordCount': 0 + } + + # Get total number of items + url = "{server}/emby/Artists?UserId={UserId}&format=json" + params = { + + 'Recursive': True, + 'Limit': 1 + } + result = self.doUtils(url, parameters=params) + try: + total = result['TotalRecordCount'] + items['TotalRecordCount'] = total + + except TypeError: # Failed to retrieve + self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2) + + else: + index = 1 + jump = self.limitIndex + + while index < total: + # Get items by chunk to increase retrieval speed at scale + params = { + + 'Recursive': True, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'StartIndex': index, + 'Limit': jump, + 'SortBy': "SortName", + 'SortOrder': "Ascending", + 'Fields': ( + + "Etag,Genres,SortName,Studios,Writer,ProductionYear," + "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," + "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" + ) + } + result = self.doUtils(url, parameters=params) + items['Items'].extend(result['Items']) + + index += jump + if dialog: + percentage = int((float(index) / float(total))*100) + dialog.update(percentage) + return items + + def getAlbums(self, basic=False, dialog=None): + return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) + + def getAlbumsbyArtist(self, artistId): + return self.getSection(artistId, "MusicAlbum", sortby="DateCreated") + + def getSongs(self, basic=False, dialog=None): + return self.getSection(None, "Audio", basic=basic, dialog=dialog) + + def getSongsbyAlbum(self, albumId): + return self.getSection(albumId, "Audio") + + + def getAdditionalParts(self, itemId): + + items = { + + 'Items': [], + 'TotalRecordCount': 0 + } + + result = self.doUtils("{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId) + if result: + items = result + + return items + + def sortby_mediatype(self, itemids): + + sorted_items = {} + + # Sort items + items = self.getFullItems(itemids) + for item in items: + + mediatype = item.get('Type') + if mediatype: + sorted_items.setdefault(mediatype, []).append(item) + + return sorted_items + + def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False): + # Updates the user rating to Emby + + if favourite: + self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="POST") + elif favourite == False: + self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="DELETE") + + if not deletelike and like: + 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: + self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid, action_type="DELETE") + + 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 diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index ecda0a9c..f068b772 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -1,487 +1,468 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import hashlib -import threading - -import xbmc -import xbmcgui -import xbmcaddon -import xbmcvfs - -import artwork -import utils -import clientinfo -import downloadutils - -################################################################################################## - - -class UserClient(threading.Thread): - - # Borg - multiple instances, shared state - _shared_state = {} - - stopClient = False - auth = True - retry = 0 - - currUser = None - currUserId = None - currServer = None - currToken = None - HasAccess = True - AdditionalUser = [] - - userSettings = None - - - def __init__(self): - - self.__dict__ = self._shared_state - self.addon = xbmcaddon.Addon() - - self.addonName = clientinfo.ClientInfo().getAddonName() - self.doUtils = downloadutils.DownloadUtils() - - threading.Thread.__init__(self) - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def getAdditionalUsers(self): - - additionalUsers = utils.settings('additionalUsers') - - if additionalUsers: - self.AdditionalUser = additionalUsers.split(',') - - def getUsername(self): - - username = utils.settings('username') - - if not username: - self.logMsg("No username saved.", 2) - return "" - - return username - - def getLogLevel(self): - - try: - logLevel = int(utils.settings('logLevel')) - except ValueError: - logLevel = 0 - - return logLevel - - def getUserId(self): - - log = self.logMsg - window = utils.window - settings = utils.settings - - username = self.getUsername() - w_userId = window('emby_currUser') - s_userId = settings('userId%s' % username) - - # Verify the window property - if w_userId: - if not s_userId: - # Save access token if it's missing from settings - settings('userId%s' % username, value=w_userId) - log("Returning userId from WINDOW for username: %s UserId: %s" - % (username, w_userId), 2) - return w_userId - # Verify the settings - elif s_userId: - log("Returning userId from SETTINGS for username: %s userId: %s" - % (username, s_userId), 2) - return s_userId - # No userId found - else: - log("No userId saved for username: %s." % username, 1) - - def getServer(self, prefix=True): - - settings = utils.settings - - alternate = settings('altip') == "true" - if alternate: - # Alternate host - HTTPS = settings('secondhttps') == "true" - host = settings('secondipaddress') - port = settings('secondport') - else: - # Original host - HTTPS = settings('https') == "true" - host = settings('ipaddress') - port = settings('port') - - server = host + ":" + port - - if not host: - self.logMsg("No server information saved.", 2) - return False - - # If https is true - if prefix and HTTPS: - server = "https://%s" % server - return server - # If https is false - elif prefix and not HTTPS: - server = "http://%s" % server - return server - # If only the host:port is required - elif not prefix: - return server - - def getToken(self): - - log = self.logMsg - window = utils.window - settings = utils.settings - - username = self.getUsername() - userId = self.getUserId() - w_token = window('emby_accessToken%s' % userId) - s_token = settings('accessToken') - - # Verify the window property - if w_token: - if not s_token: - # Save access token if it's missing from settings - settings('accessToken', value=w_token) - log("Returning accessToken from WINDOW for username: %s accessToken: %s" - % (username, w_token), 2) - return w_token - # Verify the settings - elif s_token: - log("Returning accessToken from SETTINGS for username: %s accessToken: %s" - % (username, s_token), 2) - window('emby_accessToken%s' % username, value=s_token) - return s_token - else: - log("No token found.", 1) - return "" - - def getSSLverify(self): - # Verify host certificate - settings = utils.settings - - s_sslverify = settings('sslverify') - if settings('altip') == "true": - s_sslverify = settings('secondsslverify') - - if s_sslverify == "true": - return True - else: - return False - - def getSSL(self): - # Client side certificate - settings = utils.settings - - s_cert = settings('sslcert') - if settings('altip') == "true": - s_cert = settings('secondsslcert') - - if s_cert == "None": - return None - else: - return s_cert - - def setUserPref(self): - - doUtils = self.doUtils.downloadUrl - art = artwork.Artwork() - - url = "{server}/emby/Users/{UserId}?format=json" - result = doUtils(url) - self.userSettings = result - # Set user image for skin display - if result.get('PrimaryImageTag'): - utils.window('EmbyUserImage', value=art.getUserArtwork(result['Id'], 'Primary')) - - # Set resume point max - url = "{server}/emby/System/Configuration?format=json" - result = doUtils(url) - - utils.settings('markPlayed', value=str(result['MaxResumePct'])) - - def getPublicUsers(self): - - server = self.getServer() - - # Get public Users - url = "%s/emby/Users/Public?format=json" % server - result = self.doUtils.downloadUrl(url, authenticate=False) - - if result != "": - return result - else: - # Server connection failed - return False - - def hasAccess(self): - # hasAccess is verified in service.py - log = self.logMsg - window = utils.window - - url = "{server}/emby/Users?format=json" - result = self.doUtils.downloadUrl(url) - - if result == False: - # Access is restricted, set in downloadutils.py via exception - log("Access is restricted.", 1) - self.HasAccess = False - - elif window('emby_online') != "true": - # Server connection failed - pass - - elif window('emby_serverStatus') == "restricted": - log("Access is granted.", 1) - self.HasAccess = True - window('emby_serverStatus', clear=True) - xbmcgui.Dialog().notification("Emby for Kodi", utils.language(33007)) - - def loadCurrUser(self, authenticated=False): - - window = utils.window - - doUtils = self.doUtils - username = self.getUsername() - userId = self.getUserId() - - # Only to be used if token exists - self.currUserId = userId - self.currServer = self.getServer() - self.currToken = self.getToken() - self.ssl = self.getSSLverify() - self.sslcert = self.getSSL() - - # Test the validity of current token - if authenticated == False: - url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) - window('emby_currUser', value=userId) - window('emby_accessToken%s' % userId, value=self.currToken) - result = doUtils.downloadUrl(url) - - if result == 401: - # Token is no longer valid - self.resetClient() - return False - - # Set to windows property - window('emby_currUser', value=userId) - window('emby_accessToken%s' % userId, value=self.currToken) - window('emby_server%s' % userId, value=self.currServer) - window('emby_server_%s' % userId, value=self.getServer(prefix=False)) - - # Set DownloadUtils values - doUtils.setUsername(username) - doUtils.setUserId(self.currUserId) - doUtils.setServer(self.currServer) - doUtils.setToken(self.currToken) - doUtils.setSSL(self.ssl, self.sslcert) - # parental control - let's verify if access is restricted - self.hasAccess() - # Start DownloadUtils session - doUtils.startSession() - self.getAdditionalUsers() - # Set user preferences in settings - self.currUser = username - self.setUserPref() - - - def authenticate(self): - - log = self.logMsg - lang = utils.language - window = utils.window - settings = utils.settings - dialog = xbmcgui.Dialog() - - # Get /profile/addon_data - addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') - hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) - - username = self.getUsername() - server = self.getServer() - - # If there's no settings.xml - if not hasSettings: - log("No settings.xml found.", 1) - self.auth = False - return - # If no user information - elif not server or not username: - log("Missing server information.", 1) - self.auth = False - return - # If there's a token, load the user - elif self.getToken(): - result = self.loadCurrUser() - - if result == False: - pass - else: - log("Current user: %s" % self.currUser, 1) - log("Current userId: %s" % self.currUserId, 1) - log("Current accessToken: %s" % self.currToken, 2) - return - - ##### AUTHENTICATE USER ##### - - users = self.getPublicUsers() - password = "" - - # Find user in list - for user in users: - name = user['Name'] - - if username.decode('utf-8') in name: - # If user has password - if user['HasPassword'] == True: - password = dialog.input( - heading="%s %s" % (lang(33008), username.decode('utf-8')), - option=xbmcgui.ALPHANUM_HIDE_INPUT) - # If password dialog is cancelled - if not password: - log("No password entered.", 0) - window('emby_serverStatus', value="Stop") - self.auth = False - return - break - else: - # Manual login, user is hidden - password = dialog.input( - heading="%s %s" % (lang(33008), username), - option=xbmcgui.ALPHANUM_HIDE_INPUT) - sha1 = hashlib.sha1(password) - sha1 = sha1.hexdigest() - - # Authenticate username and password - url = "%s/emby/Users/AuthenticateByName?format=json" % server - data = {'username': username, 'password': sha1} - log(data, 2) - - result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False) - - try: - log("Auth response: %s" % result, 1) - accessToken = result['AccessToken'] - - except (KeyError, TypeError): - log("Failed to retrieve the api key.", 1) - accessToken = None - - if accessToken is not None: - self.currUser = username - dialog.notification("Emby for Kodi", - "%s %s!" % (lang(33000), self.currUser.decode('utf-8'))) - userId = result['User']['Id'] - settings('accessToken', value=accessToken) - settings('userId%s' % username, value=userId) - log("User Authenticated: %s" % accessToken, 1) - self.loadCurrUser(authenticated=True) - window('emby_serverStatus', clear=True) - self.retry = 0 - else: - log("User authentication failed.", 1) - settings('accessToken', value="") - settings('userId%s' % username, value="") - dialog.ok(lang(33001), lang(33009)) - - # Give two attempts at entering password - if self.retry == 2: - log("Too many retries. " - "You can retry by resetting attempts in the addon settings.", 1) - window('emby_serverStatus', value="Stop") - dialog.ok(lang(33001), lang(33010)) - - self.retry += 1 - self.auth = False - - def resetClient(self): - - log = self.logMsg - - log("Reset UserClient authentication.", 1) - userId = self.getUserId() - - if self.currToken is not None: - # In case of 401, removed saved token - utils.settings('accessToken', value="") - utils.window('emby_accessToken%s' % userId, clear=True) - self.currToken = None - log("User token has been removed.", 1) - - self.auth = True - self.currUser = None - - def run(self): - - log = self.logMsg - window = utils.window - - monitor = xbmc.Monitor() - log("----===## Starting UserClient ##===----", 0) - - while not monitor.abortRequested(): - - status = window('emby_serverStatus') - if status: - # Verify the connection status to server - if status == "restricted": - # Parental control is restricting access - self.HasAccess = False - - elif status == "401": - # Unauthorized access, revoke token - window('emby_serverStatus', value="Auth") - self.resetClient() - - if self.auth and (self.currUser is None): - # Try to authenticate user - status = window('emby_serverStatus') - if not status or status == "Auth": - # Set auth flag because we no longer need - # to authenticate the user - self.auth = False - self.authenticate() - - - if not self.auth and (self.currUser is None): - # If authenticate failed. - server = self.getServer() - username = self.getUsername() - status = window('emby_serverStatus') - - # The status Stop is for when user cancelled password dialog. - if server and username and status != "Stop": - # Only if there's information found to login - log("Server found: %s" % server, 2) - log("Username found: %s" % username, 2) - self.auth = True - - - if self.stopClient == True: - # If stopping the client didn't work - break - - if monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - self.doUtils.stopSession() - log("##===---- UserClient Stopped ----===##", 0) - - def stopClient(self): - # When emby for kodi terminates +# -*- coding: utf-8 -*- + +################################################################################################## + +import hashlib +import threading + +import xbmc +import xbmcgui +import xbmcaddon +import xbmcvfs + +import artwork +import utils +import clientinfo +import downloadutils + +################################################################################################## + + +class UserClient(threading.Thread): + + # Borg - multiple instances, shared state + _shared_state = {} + + stopClient = False + auth = True + retry = 0 + + currUser = None + currUserId = None + currServer = None + currToken = None + HasAccess = True + AdditionalUser = [] + + userSettings = None + + + def __init__(self): + + self.__dict__ = self._shared_state + self.addon = xbmcaddon.Addon() + + self.addonName = clientinfo.ClientInfo().getAddonName() + self.doUtils = downloadutils.DownloadUtils() + + threading.Thread.__init__(self) + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def getAdditionalUsers(self): + + additionalUsers = utils.settings('additionalUsers') + + if additionalUsers: + self.AdditionalUser = additionalUsers.split(',') + + def getUsername(self): + + username = utils.settings('username') + + if not username: + self.logMsg("No username saved.", 2) + return "" + + return username + + def getLogLevel(self): + + try: + logLevel = int(utils.settings('logLevel')) + except ValueError: + logLevel = 0 + + return logLevel + + def getUserId(self): + + window = utils.window + settings = utils.settings + + username = self.getUsername() + w_userId = window('emby_currUser') + s_userId = settings('userId%s' % username) + + # Verify the window property + if w_userId: + if not s_userId: + # Save access token if it's missing from settings + settings('userId%s' % username, value=w_userId) + self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" + % (username, w_userId), 2) + return w_userId + # Verify the settings + elif s_userId: + self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" + % (username, s_userId), 2) + return s_userId + # No userId found + else: + self.logMsg("No userId saved for username: %s." % username, 1) + + def getServer(self, prefix=True): + + settings = utils.settings + + alternate = settings('altip') == "true" + if alternate: + # Alternate host + HTTPS = settings('secondhttps') == "true" + host = settings('secondipaddress') + port = settings('secondport') + else: + # Original host + HTTPS = settings('https') == "true" + host = settings('ipaddress') + port = settings('port') + + server = host + ":" + port + + if not host: + self.logMsg("No server information saved.", 2) + return False + + # If https is true + if prefix and HTTPS: + server = "https://%s" % server + return server + # If https is false + elif prefix and not HTTPS: + server = "http://%s" % server + return server + # If only the host:port is required + elif not prefix: + return server + + def getToken(self): + + window = utils.window + settings = utils.settings + + username = self.getUsername() + userId = self.getUserId() + w_token = window('emby_accessToken%s' % userId) + s_token = settings('accessToken') + + # Verify the window property + if w_token: + if not s_token: + # Save access token if it's missing from settings + settings('accessToken', value=w_token) + self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" + % (username, w_token), 2) + return w_token + # Verify the settings + elif s_token: + self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" + % (username, s_token), 2) + window('emby_accessToken%s' % username, value=s_token) + return s_token + else: + self.logMsg("No token found.", 1) + return "" + + def getSSLverify(self): + # Verify host certificate + settings = utils.settings + + s_sslverify = settings('sslverify') + if settings('altip') == "true": + s_sslverify = settings('secondsslverify') + + if s_sslverify == "true": + return True + else: + return False + + def getSSL(self): + # Client side certificate + settings = utils.settings + + s_cert = settings('sslcert') + if settings('altip') == "true": + s_cert = settings('secondsslcert') + + if s_cert == "None": + return None + else: + return s_cert + + def setUserPref(self): + + doUtils = self.doUtils.downloadUrl + + result = doUtils("{server}/emby/Users/{UserId}?format=json") + self.userSettings = result + # Set user image for skin display + if result.get('PrimaryImageTag'): + utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary')) + + # Set resume point max + result = doUtils("{server}/emby/System/Configuration?format=json") + + utils.settings('markPlayed', value=str(result['MaxResumePct'])) + + def getPublicUsers(self): + # Get public Users + result = self.doUtils.downloadUrl("%s/emby/Users/Public?format=json" % self.getServer(), authenticate=False) + if result != "": + return result + else: + # Server connection failed + return False + + + def hasAccess(self): + # hasAccess is verified in service.py + window = utils.window + + result = self.doUtils.downloadUrl("{server}/emby/Users?format=json") + + if result == False: + # Access is restricted, set in downloadutils.py via exception + self.logMsg("Access is restricted.", 1) + self.HasAccess = False + + elif window('emby_online') != "true": + # Server connection failed + pass + + elif window('emby_serverStatus') == "restricted": + self.logMsg("Access is granted.", 1) + self.HasAccess = True + window('emby_serverStatus', clear=True) + xbmcgui.Dialog().notification("Emby for Kodi", utils.language(33007)) + + def loadCurrUser(self, authenticated=False): + + window = utils.window + + doUtils = self.doUtils + username = self.getUsername() + userId = self.getUserId() + + # Only to be used if token exists + self.currUserId = userId + self.currServer = self.getServer() + self.currToken = self.getToken() + self.ssl = self.getSSLverify() + self.sslcert = self.getSSL() + + # Test the validity of current token + if authenticated == False: + url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) + window('emby_currUser', value=userId) + window('emby_accessToken%s' % userId, value=self.currToken) + result = doUtils.downloadUrl(url) + + if result == 401: + # Token is no longer valid + self.resetClient() + return False + + # Set to windows property + window('emby_currUser', value=userId) + window('emby_accessToken%s' % userId, value=self.currToken) + window('emby_server%s' % userId, value=self.currServer) + window('emby_server_%s' % userId, value=self.getServer(prefix=False)) + + # Set DownloadUtils values + doUtils.setUsername(username) + doUtils.setUserId(self.currUserId) + doUtils.setServer(self.currServer) + doUtils.setToken(self.currToken) + doUtils.setSSL(self.ssl, self.sslcert) + # parental control - let's verify if access is restricted + self.hasAccess() + # Start DownloadUtils session + doUtils.startSession() + self.getAdditionalUsers() + # Set user preferences in settings + self.currUser = username + self.setUserPref() + + + def authenticate(self): + + lang = utils.language + window = utils.window + settings = utils.settings + dialog = xbmcgui.Dialog() + + # Get /profile/addon_data + addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') + hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) + + username = self.getUsername() + server = self.getServer() + + # If there's no settings.xml + if not hasSettings: + self.logMsg("No settings.xml found.", 1) + self.auth = False + return + # If no user information + elif not server or not username: + self.logMsg("Missing server information.", 1) + self.auth = False + return + # If there's a token, load the user + elif self.getToken(): + result = self.loadCurrUser() + + if result is False: + pass + else: + self.logMsg("Current user: %s" % self.currUser, 1) + self.logMsg("Current userId: %s" % self.currUserId, 1) + self.logMsg("Current accessToken: %s" % self.currToken, 2) + return + + ##### AUTHENTICATE USER ##### + + users = self.getPublicUsers() + password = "" + + # Find user in list + for user in users: + name = user['Name'] + + if username.decode('utf-8') in name: + # If user has password + if user['HasPassword'] == True: + password = dialog.input( + heading="%s %s" % (lang(33008), username.decode('utf-8')), + option=xbmcgui.ALPHANUM_HIDE_INPUT) + # If password dialog is cancelled + if not password: + self.logMsg("No password entered.", 0) + window('emby_serverStatus', value="Stop") + self.auth = False + return + break + else: + # Manual login, user is hidden + password = dialog.input( + heading="%s %s" % (lang(33008), username), + option=xbmcgui.ALPHANUM_HIDE_INPUT) + sha1 = hashlib.sha1(password) + sha1 = sha1.hexdigest() + + # Authenticate username and password + data = {'username': username, 'password': sha1} + self.logMsg(data, 2) + + result = self.doUtils.downloadUrl("%s/emby/Users/AuthenticateByName?format=json" % server, postBody=data, action_type="POST", authenticate=False) + + try: + self.logMsg("Auth response: %s" % result, 1) + accessToken = result['AccessToken'] + + except (KeyError, TypeError): + self.logMsg("Failed to retrieve the api key.", 1) + accessToken = None + + if accessToken is not None: + self.currUser = username + dialog.notification("Emby for Kodi", + "%s %s!" % (lang(33000), self.currUser.decode('utf-8'))) + settings('accessToken', value=accessToken) + settings('userId%s' % username, value=result['User']['Id']) + self.logMsg("User Authenticated: %s" % accessToken, 1) + self.loadCurrUser(authenticated=True) + window('emby_serverStatus', clear=True) + self.retry = 0 + else: + self.logMsg("User authentication failed.", 1) + settings('accessToken', value="") + settings('userId%s' % username, value="") + dialog.ok(lang(33001), lang(33009)) + + # Give two attempts at entering password + if self.retry == 2: + self.logMsg("Too many retries. " + "You can retry by resetting attempts in the addon settings.", 1) + window('emby_serverStatus', value="Stop") + dialog.ok(lang(33001), lang(33010)) + + self.retry += 1 + self.auth = False + + def resetClient(self): + + self.logMsg("Reset UserClient authentication.", 1) + if self.currToken is not None: + # In case of 401, removed saved token + utils.settings('accessToken', value="") + utils.window('emby_accessToken%s' % self.getUserId(), clear=True) + self.currToken = None + self.logMsg("User token has been removed.", 1) + + self.auth = True + self.currUser = None + + def run(self): + + window = utils.window + + monitor = xbmc.Monitor() + self.logMsg("----===## Starting UserClient ##===----", 0) + + while not monitor.abortRequested(): + + status = window('emby_serverStatus') + if status: + # Verify the connection status to server + if status == "restricted": + # Parental control is restricting access + self.HasAccess = False + + elif status == "401": + # Unauthorized access, revoke token + window('emby_serverStatus', value="Auth") + self.resetClient() + + if self.auth and (self.currUser is None): + # Try to authenticate user + status = window('emby_serverStatus') + if not status or status == "Auth": + # Set auth flag because we no longer need + # to authenticate the user + self.auth = False + self.authenticate() + + + if not self.auth and (self.currUser is None): + # If authenticate failed. + server = self.getServer() + username = self.getUsername() + status = window('emby_serverStatus') + + # The status Stop is for when user cancelled password dialog. + if server and username and status != "Stop": + # Only if there's information found to login + self.logMsg("Server found: %s" % server, 2) + self.logMsg("Username found: %s" % username, 2) + self.auth = True + + + if self.stopClient == True: + # If stopping the client didn't work + break + + if monitor.waitForAbort(1): + # Abort was requested while waiting. We should exit + break + + self.doUtils.stopSession() + self.logMsg("##===---- UserClient Stopped ----===##", 0) + + def stopClient(self): + # When emby for kodi terminates self.stopClient = True \ No newline at end of file diff --git a/resources/lib/utils.py b/resources/lib/utils.py index c9b3474f..a8b97c0b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1,577 +1,564 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import cProfile -import inspect -import json -import pstats -import sqlite3 -import StringIO -import os -from datetime import datetime, time -import time -import unicodedata -import xml.etree.ElementTree as etree - -import xbmc -import xbmcaddon -import xbmcgui -import xbmcvfs - -################################################################################################# - - -def logMsg(title, msg, level=1): - - # Get the logLevel set in UserClient - try: - logLevel = int(window('emby_logLevel')) - except ValueError: - logLevel = 0 - - if logLevel >= level: - - if logLevel == 2: # inspect.stack() is expensive - try: - xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg)) - except UnicodeEncodeError: - xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8'))) - else: - try: - xbmc.log("%s -> %s" % (title, msg)) - except UnicodeEncodeError: - xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) - -def window(property, value=None, clear=False, windowid=10000): - # Get or set window property - 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") - if isinstance(value, unicode): - value = value.encode("utf-8")''' - - if clear: - WINDOW.clearProperty(property) - elif value is not None: - WINDOW.setProperty(property, value) - else: #getproperty returns string so convert to unicode - return WINDOW.getProperty(property)#.decode("utf-8") - -def settings(setting, value=None): - # Get or add addon setting - addon = xbmcaddon.Addon(id='plugin.video.emby') - - if value is not None: - addon.setSetting(setting, value) - else: - return addon.getSetting(setting) #returns unicode object - -def language(stringid): - # Central string retrieval - addon = xbmcaddon.Addon(id='plugin.video.emby') - string = addon.getLocalizedString(stringid) #returns unicode object - return string - -def kodiSQL(type="video"): - - if type == "emby": - dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8') - elif type == "music": - dbPath = getKodiMusicDBPath() - elif 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 - "14": 90, # Helix - "15": 93, # Isengard - "16": 99 # Jarvis - } - - dbPath = xbmc.translatePath( - "special://database/MyVideos%s.db" - % dbVersion.get(kodibuild, "")).decode('utf-8') - return dbPath - -def getKodiMusicDBPath(): - - kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2] - dbVersion = { - - "13": 46, # Gotham - "14": 48, # Helix - "15": 52, # Isengard - "16": 56 # Jarvis - } - - dbPath = xbmc.translatePath( - "special://database/MyMusic%s.db" - % dbVersion.get(kodibuild, "")).decode('utf-8') - return dbPath - -def getScreensaver(): - # Get the current screensaver value - query = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Settings.getSettingValue", - 'params': { - - 'setting': "screensaver.mode" - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - screensaver = result['result']['value'] - - return screensaver - -def setScreensaver(value): - # Toggle the screensaver - query = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Settings.setSettingValue", - 'params': { - - 'setting': "screensaver.mode", - 'value': value - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - logMsg("EMBY", "Toggling screensaver: %s %s" % (value, result), 1) - -def reset(): - - dialog = xbmcgui.Dialog() - - resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") - if resp == 0: - return - - # first stop any db sync - window('emby_shouldStop', value="true") - count = 10 - while window('emby_dbScan') == "true": - logMsg("EMBY", "Sync is running, will retry: %s..." % count) - count -= 1 - if count == 0: - dialog.ok("Warning", "Could not stop the database from running. Try again.") - return - xbmc.sleep(1000) - - # Clean up the playlists - deletePlaylists() - - # Clean up the video nodes - deleteNodes() - - # Wipe the kodi databases - logMsg("EMBY", "Resetting the Kodi video database.", 0) - connection = kodiSQL('video') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - connection.commit() - cursor.close() - - if settings('enableMusic') == "true": - logMsg("EMBY", "Resetting the Kodi music database.") - connection = kodiSQL('music') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - connection.commit() - cursor.close() - - # Wipe the emby database - logMsg("EMBY", "Resetting the Emby database.", 0) - connection = kodiSQL('emby') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - cursor.execute('DROP table IF EXISTS emby') - cursor.execute('DROP table IF EXISTS view') - connection.commit() - cursor.close() - - # Offer to wipe cached thumbnails - resp = dialog.yesno("Warning", "Removed all cached artwork?") - if resp: - logMsg("EMBY", "Resetting all cached artwork.", 0) - # Remove all existing textures first - path = xbmc.translatePath("special://thumbnails/").decode('utf-8') - if xbmcvfs.exists(path): - allDirs, allFiles = xbmcvfs.listdir(path) - for dir in allDirs: - allDirs, allFiles = xbmcvfs.listdir(path+dir) - for file in allFiles: - if os.path.supports_unicode_filenames: - 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() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tableName = row[0] - if(tableName != "version"): - cursor.execute("DELETE FROM " + tableName) - connection.commit() - cursor.close() - - # reset the install run flag - settings('SyncInstallRunDone', value="false") - - # Remove emby info - resp = dialog.yesno("Warning", "Reset all Emby Addon settings?") - if resp: - # Delete the settings - addon = xbmcaddon.Addon() - addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') - dataPath = "%ssettings.xml" % addondir - xbmcvfs.delete(dataPath) - logMsg("EMBY", "Deleting: settings.xml", 1) - - dialog.ok( - heading="Emby for Kodi", - line1="Database reset has completed, Kodi will now restart to apply the changes.") - xbmc.executebuiltin('RestartApp') - -def profiling(sortby="cumulative"): - # Will print results to Kodi log - def decorator(func): - def wrapper(*args, **kwargs): - - pr = cProfile.Profile() - - pr.enable() - result = func(*args, **kwargs) - pr.disable() - - s = StringIO.StringIO() - ps = pstats.Stats(pr, stream=s).sort_stats(sortby) - ps.print_stats() - logMsg("EMBY Profiling", s.getvalue(), 1) - - return result - - return wrapper - return decorator - -def convertdate(date): - try: - date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") - except TypeError: - # TypeError: attribute of type 'NoneType' is not callable - # Known Kodi/python error - date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) - - return date - -def normalize_nodes(text): - # For video nodes - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.replace('(', "") - text = text.replace(')', "") - text = text.strip() - # Remove dots from the last character as windows can not have directories - # 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): - # For theme media, do not modify unless - # modified in TV Tunes - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.strip() - # Remove dots from the last character as windows can not have directories - # with dots at the end - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') - - return text - -def indent(elem, level=0): - # Prettify xml trees - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indent(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - -def sourcesXML(): - # To make Master lock compatible - path = xbmc.translatePath("special://profile/").decode('utf-8') - xmlpath = "%ssources.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('sources') - else: - root = xmlparse.getroot() - - - video = root.find('video') - if video is None: - video = etree.SubElement(root, 'video') - etree.SubElement(video, 'default', attrib={'pathversion': "1"}) - - # Add elements - count = 2 - for source in root.findall('.//path'): - if source.text == "smb://": - count -= 1 - - if count == 0: - # sources already set - break - else: - # Missing smb:// occurences, re-add. - for i in range(0, count): - source = etree.SubElement(video, 'source') - etree.SubElement(source, 'name').text = "Emby" - etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" - etree.SubElement(source, 'allowsharing').text = "true" - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath) - -def passwordsXML(): - - # To add network credentials - path = xbmc.translatePath("special://userdata/").decode('utf-8') - xmlpath = "%spasswords.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('passwords') - else: - root = xmlparse.getroot() - - dialog = xbmcgui.Dialog() - credentials = settings('networkCreds') - if credentials: - # Present user with options - option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"]) - - if option < 0: - # User cancelled dialog - return - - elif option == 1: - # User selected remove - iterator = root.getiterator('passwords') - - for paths in iterator: - for path in paths: - if path.find('.//from').text == "smb://%s/" % credentials: - paths.remove(path) - logMsg("EMBY", "Successfully removed credentials for: %s" - % credentials, 1) - etree.ElementTree(root).write(xmlpath) - break - else: - logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1) - - settings('networkCreds', value="") - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message="%s removed from passwords.xml" % credentials, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - return - - elif option == 0: - # User selected to modify - server = dialog.input("Modify the computer name or ip address", credentials) - if not server: - return - else: - # No credentials added - dialog.ok( - heading="Network credentials", - line1= ( - "Input the server name or IP address as indicated in your emby library paths. " - 'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".')) - server = dialog.input("Enter the server name or IP address") - if not server: - return - - # Network username - user = dialog.input("Enter the network username") - if not user: - return - # Network password - password = dialog.input( - heading="Enter the network password", - option=xbmcgui.ALPHANUM_HIDE_INPUT) - if not password: - return - - # Add elements - for path in root.findall('.//path'): - if path.find('.//from').text.lower() == "smb://%s/" % server.lower(): - # Found the server, rewrite credentials - path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server) - break - else: - # Server not found, add it. - path = etree.SubElement(root, 'path') - etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server - topath = "smb://%s:%s@%s/" % (user, password, server) - etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath - # Force Kodi to see the credentials without restarting - xbmcvfs.exists(topath) - - # Add credentials - settings('networkCreds', value="%s" % server) - logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1) - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath) - - dialog.notification( - heading="Emby for Kodi", - message="%s added to passwords.xml" % server, - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - -def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): - # Tagname is in unicode - actions: add or delete - tagname = tagname.encode('utf-8') - - path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') - if viewtype == "mixed": - plname = "%s - %s" % (tagname, mediatype) - xsppath = "%sEmby %s - %s.xsp" % (path, viewid, mediatype) - else: - plname = tagname - xsppath = "%sEmby %s.xsp" % (path, viewid) - - # Create the playlist directory - if not xbmcvfs.exists(path): - logMsg("EMBY", "Creating directory: %s" % path, 1) - xbmcvfs.mkdirs(path) - - # Only add the playlist if it doesn't already exists - if xbmcvfs.exists(xsppath): - - if delete: - xbmcvfs.delete(xsppath) - logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1) - - return - - # Using write process since there's no guarantee the xml declaration works with etree - itemtypes = { - 'homevideos': "movies" - } - logMsg("EMBY", "Writing playlist file to: %s" % xsppath, 1) - try: - f = xbmcvfs.File(xsppath, 'w') - except: - logMsg("EMBY", "Failed to create playlist: %s" % xsppath, 1) - return - else: - f.write( - '\n' - '\n\t' - 'Emby %s\n\t' - 'all\n\t' - '\n\t\t' - '%s\n\t' - '' - '' - % (itemtypes.get(mediatype, mediatype), plname, tagname)) - f.close() - logMsg("EMBY", "Successfully added playlist: %s" % tagname) - -def deletePlaylists(): - - # Clean up the playlists - path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') - dirs, files = xbmcvfs.listdir(path) - for file in files: - if file.decode('utf-8').startswith('Emby'): - xbmcvfs.delete("%s%s" % (path, file)) - -def deleteNodes(): - - # Clean up video nodes - import shutil - path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - dirs, files = xbmcvfs.listdir(path) - for dir in dirs: - if dir.decode('utf-8').startswith('Emby'): - try: - shutil.rmtree("%s%s" % (path, dir.decode('utf-8'))) - except: - logMsg("EMBY", "Failed to delete directory: %s" % dir.decode('utf-8')) - for file in files: - if file.decode('utf-8').startswith('emby'): - try: - xbmcvfs.delete("%s%s" % (path, file.decode('utf-8'))) - except: +# -*- coding: utf-8 -*- + +################################################################################################# + +import cProfile +import inspect +import json +import pstats +import sqlite3 +import StringIO +import os +from datetime import datetime, time +import time +import unicodedata +import xml.etree.ElementTree as etree + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcvfs + +################################################################################################# + + +def logMsg(title, msg, level=1): + + # Get the logLevel set in UserClient + try: + logLevel = int(window('emby_logLevel')) + except ValueError: + logLevel = 0 + + if logLevel >= level: + + if logLevel == 2: # inspect.stack() is expensive + try: + xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg)) + except UnicodeEncodeError: + xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8'))) + else: + try: + xbmc.log("%s -> %s" % (title, msg)) + except UnicodeEncodeError: + xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) + +def window(property, value=None, clear=False, windowid=10000): + # Get or set window property + 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") + if isinstance(value, unicode): + value = value.encode("utf-8")''' + + if clear: + WINDOW.clearProperty(property) + elif value is not None: + WINDOW.setProperty(property, value) + else: #getproperty returns string so convert to unicode + return WINDOW.getProperty(property)#.decode("utf-8") + +def settings(setting, value=None): + # Get or add addon setting + if value is not None: + xbmcaddon.Addon(id='plugin.video.metaman').setSetting(setting, value) + else: + return xbmcaddon.Addon(id='plugin.video.metaman').getSetting(setting) #returns unicode object + +def language(stringid): + # Central string retrieval + string = xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(stringid) #returns unicode object + return string + +def kodiSQL(media_type="video"): + + if media_type == "emby": + dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8') + elif media_type == "music": + dbPath = getKodiMusicDBPath() + 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(): + + dbVersion = { + + "13": 78, # Gotham + "14": 90, # Helix + "15": 93, # Isengard + "16": 99 # Jarvis + } + + dbPath = xbmc.translatePath( + "special://database/MyVideos%s.db" + % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') + return dbPath + +def getKodiMusicDBPath(): + + dbVersion = { + + "13": 46, # Gotham + "14": 48, # Helix + "15": 52, # Isengard + "16": 56 # Jarvis + } + + dbPath = xbmc.translatePath( + "special://database/MyMusic%s.db" + % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') + return dbPath + +def getScreensaver(): + # Get the current screensaver value + query = { + + 'jsonrpc': "2.0", + 'id': 0, + 'method': "Settings.getSettingValue", + 'params': { + + 'setting': "screensaver.mode" + } + } + return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value'] + +def setScreensaver(value): + # Toggle the screensaver + query = { + + 'jsonrpc': "2.0", + 'id': 0, + 'method': "Settings.setSettingValue", + 'params': { + + 'setting': "screensaver.mode", + 'value': value + } + } + logMsg("EMBY", "Toggling screensaver: %s %s" % (value, xbmc.executeJSONRPC(json.dumps(query))), 1) + +def reset(): + + dialog = xbmcgui.Dialog() + + if dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") == 0: + return + + # first stop any db sync + window('emby_shouldStop', value="true") + count = 10 + while window('emby_dbScan') == "true": + logMsg("EMBY", "Sync is running, will retry: %s..." % count) + count -= 1 + if count == 0: + dialog.ok("Warning", "Could not stop the database from running. Try again.") + return + xbmc.sleep(1000) + + # Clean up the playlists + deletePlaylists() + + # Clean up the video nodes + deleteNodes() + + # Wipe the kodi databases + logMsg("EMBY", "Resetting the Kodi video database.", 0) + connection = kodiSQL('video') + cursor = connection.cursor() + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + connection.commit() + cursor.close() + + if settings('enableMusic') == "true": + logMsg("EMBY", "Resetting the Kodi music database.") + connection = kodiSQL('music') + cursor = connection.cursor() + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + connection.commit() + cursor.close() + + # Wipe the emby database + logMsg("EMBY", "Resetting the Emby database.", 0) + connection = kodiSQL('emby') + cursor = connection.cursor() + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + cursor.execute('DROP table IF EXISTS emby') + cursor.execute('DROP table IF EXISTS view') + connection.commit() + cursor.close() + + # Offer to wipe cached thumbnails + resp = dialog.yesno("Warning", "Remove all cached artwork?") + if resp: + logMsg("EMBY", "Resetting all cached artwork.", 0) + # Remove all existing textures first + path = xbmc.translatePath("special://thumbnails/").decode('utf-8') + if xbmcvfs.exists(path): + allDirs, allFiles = xbmcvfs.listdir(path) + for dir in allDirs: + allDirs, allFiles = xbmcvfs.listdir(path+dir) + for file in allFiles: + if os.path.supports_unicode_filenames: + 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() + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tableName = row[0] + if(tableName != "version"): + cursor.execute("DELETE FROM " + tableName) + connection.commit() + cursor.close() + + # reset the install run flag + settings('SyncInstallRunDone', value="false") + + # Remove emby info + resp = dialog.yesno("Warning", "Reset all Emby Addon settings?") + if resp: + # Delete the settings + addon = xbmcaddon.Addon() + addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') + dataPath = "%ssettings.xml" % addondir + xbmcvfs.delete(dataPath) + logMsg("EMBY", "Deleting: settings.xml", 1) + + dialog.ok( + heading="Emby for Kodi", + line1="Database reset has completed, Kodi will now restart to apply the changes.") + xbmc.executebuiltin('RestartApp') + +def profiling(sortby="cumulative"): + # Will print results to Kodi log + def decorator(func): + def wrapper(*args, **kwargs): + + pr = cProfile.Profile() + + pr.enable() + result = func(*args, **kwargs) + pr.disable() + + s = StringIO.StringIO() + ps = pstats.Stats(pr, stream=s).sort_stats(sortby) + ps.print_stats() + logMsg("EMBY Profiling", s.getvalue(), 1) + + return result + + return wrapper + return decorator + +def convertdate(date): + try: + date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + except TypeError: + # TypeError: attribute of type 'NoneType' is not callable + # Known Kodi/python error + date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) + + return date + +def normalize_nodes(text): + # For video nodes + text = text.replace(":", "") + text = text.replace("/", "-") + text = text.replace("\\", "-") + text = text.replace("<", "") + text = text.replace(">", "") + text = text.replace("*", "") + text = text.replace("?", "") + text = text.replace('|', "") + text = text.replace('(', "") + text = text.replace(')', "") + text = text.strip() + # Remove dots from the last character as windows can not have directories + # 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): + # For theme media, do not modify unless + # modified in TV Tunes + text = text.replace(":", "") + text = text.replace("/", "-") + text = text.replace("\\", "-") + text = text.replace("<", "") + text = text.replace(">", "") + text = text.replace("*", "") + text = text.replace("?", "") + text = text.replace('|', "") + text = text.strip() + # Remove dots from the last character as windows can not have directories + # with dots at the end + text = text.rstrip('.') + text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') + + return text + +def indent(elem, level=0): + # Prettify xml trees + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +def sourcesXML(): + # To make Master lock compatible + path = xbmc.translatePath("special://profile/").decode('utf-8') + xmlpath = "%ssources.xml" % path + + try: + xmlparse = etree.parse(xmlpath) + except: # Document is blank or missing + root = etree.Element('sources') + else: + root = xmlparse.getroot() + + + video = root.find('video') + if video is None: + video = etree.SubElement(root, 'video') + etree.SubElement(video, 'default', attrib={'pathversion': "1"}) + + # Add elements + count = 2 + for source in root.findall('.//path'): + if source.text == "smb://": + count -= 1 + + if count == 0: + # sources already set + break + else: + # Missing smb:// occurences, re-add. + for i in range(0, count): + source = etree.SubElement(video, 'source') + etree.SubElement(source, 'name').text = "Emby" + etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" + etree.SubElement(source, 'allowsharing').text = "true" + # Prettify and write to file + try: + indent(root) + except: pass + etree.ElementTree(root).write(xmlpath) + +def passwordsXML(): + + # To add network credentials + path = xbmc.translatePath("special://userdata/").decode('utf-8') + xmlpath = "%spasswords.xml" % path + + try: + xmlparse = etree.parse(xmlpath) + except: # Document is blank or missing + root = etree.Element('passwords') + else: + root = xmlparse.getroot() + + dialog = xbmcgui.Dialog() + credentials = settings('networkCreds') + if credentials: + # Present user with options + option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"]) + + if option < 0: + # User cancelled dialog + return + + elif option == 1: + # User selected remove + for paths in root.getiterator('passwords'): + for path in paths: + if path.find('.//from').text == "smb://%s/" % credentials: + paths.remove(path) + logMsg("EMBY", "Successfully removed credentials for: %s" + % credentials, 1) + etree.ElementTree(root).write(xmlpath) + break + else: + logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1) + + settings('networkCreds', value="") + xbmcgui.Dialog().notification( + heading="Emby for Kodi", + message="%s removed from passwords.xml" % credentials, + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000, + sound=False) + return + + elif option == 0: + # User selected to modify + server = dialog.input("Modify the computer name or ip address", credentials) + if not server: + return + else: + # No credentials added + dialog.ok( + heading="Network credentials", + line1= ( + "Input the server name or IP address as indicated in your emby library paths. " + 'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".')) + server = dialog.input("Enter the server name or IP address") + if not server: + return + + # Network username + user = dialog.input("Enter the network username") + if not user: + return + # Network password + password = dialog.input( + heading="Enter the network password", + option=xbmcgui.ALPHANUM_HIDE_INPUT) + if not password: + return + + # Add elements + for path in root.findall('.//path'): + if path.find('.//from').text.lower() == "smb://%s/" % server.lower(): + # Found the server, rewrite credentials + path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server) + break + else: + # Server not found, add it. + path = etree.SubElement(root, 'path') + etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server + topath = "smb://%s:%s@%s/" % (user, password, server) + etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath + # Force Kodi to see the credentials without restarting + xbmcvfs.exists(topath) + + # Add credentials + settings('networkCreds', value="%s" % server) + logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1) + # Prettify and write to file + try: + indent(root) + except: pass + etree.ElementTree(root).write(xmlpath) + + dialog.notification( + heading="Emby for Kodi", + message="%s added to passwords.xml" % server, + icon="special://home/addons/plugin.video.emby/icon.png", + time=1000, + sound=False) + +def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): + # Tagname is in unicode - actions: add or delete + tagname = tagname.encode('utf-8') + + path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') + if viewtype == "mixed": + plname = "%s - %s" % (tagname, mediatype) + xsppath = "%sEmby %s - %s.xsp" % (path, viewid, mediatype) + else: + plname = tagname + xsppath = "%sEmby %s.xsp" % (path, viewid) + + # Create the playlist directory + if not xbmcvfs.exists(path): + logMsg("EMBY", "Creating directory: %s" % path, 1) + xbmcvfs.mkdirs(path) + + # Only add the playlist if it doesn't already exists + if xbmcvfs.exists(xsppath): + + if delete: + xbmcvfs.delete(xsppath) + logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1) + + return + + # Using write process since there's no guarantee the xml declaration works with etree + itemtypes = { + 'homevideos': "movies" + } + logMsg("EMBY", "Writing playlist file to: %s" % xsppath, 1) + try: + f = xbmcvfs.File(xsppath, 'w') + except: + logMsg("EMBY", "Failed to create playlist: %s" % xsppath, 1) + return + else: + f.write( + '\n' + '\n\t' + 'Emby %s\n\t' + 'all\n\t' + '\n\t\t' + '%s\n\t' + '' + '' + % (itemtypes.get(mediatype, mediatype), plname, tagname)) + f.close() + logMsg("EMBY", "Successfully added playlist: %s" % tagname) + +def deletePlaylists(): + + # Clean up the playlists + path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') + dirs, files = xbmcvfs.listdir(path) + for file in files: + if file.decode('utf-8').startswith('Emby'): + xbmcvfs.delete("%s%s" % (path, file)) + +def deleteNodes(): + + # Clean up video nodes + import shutil + path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + dirs, files = xbmcvfs.listdir(path) + for dir in dirs: + if dir.decode('utf-8').startswith('Emby'): + try: + shutil.rmtree("%s%s" % (path, dir.decode('utf-8'))) + except: + logMsg("EMBY", "Failed to delete directory: %s" % dir.decode('utf-8')) + for file in files: + if file.decode('utf-8').startswith('emby'): + try: + xbmcvfs.delete("%s%s" % (path, file.decode('utf-8'))) + except: logMsg("EMBY", "Failed to file: %s" % file.decode('utf-8')) \ No newline at end of file diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 53c18385..f7f63c3c 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -1,395 +1,394 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import shutil -import xml.etree.ElementTree as etree - -import xbmc -import xbmcaddon -import xbmcvfs - -import clientinfo -import utils - -################################################################################################# - - -class VideoNodes(object): - - - def __init__(self): - - clientInfo = clientinfo.ClientInfo() - self.addonName = clientInfo.getAddonName() - - self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - - def logMsg(self, msg, lvl=1): - - className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) - - - def commonRoot(self, order, label, tagname, roottype=1): - - if roottype == 0: - # Index - root = etree.Element('node', attrib={'order': "%s" % order}) - elif roottype == 1: - # Filter - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"}) - etree.SubElement(root, 'match').text = "all" - # Add tag rule - rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"}) - etree.SubElement(rule, 'value').text = tagname - else: - # Folder - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"}) - - etree.SubElement(root, 'label').text = label - etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png" - - return root - - def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False): - - window = utils.window - kodiversion = self.kodiversion - - if viewtype == "mixed": - dirname = "%s - %s" % (viewid, mediatype) - else: - dirname = viewid - - path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - nodepath = xbmc.translatePath( - "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8') - - # Verify the video directory - if not xbmcvfs.exists(path): - shutil.copytree( - src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), - dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) - xbmcvfs.exists(path) - - # Create the node directory - if not xbmcvfs.exists(nodepath) and not mediatype == "photos": - # We need to copy over the default items - xbmcvfs.mkdirs(nodepath) - else: - if delete: - dirs, files = xbmcvfs.listdir(nodepath) - for file in files: - xbmcvfs.delete(nodepath + file) - - self.logMsg("Sucessfully removed videonode: %s." % tagname, 1) - return - - # Create index entry - nodeXML = "%sindex.xml" % nodepath - # Set windows property - path = "library://video/Emby - %s/" % dirname - for i in range(1, indexnumber): - # Verify to make sure we don't create duplicates - if window('Emby.nodes.%s.index' % i) == path: - return - - if mediatype == "photos": - path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber - - window('Emby.nodes.%s.index' % indexnumber, value=path) - - # Root - if not mediatype == "photos": - if viewtype == "mixed": - specialtag = "%s - %s" % (tagname, mediatype) - root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0) - else: - root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0) - try: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - nodetypes = { - - '1': "all", - '2': "recent", - '3': "recentepisodes", - '4': "inprogress", - '5': "inprogressepisodes", - '6': "unwatched", - '7': "nextepisodes", - '8': "sets", - '9': "genres", - '10': "random", - '11': "recommended", - } - mediatypes = { - # label according to nodetype per mediatype - 'movies': - { - '1': tagname, - '2': 30174, - '4': 30177, - '6': 30189, - '8': 20434, - '9': 135, - '10': 30229, - '11': 30230 - }, - - 'tvshows': - { - '1': tagname, - '2': 30170, - '3': 30175, - '4': 30171, - '5': 30178, - '7': 30179, - '9': 135, - '10': 30229, - '11': 30230 - }, - - 'homevideos': - { - '1': tagname, - '2': 30251, - '11': 30253 - }, - - 'photos': - { - '1': tagname, - '2': 30252, - '8': 30255, - '11': 30254 - }, - - 'musicvideos': - { - '1': tagname, - '2': 30256, - '4': 30257, - '6': 30258 - } - } - - nodes = mediatypes[mediatype] - for node in nodes: - - nodetype = nodetypes[node] - nodeXML = "%s%s_%s.xml" % (nodepath, viewid, nodetype) - # Get label - stringid = nodes[node] - if node != "1": - label = utils.language(stringid) - if not label: - label = xbmc.getLocalizedString(stringid) - else: - label = stringid - - # Set window properties - if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all": - # Custom query - path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s" - % (tagname, mediatype)) - elif (mediatype == "homevideos" or mediatype == "photos"): - # Custom query - path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s" - % (tagname, mediatype, nodetype)) - elif nodetype == "nextepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname - elif kodiversion == 14 and nodetype == "recentepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname - elif kodiversion == 14 and nodetype == "inprogressepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname - else: - path = "library://video/Emby - %s/%s_%s.xml" % (dirname, viewid, nodetype) - - if mediatype == "photos": - windowpath = "ActivateWindow(Pictures,%s,return)" % path - else: - windowpath = "ActivateWindow(Video,%s,return)" % path - - if nodetype == "all": - - if viewtype == "mixed": - templabel = "%s - %s" % (tagname, mediatype) - else: - templabel = label - - embynode = "Emby.nodes.%s" % indexnumber - window('%s.title' % embynode, value=templabel) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=mediatype) - else: - embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - - if mediatype == "photos": - # For photos, we do not create a node in videos but we do want the window props - # to be created. - # To do: add our photos nodes to kodi picture sources somehow - continue - - if xbmcvfs.exists(nodeXML): - # Don't recreate xml if already exists - continue - - # Create the root - if (nodetype == "nextepisodes" or mediatype == "homevideos" or - (kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))): - # Folder type with plugin path - root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = path - etree.SubElement(root, 'content').text = "episodes" - else: - root = self.commonRoot(order=node, label=label, tagname=tagname) - if nodetype in ('recentepisodes', 'inprogressepisodes'): - etree.SubElement(root, 'content').text = "episodes" - else: - etree.SubElement(root, 'content').text = mediatype - - limit = "25" - # Elements per nodetype - if nodetype == "all": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - - elif nodetype == "recent": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "inprogress": - etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) - etree.SubElement(root, 'limit').text = limit - - elif nodetype == "genres": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - etree.SubElement(root, 'group').text = "genres" - - elif nodetype == "unwatched": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "sets": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - etree.SubElement(root, 'group').text = "sets" - - elif nodetype == "random": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" - etree.SubElement(root, 'limit').text = limit - - elif nodetype == "recommended": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - rule2 = etree.SubElement(root, 'rule', - attrib={'field': "rating", 'operator': "greaterthan"}) - etree.SubElement(rule2, 'value').text = "7" - - elif nodetype == "recentepisodes": - # Kodi Isengard, Jarvis - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "inprogressepisodes": - # Kodi Isengard, Jarvis - etree.SubElement(root, 'limit').text = "25" - rule = etree.SubElement(root, 'rule', - attrib={'field': "inprogress", 'operator':"true"}) - - try: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - def singleNode(self, indexnumber, tagname, mediatype, itemtype): - - window = utils.window - - tagname = tagname.encode('utf-8') - cleantagname = utils.normalize_nodes(tagname) - nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) - path = "library://video/emby_%s.xml" % cleantagname - windowpath = "ActivateWindow(Video,%s,return)" % path - - # Create the video node directory - if not xbmcvfs.exists(nodepath): - # We need to copy over the default items - shutil.copytree( - src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), - dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) - xbmcvfs.exists(path) - - labels = { - - 'Favorite movies': 30180, - 'Favorite tvshows': 30181, - 'channels': 30173 - } - label = utils.language(labels[tagname]) - embynode = "Emby.nodes.%s" % indexnumber - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=itemtype) - - if xbmcvfs.exists(nodeXML): - # Don't recreate xml if already exists - return - - if itemtype == "channels": - root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels" - else: - root = self.commonRoot(order=1, label=label, tagname=tagname) - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - - etree.SubElement(root, 'content').text = mediatype - - try: - utils.indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - def clearProperties(self): - - window = utils.window - - self.logMsg("Clearing nodes properties.", 1) - embyprops = window('Emby.nodes.total') - propnames = [ - - "index","path","title","content", - "inprogress.content","inprogress.title", - "inprogress.content","inprogress.path", - "nextepisodes.title","nextepisodes.content", - "nextepisodes.path","unwatched.title", - "unwatched.content","unwatched.path", - "recent.title","recent.content","recent.path", - "recentepisodes.title","recentepisodes.content", - "recentepisodes.path","inprogressepisodes.title", - "inprogressepisodes.content","inprogressepisodes.path" - ] - - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - for prop in propnames: +# -*- coding: utf-8 -*- + +################################################################################################# + +import shutil +import xml.etree.ElementTree as etree + +import xbmc +import xbmcaddon +import xbmcvfs + +import clientinfo +import utils + +################################################################################################# + + +class VideoNodes(object): + + + def __init__(self): + + clientInfo = clientinfo.ClientInfo() + self.addonName = clientInfo.getAddonName() + + self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + + def logMsg(self, msg, lvl=1): + + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) + + + def commonRoot(self, order, label, tagname, roottype=1): + + if roottype == 0: + # Index + root = etree.Element('node', attrib={'order': "%s" % order}) + elif roottype == 1: + # Filter + root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"}) + etree.SubElement(root, 'match').text = "all" + # Add tag rule + rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"}) + etree.SubElement(rule, 'value').text = tagname + else: + # Folder + root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"}) + + etree.SubElement(root, 'label').text = label + etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png" + + return root + + def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False): + + window = utils.window + + if viewtype == "mixed": + dirname = "%s - %s" % (viewid, mediatype) + else: + dirname = viewid + + path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + nodepath = xbmc.translatePath( + "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8') + + # Verify the video directory + if not xbmcvfs.exists(path): + shutil.copytree( + src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), + dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) + xbmcvfs.exists(path) + + # Create the node directory + if not xbmcvfs.exists(nodepath) and not mediatype == "photos": + # We need to copy over the default items + xbmcvfs.mkdirs(nodepath) + else: + if delete: + dirs, files = xbmcvfs.listdir(nodepath) + for file in files: + xbmcvfs.delete(nodepath + file) + + self.logMsg("Sucessfully removed videonode: %s." % tagname, 1) + return + + # Create index entry + nodeXML = "%sindex.xml" % nodepath + # Set windows property + path = "library://video/Emby - %s/" % dirname + for i in range(1, indexnumber): + # Verify to make sure we don't create duplicates + if window('Emby.nodes.%s.index' % i) == path: + return + + if mediatype == "photos": + path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber + + window('Emby.nodes.%s.index' % indexnumber, value=path) + + # Root + if not mediatype == "photos": + if viewtype == "mixed": + specialtag = "%s - %s" % (tagname, mediatype) + root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0) + else: + root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0) + try: + utils.indent(root) + except: pass + etree.ElementTree(root).write(nodeXML) + + nodetypes = { + + '1': "all", + '2': "recent", + '3': "recentepisodes", + '4': "inprogress", + '5': "inprogressepisodes", + '6': "unwatched", + '7': "nextepisodes", + '8': "sets", + '9': "genres", + '10': "random", + '11': "recommended", + } + mediatypes = { + # label according to nodetype per mediatype + 'movies': + { + '1': tagname, + '2': 30174, + '4': 30177, + '6': 30189, + '8': 20434, + '9': 135, + '10': 30229, + '11': 30230 + }, + + 'tvshows': + { + '1': tagname, + '2': 30170, + '3': 30175, + '4': 30171, + '5': 30178, + '7': 30179, + '9': 135, + '10': 30229, + '11': 30230 + }, + + 'homevideos': + { + '1': tagname, + '2': 30251, + '11': 30253 + }, + + 'photos': + { + '1': tagname, + '2': 30252, + '8': 30255, + '11': 30254 + }, + + 'musicvideos': + { + '1': tagname, + '2': 30256, + '4': 30257, + '6': 30258 + } + } + + nodes = mediatypes[mediatype] + for node in nodes: + + nodetype = nodetypes[node] + nodeXML = "%s%s_%s.xml" % (nodepath, viewid, nodetype) + # Get label + stringid = nodes[node] + if node != "1": + label = utils.language(stringid) + if not label: + label = xbmc.getLocalizedString(stringid) + else: + label = stringid + + # Set window properties + if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all": + # Custom query + path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s" + % (tagname, mediatype)) + elif (mediatype == "homevideos" or mediatype == "photos"): + # Custom query + path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s" + % (tagname, mediatype, nodetype)) + elif nodetype == "nextepisodes": + # Custom query + path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname + elif self.kodiversion == 14 and nodetype == "recentepisodes": + # Custom query + path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname + elif self.kodiversion == 14 and nodetype == "inprogressepisodes": + # Custom query + path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname + else: + path = "library://video/Emby - %s/%s_%s.xml" % (dirname, viewid, nodetype) + + if mediatype == "photos": + windowpath = "ActivateWindow(Pictures,%s,return)" % path + else: + windowpath = "ActivateWindow(Video,%s,return)" % path + + if nodetype == "all": + + if viewtype == "mixed": + templabel = "%s - %s" % (tagname, mediatype) + else: + templabel = label + + embynode = "Emby.nodes.%s" % indexnumber + window('%s.title' % embynode, value=templabel) + window('%s.path' % embynode, value=windowpath) + window('%s.content' % embynode, value=path) + window('%s.type' % embynode, value=mediatype) + else: + embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) + window('%s.title' % embynode, value=label) + window('%s.path' % embynode, value=windowpath) + window('%s.content' % embynode, value=path) + + if mediatype == "photos": + # For photos, we do not create a node in videos but we do want the window props + # to be created. + # To do: add our photos nodes to kodi picture sources somehow + continue + + if xbmcvfs.exists(nodeXML): + # Don't recreate xml if already exists + continue + + # Create the root + if (nodetype == "nextepisodes" or mediatype == "homevideos" or + (self.kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))): + # Folder type with plugin path + root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2) + etree.SubElement(root, 'path').text = path + etree.SubElement(root, 'content').text = "episodes" + else: + root = self.commonRoot(order=node, label=label, tagname=tagname) + if nodetype in ('recentepisodes', 'inprogressepisodes'): + etree.SubElement(root, 'content').text = "episodes" + else: + etree.SubElement(root, 'content').text = mediatype + + limit = "25" + # Elements per nodetype + if nodetype == "all": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + elif nodetype == "recent": + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "inprogress": + etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + etree.SubElement(root, 'limit').text = limit + + elif nodetype == "genres": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, 'group').text = "genres" + + elif nodetype == "unwatched": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "sets": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, 'group').text = "sets" + + elif nodetype == "random": + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" + etree.SubElement(root, 'limit').text = limit + + elif nodetype == "recommended": + etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + rule2 = etree.SubElement(root, 'rule', + attrib={'field': "rating", 'operator': "greaterthan"}) + etree.SubElement(rule2, 'value').text = "7" + + elif nodetype == "recentepisodes": + # Kodi Isengard, Jarvis + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, 'limit').text = limit + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + elif nodetype == "inprogressepisodes": + # Kodi Isengard, Jarvis + etree.SubElement(root, 'limit').text = "25" + rule = etree.SubElement(root, 'rule', + attrib={'field': "inprogress", 'operator':"true"}) + + try: + utils.indent(root) + except: pass + etree.ElementTree(root).write(nodeXML) + + def singleNode(self, indexnumber, tagname, mediatype, itemtype): + + window = utils.window + + tagname = tagname.encode('utf-8') + cleantagname = utils.normalize_nodes(tagname) + nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) + path = "library://video/emby_%s.xml" % cleantagname + windowpath = "ActivateWindow(Video,%s,return)" % path + + # Create the video node directory + if not xbmcvfs.exists(nodepath): + # We need to copy over the default items + shutil.copytree( + src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), + dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) + xbmcvfs.exists(path) + + labels = { + + 'Favorite movies': 30180, + 'Favorite tvshows': 30181, + 'channels': 30173 + } + label = utils.language(labels[tagname]) + embynode = "Emby.nodes.%s" % indexnumber + window('%s.title' % embynode, value=label) + window('%s.path' % embynode, value=windowpath) + window('%s.content' % embynode, value=path) + window('%s.type' % embynode, value=itemtype) + + if xbmcvfs.exists(nodeXML): + # Don't recreate xml if already exists + return + + if itemtype == "channels": + root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) + etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels" + else: + root = self.commonRoot(order=1, label=label, tagname=tagname) + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + etree.SubElement(root, 'content').text = mediatype + + try: + utils.indent(root) + except: pass + etree.ElementTree(root).write(nodeXML) + + def clearProperties(self): + + window = utils.window + + self.logMsg("Clearing nodes properties.", 1) + embyprops = window('Emby.nodes.total') + propnames = [ + + "index","path","title","content", + "inprogress.content","inprogress.title", + "inprogress.content","inprogress.path", + "nextepisodes.title","nextepisodes.content", + "nextepisodes.path","unwatched.title", + "unwatched.content","unwatched.path", + "recent.title","recent.content","recent.path", + "recentepisodes.title","recentepisodes.content", + "recentepisodes.path","inprogressepisodes.title", + "inprogressepisodes.content","inprogressepisodes.path" + ] + + if embyprops: + totalnodes = int(embyprops) + for i in range(totalnodes): + for prop in propnames: window('Emby.nodes.%s.%s' % (str(i), prop), clear=True) \ No newline at end of file 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() diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index acf0df36..559cb152 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -1,327 +1,319 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import threading -import websocket - -import xbmc -import xbmcgui - -import clientinfo -import downloadutils -import librarysync -import playlist -import userclient -import utils - -import logging -logging.basicConfig() - -################################################################################################# - - -class WebSocket_Client(threading.Thread): - - _shared_state = {} - - client = None - stopWebsocket = False - - - def __init__(self): - - self.__dict__ = self._shared_state - self.monitor = xbmc.Monitor() - - self.doUtils = downloadutils.DownloadUtils() - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.deviceId = self.clientInfo.getDeviceId() - self.librarySync = librarysync.LibrarySync() - - threading.Thread.__init__(self) - - def logMsg(self, msg, lvl=1): - - self.className = self.__class__.__name__ - utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) - - - def sendProgressUpdate(self, data): - - log = self.logMsg - - log("sendProgressUpdate", 2) - try: - messageData = { - - 'MessageType': "ReportPlaybackProgress", - 'Data': data - } - messageString = json.dumps(messageData) - self.client.send(messageString) - log("Message data: %s" % messageString, 2) - - except Exception as e: - log("Exception: %s" % e, 1) - - def on_message(self, ws, message): - - log = self.logMsg - window = utils.window - lang = utils.language - - result = json.loads(message) - messageType = result['MessageType'] - data = result['Data'] - - if messageType not in ('SessionEnded'): - # Mute certain events - log("Message: %s" % message, 1) - - if messageType == "Play": - # A remote control play command has been sent from the server. - itemIds = data['ItemIds'] - command = data['PlayCommand'] - - pl = playlist.Playlist() - dialog = xbmcgui.Dialog() - - if command == "PlayNow": - dialog.notification( - heading="Emby for Kodi", - message="%s %s" % (len(itemIds), lang(33004)), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - startat = data.get('StartPositionTicks', 0) - pl.playAll(itemIds, startat) - - elif command == "PlayNext": - dialog.notification( - heading="Emby for Kodi", - message="%s %s" % (len(itemIds), lang(33005)), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - newplaylist = pl.modifyPlaylist(itemIds) - player = xbmc.Player() - if not player.isPlaying(): - # Only start the playlist if nothing is playing - player.play(newplaylist) - - elif messageType == "Playstate": - # A remote control update playstate command has been sent from the server. - command = data['Command'] - player = xbmc.Player() - - actions = { - - 'Stop': player.stop, - 'Unpause': player.pause, - 'Pause': player.pause, - 'NextTrack': player.playnext, - 'PreviousTrack': player.playprevious, - 'Seek': player.seekTime - } - action = actions[command] - if command == "Seek": - seekto = data['SeekPositionTicks'] - seektime = seekto / 10000000.0 - action(seektime) - log("Seek to %s." % seektime, 1) - else: - action() - log("Command: %s completed." % command, 1) - - window('emby_command', value="true") - - elif messageType == "UserDataChanged": - # A user changed their personal rating for an item, or their playstate was updated - userdata_list = data['UserDataList'] - self.librarySync.triage_items("userdata", userdata_list) - - elif messageType == "LibraryChanged": - - librarySync = self.librarySync - processlist = { - - 'added': data['ItemsAdded'], - 'update': data['ItemsUpdated'], - 'remove': data['ItemsRemoved'] - } - for action in processlist: - librarySync.triage_items(action, processlist[action]) - - elif messageType == "GeneralCommand": - - command = data['Name'] - arguments = data['Arguments'] - - if command in ('Mute', 'Unmute', 'SetVolume', - 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'): - - player = xbmc.Player() - # These commands need to be reported back - if command == "Mute": - xbmc.executebuiltin('Mute') - elif command == "Unmute": - xbmc.executebuiltin('Mute') - elif command == "SetVolume": - volume = arguments['Volume'] - xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) - elif command == "SetAudioStreamIndex": - index = int(arguments['Index']) - player.setAudioStream(index - 1) - elif command == "SetSubtitleStreamIndex": - embyindex = int(arguments['Index']) - currentFile = player.getPlayingFile() - - mapping = window('emby_%s.indexMapping' % currentFile) - if mapping: - externalIndex = json.loads(mapping) - # If there's external subtitles added via playbackutils - for index in externalIndex: - if externalIndex[index] == embyindex: - player.setSubtitleStream(int(index)) - break - else: - # User selected internal subtitles - external = len(externalIndex) - audioTracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(external + embyindex - audioTracks - 1) - else: - # Emby merges audio and subtitle index together - audioTracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(index - audioTracks - 1) - - # Let service know - window('emby_command', value="true") - - elif command == "DisplayMessage": - - header = arguments['Header'] - text = arguments['Text'] - xbmcgui.Dialog().notification( - heading=header, - message=text, - icon="special://home/addons/plugin.video.emby/icon.png", - time=4000) - - elif command == "SendString": - - string = arguments['String'] - text = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Input.SendText", - 'params': { - - 'text': "%s" % string, - 'done': False - } - } - result = xbmc.executeJSONRPC(json.dumps(text)) - - else: - builtin = { - - 'ToggleFullscreen': 'Action(FullScreen)', - 'ToggleOsdMenu': 'Action(OSD)', - 'ToggleContextMenu': 'Action(ContextMenu)', - 'MoveUp': 'Action(Up)', - 'MoveDown': 'Action(Down)', - 'MoveLeft': 'Action(Left)', - 'MoveRight': 'Action(Right)', - 'Select': 'Action(Select)', - 'Back': 'Action(back)', - 'GoHome': 'ActivateWindow(Home)', - 'PageUp': 'Action(PageUp)', - 'NextLetter': 'Action(NextLetter)', - 'GoToSearch': 'VideoLibrary.Search', - 'GoToSettings': 'ActivateWindow(Settings)', - 'PageDown': 'Action(PageDown)', - 'PreviousLetter': 'Action(PrevLetter)', - 'TakeScreenshot': 'TakeScreenshot', - 'ToggleMute': 'Mute', - 'VolumeUp': 'Action(VolumeUp)', - 'VolumeDown': 'Action(VolumeDown)', - } - action = builtin.get(command) - if action: - xbmc.executebuiltin(action) - - elif messageType == "ServerRestarting": - if utils.settings('supressRestartMsg') == "true": - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message=lang(33006), - icon="special://home/addons/plugin.video.emby/icon.png") - - elif messageType == "UserConfigurationUpdated": - # Update user data set in userclient - userclient.UserClient().userSettings = data - self.librarySync.refresh_views = True - - def on_close(self, ws): - self.logMsg("Closed.", 2) - - def on_open(self, ws): - self.doUtils.postCapabilities(self.deviceId) - - def on_error(self, ws, error): - if "10061" in str(error): - # Server is offline - pass - else: - self.logMsg("Error: %s" % error, 2) - - def run(self): - - log = self.logMsg - window = utils.window - monitor = self.monitor - - loglevel = int(window('emby_logLevel')) - # websocket.enableTrace(True) - - userId = window('emby_currUser') - server = window('emby_server%s' % userId) - token = window('emby_accessToken%s' % userId) - deviceId = self.deviceId - - # Get the appropriate prefix for the websocket - if "https" in server: - server = server.replace('https', "wss") - else: - server = server.replace('http', "ws") - - websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, deviceId) - log("websocket url: %s" % websocket_url, 1) - - self.client = websocket.WebSocketApp(websocket_url, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close) - - self.client.on_open = self.on_open - log("----===## Starting WebSocketClient ##===----", 0) - - while not monitor.abortRequested(): - - self.client.run_forever(ping_interval=10) - if self.stopWebsocket: - break - - if monitor.waitForAbort(5): - # Abort was requested, exit - break - - log("##===---- WebSocketClient Stopped ----===##", 0) - - def stopClient(self): - - self.stopWebsocket = True - self.client.close() +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import threading +import websocket + +import xbmc +import xbmcgui + +import clientinfo +import downloadutils +import librarysync +import playlist +import userclient +import utils + +import logging +logging.basicConfig() + +################################################################################################# + + +class WebSocket_Client(threading.Thread): + + _shared_state = {} + + client = None + stopWebsocket = False + + + def __init__(self): + + self.__dict__ = self._shared_state + self.monitor = xbmc.Monitor() + + self.doUtils = downloadutils.DownloadUtils() + self.clientInfo = clientinfo.ClientInfo() + self.addonName = self.clientInfo.getAddonName() + self.deviceId = self.clientInfo.getDeviceId() + self.librarySync = librarysync.LibrarySync() + + threading.Thread.__init__(self) + + def logMsg(self, msg, lvl=1): + + self.className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) + + + def sendProgressUpdate(self, data): + + self.logMsg("sendProgressUpdate", 2) + try: + messageData = { + + 'MessageType': "ReportPlaybackProgress", + 'Data': data + } + messageString = json.dumps(messageData) + self.client.send(messageString) + self.logMsg("Message data: %s" % messageString, 2) + + except Exception as e: + self.logMsg("Exception: %s" % e, 1) + + def on_message(self, ws, message): + + window = utils.window + lang = utils.language + + result = json.loads(message) + messageType = result['MessageType'] + data = result['Data'] + + if messageType not in ('SessionEnded'): + # Mute certain events + self.logMsg("Message: %s" % message, 1) + + if messageType == "Play": + # A remote control play command has been sent from the server. + itemIds = data['ItemIds'] + command = data['PlayCommand'] + + pl = playlist.Playlist() + dialog = xbmcgui.Dialog() + + if command == "PlayNow": + dialog.notification( + heading="Emby for Kodi", + message="%s %s" % (len(itemIds), lang(33004)), + icon="special://home/addons/plugin.video.emby/icon.png", + sound=False) + startat = data.get('StartPositionTicks', 0) + pl.playAll(itemIds, startat) + + elif command == "PlayNext": + dialog.notification( + heading="Emby for Kodi", + message="%s %s" % (len(itemIds), lang(33005)), + icon="special://home/addons/plugin.video.emby/icon.png", + sound=False) + newplaylist = pl.modifyPlaylist(itemIds) + player = xbmc.Player() + if not player.isPlaying(): + # Only start the playlist if nothing is playing + player.play(newplaylist) + + elif messageType == "Playstate": + # A remote control update playstate command has been sent from the server. + command = data['Command'] + player = xbmc.Player() + + actions = { + + 'Stop': player.stop, + 'Unpause': player.pause, + 'Pause': player.pause, + 'NextTrack': player.playnext, + 'PreviousTrack': player.playprevious, + 'Seek': player.seekTime + } + action = actions[command] + if command == "Seek": + seekto = data['SeekPositionTicks'] + seektime = seekto / 10000000.0 + action(seektime) + self.logMsg("Seek to %s." % seektime, 1) + else: + action() + self.logMsg("Command: %s completed." % command, 1) + + window('emby_command', value="true") + + elif messageType == "UserDataChanged": + # A user changed their personal rating for an item, or their playstate was updated + userdata_list = data['UserDataList'] + self.librarySync.triage_items("userdata", userdata_list) + + elif messageType == "LibraryChanged": + + librarySync = self.librarySync + processlist = { + + 'added': data['ItemsAdded'], + 'update': data['ItemsUpdated'], + 'remove': data['ItemsRemoved'] + } + for action in processlist: + librarySync.triage_items(action, processlist[action]) + + elif messageType == "GeneralCommand": + + command = data['Name'] + arguments = data['Arguments'] + + if command in ('Mute', 'Unmute', 'SetVolume', + 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'): + + player = xbmc.Player() + # These commands need to be reported back + if command == "Mute": + xbmc.executebuiltin('Mute') + elif command == "Unmute": + xbmc.executebuiltin('Mute') + elif command == "SetVolume": + volume = arguments['Volume'] + xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) + elif command == "SetAudioStreamIndex": + index = int(arguments['Index']) + player.setAudioStream(index - 1) + elif command == "SetSubtitleStreamIndex": + embyindex = int(arguments['Index']) + currentFile = player.getPlayingFile() + + mapping = window('emby_%s.indexMapping' % currentFile) + if mapping: + externalIndex = json.loads(mapping) + # If there's external subtitles added via playbackutils + for index in externalIndex: + if externalIndex[index] == embyindex: + player.setSubtitleStream(int(index)) + break + else: + # User selected internal subtitles + external = len(externalIndex) + audioTracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(external + embyindex - audioTracks - 1) + else: + # Emby merges audio and subtitle index together + audioTracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(index - audioTracks - 1) + + # Let service know + window('emby_command', value="true") + + elif command == "DisplayMessage": + + header = arguments['Header'] + text = arguments['Text'] + xbmcgui.Dialog().notification( + heading=header, + message=text, + icon="special://home/addons/plugin.video.emby/icon.png", + time=4000) + + elif command == "SendString": + + string = arguments['String'] + text = { + + 'jsonrpc': "2.0", + 'id': 0, + 'method': "Input.SendText", + 'params': { + + 'text': "%s" % string, + 'done': False + } + } + result = xbmc.executeJSONRPC(json.dumps(text)) + + else: + builtin = { + + 'ToggleFullscreen': 'Action(FullScreen)', + 'ToggleOsdMenu': 'Action(OSD)', + 'ToggleContextMenu': 'Action(ContextMenu)', + 'MoveUp': 'Action(Up)', + 'MoveDown': 'Action(Down)', + 'MoveLeft': 'Action(Left)', + 'MoveRight': 'Action(Right)', + 'Select': 'Action(Select)', + 'Back': 'Action(back)', + 'GoHome': 'ActivateWindow(Home)', + 'PageUp': 'Action(PageUp)', + 'NextLetter': 'Action(NextLetter)', + 'GoToSearch': 'VideoLibrary.Search', + 'GoToSettings': 'ActivateWindow(Settings)', + 'PageDown': 'Action(PageDown)', + 'PreviousLetter': 'Action(PrevLetter)', + 'TakeScreenshot': 'TakeScreenshot', + 'ToggleMute': 'Mute', + 'VolumeUp': 'Action(VolumeUp)', + 'VolumeDown': 'Action(VolumeDown)', + } + action = builtin.get(command) + if action: + xbmc.executebuiltin(action) + + elif messageType == "ServerRestarting": + if utils.settings('supressRestartMsg') == "true": + xbmcgui.Dialog().notification( + heading="Emby for Kodi", + message=lang(33006), + icon="special://home/addons/plugin.video.emby/icon.png") + + elif messageType == "UserConfigurationUpdated": + # Update user data set in userclient + userclient.UserClient().userSettings = data + self.librarySync.refresh_views = True + + def on_close(self, ws): + self.logMsg("Closed.", 2) + + def on_open(self, ws): + self.doUtils.postCapabilities(self.deviceId) + + def on_error(self, ws, error): + if "10061" in str(error): + # Server is offline + pass + else: + self.logMsg("Error: %s" % error, 2) + + def run(self): + + window = utils.window + loglevel = int(window('emby_logLevel')) + # websocket.enableTrace(True) + + userId = window('emby_currUser') + server = window('emby_server%s' % userId) + token = window('emby_accessToken%s' % userId) + # Get the appropriate prefix for the websocket + if "https" in server: + server = server.replace('https', "wss") + else: + server = server.replace('http', "ws") + + websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.deviceId) + self.logMsg("websocket url: %s" % websocket_url, 1) + + self.client = websocket.WebSocketApp(websocket_url, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + + self.client.on_open = self.on_open + self.logMsg("----===## Starting WebSocketClient ##===----", 0) + + while not self.monitor.abortRequested(): + + self.client.run_forever(ping_interval=10) + if self.stopWebsocket: + break + + if self.monitor.waitForAbort(5): + # Abort was requested, exit + break + + self.logMsg("##===---- WebSocketClient Stopped ----===##", 0) + + def stopClient(self): + + self.stopWebsocket = True + self.client.close() self.logMsg("Stopping thread.", 1) \ No newline at end of file