diff --git a/addon.xml b/addon.xml index a43e3393..ffe99310 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/resources/lib/Entrypoint.py b/resources/lib/Entrypoint.py index d64bd28d..02556930 100644 --- a/resources/lib/Entrypoint.py +++ b/resources/lib/Entrypoint.py @@ -144,6 +144,20 @@ def getThemeMedia(): playback = "DirectStream" else:return + # Set custom path for user + tvtunes_path = xbmc.translatePath("special://profile/addon_data/script.tvtunes/") + if xbmcvfs.exists(tvtunes_path): + tvtunes = xbmcaddon.Addon(id="script.tvtunes") + tvtunes.setSetting('custom_path_enable', "true") + tvtunes.setSetting('custom_path', library) + xbmc.log("TV Tunes custom path is enabled and set.") + 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 = xbmcgui.Dialog() + dialog.ok('Warning', 'The settings file does not exist in tvtunes. Go to the tvtunes addon and change a setting, then come back and re-run') + return + + # Create library directory if not xbmcvfs.exists(library): xbmcvfs.mkdir(library) @@ -157,18 +171,20 @@ def getThemeMedia(): userviewId = view[u'Id'] userViews.append(userviewId) - # Get Ids with Theme songs + + # Get Ids with Theme Videos itemIds = {} for view in userViews: - url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view + url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view result = doUtils.downloadUrl(url) if result[u'TotalRecordCount'] != 0: for item in result[u'Items']: itemId = item[u'Id'] folderName = item[u'Name'] + folderName = utils.normalize_string(folderName.encode('utf-8')) itemIds[itemId] = folderName - # Get paths + # 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 @@ -177,6 +193,64 @@ def getThemeMedia(): # Where to put the nfos nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") + url = "{server}/mediabrowser/Items/%s/ThemeVideos?format=json" % itemId + result = doUtils.downloadUrl(url) + + # Create nfo and write themes to it + nfo_file = open(nfo_path, 'w') + pathstowrite = "" + # May be more than one theme + for theme in result[u'Items']: + if playback == "DirectPlay": + playurl = playUtils.directPlay(theme) + else: + playurl = playUtils.directStream(result, server, theme[u'Id']) + pathstowrite += ('%s' % playurl.encode('utf-8')) + + # Check if the item has theme songs and add them + url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId + result = doUtils.downloadUrl(url) + + # May be more than one theme + for theme in result[u'Items']: + if playback == "DirectPlay": + playurl = playUtils.directPlay(theme) + else: + playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") + 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 userViews: + url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view + result = doUtils.downloadUrl(url) + if result[u'TotalRecordCount'] != 0: + for item in result[u'Items']: + itemId = item[u'Id'] + folderName = item[u'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}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId result = doUtils.downloadUrl(url) @@ -189,7 +263,7 @@ def getThemeMedia(): playurl = playUtils.directPlay(theme) else: playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio") - pathstowrite += ('%s' % playurl) + pathstowrite += ('%s' % playurl.encode('utf-8')) nfo_file.write( '%s' % pathstowrite @@ -576,12 +650,12 @@ def doMainListing(): addDirectoryItem(label, path) # some extra entries for settings and stuff. TODO --> localize the labels - addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings") - addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync") - addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser") - addDirectoryItem("Configure user preferences", "plugin://plugin.video.emby/?mode=userprefs") - addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset") - addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache") - addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia") + addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings", False) + addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync", False) + addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser", False) + addDirectoryItem("Configure user preferences", "plugin://plugin.video.emby/?mode=userprefs", False) + addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset", False) + addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache", False) + addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia", False) xbmcplugin.endOfDirectory(int(sys.argv[1])) diff --git a/resources/lib/LibrarySync.py b/resources/lib/LibrarySync.py index ebfaee9e..90be6ec3 100644 --- a/resources/lib/LibrarySync.py +++ b/resources/lib/LibrarySync.py @@ -131,6 +131,9 @@ class LibrarySync(threading.Thread): # set prop to show we have run for the first time WINDOW.setProperty("startup", "done") + # tell any widgets to refresh because the content has changed + WINDOW.setProperty("widgetreload", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + finally: WINDOW.setProperty("SyncDatabaseRunning", "false") utils.logMsg("Sync DB", "syncDatabase Exiting", 0) @@ -596,6 +599,16 @@ class LibrarySync(threading.Thread): #tv show doesn't exist #perform full tvshow sync instead so both the show and episodes get added self.TvShowsFullSync(connection,cursor,None) + + elif u"Season" in MBitem['Type']: + + #get the tv show + cursor.execute("SELECT kodi_id FROM emby WHERE media_type='tvshow' AND emby_id=?", (MBitem["SeriesId"],)) + result = cursor.fetchone() + if result: + kodi_show_id = result[0] + # update season + WriteKodiVideoDB().updateSeasons(MBitem["SeriesId"], kodi_show_id, connection, cursor) #### PROCESS BOXSETS ###### elif MBitem["Type"] == "BoxSet": @@ -630,9 +643,10 @@ class LibrarySync(threading.Thread): cursor.close() finally: - xbmc.executebuiltin("UpdateLibrary(video)") WINDOW.setProperty("SyncDatabaseRunning", "false") + # tell any widgets to refresh because the content has changed + WINDOW.setProperty("widgetreload", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) def ShouldStop(self): @@ -673,4 +687,4 @@ class LibrarySync(threading.Thread): # Abort was requested while waiting. We should exit break - self.logMsg("--- Library Sync Thread stopped ---", 0) \ No newline at end of file + self.logMsg("--- Library Sync Thread stopped ---", 0) diff --git a/resources/lib/TextureCache.py b/resources/lib/TextureCache.py index e095cbec..81762afb 100644 --- a/resources/lib/TextureCache.py +++ b/resources/lib/TextureCache.py @@ -3,11 +3,11 @@ ################################################################################################# -import xbmc -import xbmcaddon +import xbmc, xbmcaddon, xbmcvfs import json import requests import urllib +import os import Utils as utils @@ -42,15 +42,38 @@ class TextureCache(): def FullTextureCacheSync(self): #this method can be called from the plugin to sync all Kodi textures to the texture cache. #Warning: this means that every image will be cached locally, this takes diskspace! + + #remove all existing textures first + path = "special://thumbnails/" + if xbmcvfs.exists(path): + allDirs, allFiles = xbmcvfs.listdir(path) + for dir in allDirs: + allDirs, allFiles = xbmcvfs.listdir(path+dir) + for file in allFiles: + xbmcvfs.delete(os.path.join(path+dir,file)) + + 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") result = cursor.fetchall() for url in result: self.CacheTexture(url[0]) - cursor.close() + #cache all entries in music DB connection = utils.KodiSQL("music") cursor = connection.cursor() cursor.execute("SELECT url FROM art") @@ -73,7 +96,7 @@ class TextureCache(): except: #extreme short timeouts so we will have a exception, but we don't need the result so pass pass - + def setKodiWebServerDetails(self): # Get the Kodi webserver details - used to set the texture cache diff --git a/resources/lib/Utils.py b/resources/lib/Utils.py index 40bba457..5ea5bcfa 100644 --- a/resources/lib/Utils.py +++ b/resources/lib/Utils.py @@ -54,6 +54,8 @@ def KodiSQL(type="video"): if type == "music": dbPath = getKodiMusicDBPath() + elif type == "texture": + dbPath = xbmc.translatePath("special://database/Textures13.db") else: dbPath = getKodiVideoDBPath() @@ -73,12 +75,7 @@ def getKodiVideoDBPath(): dbVersion = "90" elif kodibuild.startswith("15"): # Isengard - if "BETA1" in kodibuild: - # Beta 1 - dbVersion = "92" - elif "BETA2" in kodibuild: - # Beta 2 - dbVersion = "93" + dbVersion = "93" else: # Not a compatible build xbmc.log("This Kodi version is incompatible. Current version: %s" % kodibuild) @@ -100,17 +97,7 @@ def getKodiMusicDBPath(): dbPath = xbmc.translatePath("special://profile/Database/MyMusic" + dbVersion + ".db") - return dbPath - - -def checkAuthentication(): - #check authentication - if addonSettings.getSetting('username') != "" and addonSettings.getSetting('ipaddress') != "": - try: - downloadUtils.authenticate() - except Exception, e: - logMsg("Emby authentication failed",e) - pass + return dbPath def prettifyXml(elem): rough_string = etree.tostring(elem, "utf-8") @@ -174,6 +161,25 @@ def CleanName(filename): validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits) cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore') return ''.join(c for c in cleanedFilename if c in validFilenameChars) + +def normalize_string(text): + try: + 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') + except: + pass + return text def reset(): diff --git a/resources/lib/WriteKodiVideoDB.py b/resources/lib/WriteKodiVideoDB.py index a980ce22..5de8f6b6 100644 --- a/resources/lib/WriteKodiVideoDB.py +++ b/resources/lib/WriteKodiVideoDB.py @@ -39,18 +39,11 @@ class WriteKodiVideoDB(): cursor = connection.cursor() cursor.execute("SELECT emby_id FROM emby WHERE media_type=? AND kodi_id=?",(type,id)) - emby_id = cursor.fetchone()[0] - cursor.close + emby_id = cursor.fetchone() if(emby_id != None): + emby_id = emby_id[0] # Erase resume point when user marks watched/unwatched to follow Emby behavior - # Also force sets the playcount to instantly reflect the appropriate playstate. - if type == "episode": - resume = '{"jsonrpc": "2.0", "method": "VideoLibrary.SetEpisodeDetails", "params": {"episodeid": %d, "playcount": %d, "resume": {"position": 0}}, "id": "setResumePoint"}' % (id, playcount) - elif type == "movie": - resume = '{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": {"movieid": %d, "playcount": %d, "resume": {"position": 0}}, "id": "setResumePoint"}' % (id, playcount) - xbmc.executeJSONRPC(resume) - addon = xbmcaddon.Addon(id='plugin.video.emby') downloadUtils = DownloadUtils() watchedurl = "{server}/mediabrowser/Users/{UserId}/PlayedItems/%s" % emby_id @@ -58,6 +51,9 @@ class WriteKodiVideoDB(): downloadUtils.downloadUrl(watchedurl, type="POST") else: downloadUtils.downloadUrl(watchedurl, type="DELETE") + + self.setKodiResumePoint(id, 0, 0, cursor) + cursor.close def addOrUpdateMovieToKodiLibrary( self, embyId ,connection, cursor, viewTag): @@ -123,6 +119,13 @@ class WriteKodiVideoDB(): if(jsonData != ""): trailerItem = jsonData trailerUrl = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % trailerItem[0][u'Id'] + elif MBitem.get("RemoteTrailers"): + try: + trailerUrl = MBitem.get("RemoteTrailers")[0].get("Url") + trailerId = trailerUrl.split('=')[1] + trailerUrl = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId + except: + trailerUrl = MBitem.get("RemoteTrailers")[0].get("Url") if MBitem.get("DateCreated") != None: dateadded = MBitem["DateCreated"].split('.')[0].replace('T', " ") @@ -142,24 +145,20 @@ class WriteKodiVideoDB(): #### ADD OR UPDATE THE FILE AND PATH ########### #### NOTE THAT LASTPLAYED AND PLAYCOUNT ARE STORED AT THE FILE ENTRY - if addon.getSetting('useDirectPaths')=='true': - if PlayUtils().isDirectPlay(MBitem): - playurl = PlayUtils().directPlay(MBitem) - #use the direct file path - if "\\" in playurl: - filename = playurl.rsplit("\\",1)[-1] - path = playurl.replace(filename,"") - elif "/" in playurl: - filename = playurl.rsplit("/",1)[-1] - path = playurl.replace(filename,"") - else: - #for transcoding we just use the server's streaming path because I couldn't figure out how to set the plugin path in the music DB - path = server + "/Video/%s/" %MBitem["Id"] - filename = "stream.mp4" + if addon.getSetting('useDirectPaths') == 'true': + playurl = PlayUtils().directPlay(MBitem) + if playurl == False: + return + elif "\\" in playurl: + filename = playurl.rsplit("\\",1)[-1] + path = playurl.replace(filename, "") + elif "/" in playurl: + filename = playurl.rsplit("/",1)[-1] + path = playurl.replace(filename, "") else: path = "plugin://plugin.video.emby/movies/%s/" % MBitem["Id"] filename = "plugin://plugin.video.emby/movies/%s/?id=%s&mode=play" % (MBitem["Id"],MBitem["Id"]) - + #create the path cursor.execute("SELECT idPath as pathid FROM path WHERE strPath = ?",(path,)) result = cursor.fetchone() @@ -757,7 +756,22 @@ class WriteKodiVideoDB(): self.addOrUpdateArt(imageUrl, seasonid, "season", "poster", cursor) imageUrl = API().getArtwork(season, "Banner") - self.addOrUpdateArt(imageUrl, seasonid, "season", "banner", cursor) + self.addOrUpdateArt(imageUrl, seasonid, "season", "banner", cursor) + # Set All season poster + MBitem = ReadEmbyDB().getFullItem(embyTvShowId) + seasonNum = -1 + cursor.execute("SELECT idSeason as seasonid FROM seasons WHERE idShow = ? and season = ?", (kodiTvShowId, seasonNum)) + result = cursor.fetchone() + if result == None: + # Create the all-season + cursor.execute("select coalesce(max(idSeason),0) as seasonid from seasons") + seasonid = cursor.fetchone()[0] + seasonid = seasonid + 1 + cursor.execute("INSERT into seasons(idSeason, idShow, season) values(?, ?, ?)", (seasonid, kodiTvShowId, seasonNum)) + else: + seasonid = result[0] + imageUrl = API().getArtwork(MBitem, "Primary") + self.addOrUpdateArt(imageUrl, seasonid, "season", "poster", cursor) def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): updateDone = False @@ -774,7 +788,8 @@ class WriteKodiVideoDB(): cursor.execute("UPDATE art set url = ? WHERE media_id = ? AND media_type = ? AND type = ?", (imageUrl, kodiId, mediaType, imageType)) #cache fanart and poster in Kodi texture cache - if imageType == "fanart" or imageType == "poster": + if "fanart" in imageType or "poster" in imageType: + utils.logMsg("ArtworkSync", "Adding or Updating Fanart: %s" % imageUrl) self.textureCache.CacheTexture(imageUrl) def setKodiResumePoint(self, fileid, resume_seconds, total_seconds, cursor):