mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-12-27 03:06:10 +00:00
1691fd5e8b
Add external subtitles as tracks for direct play and direct stream. Important to note, when direct streaming, the tracks are defined as Unknown(external). There's no way to correct this.
446 lines
No EOL
18 KiB
Python
446 lines
No EOL
18 KiB
Python
import xbmcaddon
|
|
import xbmcplugin
|
|
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
|
|
|
|
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()
|
|
|
|
addonName = clientInfo.getAddonName()
|
|
|
|
WINDOW = xbmcgui.Window(10000)
|
|
|
|
logLevel = 0
|
|
played_information = {}
|
|
settings = None
|
|
playStats = {}
|
|
|
|
audioPref = "default"
|
|
subsPref = "default"
|
|
|
|
def __init__( self, *args ):
|
|
|
|
self.__dict__ = self._shared_state
|
|
self.logMsg("Starting playback monitor service", 1)
|
|
|
|
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
|
|
else:
|
|
return True
|
|
|
|
def stopAll(self):
|
|
|
|
WINDOW = xbmcgui.Window(10000)
|
|
|
|
if(len(self.played_information) == 0):
|
|
return
|
|
|
|
self.logMsg("emby Service -> played_information : " + str(self.played_information))
|
|
|
|
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))
|
|
|
|
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')
|
|
|
|
# Prevent websocket feedback
|
|
self.WINDOW.setProperty("played_itemId", item_id)
|
|
|
|
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
|
|
|
|
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')
|
|
|
|
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")
|
|
|
|
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)
|
|
|
|
url = "{server}/mediabrowser/Sessions/Playing/Stopped"
|
|
|
|
postdata = {
|
|
'ItemId': item_id,
|
|
'MediaSourceId': item_id,
|
|
'PositionTicks': positionTicks
|
|
}
|
|
|
|
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
|
|
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()
|
|
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:
|
|
|
|
# Get playback information
|
|
item_id = data.get("item_id")
|
|
audioindex = data.get("AudioStreamIndex")
|
|
subtitleindex = data.get("SubtitleStreamIndex")
|
|
playTime = data.get("currentPosition")
|
|
playMethod = data.get("playmethod")
|
|
paused = data.get("paused")
|
|
|
|
if paused is None:
|
|
paused = False
|
|
|
|
# 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')
|
|
|
|
postdata = {
|
|
'QueueableMediaTypes': "Video",
|
|
'CanSeek': True,
|
|
'ItemId': item_id,
|
|
'MediaSourceId': item_id,
|
|
'PlayMethod': playMethod,
|
|
'IsPaused': paused,
|
|
'VolumeLevel': volume,
|
|
'IsMuted': muted
|
|
}
|
|
|
|
if playTime:
|
|
postdata['PositionTicks'] = int(playTime * 10000000)
|
|
|
|
if playMethod != "Transcode":
|
|
# 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)
|
|
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', "")
|
|
|
|
# Convert back into an Emby index
|
|
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
|
indexAudio = indexAudio + 1
|
|
|
|
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
|
WINDOW = xbmcgui.Window(10000)
|
|
mapping = WINDOW.getProperty("%sIndexMapping" % currentFile)
|
|
externalIndex = json.loads(mapping)
|
|
if externalIndex:
|
|
# If there's external subtitles added via PlaybackUtils
|
|
if externalIndex.get(str(indexSubs)):
|
|
# If the current subtitle is in the mapping
|
|
indexSubs = externalIndex[str(indexSubs)]
|
|
else:
|
|
# Internal subtitle currently selected
|
|
external = len(externalIndex)
|
|
indexSubs = indexSubs - external + audioTracks + 1
|
|
else:
|
|
# No external subtitles added via PlayUtils
|
|
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
|
indexSubs = indexSubs + audioTracks + 1
|
|
else:
|
|
indexSubs = ""
|
|
|
|
if audioindex == indexAudio:
|
|
postdata['AudioStreamIndex'] = audioindex
|
|
else:
|
|
postdata['AudioStreamIndex'] = indexAudio
|
|
data['AudioStreamIndex'] = indexAudio
|
|
|
|
if subtitleindex == indexSubs:
|
|
postdata['SubtitleStreamIndex'] = subtitleindex
|
|
else:
|
|
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"
|
|
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"
|
|
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:
|
|
externalIndex = json.loads(mapping)
|
|
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() |