From 2791da9f651eb138af86f5dfc14ecc145c54b543 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 21 Jan 2018 18:31:49 +0100 Subject: [PATCH] Revamp playback start, part 4 --- resources/lib/PlexAPI.py | 9 + resources/lib/kodimonitor.py | 21 +- resources/lib/playback.py | 7 +- resources/lib/player.py | 379 +++++++-------------------------- resources/lib/playlist_func.py | 4 + resources/lib/state.py | 70 +++--- 6 files changed, 148 insertions(+), 342 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index ae03b4d1..b6affbf5 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1268,6 +1268,15 @@ class API(): res = '2000-01-01 10:00:00' 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): """ Returns a dict with None if a value is missing diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 574816ff..38e848f1 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -45,6 +45,7 @@ STATE_SETTINGS = { 'enableMusic': 'ENABLE_MUSIC', 'enableBackgroundSync': 'BACKGROUND_SYNC' } + ############################################################################### @@ -55,6 +56,8 @@ class KodiMonitor(Monitor): def __init__(self): self.xbmcplayer = Player() Monitor.__init__(self) + for playerid in state.PLAYER_STATES: + state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) LOG.info("Kodi monitor started.") def onScanStarted(self, library): @@ -315,6 +318,8 @@ class KodiMonitor(Monitor): LOG.info('Aborting playback report - item invalid for updates %s', data) return + # Remember that this player has been active + state.ACTIVE_PLAYERS.append(playerid) playqueue = PQ.PLAYQUEUES[playerid] info = js.get_player_props(playerid) json_item = js.get_item(playerid) @@ -356,13 +361,15 @@ class KodiMonitor(Monitor): container_key = '/library/metadata/%s' % plex_id state.PLAYER_STATES[playerid]['container_key'] = container_key LOG.debug('Set the Plex container_key to: %s', container_key) - - state.PLAYER_STATES[playerid].update(info) - state.PLAYER_STATES[playerid]['file'] = path - state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id - state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type - state.PLAYER_STATES[playerid]['plex_id'] = plex_id - state.PLAYER_STATES[playerid]['plex_type'] = plex_type + status = state.PLAYER_STATES[playerid] + status.update(info) + status['file'] = path + status['kodi_id'] = kodi_id + status['kodi_type'] = kodi_type + status['plex_id'] = plex_id + status['plex_type'] = plex_type + status['playmethod'] = item.playmethod + status['playcount'] = item.playcount LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 99615fb1..ddfb02d3 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -138,6 +138,7 @@ def _prep_playlist_stack(xml): # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None + resume, _ = api.getRuntime() for part, _ in enumerate(item[0]): api.setPartNumber(part) if kodi_id is None: @@ -161,7 +162,9 @@ def _prep_playlist_stack(xml): 'file': path, 'xml_video_element': item, 'listitem': listitem, - 'part': part + 'part': part, + 'playcount': api.getViewCount(), + 'offset': resume }) return stack @@ -188,6 +191,8 @@ def _process_stack(playqueue, stack): kodi_id=item['kodi_id'], kodi_type=item['kodi_type'], xml_video_element=item['xml_video_element']) + playlist_item.playcount = item['playcount'] + playlist_item.offset = item['offset'] playlist_item.part = item['part'] playlist_item.init_done = True pos += 1 diff --git a/resources/lib/player.py b/resources/lib/player.py index be006ff2..73ea0463 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -2,15 +2,14 @@ ############################################################################### 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 -import downloadutils +from utils import window, DateToKodi, getUnixTimestamp, kodi_time_to_millis +from downloadutils import DownloadUtils as DU import plexdb_functions as plexdb import kodidb_functions as kodidb -import json_rpc as js +from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -22,326 +21,104 @@ LOG = getLogger("PLEX." + __name__) class PKC_Player(Player): - - played_info = state.PLAYED_INFO - playStats = state.PLAYER_STATES - currentFile = None - def __init__(self): - self.doUtils = downloadutils.DownloadUtils Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): """ 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 - 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''' + pass def onPlayBackPaused(self): - - currentFile = self.currentFile - LOG.info("PLAYBACK_PAUSED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = True + """ + Will be called when playback is paused + """ + pass def onPlayBackResumed(self): - - currentFile = self.currentFile - LOG.info("PLAYBACK_RESUMED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = False + """ + Will be called when playback is resumed + """ + pass def onPlayBackSeek(self, time, seekOffset): - # Make position when seeking a bit more accurate - currentFile = self.currentFile - LOG.info("PLAYBACK_SEEK: %s" % currentFile) - - if self.played_info.get(currentFile): - try: - position = self.getTime() - except RuntimeError: - # When Kodi is not playing - return - self.played_info[currentFile]['currentPosition'] = position + """ + Will be called when user seeks to a certain time during playback + """ + pass 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") + 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', 'plex_customplaylist', 'plex_customplaylist.seektime', 'plex_forcetranscode'): window(item, clear=True) - # 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 - LOG.debug("Cleared playlist properties.") - - def onPlayBackEnded(self): - # 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: + for playerid in state.ACTIVE_PLAYERS: + status = state.PLAYER_STATES[playerid] + # Check whether we need to mark an item as completely watched + if not status['kodi_id'] or not status['plex_id']: + LOG.info('No PKC info safed for the element just played by Kodi' + ' player %s', playerid) continue - LOG.debug("Item path: %s" % item) - LOG.debug("Item data: %s" % data) - - runtime = data['runtime'] - currentPosition = data['currentPosition'] - itemid = data['item_id'] - refresh_id = data['refresh_id'] - currentFile = data['currentfile'] - 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: + # Stop transcoding + if status['playmethod'] == 'Transcode': + LOG.info('Tell the PMS to stop transcoding') + DU().downloadUrl( + '{server}/video/:/transcode/universal/stop', + parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + if status['plex_type'] == v.PLEX_TYPE_SONG: + LOG.debug('Song has been played, not cleaning up playstate') continue - try: - percentComplete = float(currentPosition) / float(runtime) - except ZeroDivisionError: - # Runtime is 0. - percentComplete = 0 - LOG.info("Percent complete: %s Mark played at: %s" - % (percentComplete, v.MARK_PLAYED_AT)) - if percentComplete >= v.MARK_PLAYED_AT: + resume = kodi_time_to_millis(status['time']) + runtime = kodi_time_to_millis(status['totaltime']) + LOG.info('Item playback progress %s out of %s', resume, runtime) + if not resume or not runtime: + continue + complete = float(resume) / float(runtime) + LOG.info("Percent complete: %s. Mark played at: %s", + complete, v.MARK_PLAYED_AT) + if complete >= v.MARK_PLAYED_AT: # Tell Kodi that we've finished watching (Plex knows) - if (data['fileid'] is not None and - data['itemType'] in (v.KODI_TYPE_MOVIE, - v.KODI_TYPE_EPISODE)): - with kodidb.GetKodiDB('video') as kodi_db: - kodi_db.addPlaystate( - data['fileid'], - None, - None, - data['playcount'] + 1, - DateToKodi(getUnixTimestamp())) - - # Clean the WINDOW properties - for filename in self.played_info: - plex_item = 'plex_%s' % tryEncode(filename) - cleanup = ( - '%s.itemid' % plex_item, - '%s.runtime' % plex_item, - '%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() + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(status['plex_id']) + 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: + kodi_db.addPlaystate( + file_id, + None, + None, + status['playcount'] + 1, + DateToKodi(getUnixTimestamp())) + LOG.info('Marked plex element %s as completely watched', + status['plex_id']) + # As all playback has halted, reset the players that have been active + state.ACTIVE_PLAYERS = [] + for playerid in state.PLAYER_STATES: + state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) + LOG.info('Finished PKC playback cleanup') diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 1d03237f..77ec1bf5 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -190,6 +190,8 @@ class Playlist_Item(object): guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below 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 init_done = False Set to True only if run through playback init """ @@ -205,6 +207,8 @@ class Playlist_Item(object): self.guid = None self.xml = None self.playmethod = None + self.playcount = None + self.offset = None # If Plex video consists of several parts; part number self.part = 0 self.init_done = False diff --git a/resources/lib/state.py b/resources/lib/state.py index a964ee51..db267be5 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -82,44 +82,48 @@ COMMAND_PIPELINE_QUEUE = None # Websocket_client queue to communicate with librarysync 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 PLAYER_STATES = { - 1: { - 'type': 'movie', - 'time': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0 - }, - 'totaltime': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0 - }, - 'speed': 0, - 'shuffled': False, - 'repeat': 'off', - 'position': -1, - 'playlistid': -1, - 'currentvideostream': -1, - 'currentaudiostream': -1, - 'subtitleenabled': False, - 'currentsubtitle': -1, - ###### - 'file': '', - 'kodi_id': None, - 'kodi_type': None, - 'plex_id': None, - 'plex_type': None, - 'container_key': None, - 'volume': 100, - 'muted': False - }, + 1: {}, 2: {}, 3: {} } +# "empty" dict for the PLAYER_STATES above +PLAYSTATE = { + 'type': None, + 'time': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'totaltime': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'speed': 0, + 'shuffled': False, + 'repeat': 'off', + 'position': None, + 'playlistid': None, + 'currentvideostream': -1, + 'currentaudiostream': -1, + 'subtitleenabled': False, + 'currentsubtitle': -1, + 'file': None, + 'kodi_id': None, + 'kodi_type': None, + 'plex_id': None, + 'plex_type': None, + 'container_key': None, + 'volume': 100, + 'muted': False, + 'playmethod': None, + 'playcount': None +} # 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) PLEX_IDS = {}