diff --git a/addon.xml b/addon.xml index 223071ae..9fe5d2a3 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ @@ -11,7 +11,7 @@ - executable video audio image + video audio image @@ -19,7 +19,7 @@ Settings for the Emby Server - [!IsEmpty(ListItem.DBID) + !IsEmpty(ListItem.DBTYPE)] | !IsEmpty(ListItem.Property(embyid)) + [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1)] | !IsEmpty(ListItem.Property(embyid)) diff --git a/changelog.txt b/changelog.txt index 34f9cb56..b8a87643 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +version 1.1.76 +- Add music rating system +- Add home videos as a dynamic plugin entry (requires a reset) +- Add photo library +- Add/Fix force transcode setting for 720p-1080p/HEVC-H265 formats +- Fix to incremental sync, caused by the server restarting +- Fix for image caching during the initial sync on rpi devices +- Fix to audio/subtitles tracks (requires a repair, or reset) + version 1.1.72 - Fix to extrafanart - Fix for artists deletion diff --git a/contextmenu.py b/contextmenu.py index e7bee464..ab557b9f 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -112,9 +112,11 @@ if __name__ == '__main__': if newvalue: newvalue = int(newvalue) if newvalue > 5: newvalue = "5" - musicutils.updateRatingToFile(newvalue, API.getFilePath()) - like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue) - API.updateUserRating(embyid, like, favourite, deletelike) + if utils.settings('enableUpdateSongRating') == "true": + musicutils.updateRatingToFile(newvalue, API.getFilePath()) + if utils.settings('enableExportSongRating') == "true": + like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue) + API.updateUserRating(embyid, like, favourite, deletelike) query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" )) kodicursor.execute(query, (newvalue,itemid,)) kodiconn.commit() diff --git a/default.py b/default.py index ee617922..f8401cc0 100644 --- a/default.py +++ b/default.py @@ -33,7 +33,6 @@ class Main: # Parse parameters xbmc.log("Full sys.argv received: %s" % sys.argv) base_url = sys.argv[0] - addon_handle = int(sys.argv[1]) params = urlparse.parse_qs(sys.argv[2][1:]) xbmc.log("Parameter string: %s" % sys.argv[2]) try: @@ -62,6 +61,7 @@ class Main: 'channels': entrypoint.BrowseChannels, 'channelsfolder': entrypoint.BrowseChannels, 'browsecontent': entrypoint.BrowseContent, + 'getsubfolders': entrypoint.GetSubFolders, 'nextup': entrypoint.getNextUpEpisodes, 'inprogressepisodes': entrypoint.getInProgressEpisodes, 'recentepisodes': entrypoint.getRecentEpisodes, @@ -82,11 +82,11 @@ class Main: limit = int(params['limit'][0]) modes[mode](itemid, limit) - elif mode == "channels": + elif mode in ["channels","getsubfolders"]: modes[mode](itemid) elif mode == "browsecontent": - modes[mode]( itemid, params.get('type',[""])[0], params.get('folderid',[""])[0], params.get('filter',[""])[0] ) + modes[mode]( itemid, params.get('type',[""])[0], params.get('folderid',[""])[0] ) elif mode == "channelsfolder": folderid = params['folderid'][0] diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index abcf07a5..5a458fef 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -248,6 +248,10 @@ Favourite Photos Favourite Albums + Recently added Music videos + In progress Music videos + Unwatched Music videos + Active Clear Settings @@ -272,7 +276,6 @@ Remove from Emby favorites Set custom song rating Emby addon settings - Delete item from the server - + Delete item from the server diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 8b5e4e0f..2af4a4e4 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -8,10 +8,12 @@ import os import urllib import xbmc +import xbmcgui import xbmcvfs import utils import clientinfo +import image_cache_thread ################################################################################################# @@ -23,13 +25,18 @@ class Artwork(): 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() @@ -37,7 +44,6 @@ class Artwork(): 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) @@ -49,10 +55,11 @@ class Artwork(): return text def single_urlencode(self, text): - text = urllib.urlencode({'blahblahblah':text}) + + text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) #urlencode needs a utf- string text = text[13:] - return text + 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 @@ -159,63 +166,137 @@ class Artwork(): def FullTextureCacheSync(self): # This method will sync all Kodi artwork to textures13.db # and cache them locally. This takes diskspace! - - # 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: - 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() - + 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') + connection = utils.kodiSQL('video') cursor = connection.cursor() - cursor.execute("SELECT url FROM art") + 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]) - cursor.close() + count += 1 + cursor.close() # Cache all entries in music DB - connection = utils.KodiSQL('music') + 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) - # 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 - + def addArtwork(self, artwork, kodiId, mediaType, cursor): # Kodi conversion table kodiart = { @@ -264,7 +345,6 @@ class Artwork(): # Process backdrops and extra fanart index = "" for backdrop in backdrops: - self.logMsg("imageURL: %s, kodiId: %s, mediatype: %s, imagetype: %s, cursor: %s" % (backdrop, kodiId, mediaType, index, cursor), 2) self.addOrUpdateArt( imageUrl=backdrop, kodiId=kodiId, @@ -429,7 +509,7 @@ def getAllArtwork(self, item, parentInfo=False): id = item['Id'] artworks = item['ImageTags'] - backdrops = item['BackdropImageTags'] + backdrops = item.get('BackdropImageTags',[]) maxHeight = 10000 maxWidth = 10000 diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 8d5b6593..f7c55786 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -7,6 +7,7 @@ from uuid import uuid4 import xbmc import xbmcaddon +import xbmcvfs import utils @@ -43,7 +44,7 @@ class ClientInfo(): if utils.settings('deviceNameOpt') == "false": # Use Kodi's deviceName - deviceName = xbmc.getInfoLabel('System.FriendlyName') + deviceName = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8') else: deviceName = utils.settings('deviceName') deviceName = deviceName.replace("\"", "_") diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 6afc1c22..913cce98 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -316,7 +316,7 @@ class DownloadUtils(): elif r.status_code == requests.codes.ok: try: - # UTF-8 - JSON object + # UNICODE - JSON object r = r.json() self.logMsg("====== 200 Success ======", 2) self.logMsg("Response: %s" % r, 2) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 6c106191..afe21f6d 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -98,31 +98,29 @@ def doMainListing(): path = utils.window('Emby.nodes.%s.content' % i) label = utils.window('Emby.nodes.%s.title' % i) type = utils.window('Emby.nodes.%s.type' % i) - if path and ((xbmc.getCondVisibility("Window.IsActive(Pictures)") and type=="photos") or (xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and type != "photos")): + #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.plexkodiconnect/?mode=passwords", False) - addDirectoryItem("Settings", "plugin://plugin.video.plexkodiconnect/?mode=settings", False) - addDirectoryItem("Switch Plex user", "plugin://plugin.video.plexkodiconnect/?mode=switchuser", False) - #addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.plexkodiconnect/?mode=texturecache") - addDirectoryItem( - label="Refresh Emby playlists", - path="plugin://plugin.video.plexkodiconnect/?mode=refreshplaylist", - folder=False) - addDirectoryItem("Perform manual sync", "plugin://plugin.video.plexkodiconnect/?mode=manualsync", False) - addDirectoryItem( - label="Repair local database (force update all content)", - path="plugin://plugin.video.plexkodiconnect/?mode=repair", - folder=False) - addDirectoryItem( - label="Perform local database reset (full resync)", - path="plugin://plugin.video.plexkodiconnect/?mode=reset", - folder=False) - addDirectoryItem( - label="Sync Emby Theme Media to Kodi", - path="plugin://plugin.video.plexkodiconnect/?mode=thememedia", - folder=False) + 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", "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])) @@ -321,7 +319,7 @@ def getThemeMedia(): result = doUtils.downloadUrl(url) # Create nfo and write themes to it - nfo_file = open(nfo_path, 'w') + nfo_file = xbmcvfs.File(nfo_path, 'w') pathstowrite = "" # May be more than one theme for theme in result['Items']: @@ -382,7 +380,7 @@ def getThemeMedia(): result = doUtils.downloadUrl(url) # Create nfo and write themes to it - nfo_file = open(nfo_path, 'w') + nfo_file = xbmcvfs.File(nfo_path, 'w') pathstowrite = "" # May be more than one theme for theme in result['Items']: @@ -431,11 +429,31 @@ def refreshPlaylist(): time=1000, sound=False) -##### BROWSE EMBY HOMEVIDEOS AND PICTURES ##### -def BrowseContent(viewname, type="", folderid=None, filter=""): +#### 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() - utils.logMsg("BrowseHomeVideos","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname, type, folderid, filter)) + 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: @@ -444,6 +462,7 @@ def BrowseContent(viewname, type="", folderid=None, filter=""): if view.get("name") == viewname: folderid = view.get("id") + utils.logMsg("BrowseContent","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname, type, folderid, filter)) #set the correct params for the content type #only proceed if we have a folderid if folderid: @@ -457,21 +476,25 @@ def BrowseContent(viewname, type="", folderid=None, filter=""): itemtype = "" #get the actual listing - if filter == "recent": - listing = emby.getFilteredSection("", itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending") + 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("", itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") elif filter == "recommended": - listing = emby.getFilteredSection("", itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite") + listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite") elif filter == "sets": - listing = emby.getFilteredSection("", itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite") + 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) + 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], viewname, type, item.get("Id")) @@ -491,10 +514,8 @@ def BrowseContent(viewname, type="", folderid=None, filter=""): xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) ##### CREATE LISTITEM FROM EMBY METADATA ##### -def createListItemFromEmbyItem(item): +def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()): API = api.API(item) - art = artwork.Artwork() - doUtils = downloadutils.DownloadUtils() itemid = item['Id'] title = item.get('Name') @@ -531,10 +552,11 @@ def createListItemFromEmbyItem(item): genre = API.getGenres() overlay = 0 userdata = API.getUserData() + runtime = item.get("RunTimeTicks",0)/ 10000000.0 seektime = userdata['Resume'] if seektime: li.setProperty("resumetime", seektime) - li.setProperty("totaltime", item.get("RunTimeTicks")/ 10000000.0) + li.setProperty("totaltime", str(runtime)) played = userdata['Played'] if played: overlay = 7 @@ -551,26 +573,35 @@ def createListItemFromEmbyItem(item): 'id': itemid, 'rating': rating, 'year': item.get('ProductionYear'), - 'premieredate': premieredate, - 'date': premieredate, '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) - li.setThumbnailImage(allart.get('Primary')) + 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 @@ -594,84 +625,20 @@ def BrowseChannels(itemid, folderid=None): url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid result = doUtils.downloadUrl(url) - try: - channels = result['Items'] - except TypeError: - pass - else: - for item in channels: - - API = api.API(item) + if result and result.get("Items"): + for item in result.get("Items"): itemid = item['Id'] itemtype = item['Type'] - title = item.get('Name', "Missing Title") - li = xbmcgui.ListItem(title) - + li = createListItemFromEmbyItem(item,art,doUtils) if itemtype == "ChannelFolderItem": isFolder = True else: isFolder = False - channelId = item.get('ChannelId', "") channelName = item.get('ChannelName', "") - - premieredate = API.getPremiereDate() - # Process Genres - genre = API.getGenres() - # Process UserData - overlay = 0 - - userdata = API.getUserData() - seektime = userdata['Resume'] - played = userdata['Played'] - if played: - overlay = 7 - else: - overlay = 6 - - favorite = userdata['Favorite'] - if favorite: - overlay = 5 - - playcount = userdata['PlayCount'] - if playcount is None: - playcount = 0 - - # Populate the details list - details = { - - 'title': title, - 'channelname': channelName, - 'plot': API.getOverview(), - 'Overlay': str(overlay), - 'playcount': str(playcount) - } - - if itemtype == "ChannelVideoItem": - xbmcplugin.setContent(_addon_id, 'movies') - elif itemtype == "ChannelAudioItem": - xbmcplugin.setContent(_addon_id, 'songs') - - # Populate the extradata list and artwork - pbutils.PlaybackUtils(item).setArtwork(li) - extradata = { - - 'id': itemid, - 'rating': item.get('CommunityRating'), - 'year': item.get('ProductionYear'), - 'premieredate': premieredate, - 'genre': genre, - 'playcount': str(playcount), - 'itemtype': itemtype - } - li.setInfo('video', infoLabels=extradata) - li.setThumbnailImage(art.getAllArtwork(item)['Primary']) - li.setIconImage('DefaultTVShows.png') - 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) @@ -998,12 +965,12 @@ def getExtraFanArt(): try: # for tvshows we get the embyid just from the path if xbmc.getCondVisibility("Container.Content(tvshows) | Container.Content(seasons) | Container.Content(episodes)"): - itemPath = xbmc.getInfoLabel("ListItem.Path") + itemPath = xbmc.getInfoLabel("ListItem.Path").decode('utf-8') if "plugin.video.emby" in itemPath: embyId = itemPath.split("/")[-2] else: #for movies we grab the emby id from the params - itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath") + itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath").decode('utf-8') if "plugin.video.emby" in itemPath: params = urlparse.parse_qs(itemPath) embyId = params.get('id') @@ -1028,7 +995,10 @@ def getExtraFanArt(): for backdrop in backdrops: # Same ordering as in artwork tag = tags[count] - fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag) + 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]), @@ -1041,7 +1011,7 @@ def getExtraFanArt(): # Use existing cached images dirs, files = xbmcvfs.listdir(fanartDir) for file in files: - fanartFile = os.path.join(fanartDir, file) + fanartFile = os.path.join(fanartDir, file.decode('utf-8')) li = xbmcgui.ListItem(file, path=fanartFile) xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), diff --git a/resources/lib/image_cache_thread.py b/resources/lib/image_cache_thread.py new file mode 100644 index 00000000..626be481 --- /dev/null +++ b/resources/lib/image_cache_thread.py @@ -0,0 +1,52 @@ +import threading +import utils +import xbmc +import requests + +class image_cache_thread(threading.Thread): + + urlToProcess = None + isFinished = False + + xbmc_host = "" + xbmc_port = "" + xbmc_username = "" + xbmc_password = "" + + def __init__(self): + self.monitor = xbmc.Monitor() + threading.Thread.__init__(self) + + def logMsg(self, msg, lvl=1): + className = self.__class__.__name__ + utils.logMsg("%s" % className, msg, lvl) + + def setUrl(self, url): + self.urlToProcess = url + + def setHost(self, host, port): + self.xbmc_host = host + self.xbmc_port = port + + def setAuth(self, user, pwd): + self.xbmc_username = user + self.xbmc_password = pwd + + def run(self): + + self.logMsg("Image Caching Thread Processing : " + self.urlToProcess, 2) + + try: + response = requests.head( + url=( + "http://%s:%s/image/image://%s" + % (self.xbmc_host, self.xbmc_port, self.urlToProcess)), + auth=(self.xbmc_username, self.xbmc_password), + timeout=(35.1, 35.1)) + # We don't need the result + except: pass + + self.logMsg("Image Caching Thread Exited", 2) + + self.isFinished = True + \ No newline at end of file diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index eb88f815..b92225c9 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1498,6 +1498,9 @@ class Music(Items): 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) @@ -1830,17 +1833,16 @@ class Music(Items): track = disc*2**16 + tracknumber year = item.get('ProductionYear') duration = API.getRuntime() - - #the server only returns the rating based on like/love and not the actual rating from the song - rating = userdata['UserRating'] - - #the server doesn't support comment on songs so this will always be empty - comment = API.getOverview() - - #if enabled, try to get the rating and comment value from the file itself + + #if enabled, try to get the rating from file and/or emby if not self.directstream: - rating, comment = self.getSongRatingAndComment(itemid, rating, API) - + rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) + else: + hasEmbeddedCover = False + comment = API.getOverview() + rating = userdata['UserRating'] + + ##### GET THE FILE AND PATH ##### if self.directstream: path = "%s/emby/Audio/%s/" % (self.server, itemid) @@ -2043,7 +2045,10 @@ class Music(Items): # Add genres kodi_db.addMusicGenres(songid, genres, "song") # Update artwork - artwork.addArtwork(artwork.getAllArtwork(item, parentInfo=True), songid, "song", kodicursor) + allart = artwork.getAllArtwork(item, parentInfo=True) + if hasEmbeddedCover: + allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl ) + artwork.addArtwork(allart, songid, "song", kodicursor) def updateUserdata(self, item): # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks @@ -2070,10 +2075,19 @@ class Music(Items): 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'] - rating, comment = self.getSongRatingAndComment(itemid, rating, API) + + #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)) @@ -2085,86 +2099,6 @@ class Music(Items): emby_db.updateReference(itemid, checksum) - def getSongRatingAndComment(self, embyid, emby_rating, API): - - kodicursor = self.kodicursor - - previous_values = None - filename = API.getFilePath() - rating = 0 - emby_rating = int(round(emby_rating, 0)) - file_rating, comment = musicutils.getSongTags(filename) - - - emby_dbitem = self.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,)) - currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) - - # Only proceed if we actually have a rating from the file - if file_rating is None and currentvalue: - return (currentvalue, comment) - elif file_rating is None and currentvalue is None: - return (emby_rating, comment) - - file_rating = int(round(file_rating,0)) - self.logMsg("getSongRatingAndComment --> 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 (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, always prefer file details... - rating = file_rating - updateEmbyRating = True - - if updateFileRating: - musicutils.updateRatingToFile(rating, filename) - - if updateEmbyRating: - # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: - like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(rating) - API.updateUserRating(embyid, like, favourite, deletelike) - - return (rating, comment) - def remove(self, itemid): # Remove kodiid, fileid, pathid, emby reference emby_db = self.emby_db diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index ff1a5e6d..6adae64e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -74,7 +74,7 @@ class KodiMonitor(xbmc.Monitor): self.logMsg("Method: %s Data: %s" % (method, data), 1) if data: - data = json.loads(data) + data = json.loads(data,'utf-8') if method == "Player.OnPlay": @@ -205,6 +205,5 @@ class KodiMonitor(xbmc.Monitor): utils.window('emby_onWake', value="true") elif method == "Playlist.OnClear": - utils.window('emby_customPlaylist', clear=True, windowid=10101) - #xbmcgui.Window(10101).clearProperties() + utils.window('emby_customPlaylist', clear=True) self.logMsg("Clear playlist properties.") \ No newline at end of file diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 11085b1d..04e4f1b3 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -395,7 +395,10 @@ class LibrarySync(threading.Thread): elapsedTime = datetime.now() - startTime self.logMsg( "SyncDatabase (finished %s in: %s)" - % (itemtype, str(elapsedTime).split('.')[0]), 0) + % (itemtype, str(elapsedTime).split('.')[0]), 1) + else: + # Close the Kodi cursor + kodicursor.close() # # sync music # if music_enabled: @@ -424,8 +427,6 @@ class LibrarySync(threading.Thread): utils.settings('SyncInstallRunDone', value="true") utils.settings("dbCreatedWithVersion", self.clientInfo.getVersion()) self.saveLastSync() - # tell any widgets to refresh because the content has changed - utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S')) xbmc.executebuiltin('UpdateLibrary(video)') elapsedtotal = datetime.now() - starttotal @@ -485,12 +486,12 @@ class LibrarySync(threading.Thread): self.logMsg("Creating viewid: %s in Emby database." % folderid, 1) tagid = kodi_db.createTag(foldername) # Create playlist for the video library - if mediatype != "music": + if mediatype in ['movies', 'tvshows', 'musicvideos']: utils.playlistXSP(mediatype, foldername, viewtype) - # Create the video node - if mediatype != "musicvideos": - vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) - totalnodes += 1 + # Create the video node + if mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos']: + vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) + totalnodes += 1 # Add view to emby database emby_db.addView(folderid, foldername, viewtype, tagid) @@ -525,7 +526,8 @@ class LibrarySync(threading.Thread): current_viewtype, delete=True) # Added new playlist - utils.playlistXSP(mediatype, foldername, viewtype) + if mediatype in ['movies', 'tvshows', 'musicvideos']: + utils.playlistXSP(mediatype, foldername, viewtype) # Add new video node if mediatype != "musicvideos": vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) @@ -540,7 +542,8 @@ class LibrarySync(threading.Thread): else: if mediatype != "music": # Validate the playlist exists or recreate it - utils.playlistXSP(mediatype, foldername, viewtype) + if mediatype in ['movies', 'tvshows', 'musicvideos']: + utils.playlistXSP(mediatype, foldername, viewtype) # Create the video node if not already exists if mediatype != "musicvideos": vnodes.viewNode(totalnodes, foldername, mediatype, viewtype) @@ -835,7 +838,7 @@ class LibrarySync(threading.Thread): heading="Emby for Kodi", message="Comparing musicvideos from view: %s..." % viewName) - all_embymvideos = emby.getMusicVideos(viewId, basic=True) + all_embymvideos = emby.getMusicVideos(viewId, basic=True, dialog=pdialog) for embymvideo in all_embymvideos['Items']: if self.shouldStop(): @@ -856,7 +859,7 @@ class LibrarySync(threading.Thread): del updatelist[:] else: # Initial or repair sync - all_embymvideos = emby.getMusicVideos(viewId) + all_embymvideos = emby.getMusicVideos(viewId, dialog=pdialog) total = all_embymvideos['TotalRecordCount'] embymvideos = all_embymvideos['Items'] @@ -1051,7 +1054,7 @@ class LibrarySync(threading.Thread): heading="Emby for Kodi", message="Comparing tvshows from view: %s..." % viewName) - all_embytvshows = emby.getShows(viewId, basic=True) + all_embytvshows = emby.getShows(viewId, basic=True, dialog=pdialog) for embytvshow in all_embytvshows['Items']: if self.shouldStop(): @@ -1071,7 +1074,7 @@ class LibrarySync(threading.Thread): total = len(updatelist) del updatelist[:] else: - all_embytvshows = emby.getShows(viewId) + all_embytvshows = emby.getShows(viewId, dialog=pdialog) total = all_embytvshows['TotalRecordCount'] embytvshows = all_embytvshows['Items'] @@ -1114,7 +1117,7 @@ class LibrarySync(threading.Thread): heading="Emby for Kodi", message="Comparing episodes from view: %s..." % viewName) - all_embyepisodes = emby.getEpisodes(viewId, basic=True) + all_embyepisodes = emby.getEpisodes(viewId, basic=True, dialog=pdialog) for embyepisode in all_embyepisodes['Items']: if self.shouldStop(): @@ -1212,9 +1215,9 @@ class LibrarySync(threading.Thread): pass if type != "artists": - all_embyitems = process[type][0](basic=True) + all_embyitems = process[type][0](basic=True, dialog=pdialog) else: - all_embyitems = process[type][0]() + all_embyitems = process[type][0](dialog=pdialog) for embyitem in all_embyitems['Items']: if self.shouldStop(): @@ -1243,7 +1246,7 @@ class LibrarySync(threading.Thread): total = len(updatelist) del updatelist[:] else: - all_embyitems = process[type][0]() + all_embyitems = process[type][0](dialog=pdialog) total = all_embyitems['TotalRecordCount'] embyitems = all_embyitems['Items'] @@ -1379,9 +1382,6 @@ class LibrarySync(threading.Thread): embyconn.commit() self.saveLastSync() - # tell any widgets to refresh because the content has changed - utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - self.logMsg("Updating video library.", 1) utils.window('emby_kodiScan', value="true") xbmc.executebuiltin('UpdateLibrary(video)') diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py index d9715749..92fbaf66 100644 --- a/resources/lib/musicutils.py +++ b/resources/lib/musicutils.py @@ -5,9 +5,10 @@ import os import xbmc, xbmcaddon, xbmcvfs import utils -from mutagen.flac import FLAC +from mutagen.flac import FLAC, Picture from mutagen.id3 import ID3 from mutagen import id3 +import base64 ################################################################################################# @@ -16,18 +17,23 @@ from mutagen import id3 def logMsg(msg, lvl=1): utils.logMsg("%s %s" % ("Emby", "musictools"), msg, lvl) -def getRealFileName(filename): +def getRealFileName(filename, isTemp=False): #get the filename path accessible by python if possible... - isTemp = False 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(filename): + if os.path.exists(checkfile): filename = filename - elif os.path.exists(filename.replace("smb://","\\\\").replace("/","\\")): + 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... @@ -54,26 +60,145 @@ def getEmbyRatingFromKodiRating(rating): 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): + 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 + API.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 = None + 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"): @@ -83,45 +208,66 @@ def getSongTags(file): 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 ? logMsg("Exception in getSongTags %s" %e,0) - + rating = None + #remove tempfile if needed.... if isTemp: xbmcvfs.delete(filename) - return (rating, comment) + return (rating, comment, hasEmbeddedCover) def updateRatingToFile(rating, file): #update the rating from Emby to the file - isTemp,filename = getRealFileName(file) - logMsg( "setting song rating: %s for filename: %s" %(rating,filename)) + f = xbmcvfs.File(file) + org_size = f.size() + f.close() - if not filename: + #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 filename.lower().endswith(".flac"): - audio = FLAC(filename) + if tempfile.lower().endswith(".flac"): + audio = FLAC(tempfile) calcrating = int(round((float(rating) / 5) * 100, 0)) audio["rating"] = str(calcrating) audio.save() - elif filename.lower().endswith(".mp3"): - audio = ID3(filename) + 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" %(filename)) + logMsg( "Not supported fileformat: %s" %(tempfile)) - #remove tempfile if needed.... - if isTemp: + #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(filename,file) - xbmcvfs.delete(filename) + 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 ? diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index ad738098..a7b231d0 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -78,7 +78,7 @@ class PlaybackUtils(): sizePlaylist = playlist.size() currentPosition = startPos - propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true" + propertiesPlayback = utils.window('emby_playbackProps') == "true" introsPlaylist = False partsPlaylist = False dummyPlaylist = False @@ -96,11 +96,11 @@ class PlaybackUtils(): # Otherwise we get a loop. if not propertiesPlayback: - utils.window('emby_playbackProps', value="true", windowid=10101) + utils.window('emby_playbackProps', value="true") self.logMsg("Setting up properties in playlist.", 1) if (not homeScreen and not seektime and - utils.window('emby_customPlaylist', windowid=10101) != "true"): + utils.window('emby_customPlaylist') != "true"): self.logMsg("Adding dummy file to playlist.", 2) dummyPlaylist = True @@ -190,21 +190,21 @@ class PlaybackUtils(): # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: self.logMsg("Resetting properties playback flag.", 2) - utils.window('emby_playbackProps', clear=True, windowid=10101) + utils.window('emby_playbackProps', clear=True) #self.pl.verifyPlaylist() ########## SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref if utils.window('emby_%s.playmethod' % playurl) == "Transcode": - playurl = playutils.audioSubsPref(playurl, child=self.API.getChildNumber()) + playurl = playutils.audioSubsPref(playurl, listitem, child=self.API.getChildNumber()) utils.window('emby_%s.playmethod' % playurl, value="Transcode") listitem.setPath(playurl) self.setProperties(playurl, listitem) ############### PLAYBACK ################ - customPlaylist = utils.window('emby_customPlaylist', windowid=10101) + customPlaylist = utils.window('emby_customPlaylist') if homeScreen and seektime: self.logMsg("Play as a widget item.", 1) self.setListItem(listitem) @@ -245,7 +245,7 @@ class PlaybackUtils(): # Only for direct play and direct stream # subtitles = self.externalSubs(playurl) subtitles = self.API.externalSubs(playurl) - if playmethod in ("DirectStream", "Transcode"): + if playmethod != "Transcode": # Direct play automatically appends external listitem.setSubtitles(subtitles) diff --git a/resources/lib/player.py b/resources/lib/player.py index 1003957e..fde26a59 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -416,14 +416,10 @@ class Player(xbmc.Player): def onPlayBackStopped( self ): # Will be called when user stops xbmc playing a file - currentFile = self.currentFile self.logMsg("ONPLAYBACK_STOPPED", 2) - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = 'stopped' - self.reportPlayback() - - xbmcgui.Window(10101).clearProperties() - self.logMsg("Clear playlist properties.") + utils.window('emby_customPlaylist', clear=True) + utils.window('emby_playbackProps', clear=True) + self.logMsg("Clear playlist properties.", 1) self.stopAll() def onPlayBackEnded( self ): diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index 03d07d39..c869f288 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -51,7 +51,7 @@ class Playlist(): playlist.clear() started = False - utils.window('emby_customplaylist', value="true", windowid=10101) + utils.window('emby_customplaylist', value="true") position = 0 diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 12535536..90fa9aca 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -43,6 +43,12 @@ class PlayUtils(): if partIndex is not None: self.API.setPartNumber(partIndex) playurl = None + + if item.get('Type') in ["Recording","TvChannel"] and item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http": + #Is this the right way to play a Live TV or recordings ? + self.logMsg("File protocol is http (livetv).", 1) + playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, item['Id']) + utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") # if item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http": # # Only play as http @@ -168,6 +174,12 @@ class PlayUtils(): if not self.h265enabled(): return False + elif (utils.settings('transcode720H265') == "true" and + item['MediaSources'][0]['Name'].startswith(("720P/HEVC","720P/H265"))): + # Avoid H265 720p + self.logMsg("Option to transcode 720P/H265 enabled.", 1) + return False + # Requirement: BitRate, supported encoding # canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] # Plex: always able?!? @@ -273,7 +285,7 @@ class PlayUtils(): # max bit rate supported by server (max signed 32bit integer) return bitrate.get(videoQuality, 2147483) - def audioSubsPref(self, url, child=0): + def audioSubsPref(self, url, listitem, child=0): self.API.setChildNumber(child) # For transcoding only # Present the list of audio to select from @@ -282,6 +294,7 @@ class PlayUtils(): audioStreamsChannelsList = {} subtitleStreamsList = {} subtitleStreams = ['No subtitles'] + downloadableStreams = [] selectAudioIndex = "" selectSubsIndex = "" playurlprefs = "%s" % url @@ -312,8 +325,8 @@ class PlayUtils(): audioStreams.append(track) elif 'Subtitle' in type: - if stream['IsExternal']: - continue + '''if stream['IsExternal']: + continue''' try: track = "%s - %s" % (index, stream['Language']) except: @@ -321,10 +334,14 @@ class PlayUtils(): 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) @@ -352,7 +369,19 @@ class PlayUtils(): # User selected subtitles selected = subtitleStreams[resp] selectSubsIndex = subtitleStreamsList[selected] - playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex + + # 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))] + 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', "") diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index d0158b0e..78e0bd91 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -149,7 +149,37 @@ class Read_EmbyServer(): } return doUtils.downloadUrl(url, parameters=params) - def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False): + 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.downloadUrl(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.downloadUrl(url, parameters=params) + + def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None): doUtils = self.doUtils items = { @@ -210,16 +240,12 @@ class Read_EmbyServer(): "MediaSources" ) result = doUtils.downloadUrl(url, parameters=params) - try: - items['Items'].extend(result['Items']) - except TypeError: - # Connection timed out, reduce the number - jump -= 50 - self.limitindex = jump - self.logMsg("New throttle for items requested: %s" % jump, 1) - else: - index += jump + items['Items'].extend(result['Items']) + index += jump + if dialog: + percentage = int((float(index) / float(total))*100) + dialog.update(percentage) return items def getViews(self, type, root=False): @@ -276,15 +302,15 @@ class Read_EmbyServer(): return views - def getMovies(self, parentId, basic=False): + def getMovies(self, parentId, basic=False, dialog=None): - items = self.getSection(parentId, "Movie", basic=basic) + items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog) return items - def getBoxset(self): + def getBoxset(self, dialog=None): - items = self.getSection(None, "BoxSet") + items = self.getSection(None, "BoxSet", dialog=dialog) return items @@ -294,9 +320,9 @@ class Read_EmbyServer(): return items - def getMusicVideos(self, parentId, basic=False): + def getMusicVideos(self, parentId, basic=False, dialog=None): - items = self.getSection(parentId, "MusicVideo", basic=basic) + items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) return items @@ -306,9 +332,9 @@ class Read_EmbyServer(): return items - def getShows(self, parentId, basic=False): + def getShows(self, parentId, basic=False, dialog=None): - items = self.getSection(parentId, "Series", basic=basic) + items = self.getSection(parentId, "Series", basic=basic, dialog=dialog) return items @@ -332,9 +358,9 @@ class Read_EmbyServer(): return items - def getEpisodes(self, parentId, basic=False): + def getEpisodes(self, parentId, basic=False, dialog=None): - items = self.getSection(parentId, "Episode", basic=basic) + items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog) return items @@ -350,7 +376,7 @@ class Read_EmbyServer(): return items - def getArtists(self): + def getArtists(self, dialog=None): doUtils = self.doUtils items = { @@ -397,21 +423,17 @@ class Read_EmbyServer(): ) } result = doUtils.downloadUrl(url, parameters=params) - try: - items['Items'].extend(result['Items']) - except TypeError: - # Connection timed out, reduce the number - jump -= 50 - self.limitindex = jump - self.logMsg("New throttle for items requested: %s" % jump, 1) - else: - index += jump + 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): + def getAlbums(self, basic=False, dialog=None): - items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic) + items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) return items @@ -421,9 +443,9 @@ class Read_EmbyServer(): return items - def getSongs(self, basic=False): + def getSongs(self, basic=False, dialog=None): - items = self.getSection(None, "Audio", basic=basic) + items = self.getSection(None, "Audio", basic=basic, dialog=dialog) return items @@ -460,4 +482,4 @@ class Read_EmbyServer(): if mediatype: sorted_items.setdefault(mediatype, []).append(item) - return sorted_items + return sorted_items \ No newline at end of file diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 2a93b208..0496fe31 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -73,12 +73,18 @@ 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: - return WINDOW.getProperty(property) + 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 @@ -87,13 +93,12 @@ def settings(setting, value=None): if value is not None: addon.setSetting(setting, value) else: - return addon.getSetting(setting) + return addon.getSetting(setting) #returns unicode object def language(stringid): # Central string retrieval addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') - string = addon.getLocalizedString(stringid).decode("utf-8") - + string = addon.getLocalizedString(stringid) #returns unicode object return string def kodiSQL(type="video"): @@ -169,11 +174,11 @@ def reset(): path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') dirs, files = xbmcvfs.listdir(path) for dir in dirs: - if dir.startswith('Emby'): - shutil.rmtree("%s%s" % (path, dir)) + if dir.decode('utf-8').startswith('Emby'): + shutil.rmtree("%s%s" % (path, dir.decode('utf-8'))) for file in files: - if file.startswith('emby'): - xbmcvfs.delete("%s%s" % (path, file)) + if file.decode('utf-8').startswith('emby'): + xbmcvfs.delete("%s%s" % (path, file.decode('utf-8'))) # Wipe the kodi databases logMsg("EMBY", "Resetting the Kodi video database.") @@ -254,7 +259,7 @@ def stopProfiling(pr, profileName): timestamp = time.strftime("%Y-%m-%d %H-%M-%S") profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp) - f = open(profile, 'wb') + f = xbmcvfs.File(profile, 'w') f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n") for (key, value) in ps.stats.items(): (filename, count, func_name) = key @@ -502,7 +507,7 @@ def playlistXSP(mediatype, tagname, viewtype="", delete=False): } logMsg("EMBY", "Writing playlist file to: %s" % xsppath, 1) try: - f = open(xsppath, 'w') + f = xbmcvfs.File(xsppath, 'w') except: logMsg("EMBY", "Failed to create playlist: %s" % xsppath, 1) return @@ -526,5 +531,5 @@ def deletePlaylists(): path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') dirs, files = xbmcvfs.listdir(path) for file in files: - if file.startswith('Emby'): + if file.decode('utf-8').startswith('Emby'): xbmcvfs.delete("%s%s" % (path, file)) \ No newline at end of file diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index ac60b7c1..1356b23a 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -96,7 +96,7 @@ class VideoNodes(object): return if mediatype=="photos": - path = "plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=photos&filter=index" % tagname + path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber utils.window('Emby.nodes.%s.index' % indexnumber, value=path) @@ -163,6 +163,13 @@ class VideoNodes(object): '8': 30255, '11': 30254 }, + 'musicvideos': + { + '1': tagname, + '2': 30256, + '4': 30257, + '6': 30258 + }, } nodes = mediatypes[mediatype] @@ -185,7 +192,7 @@ class VideoNodes(object): path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s" %(tagname,mediatype) elif (mediatype == "homevideos" or mediatype == "photos"): # Custom query - path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s&filter=%s" %(tagname,mediatype,nodetype) + path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s&folderid=%s" %(tagname,mediatype,nodetype) elif nodetype == "nextepisodes": # Custom query path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=nextup&limit=25" % tagname diff --git a/resources/settings.xml b/resources/settings.xml index 5f054eb7..0420a839 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -37,6 +37,7 @@ + @@ -55,6 +56,7 @@ + @@ -65,6 +67,10 @@ + + + + diff --git a/service.py b/service.py index 1aff254e..11307e1d 100644 --- a/service.py +++ b/service.py @@ -75,13 +75,11 @@ class Service(): "emby_online", "emby_serverStatus", "emby_onWake", "emby_syncRunning", "emby_dbCheck", "emby_kodiScan", "emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId", - "emby_initialScan" + "emby_initialScan", "emby_customplaylist", "emby_playbackProps" ] for prop in properties: utils.window(prop, clear=True) - # Clear playlist properties - xbmcgui.Window(10101).clearProperties() # Clear video nodes properties videonodes.VideoNodes().clearProperties()