diff --git a/resources/lib/PlayUtils.py b/resources/lib/PlayUtils.py index efb48d51..c902298f 100644 --- a/resources/lib/PlayUtils.py +++ b/resources/lib/PlayUtils.py @@ -1,30 +1,20 @@ # -*- coding: utf-8 -*- -################################################################################################# -# utils class ################################################################################################# import xbmc import xbmcgui -import xbmcaddon import xbmcvfs from ClientInformation import ClientInformation import Utils as utils -########################################################################### +################################################################################################# class PlayUtils(): - _shared_state = {} - clientInfo = ClientInformation() - addonName = clientInfo.getAddonName() - WINDOW = xbmcgui.Window(10000) - - def __init__(self): - self.__dict__ = self._shared_state def logMsg(self, msg, lvl=1): @@ -33,59 +23,51 @@ class PlayUtils(): def getPlayUrl(self, server, id, result): - WINDOW = self.WINDOW - username = WINDOW.getProperty('currUser') - server = WINDOW.getProperty('server%s' % username) - if self.isDirectPlay(result,True): # Try direct play playurl = self.directPlay(result) if playurl: self.logMsg("File is direct playing.", 1) - WINDOW.setProperty("%splaymethod" % playurl.encode('utf-8'), "DirectPlay") + utils.window("%splaymethod" % playurl.encode('utf-8'), value="DirectPlay") elif self.isDirectStream(result): # Try direct stream playurl = self.directStream(result, server, id) if playurl: self.logMsg("File is direct streaming.", 1) - WINDOW.setProperty("%splaymethod" % playurl, "DirectStream") + utils.window("%splaymethod" % playurl, value="DirectStream") elif self.isTranscoding(result): # Try transcoding playurl = self.transcoding(result, server, id) if playurl: self.logMsg("File is transcoding.", 1) - WINDOW.setProperty("%splaymethod" % playurl, "Transcode") - else: - # Error - return False + utils.window("%splaymethod" % playurl, value="Transcode") + + else: # Error + utils.window("playurlFalse", value="true") + return return playurl.encode('utf-8') - def isDirectPlay(self, result, dialog=False): + + def isDirectPlay(self, result, dialog = False): # Requirements for Direct play: # FileSystem, Accessible path - - playhttp = utils.settings('playFromStream') - # User forcing to play via HTTP instead of SMB - if playhttp == "true": + if utils.settings('playFromStream') == "true": + # User forcing to play via HTTP instead of SMB self.logMsg("Can't direct play: Play from HTTP is enabled.", 1) return False - canDirectPlay = result[u'MediaSources'][0][u'SupportsDirectPlay'] + canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay'] # Make sure it's supported by server if not canDirectPlay: self.logMsg("Can't direct play: Server does not allow or support it.", 1) return False - if result['Path'].endswith('.strm'): - # Allow strm loading when direct playing - return True - - location = result[u'LocationType'] + location = result['LocationType'] # File needs to be "FileSystem" - if u'FileSystem' in location: + if 'FileSystem' in location: # Verify if path is accessible if self.fileExists(result): return True @@ -107,53 +89,49 @@ class PlayUtils(): return False - def directPlay(self, result): try: - try: - playurl = result[u'MediaSources'][0][u'Path'] - except: - playurl = result[u'Path'] - except: - self.logMsg("Direct play failed. Trying Direct stream.", 1) - return False - else: - if u'VideoType' in result: - # Specific format modification - if u'Dvd' in result[u'VideoType']: - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif u'BluRay' in result[u'VideoType']: - playurl = "%s/BDMV/index.bdmv" % playurl + playurl = result['MediaSources'][0]['Path'] + except KeyError: + playurl = result['Path'] - # Network - SMB protocol - if "\\\\" in playurl: - smbuser = utils.settings('smbusername') - smbpass = utils.settings('smbpassword') - # Network share - if smbuser: - playurl = playurl.replace("\\\\", "smb://%s:%s@" % (smbuser, smbpass)) - else: - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") - - if "apple.com" in playurl: - USER_AGENT = "QuickTime/7.7.4" - playurl += "?|User-Agent=%s" % USER_AGENT + if 'VideoType' in result: + # Specific format modification + if 'Dvd' in result['VideoType']: + playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl + elif 'BluRay' in result['VideoType']: + playurl = "%s/BDMV/index.bdmv" % playurl + + # Network - SMB protocol + if "\\\\" in playurl: + smbuser = utils.settings('smbusername') + smbpass = utils.settings('smbpassword') + # Network share + if smbuser: + playurl = playurl.replace("\\\\", "smb://%s:%s@" % (smbuser, smbpass)) + else: + 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 - return playurl def isDirectStream(self, result): # Requirements for Direct stream: # FileSystem or Remote, BitRate, supported encoding - canDirectStream = result[u'MediaSources'][0][u'SupportsDirectStream'] + canDirectStream = result['MediaSources'][0]['SupportsDirectStream'] # Make sure it's supported by server if not canDirectStream: return False - location = result[u'LocationType'] + location = result['LocationType'] # File can be FileSystem or Remote, not Virtual - if u'Virtual' in location: + if 'Virtual' in location: self.logMsg("File location is virtual. Can't proceed.", 1) return False @@ -171,33 +149,29 @@ class PlayUtils(): playurl = self.directPlay(result) return playurl - try: - if "ThemeVideo" in type: - playurl ="%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) + if "ThemeVideo" in type: + playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) + + elif "Video" in type: + playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) + + elif "Audio" in type: + playurl = "%s/mediabrowser/Audio/%s/stream.mp3" % (server, id) + + return playurl - elif "Video" in type: - playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) - - elif "Audio" in type: - playurl = "%s/mediabrowser/Audio/%s/stream.mp3" % (server, id) - - return playurl - - except: - self.logMsg("Direct stream failed. Trying transcoding.", 1) - return False def isTranscoding(self, result): # Last resort, no requirements # BitRate - canTranscode = result[u'MediaSources'][0][u'SupportsTranscoding'] + canTranscode = result['MediaSources'][0]['SupportsTranscoding'] # Make sure it's supported by server if not canTranscode: return False - location = result[u'LocationType'] + location = result['LocationType'] # File can be FileSystem or Remote, not Virtual - if u'Virtual' in location: + if 'Virtual' in location: return False return True @@ -208,31 +182,28 @@ class PlayUtils(): # Allow strm loading when transcoding playurl = self.directPlay(result) return playurl - - try: - # Play transcoding - deviceId = self.clientInfo.getMachineId() - playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id) - playurl = "%s&VideoCodec=h264&AudioCodec=ac3&deviceId=%s&VideoBitrate=%s" % (playurl, deviceId, self.getVideoBitRate()*1000) - self.logMsg("Playurl: %s" % playurl) - - return playurl - except: - self.logMsg("Transcoding failed.") - return False + # Play transcoding + deviceId = self.clientInfo.getMachineId() + playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id) + playurl = "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" % (playurl, deviceId, self.getVideoBitRate()*1000) - # Works out if the network quality can play directly or if transcoding is needed + playurl = self.audioSubsPref(playurl, result.get('MediaSources')) + self.logMsg("Playurl: %s" % playurl, 1) + + return playurl + + def isNetworkQualitySufficient(self, result): - + # Works out if the network quality can play directly or if transcoding is needed settingsVideoBitRate = self.getVideoBitRate() settingsVideoBitRate = settingsVideoBitRate * 1000 try: - mediaSources = result[u'MediaSources'] - sourceBitRate = int(mediaSources[0][u'Bitrate']) - except: - self.logMsg("Bitrate value is missing.") + mediaSources = result['MediaSources'] + sourceBitRate = int(mediaSources[0]['Bitrate']) + except KeyError: + self.logMsg("Bitrate value is missing.", 1) else: self.logMsg("The video quality selected is: %s, the video bitrate required to direct stream is: %s." % (settingsVideoBitRate, sourceBitRate), 1) if settingsVideoBitRate < sourceBitRate: @@ -243,58 +214,40 @@ class PlayUtils(): def getVideoBitRate(self): # get the addon video quality videoQuality = utils.settings('videoBitRate') + bitrate = { - if (videoQuality == "0"): - return 664 - elif (videoQuality == "1"): - return 996 - elif (videoQuality == "2"): - return 1320 - elif (videoQuality == "3"): - return 2000 - elif (videoQuality == "4"): - return 3200 - elif (videoQuality == "5"): - return 4700 - elif (videoQuality == "6"): - return 6200 - elif (videoQuality == "7"): - return 7700 - elif (videoQuality == "8"): - return 9200 - elif (videoQuality == "9"): - return 10700 - elif (videoQuality == "10"): - return 12200 - elif (videoQuality == "11"): - return 13700 - elif (videoQuality == "12"): - return 15200 - elif (videoQuality == "13"): - return 16700 - elif (videoQuality == "14"): - return 18200 - elif (videoQuality == "15"): - return 20000 - elif (videoQuality == "16"): - return 40000 - elif (videoQuality == "17"): - return 100000 - elif (videoQuality == "18"): - return 1000000 - else: - return 2147483 # max bit rate supported by server (max signed 32bit integer) + '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 fileExists(self, result): - if u'Path' not in result: + if 'Path' not in result: # File has no path in server return False # Convert Emby path to a path we can verify path = self.directPlay(result) - if not path: - return False try: pathexists = xbmcvfs.exists(path) @@ -312,4 +265,87 @@ class PlayUtils(): return True else: self.logMsg("Path is detected as follow: %s. Try direct streaming." % path, 2) - return False \ No newline at end of file + return False + + def audioSubsPref(self, url, mediaSources): + # For transcoding only + # Present the list of audio to select from + audioStreamsList = {} + audioStreams = [] + audioStreamsChannelsList = {} + subtitleStreamsList = {} + subtitleStreams = ['No subtitles'] + selectAudioIndex = "" + selectSubsIndex = "" + playurlprefs = "%s" % url + + mediaStream = mediaSources[0].get('MediaStreams') + for stream in mediaStream: + # 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['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'] + if default: + track = "%s - Default" % track + if forced: + track = "%s - Forced" % track + + subtitleStreamsList[track] = index + subtitleStreams.append(track) + + + if len(audioStreams) > 1: + resp = xbmcgui.Dialog().select("Choose the audio stream", 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[0]['DefaultAudioStreamIndex'] + else: # There's only one audiotrack. + selectAudioIndex = audioStreamsList[audioStreams[0]] + playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex + + if len(subtitleStreams) > 1: + resp = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams) + if resp == 0: + # User selected no subtitles + pass + elif resp > -1: + # User selected subtitles + selected = subtitleStreams[resp] + selectSubsIndex = subtitleStreamsList[selected] + playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex + else: # User backed out of selection + playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].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/PlaybackUtils.py b/resources/lib/PlaybackUtils.py index b01bbe17..6a7e2f2b 100644 --- a/resources/lib/PlaybackUtils.py +++ b/resources/lib/PlaybackUtils.py @@ -1,190 +1,151 @@ +# -*- coding: utf-8 -*- + +################################################################################################# -import xbmc -import xbmcplugin -import xbmcgui -import xbmcaddon -import urllib import datetime -import time import json as json -import inspect import sys +import xbmc +import xbmcaddon +import xbmcplugin +import xbmcgui + +from API import API from DownloadUtils import DownloadUtils from PlayUtils import PlayUtils -from ReadKodiDB import ReadKodiDB -from ReadEmbyDB import ReadEmbyDB +from ClientInformation import ClientInformation import Utils as utils -from API import API -import os -import xbmcvfs -addon = xbmcaddon.Addon() -addondir = xbmc.translatePath(addon.getAddonInfo('profile')) - -WINDOW = xbmcgui.Window( 10000 ) +################################################################################################# class PlaybackUtils(): - settings = None + clientInfo = ClientInformation() + doUtils = DownloadUtils() + api = API() + + addon = xbmcaddon.Addon() language = addon.getLocalizedString - logLevel = 0 - downloadUtils = DownloadUtils() + addonName = clientInfo.getAddonName() - WINDOW.clearProperty('playurlFalse') - - def __init__(self, *args): - pass + username = utils.window('currUser') + userid = utils.window('userId%s' % username) + server = utils.window('server%s' % username) - def PLAY(self, result, setup="service"): - xbmc.log("PLAY Called") - WINDOW = xbmcgui.Window(10000) - - username = WINDOW.getProperty('currUser') - userid = WINDOW.getProperty('userId%s' % username) - server = WINDOW.getProperty('server%s' % username) + def logMsg(self, msg, lvl=1): - try: - id = result["Id"] - except: - return + className = self.__class__.__name__ + utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl)) - userData = result['UserData'] + def PLAY(self, result, setup = "service"): - # BOOKMARK - RESUME POINT - timeInfo = API().getTimeInfo(result) - jumpBackSec = int(utils.settings("resumeJumpBack")) + self.logMsg("PLAY Called", 1) + + api = self.api + doUtils = self.doUtils + server = self.server + + listItem = xbmcgui.ListItem() + id = result['Id'] + userdata = result['UserData'] + + # Get the playurl - direct play, direct stream or transcoding + playurl = PlayUtils().getPlayUrl(server, id, result) + + if utils.window('playurlFalse') == "true": + # Playurl failed - set in PlayUtils.py + utils.window('playurlFalse', clear=True) + self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1) + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem) + + # Resume point for widget only + timeInfo = api.getTimeInfo(result) + jumpBackSec = int(utils.settings('resumeJumpBack')) seekTime = round(float(timeInfo.get('ResumeTime')), 6) if seekTime > jumpBackSec: # To avoid negative bookmark seekTime = seekTime - jumpBackSec - itemsToPlay = [] + # Show the additional resume dialog if launched from a widget + if xbmc.getCondVisibility('Window.IsActive(home)') and seekTime: + # Dialog presentation + displayTime = str(datetime.timedelta(seconds=(int(seekTime)))) + display_list = ["%s %s" % (self.language(30106), displayTime), self.language(30107)] + resume_result = xbmcgui.Dialog().select(self.language(30105), display_list) + + if resume_result == 0: + # User selected to resume, append resume point to listitem + listItem.setProperty('StartOffset', str(seekTime)) + + elif resume_result > 0: + # User selected to start from beginning + seekTime = 0 + + elif resume_result < 0: + # User cancelled the dialog + self.logMsg("User cancelled resume dialog.", 1) + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem) + + + # In order, intros, original item requested and any additional parts + playstack = [] + # Check for intros - if seekTime == 0: + if utils.settings('disableCinema') == "false" and not seekTime: # if we have any play them when the movie/show is not being resumed - # We can add the option right here url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id - intros = self.downloadUtils.downloadUrl(url) - if intros[u'TotalRecordCount'] == 0: - pass - else: - for intro in intros[u'Items']: - introId = intro[u'Id'] - itemsToPlay.append(introId) + intros = doUtils.downloadUrl(url) + if intros['TotalRecordCount'] != 0: + for intro in intros['Items']: + introPlayurl = PlayUtils().getPlayUrl(server, intro['Id'], intro) + self.setProperties(introPlayurl, intro) + playstack.append(introPlayurl) # Add original item - itemsToPlay.append(id) + playstack.append(playurl) + self.setProperties(playurl, result) - # For split movies - if u'PartCount' in result: - partcount = result[u'PartCount'] - # Get additional parts/playurl + mainArt = API().getArtwork(result, "Primary") + listItem.setThumbnailImage(mainArt) + listItem.setIconImage(mainArt) + + # Get additional parts/playurl + if result.get('PartCount'): url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id - parts = self.downloadUtils.downloadUrl(url) - for part in parts[u'Items']: - partId = part[u'Id'] - itemsToPlay.append(partId) - - if len(itemsToPlay) > 1: - # Let's play the playlist - playlist = self.AddToPlaylist(itemsToPlay) - if xbmc.getCondVisibility("Window.IsActive(home)"): - # Widget workaround - return xbmc.Player().play(playlist) - else: - # Can't pass a playlist to setResolvedUrl, so return False - # Wait for Kodi to process setResolved failure, then launch playlist - xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) - xbmc.sleep(10) - # Since we clear the original Kodi playlist, this is to prevent - # info dialog - can't play next item from showing up. - xbmc.executebuiltin('Dialog.Close(all,true)') - return xbmc.Player().play(playlist) - - playurl = PlayUtils().getPlayUrl(server, id, result) - - if playurl == False or WINDOW.getProperty('playurlFalse') == "true": - WINDOW.clearProperty('playurlFalse') - xbmc.log("Failed to retrieve the playback path/url.") - return - - if WINDOW.getProperty("%splaymethod" % playurl) == "Transcode": - # Transcoding, we pull every track to set before playback starts - playurlprefs = self.audioSubsPref(playurl, result.get("MediaSources")) - if playurlprefs: - playurl = playurlprefs - else: # User cancelled dialog - return + parts = doUtils.downloadUrl(url) + for part in parts['Items']: + additionalPlayurl = PlayUtils().getPlayUrl(server, part['Id'], part) + self.setProperties(additionalPlayurl, part) + playstack.append(additionalPlayurl) - thumbPath = API().getArtwork(result, "Primary") - - #if the file is a virtual strm file, we need to override the path by reading it's contents - if playurl.endswith(".strm"): - xbmc.log("virtual strm file file detected, starting playback with 3th party addon...") - StrmTemp = "special://temp/temp.strm" - xbmcvfs.copy(playurl, StrmTemp) - playurl = open(xbmc.translatePath(StrmTemp), 'r').readline() - - listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + if len(playstack) > 1: + # Convert list in stack:// playurl + playMethod = utils.window('%splaymethod' % playurl) + playurl = "stack://%s" % " , ".join(playstack) + # Set new window properties for combined playurl + utils.window("%splaymethod" % playurl, value=playMethod) + self.setProperties(playurl, result) - if WINDOW.getProperty("%splaymethod" % playurl) != "Transcode": + self.logMsg("Returned playurl: %s" % playurl, 1) + listItem.setPath(playurl) + + if utils.window("%splaymethod" % playurl) != "Transcode": # Only for direct play and direct stream # Append external subtitles to stream - subtitleList = self.externalSubs(id, playurl, server, result.get('MediaSources')) + subtitleList = self.externalSubs(id, playurl, server, result['MediaSources']) listItem.setSubtitles(subtitleList) - #pass - # Can not play virtual items - if (result.get("LocationType") == "Virtual"): - xbmcgui.Dialog().ok(self.language(30128), self.language(30129)) - - watchedurl = "%s/mediabrowser/Users/%s/PlayedItems/%s" % (server, userid, id) - positionurl = "%s/mediabrowser/Users/%s/PlayingItems/%s" % (server, userid, id) - deleteurl = "%s/mediabrowser/Items/%s" % (server, id) - - # set the current playing info - WINDOW.setProperty(playurl+"watchedurl", watchedurl) - WINDOW.setProperty(playurl+"positionurl", positionurl) - WINDOW.setProperty(playurl+"deleteurl", "") - WINDOW.setProperty(playurl+"deleteurl", deleteurl) - - #show the additional resume dialog if launched from a widget - if xbmc.getCondVisibility("Window.IsActive(home)"): - if seekTime != 0: - displayTime = str(datetime.timedelta(seconds=(int(seekTime)))) - display_list = [ self.language(30106) + ' ' + displayTime, self.language(30107)] - resumeScreen = xbmcgui.Dialog() - resume_result = resumeScreen.select(self.language(30105), display_list) - if resume_result == 0: - listItem.setProperty('StartOffset', str(seekTime)) - - elif resume_result < 0: - # User cancelled dialog - xbmc.log("Emby player -> User cancelled resume dialog.") - xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem) - return - - if result.get("Type")=="Episode": - WINDOW.setProperty(playurl+"refresh_id", result.get("SeriesId")) - else: - WINDOW.setProperty(playurl+"refresh_id", id) - WINDOW.setProperty(playurl+"runtimeticks", str(result.get("RunTimeTicks"))) - WINDOW.setProperty(playurl+"type", result.get("Type")) - WINDOW.setProperty(playurl+"item_id", id) - - #launch the playback - only set the listitem props if we're not using the setresolvedurl approach - if setup == "service": + # Launch the playback - only set the listitem props if we're not using the setresolvedurl approach + if setup == "service" or xbmc.getCondVisibility('Window.IsActive(home)'): + # Sent via websocketclient.py or default.py but via widgets self.setListItemProps(server, id, listItem, result) - xbmc.Player().play(playurl,listItem) + xbmc.Player().play(playurl, listItem) elif setup == "default": - if xbmc.getCondVisibility("Window.IsActive(home)"): - self.setListItemProps(server, id, listItem, result) - xbmc.Player().play(playurl,listItem) - else: - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) + # Sent via default.py + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) def externalSubs(self, id, playurl, server, mediaSources): @@ -214,173 +175,170 @@ class PlaybackUtils(): kodiindex += 1 mapping = json.dumps(mapping) - utils.window('%sIndexMapping' % playurl, mapping) + utils.window('%sIndexMapping' % playurl, value=mapping) return externalsubs - def audioSubsPref(self, url, mediaSources): + def setProperties(self, playurl, result): + # Set runtimeticks, type, refresh_id and item_id + id = result.get('Id') + type = result.get('Type', "") - WINDOW = xbmcgui.Window(10000) - # Present the list of audio to select from - audioStreamsList = {} - audioStreams = [] - selectAudioIndex = "" - subtitleStreamsList = {} - subtitleStreams = ['No subtitles'] - selectSubsIndex = "" - playurlprefs = "%s" % url + utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks'))) + utils.window("%stype" % playurl, value=type) + utils.window("%sitem_id" % playurl, value=id) - mediaStream = mediaSources[0].get('MediaStreams') - for stream in mediaStream: - index = stream['Index'] - # Since Emby returns all possible tracks together, have to sort them. - if 'Audio' in stream['Type']: - try: - track = stream['Language'] - audioStreamsList[track] = index - audioStreams.append(track) - except: - track = stream['Codec'] - audioStreamsList[track] = index - audioStreams.append(track) + if type == "Episode": + utils.window("%srefresh_id" % playurl, value=result.get('SeriesId')) + else: + utils.window("%srefresh_id" % playurl, value=id) - elif 'Subtitle' in stream['Type']: - try: - track = stream['Language'] - subtitleStreamsList[track] = index - subtitleStreams.append(track) - except: - track = stream['Codec'] - subtitleStreamsList[track] = index - subtitleStreams.append(track) - - if len(audioStreams) > 1: - resp = xbmcgui.Dialog().select("Choose the audio stream", audioStreams) - if resp > -1: - # User selected audio - selected = audioStreams[resp] - selected_audioIndex = audioStreamsList[selected] - playurlprefs += "&AudioStreamIndex=%s" % selected_audioIndex - selectAudioIndex = str(selected_audioIndex) - else: return False - else: # There's only one audiotrack. - audioIndex = audioStreamsList[audioStreams[0]] - playurlprefs += "&AudioStreamIndex=%s" % audioIndex - selectAudioIndex = str(audioIndex) - - if len(subtitleStreams) > 1: - resp = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams) - if resp == 0: - # User selected no subtitles - pass - elif resp > -1: - # User selected subtitles - selected = subtitleStreams[resp] - selected_subsIndex = subtitleStreamsList[selected] - playurlprefs += "&SubtitleStreamIndex=%s" % selected_subsIndex - selectSubsIndex = str(selected_subsIndex) - else: return False + def setArt(self, list, name, path): - # Reset the method with the new playurl - WINDOW.setProperty("%splaymethod" % playurlprefs, "Transcode") - WINDOW.setProperty("%sAudioStreamIndex" % playurlprefs, selectAudioIndex) - WINDOW.setProperty("%sSubtitleStreamIndex" % playurlprefs, selectSubsIndex) - - return playurlprefs - - def setArt(self, list,name,path): - if name=='thumb' or name=='fanart_image' or name=='small_poster' or name=='tiny_poster' or name == "medium_landscape" or name=='medium_poster' or name=='small_fanartimage' or name=='medium_fanartimage' or name=='fanart_noindicators': + if name in ("thumb", "fanart_image", "small_poster", "tiny_poster", "medium_landscape", "medium_poster", "small_fanartimage", "medium_fanartimage", "fanart_noindicators"): list.setProperty(name, path) else: list.setArt({name:path}) + return list def setListItemProps(self, server, id, listItem, result): - # set up item and item info - thumbID = id - eppNum = -1 - seasonNum = -1 - tvshowTitle = "" - - if(result.get("Type") == "Episode"): - thumbID = result.get("SeriesId") - seasonNum = result.get("ParentIndexNumber") - eppNum = result.get("IndexNumber") - tvshowTitle = result.get("SeriesName") + # Set up item and item info + api = self.api + + type = result.get('Type') + people = api.getPeople(result) + studios = api.getStudios(result) + + metadata = { + 'title': result.get('Name', "Missing name"), + 'year': result.get('ProductionYear'), + 'plot': api.getOverview(result), + 'director': people.get('Director'), + 'writer': people.get('Writer'), + 'mpaa': api.getMpaa(result), + 'genre': api.getGenre(result), + 'studio': " / ".join(studios), + 'aired': api.getPremiereDate(result), + 'rating': result.get('CommunityRating'), + 'votes': result.get('VoteCount') + } + + if "Episode" in type: + # Only for tv shows + thumbId = result.get('SeriesId') + season = result.get('ParentIndexNumber', -1) + episode = result.get('IndexNumber', -1) + show = result.get('SeriesName', "") + + metadata['TVShowTitle'] = show + metadata['season'] = season + metadata['episode'] = episode + + listItem.setProperty('IsPlayable', 'true') + listItem.setProperty('IsFolder', 'false') + listItem.setInfo('video', infoLabels=metadata) + + # Set artwork for listitem self.setArt(listItem,'poster', API().getArtwork(result, "Primary")) self.setArt(listItem,'tvshow.poster', API().getArtwork(result, "SeriesPrimary")) self.setArt(listItem,'clearart', API().getArtwork(result, "Art")) - self.setArt(listItem,'tvshow.clearart', API().getArtwork(result, "Art")) + self.setArt(listItem,'tvshow.clearart', API().getArtwork(result, "Art")) self.setArt(listItem,'clearlogo', API().getArtwork(result, "Logo")) - self.setArt(listItem,'tvshow.clearlogo', API().getArtwork(result, "Logo")) - self.setArt(listItem,'discart', API().getArtwork(result, "Disc")) + self.setArt(listItem,'tvshow.clearlogo', API().getArtwork(result, "Logo")) + self.setArt(listItem,'discart', API().getArtwork(result, "Disc")) self.setArt(listItem,'fanart_image', API().getArtwork(result, "Backdrop")) - self.setArt(listItem,'landscape', API().getArtwork(result, "Thumb")) - - listItem.setProperty('IsPlayable', 'true') - listItem.setProperty('IsFolder', 'false') - - # Process Studios - studios = API().getStudios(result) - if studios == []: - studio = "" - else: - studio = studios[0] - listItem.setInfo('video', {'studio' : studio}) - - details = { - 'title' : result.get("Name", "Missing Name"), - 'plot' : result.get("Overview") - } - - if(eppNum > -1): - details["episode"] = str(eppNum) - - if(seasonNum > -1): - details["season"] = str(seasonNum) - - if tvshowTitle != None: - details["TVShowTitle"] = tvshowTitle - - listItem.setInfo( "Video", infoLabels=details ) - - people = API().getPeople(result) - - # Process Genres - genre = API().getGenre(result) - - listItem.setInfo('video', {'director' : people.get('Director')}) - listItem.setInfo('video', {'writer' : people.get('Writer')}) - listItem.setInfo('video', {'mpaa': result.get("OfficialRating")}) - listItem.setInfo('video', {'genre': API().getGenre(result)}) + self.setArt(listItem,'landscape', API().getArtwork(result, "Thumb")) def seekToPosition(self, seekTo): - - #Set a loop to wait for positive confirmation of playback + # Set a loop to wait for positive confirmation of playback count = 0 while not xbmc.Player().isPlaying(): - count = count + 1 + count += 1 if count >= 10: return else: xbmc.sleep(500) - #Jump to resume point - jumpBackSec = int(addon.getSetting("resumeJumpBack")) - seekToTime = seekTo - jumpBackSec + # Jump to seek position count = 0 while xbmc.Player().getTime() < (seekToTime - 5) and count < 11: # only try 10 times - count = count + 1 - #xbmc.Player().pause - #xbmc.sleep(100) - xbmc.Player().seekTime(seekToTime) + count += 1 + xbmc.Player().seekTime(seekTo) xbmc.sleep(100) - #xbmc.Player().play() def PLAYAllItems(self, items, startPositionTicks): - utils.logMsg("PlayBackUtils", "== ENTER: PLAYAllItems ==") - utils.logMsg("PlayBackUtils", "Items : " + str(items)) + + self.logMsg("== ENTER: PLAYAllItems ==") + self.logMsg("Items: %s" % items) + + doUtils = self.doUtils + + playlist = xbmc.Playlist(xbmc.PLAYLIST_VIDEO) + playlist.clear() + started = False + + for itemId in items: + self.logMsg("Adding Item to playlist: %s" % itemId, 1) + url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId + result = doUtils.downloadUrl(url) + + addition = self.addPlaylistItem(playlist, result) + if not started and addition: + started = True + self.logMsg("Starting Playback Pre", 1) + xbmc.Player().play(playlist) + + if not started: + self.logMsg("Starting Playback Post", 1) + xbmc.Player().play(playlist) + + # Seek to position + if startPositionTicks: + seekTime = startPositionTicks / 10000000.0 + self.seekToPosition(seekTime) + + def AddToPlaylist(self, itemIds): + + self.logMsg("PlayBackUtils", "== ENTER: PLAYAllItems ==") + + doUtils = self.doUtils + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + for itemId in itemIds: + self.logMsg("Adding Item to Playlist: %s" % itemId) + url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId + result = doUtils.downloadUrl(url) + + self.addPlaylistItem(playlist, result) + + return playlist + + def addPlaylistItem(self, playlist, item): + + id = item['Id'] + server = self.server + + playurl = PlayUtils().getPlayUrl(server, id, item) + + if utils.window('playurlFalse') == "true": + # Playurl failed - set in PlayUtils.py + utils.window('playurlFalse', clear=True) + self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1) + return + + self.logMsg("Playurl: %s" % playurl) + + thumb = API().getArtwork(item, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumb, thumbnailImage=thumb) + self.setListItemProps(server, id, listItem, item) + self.setProperties(playurl, item) + + playlist.add(playurl, listItem) + + # Not currently being used + '''def PLAYAllEpisodes(self, items): WINDOW = xbmcgui.Window(10000) username = WINDOW.getProperty('currUser') @@ -389,42 +347,6 @@ class PlaybackUtils(): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist.clear() - started = False - - for itemID in items: - - utils.logMsg("PlayBackUtils", "Adding Item to Playlist : " + itemID) - item_url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemID - jsonData = self.downloadUtils.downloadUrl(item_url) - - item_data = jsonData - added = self.addPlaylistItem(playlist, item_data, server, userid) - if(added and started == False): - started = True - utils.logMsg("PlayBackUtils", "Starting Playback Pre") - xbmc.Player().play(playlist) - - if(started == False): - utils.logMsg("PlayBackUtils", "Starting Playback Post") - xbmc.Player().play(playlist) - - #seek to position - seekTime = 0 - if(startPositionTicks != None): - seekTime = (startPositionTicks / 1000) / 10000 - - if seekTime > 0: - self.seekToPosition(seekTime) - - def PLAYAllEpisodes(self, items): - WINDOW = xbmcgui.Window(10000) - - username = WINDOW.getProperty('currUser') - userid = WINDOW.getProperty('userId%s' % username) - server = WINDOW.getProperty('server%s' % username) - - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - playlist.clear() for item in items: @@ -434,80 +356,4 @@ class PlaybackUtils(): item_data = jsonData self.addPlaylistItem(playlist, item_data, server, userid) - xbmc.Player().play(playlist) - - def AddToPlaylist(self, itemIds): - utils.logMsg("PlayBackUtils", "== ENTER: PLAYAllItems ==") - WINDOW = xbmcgui.Window(10000) - - username = WINDOW.getProperty('currUser') - userid = WINDOW.getProperty('userId%s' % username) - server = WINDOW.getProperty('server%s' % username) - - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - playlist.clear() - - for itemID in itemIds: - - utils.logMsg("PlayBackUtils", "Adding Item to Playlist : " + itemID) - item_url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemID - jsonData = self.downloadUtils.downloadUrl(item_url) - - item_data = jsonData - self.addPlaylistItem(playlist, item_data, server, userid) - - return playlist - - def addPlaylistItem(self, playlist, item, server, userid): - - id = item.get("Id") - - playurl = PlayUtils().getPlayUrl(server, id, item) - utils.logMsg("PlayBackUtils", "Play URL: " + playurl) - thumbPath = API().getArtwork(item, "Primary") - listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) - self.setListItemProps(server, id, listItem, item) - - WINDOW = xbmcgui.Window(10000) - - username = WINDOW.getProperty('currUser') - userid = WINDOW.getProperty('userId%s' % username) - server = WINDOW.getProperty('server%s' % username) - - # Can not play virtual items - if (item.get("LocationType") == "Virtual") or (item.get("IsPlaceHolder") == True): - - xbmcgui.Dialog().ok(self.language(30128), self.language(30129)) - return False - - else: - - watchedurl = "%s/mediabrowser/Users/%s/PlayedItems/%s" % (server, userid, id) - positionurl = "%s/mediabrowser/Users/%s/PlayingItems/%s" % (server, userid, id) - deleteurl = "%s/mediabrowser/Items/%s" % (server, id) - - # set the current playing info - WINDOW = xbmcgui.Window( 10000 ) - WINDOW.setProperty(playurl + "watchedurl", watchedurl) - WINDOW.setProperty(playurl + "positionurl", positionurl) - WINDOW.setProperty(playurl + "deleteurl", "") - - if item.get("Type") == "Episode" and addon.getSetting("offerDeleteTV")=="true": - WINDOW.setProperty(playurl + "deleteurl", deleteurl) - if item.get("Type") == "Movie" and addon.getSetting("offerDeleteMovies")=="true": - WINDOW.setProperty(playurl + "deleteurl", deleteurl) - - WINDOW.setProperty(playurl + "runtimeticks", str(item.get("RunTimeTicks"))) - WINDOW.setProperty(playurl+"type", item.get("Type")) - WINDOW.setProperty(playurl + "item_id", id) - - if (item.get("Type") == "Episode"): - WINDOW.setProperty(playurl + "refresh_id", item.get("SeriesId")) - else: - WINDOW.setProperty(playurl + "refresh_id", id) - - utils.logMsg("PlayBackUtils", "PlayList Item Url : " + str(playurl)) - - playlist.add(playurl, listItem) - - return True \ No newline at end of file + xbmc.Player().play(playlist)''' \ No newline at end of file diff --git a/resources/lib/Player.py b/resources/lib/Player.py index 985a4f95..2ce90c09 100644 --- a/resources/lib/Player.py +++ b/resources/lib/Player.py @@ -1,165 +1,229 @@ -import xbmcaddon -import xbmcplugin +# -*- coding: utf-8 -*- + +################################################################################################# + +import json as json + import xbmc import xbmcgui -import os -import threading -import json -import inspect - -import KodiMonitor -import Utils as utils from DownloadUtils import DownloadUtils from WebSocketClient import WebSocketThread -from PlayUtils import PlayUtils from ClientInformation import ClientInformation from LibrarySync import LibrarySync -from PlaybackUtils import PlaybackUtils -from ReadEmbyDB import ReadEmbyDB -from API import API +import Utils as utils -librarySync = LibrarySync() +################################################################################################# -# service class for playback monitoring class Player( xbmc.Player ): # Borg - multiple instances, shared state _shared_state = {} - + xbmcplayer = xbmc.Player() doUtils = DownloadUtils() clientInfo = ClientInformation() ws = WebSocketThread() + librarySync = LibrarySync() addonName = clientInfo.getAddonName() - WINDOW = xbmcgui.Window(10000) - - logLevel = 0 played_information = {} - settings = None playStats = {} + currentFile = None + stackFiles = None + stackElapsed = 0 + + def __init__(self, *args): - audioPref = "default" - subsPref = "default" - - def __init__( self, *args ): - self.__dict__ = self._shared_state - self.logMsg("Starting playback monitor service", 1) - + 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, int(lvl)) - def setAudioSubsPref(self, audio, subs): - self.audioPref = audio - self.subsPref = subs - - def hasData(self, data): - if(data == None or len(data) == 0 or data == "None"): - return False + def GetPlayStats(self): + return self.playStats + + def currentStackItem(self, stackItems): + # Only for stacked items - stack:// + xbmcplayer = self.xbmcplayer + + stack = stackItems.replace("stack://", "").split(" , ") + position = xbmcplayer.getTime() + totalRuntime = 0 + + for item in stack: + runtime = int(utils.window("%sruntimeticks" % item)) / 10000000 + # Verify the position compared to the totalRuntime for stacked items processed in loop so far. + if position < (runtime + totalRuntime): + self.stackElapsed = totalRuntime + self.currentFile = item + return item + else: + totalRuntime += runtime + + def onPlayBackStarted( self ): + # Will be called when xbmc starts playing a file + xbmcplayer = self.xbmcplayer + self.stopAll() + + # Get current file - if stack://, get currently playing item + currentFile = xbmcplayer.getPlayingFile() + if "stack://" in currentFile: + self.stackFiles = currentFile + currentFile = self.currentStackItem(currentFile) else: - return True - - def stopAll(self): + self.stackFiles = None + self.currentFile = currentFile + self.stackElapsed = 0 - WINDOW = xbmcgui.Window(10000) - - if(len(self.played_information) == 0): - return + self.logMsg("ONPLAYBACK_STARTED: %s" % currentFile, 0) + + # We may need to wait for info to be set in kodi monitor + itemId = utils.window("%sitem_id" % currentFile) + tryCount = 0 + while not itemId: - self.logMsg("emby Service -> played_information : " + str(self.played_information)) + xbmc.sleep(500) + itemId = utils.window("%sitem_id" % currentFile) + if tryCount == 20: # try 20 times or about 10 seconds + break + else: tryCount += 1 + + else: + # Only proceed if an itemId was found. + runtime = utils.window("%sruntimeticks" % currentFile) + refresh_id = utils.window("%srefresh_id" % currentFile) + playMethod = utils.window("%splaymethod" % currentFile) + itemType = utils.window("%stype" % currentFile) + mapping = utils.window("%sIndexMapping" % currentFile) + seekTime = xbmc.Player().getTime() - for item_url in self.played_information: - data = self.played_information.get(item_url) - if (data is not None): - self.logMsg("emby Service -> item_url : " + item_url) - self.logMsg("emby Service -> item_data : " + str(data)) + self.logMsg("Mapping for subtitles index: %s" % mapping, 2) - runtime = data.get("runtime") - currentPosition = data.get("currentPosition") - item_id = data.get("item_id") - refresh_id = data.get("refresh_id") - currentFile = data.get("currentfile") - type = data.get("Type") - playMethod = data.get('playmethod') + # Get playback volume + volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' + result = xbmc.executeJSONRPC(volume_query) + result = json.loads(result) + volume = result.get('result').get('volume') + muted = result.get('result').get('muted') - # Prevent websocket feedback - self.WINDOW.setProperty("played_itemId", item_id) + url = "{server}/mediabrowser/Sessions/Playing" + postdata = { - if(currentPosition != None and self.hasData(runtime)): - runtimeTicks = int(runtime) - self.logMsg("emby Service -> runtimeticks:" + str(runtimeTicks)) - percentComplete = (currentPosition * 10000000) / runtimeTicks - markPlayedAt = float(90) / 100 + 'QueueableMediaTypes': "Video", + 'CanSeek': True, + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PlayMethod': playMethod, + 'VolumeLevel': volume, + 'PositionTicks': int(seekTime * 10000000) - int(self.stackElapsed * 10000000), + 'IsMuted': muted + } - self.logMsg("emby Service -> Percent Complete:" + str(percentComplete) + " Mark Played At:" + str(markPlayedAt)) - if percentComplete < markPlayedAt: - # Do not mark as watched - self.WINDOW.setProperty('played_skipWatched', 'true') + # Get the current audio track and subtitles + if playMethod == "Transcode": + # property set in PlayUtils.py + postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile) + postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile) - self.stopPlayback(data) - - offerDelete=False - if data.get("Type") == "Episode" and utils.settings("offerDeleteTV")=="true": - offerDelete = True - elif data.get("Type") == "Movie" and utils.settings("offerDeleteMovies")=="true": - offerDelete = True - - if percentComplete > .80 and offerDelete == True: - return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete\n" + data.get("currentfile").split("/")[-1] + "\non Emby Server? ") - if return_value: - # Delete Kodi entry before Emby - listItem = [item_id] - LibrarySync().removefromDB(listItem, True) - - # Stop transcoding - if playMethod == "Transcode": - self.logMsg("Transcoding for %s terminated." % item_id) - deviceId = self.clientInfo.getMachineId() - url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId - self.doUtils.downloadUrl(url, type="DELETE") + else: + track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' + result = xbmc.executeJSONRPC(track_query) + result = json.loads(result) - self.played_information.clear() - - def stopPlayback(self, data): - - self.logMsg("stopPlayback called", 2) - - item_id = data.get("item_id") - currentPosition = data.get("currentPosition") - positionTicks = int(currentPosition * 10000000) + # Audio tracks + indexAudio = result.get('result', 0) + if indexAudio: + indexAudio = indexAudio.get('currentaudiostream', {}).get('index', 0) + # Subtitles tracks + indexSubs = result.get('result', 0) + if indexSubs: + indexSubs = indexSubs.get('currentsubtitle', {}).get('index', 0) + # If subtitles are enabled + subsEnabled = result.get('result', "") + if subsEnabled: + subsEnabled = subsEnabled.get('subtitleenabled', "") + + # Postdata for the audio and subs tracks + audioTracks = len(xbmc.Player().getAvailableAudioStreams()) + postdata['AudioStreamIndex'] = indexAudio + 1 + + if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: + + if mapping: + externalIndex = json.loads(mapping) + else: # Direct paths scenario + externalIndex = "" - url = "{server}/mediabrowser/Sessions/Playing/Stopped" - - postdata = { - 'ItemId': item_id, - 'MediaSourceId': item_id, - 'PositionTicks': positionTicks - } + if externalIndex: + # If there's external subtitles added via PlaybackUtils + if externalIndex.get(str(indexSubs)): + # If the current subtitle is in the mapping + postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] + else: + # Internal subtitle currently selected + external = len(externalIndex) + postdata['SubtitleStreamIndex'] = indexSubs - external + audioTracks + 1 + else: + # No external subtitles added via PlayUtils + postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 + else: + postdata['SubtitleStreamIndex'] = "" - self.doUtils.downloadUrl(url, postBody=postdata, type="POST") - + # Post playback to server + self.logMsg("Sending POST play started.", 1) + self.doUtils.downloadUrl(url, postBody=postdata, type="POST") + + # 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) - int(self.stackElapsed) + } + + self.played_information[currentFile] = data + self.logMsg("ADDING_FILE: %s" % self.played_information, 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) xbmcplayer = self.xbmcplayer - - if not xbmcplayer.isPlaying(): - self.logMsg("reportPlayback: Not playing anything so returning", 0) - return - currentFile = xbmcplayer.getPlayingFile() + # Get current file + currentFile = self.currentFile data = self.played_information.get(currentFile) # only report playback if emby has initiated the playback (item_id has value) - if data is not None and data.get("item_id") is not None: + if data: # Get playback information - item_id = data.get("item_id") + itemId = data.get("item_id") audioindex = data.get("AudioStreamIndex") subtitleindex = data.get("SubtitleStreamIndex") playTime = data.get("currentPosition") @@ -173,14 +237,15 @@ class Player( xbmc.Player ): volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' result = xbmc.executeJSONRPC(volume_query) result = json.loads(result) - volume = result.get(u'result').get(u'volume') - muted = result.get(u'result').get(u'muted') + volume = result.get('result').get('volume') + muted = result.get('result').get('muted') postdata = { + 'QueueableMediaTypes': "Video", 'CanSeek': True, - 'ItemId': item_id, - 'MediaSourceId': item_id, + 'ItemId': itemId, + 'MediaSourceId': itemId, 'PlayMethod': playMethod, 'IsPaused': paused, 'VolumeLevel': volume, @@ -188,9 +253,14 @@ class Player( xbmc.Player ): } if playTime: - postdata['PositionTicks'] = int(playTime * 10000000) + postdata['PositionTicks'] = int(playTime * 10000000) - int(self.stackElapsed * 10000000) - if playMethod != "Transcode": + if playMethod == "Transcode": + + data['AudioStreamIndex'] = audioindex + data['SubtitleStreamIndex'] = subtitleindex + + else: # Get current audio and subtitles track track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' result = xbmc.executeJSONRPC(track_query) @@ -249,208 +319,127 @@ class Player( xbmc.Player ): postdata['SubtitleStreamIndex'] = indexSubs data['SubtitleStreamIndex'] = indexSubs - else: - data['AudioStreamIndex'] = audioindex - data['SubtitleStreamIndex'] = subtitleindex - postdata = json.dumps(postdata) self.logMsg("Report: %s" % postdata, 2) self.ws.sendProgressUpdate(postdata) - + def onPlayBackPaused( self ): - currentFile = xbmc.Player().getPlayingFile() - self.logMsg("PLAYBACK_PAUSED : " + currentFile,2) - if(self.played_information.get(currentFile) != None): - self.played_information[currentFile]["paused"] = "true" + + currentFile = self.currentFile + self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) + + if self.played_information.get(currentFile): + self.played_information[currentFile]['paused'] = "true" + self.reportPlayback() - + def onPlayBackResumed( self ): - currentFile = xbmc.Player().getPlayingFile() - self.logMsg("PLAYBACK_RESUMED : " + currentFile,2) - if(self.played_information.get(currentFile) != None): - self.played_information[currentFile]["paused"] = "false" + + currentFile = self.currentFile + self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) + + if self.played_information.get(currentFile): + self.played_information[currentFile]['paused'] = "false" + + self.reportPlayback() + + def onPlayBackSeek( self, time, seekOffset ): + + self.logMsg("PLAYBACK_SEEK", 2) + xbmcplayer = self.xbmcplayer + # Make position when seeking a bit more accurate + position = xbmcplayer.getTime() + currentFile = self.currentFile + + if self.played_information.get(currentFile): + self.played_information[currentFile]['currentPosition'] = position + self.reportPlayback() - def onPlayBackSeek( self, time, seekOffset ): - self.logMsg("PLAYBACK_SEEK",2) - # Make position when seeking a bit more accurate - try: - playTime = xbmc.Player().getTime() - currentFile = xbmc.Player().getPlayingFile() - if(self.played_information.get(currentFile) != None): - self.played_information[currentFile]["currentPosition"] = playTime - except: pass - self.reportPlayback() - - def onPlayBackStarted( self ): - # Will be called when xbmc starts playing a file - WINDOW = xbmcgui.Window(10000) - xbmcplayer = self.xbmcplayer - self.stopAll() - - if xbmcplayer.isPlaying(): - - currentFile = "" - try: - currentFile = xbmcplayer.getPlayingFile() - except: pass - self.logMsg("onPlayBackStarted: %s" % currentFile, 0) - - # we may need to wait until the info is available - item_id = WINDOW.getProperty(currentFile + "item_id") - tryCount = 0 - while(item_id == None or item_id == ""): - xbmc.sleep(500) - item_id = WINDOW.getProperty(currentFile + "item_id") - tryCount += 1 - if(tryCount == 20): # try 20 times or about 10 seconds - return - xbmc.sleep(500) - - # grab all the info about this item from the stored windows props - # only ever use the win props here, use the data map in all other places - runtime = WINDOW.getProperty(currentFile + "runtimeticks") - refresh_id = WINDOW.getProperty(currentFile + "refresh_id") - playMethod = WINDOW.getProperty(currentFile + "playmethod") - itemType = WINDOW.getProperty(currentFile + "type") - mapping = WINDOW.getProperty("%sIndexMapping" % currentFile) - - self.logMsg("Mapping for index: %s" % mapping) - - # Get playback volume - volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}' - result = xbmc.executeJSONRPC(volume_query) - result = json.loads(result) - volume = result.get(u'result').get(u'volume') - muted = result.get(u'result').get(u'muted') - - seekTime = xbmc.Player().getTime() - - url = "{server}/mediabrowser/Sessions/Playing" - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': item_id, - 'MediaSourceId': item_id, - 'PlayMethod': playMethod, - 'VolumeLevel': volume, - 'PositionTicks': int(seekTime * 10000000), - 'IsMuted': muted - } - - # Get the current audio track and subtitles - if playMethod == "Transcode": - audioindex = WINDOW.getProperty(currentFile + "AudioStreamIndex") - subtitleindex = WINDOW.getProperty(currentFile + "SubtitleStreamIndex") - postdata['AudioStreamIndex'] = audioindex - postdata['SubtitleStreamIndex'] = subtitleindex - - else: - track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}' - result = xbmc.executeJSONRPC(track_query) - result = json.loads(result) - - # Audio tracks - indexAudio = result.get('result', 0) - if indexAudio: - indexAudio = indexAudio.get('currentaudiostream', {}).get('index', 0) - # Subtitles tracks - indexSubs = result.get('result', 0) - if indexSubs: - indexSubs = indexSubs.get('currentsubtitle', {}).get('index', 0) - # If subtitles are enabled - subsEnabled = result.get('result', "") - if subsEnabled: - subsEnabled = subsEnabled.get('subtitleenabled', "") - - # Postdata for the audio and subs tracks - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - postdata['AudioStreamIndex'] = indexAudio + 1 - - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - if mapping: - externalIndex = json.loads(mapping) - else: # Direct paths scenario - externalIndex = "" - - if externalIndex: - # If there's external subtitles added via PlaybackUtils - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] - else: - # Internal subtitle currently selected - external = len(externalIndex) - postdata['SubtitleStreamIndex'] = indexSubs - external + audioTracks + 1 - else: - # No external subtitles added via PlayUtils - postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 - else: - postdata['SubtitleStreamIndex'] = "" - - # Post playback to server - self.logMsg("Sending POST play started.", 1) - self.doUtils.downloadUrl(url, postBody=postdata, type="POST") - - - # save data map for updates and position calls - data = { - 'runtime': runtime, - 'item_id': item_id, - 'refresh_id': refresh_id, - 'currentfile': currentFile, - 'AudioStreamIndex': postdata['AudioStreamIndex'], - 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, - 'currentPosition': int(seekTime) - } - self.played_information[currentFile] = data - self.logMsg("ADDING_FILE: %s" % self.played_information, 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 - - # reset in progress position - #self.reportPlayback() - - def GetPlayStats(self): - return self.playStats - - def onPlayBackEnded( self ): - # Will be called when xbmc stops playing a file - self.logMsg("onPlayBackEnded", 0) - - #workaround when strm files are launched through the addon - mark watched when finished playing - #TODO --> mark watched when 95% is played of the file - WINDOW = xbmcgui.Window( 10000 ) - if WINDOW.getProperty("virtualstrm") != "": - try: - id = WINDOW.getProperty("virtualstrm") - type = WINDOW.getProperty("virtualstrmtype") - watchedurl = "{server}/mediabrowser/Users/{UserId}/PlayedItems/%s" % id - self.doUtils.downloadUrl(watchedurl, postBody="", type="POST") - librarySync.updatePlayCount(id) - except: pass - WINDOW.clearProperty("virtualstrm") - - self.stopAll() - def onPlayBackStopped( self ): # Will be called when user stops xbmc playing a file - self.logMsg("onPlayBackStopped", 0) - self.stopAll() \ No newline at end of file + self.logMsg("ONPLAYBACK_STOPPED", 2) + self.stopAll() + + def onPlayBackEnded( self ): + # Will be called when xbmc stops playing a file + self.logMsg("ONPLAYBACK_ENDED", 2) + self.stopAll() + + def stopAll(self): + + if not self.played_information: + return + + self.logMsg("Played_information: %s" % str(self.played_information), 1) + # Process each items + for item in self.played_information: + + data = self.played_information.get(item) + if data: + + self.logMsg("Item path: %s" % item, 1) + self.logMsg("Item data: %s" % str(data), 1) + + runtime = data.get('runtime') + currentPosition = data.get('currentPosition') + itemId = data.get('item_id') + refresh_id = data.get('refresh_id') + currentFile = data.get('currentfile') + type = data.get('Type') + playMethod = data.get('playmethod') + + if currentPosition and runtime: + self.logMsg("RuntimeTicks: %s" % runtime, 1) + percentComplete = (currentPosition * 10000000) / int(runtime) + markPlayedAt = float(utils.settings('markPlayed')) / 100 + + self.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt)) + if percentComplete < markPlayedAt: + # Do not mark as watched for Kodi Monitor + utils.window('played_skipWatched', value="true") + + self.stopPlayback(data) + offerDelete = False + + if type == "Episode" and utils.settings('offerDeleteTV') == "true": + offerDelete = True + + elif type == "Movie" and utils.settings('offerDeleteMovies') == "true": + offerDelete = True + + if percentComplete >= markPlayedAt and offerDelete: + # Item could be stacked, so only offer to delete the main item. + if not self.stackFiles or itemId == utils.window('%sitem_id' % self.stackFiles): + return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete %s" % data.get('currentfile').split("/")[-1], "on Emby Server?") + if return_value: + # Delete Kodi entry before Emby + listItem = [itemId] + LibrarySync().removefromDB(listItem, True) + + # Stop transcoding + if playMethod == "Transcode": + self.logMsg("Transcoding for %s terminated." % itemId, 1) + deviceId = self.clientInfo.getMachineId() + url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId + self.doUtils.downloadUrl(url, type="DELETE") + + self.played_information.clear() + + def stopPlayback(self, data): + + self.logMsg("stopPlayback called", 2) + + itemId = data.get('item_id') + currentPosition = data.get('currentPosition') + positionTicks = int(currentPosition * 10000000) + + url = "{server}/mediabrowser/Sessions/Playing/Stopped" + postdata = { + + 'ItemId': itemId, + 'MediaSourceId': itemId, + 'PositionTicks': positionTicks + } + + self.doUtils.downloadUrl(url, postBody=postdata, type="POST") \ No newline at end of file diff --git a/resources/settings.xml b/resources/settings.xml index dfb0cd73..1fb1ba6d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -35,8 +35,10 @@ + + - + diff --git a/service.py b/service.py index 76b98270..cf6834f1 100644 --- a/service.py +++ b/service.py @@ -140,7 +140,7 @@ class Service(): # Update and report progress playTime = xbmc.Player().getTime() totalTime = xbmc.Player().getTotalTime() - currentFile = xbmc.Player().getPlayingFile() + currentFile = player.currentFile # Update positionticks if player.played_information.get(currentFile) is not None: