Revamp playback start, part 4

This commit is contained in:
tomkat83 2018-01-21 18:31:49 +01:00
parent 7ecaa376a2
commit 2791da9f65
6 changed files with 148 additions and 342 deletions

View file

@ -1268,6 +1268,15 @@ class API():
res = '2000-01-01 10:00:00' res = '2000-01-01 10:00:00'
return res return res
def getViewCount(self):
"""
Returns the play count for the item as an int or the int 0 if not found
"""
try:
return int(self.item.attrib['viewCount'])
except (KeyError, ValueError):
return 0
def getUserData(self): def getUserData(self):
""" """
Returns a dict with None if a value is missing Returns a dict with None if a value is missing

View file

@ -45,6 +45,7 @@ STATE_SETTINGS = {
'enableMusic': 'ENABLE_MUSIC', 'enableMusic': 'ENABLE_MUSIC',
'enableBackgroundSync': 'BACKGROUND_SYNC' 'enableBackgroundSync': 'BACKGROUND_SYNC'
} }
############################################################################### ###############################################################################
@ -55,6 +56,8 @@ class KodiMonitor(Monitor):
def __init__(self): def __init__(self):
self.xbmcplayer = Player() self.xbmcplayer = Player()
Monitor.__init__(self) Monitor.__init__(self)
for playerid in state.PLAYER_STATES:
state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE)
LOG.info("Kodi monitor started.") LOG.info("Kodi monitor started.")
def onScanStarted(self, library): def onScanStarted(self, library):
@ -315,6 +318,8 @@ class KodiMonitor(Monitor):
LOG.info('Aborting playback report - item invalid for updates %s', LOG.info('Aborting playback report - item invalid for updates %s',
data) data)
return return
# Remember that this player has been active
state.ACTIVE_PLAYERS.append(playerid)
playqueue = PQ.PLAYQUEUES[playerid] playqueue = PQ.PLAYQUEUES[playerid]
info = js.get_player_props(playerid) info = js.get_player_props(playerid)
json_item = js.get_item(playerid) json_item = js.get_item(playerid)
@ -356,13 +361,15 @@ class KodiMonitor(Monitor):
container_key = '/library/metadata/%s' % plex_id container_key = '/library/metadata/%s' % plex_id
state.PLAYER_STATES[playerid]['container_key'] = container_key state.PLAYER_STATES[playerid]['container_key'] = container_key
LOG.debug('Set the Plex container_key to: %s', container_key) LOG.debug('Set the Plex container_key to: %s', container_key)
status = state.PLAYER_STATES[playerid]
state.PLAYER_STATES[playerid].update(info) status.update(info)
state.PLAYER_STATES[playerid]['file'] = path status['file'] = path
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id status['kodi_id'] = kodi_id
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type status['kodi_type'] = kodi_type
state.PLAYER_STATES[playerid]['plex_id'] = plex_id status['plex_id'] = plex_id
state.PLAYER_STATES[playerid]['plex_type'] = plex_type status['plex_type'] = plex_type
status['playmethod'] = item.playmethod
status['playcount'] = item.playcount
LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid])
def StartDirectPath(self, plex_id, type, currentFile): def StartDirectPath(self, plex_id, type, currentFile):

View file

@ -138,6 +138,7 @@ def _prep_playlist_stack(xml):
# We will never store clips (trailers) in the Kodi DB # We will never store clips (trailers) in the Kodi DB
kodi_id = None kodi_id = None
kodi_type = None kodi_type = None
resume, _ = api.getRuntime()
for part, _ in enumerate(item[0]): for part, _ in enumerate(item[0]):
api.setPartNumber(part) api.setPartNumber(part)
if kodi_id is None: if kodi_id is None:
@ -161,7 +162,9 @@ def _prep_playlist_stack(xml):
'file': path, 'file': path,
'xml_video_element': item, 'xml_video_element': item,
'listitem': listitem, 'listitem': listitem,
'part': part 'part': part,
'playcount': api.getViewCount(),
'offset': resume
}) })
return stack return stack
@ -188,6 +191,8 @@ def _process_stack(playqueue, stack):
kodi_id=item['kodi_id'], kodi_id=item['kodi_id'],
kodi_type=item['kodi_type'], kodi_type=item['kodi_type'],
xml_video_element=item['xml_video_element']) xml_video_element=item['xml_video_element'])
playlist_item.playcount = item['playcount']
playlist_item.offset = item['offset']
playlist_item.part = item['part'] playlist_item.part = item['part']
playlist_item.init_done = True playlist_item.init_done = True
pos += 1 pos += 1

View file

@ -2,15 +2,14 @@
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from json import loads
from xbmc import Player, sleep from xbmc import Player
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode from utils import window, DateToKodi, getUnixTimestamp, kodi_time_to_millis
import downloadutils from downloadutils import DownloadUtils as DU
import plexdb_functions as plexdb import plexdb_functions as plexdb
import kodidb_functions as kodidb import kodidb_functions as kodidb
import json_rpc as js from plexbmchelper.subscribers import LOCKER
import variables as v import variables as v
import state import state
@ -22,326 +21,104 @@ LOG = getLogger("PLEX." + __name__)
class PKC_Player(Player): class PKC_Player(Player):
played_info = state.PLAYED_INFO
playStats = state.PLAYER_STATES
currentFile = None
def __init__(self): def __init__(self):
self.doUtils = downloadutils.DownloadUtils
Player.__init__(self) Player.__init__(self)
LOG.info("Started playback monitor.") LOG.info("Started playback monitor.")
def onPlayBackStarted(self): def onPlayBackStarted(self):
""" """
Will be called when xbmc starts playing a file. Will be called when xbmc starts playing a file.
Window values need to have been set in Kodimonitor.py
""" """
return
self.stopAll()
# Get current file (in utf-8!)
try:
currentFile = tryDecode(self.getPlayingFile())
sleep(300)
except:
currentFile = ""
count = 0
while not currentFile:
sleep(100)
try:
currentFile = tryDecode(self.getPlayingFile())
except:
pass pass
if count == 20:
break
else:
count += 1
if not currentFile:
LOG.warn('Error getting currently playing file; abort reporting')
return
# Save currentFile for cleanup later and for references
self.currentFile = currentFile
window('plex_lastPlayedFiled', value=currentFile)
# We may need to wait for info to be set in kodi monitor
itemId = window("plex_%s.itemid" % tryEncode(currentFile))
count = 0
while not itemId:
sleep(200)
itemId = window("plex_%s.itemid" % tryEncode(currentFile))
if count == 5:
LOG.warn("Could not find itemId, cancelling playback report!")
return
count += 1
LOG.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
plexitem = "plex_%s" % tryEncode(currentFile)
runtime = window("%s.runtime" % plexitem)
refresh_id = window("%s.refreshid" % plexitem)
playMethod = window("%s.playmethod" % plexitem)
itemType = window("%s.type" % plexitem)
try:
playcount = int(window("%s.playcount" % plexitem))
except ValueError:
playcount = 0
window('plex_skipWatched%s' % itemId, value="true")
LOG.debug("Playing itemtype is: %s" % itemType)
customseek = window('plex_customplaylist.seektime')
if customseek:
# Start at, when using custom playlist (play to Kodi from
# webclient)
LOG.info("Seeking to: %s" % customseek)
try:
self.seekTime(int(customseek))
except:
LOG.error('Could not seek!')
window('plex_customplaylist.seektime', clear=True)
try:
seekTime = self.getTime()
except RuntimeError:
LOG.error('Could not get current seektime from xbmc player')
seekTime = 0
volume = js.get_volume()
muted = js.get_muted()
# Postdata structure to send to plex server
url = "{server}/:/timeline?"
postdata = {
'QueueableMediaTypes': "Video",
'CanSeek': True,
'ItemId': itemId,
'MediaSourceId': itemId,
'PlayMethod': playMethod,
'VolumeLevel': volume,
'PositionTicks': int(seekTime * 10000000),
'IsMuted': muted
}
# Get the current audio track and subtitles
if playMethod == "Transcode":
# property set in PlayUtils.py
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex"
% tryEncode(currentFile))
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex"
% tryEncode(currentFile))
else:
# Get the current kodi audio and subtitles and convert to plex equivalent
indexAudio = js.current_audiostream(1).get('index', 0)
subsEnabled = js.subtitle_enabled(1)
if subsEnabled:
indexSubs = js.current_subtitle(1).get('index', 0)
else:
indexSubs = 0
# Postdata for the audio
postdata['AudioStreamIndex'] = indexAudio + 1
# Postdata for the subtitles
if subsEnabled and len(Player().getAvailableSubtitleStreams()) > 0:
# Number of audiotracks to help get plex Index
audioTracks = len(Player().getAvailableAudioStreams())
mapping = window("%s.indexMapping" % plexitem)
if mapping: # Set in playbackutils.py
LOG.debug("Mapping for external subtitles index: %s"
% mapping)
externalIndex = loads(mapping)
if externalIndex.get(str(indexSubs)):
# If the current subtitle is in the mapping
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
else:
# Internal subtitle currently selected
subindex = indexSubs - len(externalIndex) + audioTracks + 1
postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
else:
postdata['SubtitleStreamIndex'] = ""
# Post playback to server
# log("Sending POST play started: %s." % postdata, 2)
# self.doUtils(url, postBody=postdata, type="POST")
# Ensure we do have a runtime
try:
runtime = int(runtime)
except ValueError:
try:
runtime = self.getTotalTime()
LOG.error("Runtime is missing, Kodi runtime: %s" % runtime)
except:
LOG.error('Could not get kodi runtime, setting to zero')
runtime = 0
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byId(itemId)
try:
fileid = plex_dbitem[1]
except TypeError:
LOG.info("Could not find fileid in plex db.")
fileid = None
# 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),
'fileid': fileid,
'itemType': itemType,
'playcount': playcount
}
self.played_info[currentFile] = data
LOG.info("ADDING_FILE: %s" % data)
# 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 onPlayBackPaused(self): def onPlayBackPaused(self):
"""
currentFile = self.currentFile Will be called when playback is paused
LOG.info("PLAYBACK_PAUSED: %s" % currentFile) """
pass
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = True
def onPlayBackResumed(self): def onPlayBackResumed(self):
"""
currentFile = self.currentFile Will be called when playback is resumed
LOG.info("PLAYBACK_RESUMED: %s" % currentFile) """
pass
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = False
def onPlayBackSeek(self, time, seekOffset): def onPlayBackSeek(self, time, seekOffset):
# Make position when seeking a bit more accurate """
currentFile = self.currentFile Will be called when user seeks to a certain time during playback
LOG.info("PLAYBACK_SEEK: %s" % currentFile) """
pass
if self.played_info.get(currentFile):
try:
position = self.getTime()
except RuntimeError:
# When Kodi is not playing
return
self.played_info[currentFile]['currentPosition'] = position
def onPlayBackStopped(self): def onPlayBackStopped(self):
# Will be called when user stops xbmc playing a file """
Will be called when playback is stopped by the user
"""
LOG.info("ONPLAYBACK_STOPPED") LOG.info("ONPLAYBACK_STOPPED")
self.cleanup_playback()
self.stopAll() def onPlayBackEnded(self):
"""
Will be called when playback ends due to the media file being finished
"""
LOG.info("ONPLAYBACK_ENDED")
self.cleanup_playback()
@LOCKER.lockthis
def cleanup_playback(self):
"""
PKC cleanup after playback ends/is stopped
"""
# We might have saved a transient token from a user flinging media via
# Companion (if we could not use the playqueue to store the token)
state.PLEX_TRANSIENT_TOKEN = None
for item in ('plex_currently_playing_itemid', for item in ('plex_currently_playing_itemid',
'plex_customplaylist', 'plex_customplaylist',
'plex_customplaylist.seektime', 'plex_customplaylist.seektime',
'plex_forcetranscode'): 'plex_forcetranscode'):
window(item, clear=True) window(item, clear=True)
# We might have saved a transient token from a user flinging media via for playerid in state.ACTIVE_PLAYERS:
# Companion (if we could not use the playqueue to store the token) status = state.PLAYER_STATES[playerid]
state.PLEX_TRANSIENT_TOKEN = None # Check whether we need to mark an item as completely watched
LOG.debug("Cleared playlist properties.") if not status['kodi_id'] or not status['plex_id']:
LOG.info('No PKC info safed for the element just played by Kodi'
def onPlayBackEnded(self): ' player %s', playerid)
# Will be called when xbmc stops playing a file, because the file ended
LOG.info("ONPLAYBACK_ENDED")
self.onPlayBackStopped()
def stopAll(self):
if not self.played_info:
return
LOG.info("Played_information: %s" % self.played_info)
# Process each items
for item in self.played_info:
data = self.played_info.get(item)
if not data:
continue continue
LOG.debug("Item path: %s" % item) # Stop transcoding
LOG.debug("Item data: %s" % data) if status['playmethod'] == 'Transcode':
LOG.info('Tell the PMS to stop transcoding')
runtime = data['runtime'] DU().downloadUrl(
currentPosition = data['currentPosition'] '{server}/video/:/transcode/universal/stop',
itemid = data['item_id'] parameters={'session': v.PKC_MACHINE_IDENTIFIER})
refresh_id = data['refresh_id'] if status['plex_type'] == v.PLEX_TYPE_SONG:
currentFile = data['currentfile'] LOG.debug('Song has been played, not cleaning up playstate')
media_type = data['Type']
playMethod = data['playmethod']
# Prevent manually mark as watched in Kodi monitor
window('plex_skipWatched%s' % itemid, value="true")
if not currentPosition or not runtime:
continue continue
try: resume = kodi_time_to_millis(status['time'])
percentComplete = float(currentPosition) / float(runtime) runtime = kodi_time_to_millis(status['totaltime'])
except ZeroDivisionError: LOG.info('Item playback progress %s out of %s', resume, runtime)
# Runtime is 0. if not resume or not runtime:
percentComplete = 0 continue
LOG.info("Percent complete: %s Mark played at: %s" complete = float(resume) / float(runtime)
% (percentComplete, v.MARK_PLAYED_AT)) LOG.info("Percent complete: %s. Mark played at: %s",
if percentComplete >= v.MARK_PLAYED_AT: complete, v.MARK_PLAYED_AT)
if complete >= v.MARK_PLAYED_AT:
# Tell Kodi that we've finished watching (Plex knows) # Tell Kodi that we've finished watching (Plex knows)
if (data['fileid'] is not None and with plexdb.Get_Plex_DB() as plex_db:
data['itemType'] in (v.KODI_TYPE_MOVIE, plex_dbitem = plex_db.getItem_byId(status['plex_id'])
v.KODI_TYPE_EPISODE)): file_id = plex_dbitem[1] if plex_dbitem else None
if file_id is None:
LOG.error('No file_id found for %s', status)
continue
with kodidb.GetKodiDB('video') as kodi_db: with kodidb.GetKodiDB('video') as kodi_db:
kodi_db.addPlaystate( kodi_db.addPlaystate(
data['fileid'], file_id,
None, None,
None, None,
data['playcount'] + 1, status['playcount'] + 1,
DateToKodi(getUnixTimestamp())) DateToKodi(getUnixTimestamp()))
LOG.info('Marked plex element %s as completely watched',
# Clean the WINDOW properties status['plex_id'])
for filename in self.played_info: # As all playback has halted, reset the players that have been active
plex_item = 'plex_%s' % tryEncode(filename) state.ACTIVE_PLAYERS = []
cleanup = ( for playerid in state.PLAYER_STATES:
'%s.itemid' % plex_item, state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE)
'%s.runtime' % plex_item, LOG.info('Finished PKC playback cleanup')
'%s.refreshid' % plex_item,
'%s.playmethod' % plex_item,
'%s.type' % plex_item,
'%s.runtime' % plex_item,
'%s.playcount' % plex_item,
'%s.playlistPosition' % plex_item,
'%s.subtitle' % plex_item,
)
for item in cleanup:
window(item, clear=True)
# Stop transcoding
if playMethod == "Transcode":
LOG.info("Transcoding for %s terminating" % itemid)
self.doUtils().downloadUrl(
"{server}/video/:/transcode/universal/stop",
parameters={'session': v.PKC_MACHINE_IDENTIFIER})
self.played_info.clear()

View file

@ -190,6 +190,8 @@ class Playlist_Item(object):
guid = None [str] Weird Plex guid guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer> xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
playcount = None [int] how many times the item has already been played
offset = None [int] the item's view offset UPON START in Plex time
part = 0 [int] part number if Plex video consists of mult. parts part = 0 [int] part number if Plex video consists of mult. parts
init_done = False Set to True only if run through playback init init_done = False Set to True only if run through playback init
""" """
@ -205,6 +207,8 @@ class Playlist_Item(object):
self.guid = None self.guid = None
self.xml = None self.xml = None
self.playmethod = None self.playmethod = None
self.playcount = None
self.offset = None
# If Plex video consists of several parts; part number # If Plex video consists of several parts; part number
self.part = 0 self.part = 0
self.init_done = False self.init_done = False

View file

@ -82,43 +82,47 @@ COMMAND_PIPELINE_QUEUE = None
# Websocket_client queue to communicate with librarysync # Websocket_client queue to communicate with librarysync
WEBSOCKET_QUEUE = None WEBSOCKET_QUEUE = None
# Which Kodi player is/has been active? (either int 1, 2 or 3)
ACTIVE_PLAYERS = []
# Kodi player states - here, initial values are set # Kodi player states - here, initial values are set
PLAYER_STATES = { PLAYER_STATES = {
1: { 1: {},
'type': 'movie', 2: {},
3: {}
}
# "empty" dict for the PLAYER_STATES above
PLAYSTATE = {
'type': None,
'time': { 'time': {
'hours': 0, 'hours': 0,
'minutes': 0, 'minutes': 0,
'seconds': 0, 'seconds': 0,
'milliseconds': 0 'milliseconds': 0},
},
'totaltime': { 'totaltime': {
'hours': 0, 'hours': 0,
'minutes': 0, 'minutes': 0,
'seconds': 0, 'seconds': 0,
'milliseconds': 0 'milliseconds': 0},
},
'speed': 0, 'speed': 0,
'shuffled': False, 'shuffled': False,
'repeat': 'off', 'repeat': 'off',
'position': -1, 'position': None,
'playlistid': -1, 'playlistid': None,
'currentvideostream': -1, 'currentvideostream': -1,
'currentaudiostream': -1, 'currentaudiostream': -1,
'subtitleenabled': False, 'subtitleenabled': False,
'currentsubtitle': -1, 'currentsubtitle': -1,
###### 'file': None,
'file': '',
'kodi_id': None, 'kodi_id': None,
'kodi_type': None, 'kodi_type': None,
'plex_id': None, 'plex_id': None,
'plex_type': None, 'plex_type': None,
'container_key': None, 'container_key': None,
'volume': 100, 'volume': 100,
'muted': False 'muted': False,
}, 'playmethod': None,
2: {}, 'playcount': None
3: {}
} }
# Dict containing all filenames as keys with plex id as values - used for addon # Dict containing all filenames as keys with plex id as values - used for addon
# paths for playback (since we're not receiving a Kodi id) # paths for playback (since we're not receiving a Kodi id)