From 76e721b78a61667672018c591ecaa0c4abb7c761 Mon Sep 17 00:00:00 2001 From: Croneter Date: Mon, 9 Apr 2018 08:13:54 +0200 Subject: [PATCH] Incorporate PKC player in kodimonitor module --- resources/lib/PlexCompanion.py | 5 +- resources/lib/kodidb_functions.py | 54 ++++------ resources/lib/kodimonitor.py | 155 +++++++++++++++++++++++++--- resources/lib/player.py | 163 ------------------------------ 4 files changed, 160 insertions(+), 217 deletions(-) delete mode 100644 resources/lib/player.py diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index f0fc4e0c..eb63ccec 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -7,7 +7,7 @@ from Queue import Empty from socket import SHUT_RDWR from urllib import urlencode -from xbmc import sleep, executebuiltin +from xbmc import sleep, executebuiltin, Player from utils import settings, thread_methods, language as lang, dialog from plexbmchelper import listener, plexgdm, subscribers, httppersist @@ -18,7 +18,6 @@ from playlist_func import get_pms_playqueue, get_plextype_from_xml, \ get_playlist_details_from_xml from playback import playback_triage, play_xml import json_rpc as js -import player import variables as v import state import playqueue as PQ @@ -43,7 +42,7 @@ class PlexCompanion(Thread): self.client.clientDetails() LOG.debug("Registration string is:\n%s", self.client.getClientDetails()) # kodi player instance - self.player = player.PKC_Player() + self.player = Player() self.httpd = False self.subscription_manager = None Thread.__init__(self) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 1af60834..a27dcde5 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -5,8 +5,6 @@ from logging import getLogger from ntpath import dirname from sqlite3 import IntegrityError -import xbmc - import artwork from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi import variables as v @@ -206,39 +204,20 @@ class KodiDBMethods(object): self.cursor.execute(query, (file_id, path_id, filename, date_added)) return file_id - def clean_file_table(self): + def obsolete_file_ids(self): """ - Hack: using Direct Paths, Kodi adds all addon paths to the files table - but without a dateAdded entry. This method cleans up all file entries - without a dateAdded entry - to be called after playback has ended. + Returns a list of (idFile,) tuples (ints) of all Kodi file ids that do + not have a dateAdded set (dateAdded is NULL) and the filename start with + 'plugin://plugin.video.plexkodiconnect' + These entries should be deleted as they're created falsely by Kodi. """ query = ''' SELECT idFile FROM files WHERE dateAdded IS NULL AND strFilename LIKE \'plugin://plugin.video.plexkodiconnect%\' ''' - i = 0 - while i < 100: - self.cursor.execute(query) - files = self.cursor.fetchall() - if files: - break - # Make sure Kodi recorded "false" playstate FIRST before - # cleaning it - i += 1 - xbmc.sleep(100) - for item in files: - LOG.debug('Cleaning file id: %s', item[0]) - self.cursor.execute('DELETE FROM files WHERE idFile = ?', - (item[0],)) - self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', - (item[0],)) - self.cursor.execute('DELETE FROM settings WHERE idFile = ?', - (item[0],)) - self.cursor.execute('DELETE FROM streamdetails WHERE idFile = ?', - (item[0],)) - self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', - (item[0],)) + self.cursor.execute(query) + return self.cursor.fetchall() def show_id_from_path(self, path): """ @@ -258,10 +237,12 @@ class KodiDBMethods(object): show_id = None return show_id - def remove_file(self, file_id): + def remove_file(self, file_id, remove_orphans=True): """ Removes the entry for file_id from the files table. Will also delete - entries from the associated tables: bookmark, settings, streamdetails + entries from the associated tables: bookmark, settings, streamdetails. + If remove_orphans is true, this method will delete any orphaned path + entries in the Kodi path table """ self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1', (file_id,)) @@ -279,12 +260,13 @@ class KodiDBMethods(object): (file_id,)) self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', (file_id,)) - # Delete orphaned path entry - self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? LIMIT 1', - (path_id,)) - if self.cursor.fetchone() is None: - self.cursor.execute('DELETE FROM path WHERE idPath = ?', - (path_id,)) + if remove_orphans: + # Delete orphaned path entry + query = 'SELECT idFile FROM files WHERE idPath = ? LIMIT 1' + self.cursor.execute(query, (path_id,)) + if self.cursor.fetchone() is None: + self.cursor.execute('DELETE FROM path WHERE idPath = ?', + (path_id,)) def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, table, key): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 51c49ed4..36812e43 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -6,13 +6,15 @@ from json import loads from threading import Thread import copy -from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ - getLocalizedString +import xbmc from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, plex_command, thread_methods, try_encode +import kodidb_functions as kodidb +from utils import window, settings, plex_command, thread_methods, try_encode, \ + kodi_time_to_millis, unix_date_to_kodi, unix_timestamp from PlexFunctions import scrobble +import downloadutils as DU from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER from playback import playback_triage @@ -21,6 +23,7 @@ import playqueue as PQ import json_rpc as js import playlist_func as PL import state +import variables as v ############################################################################### @@ -53,14 +56,14 @@ STATE_SETTINGS = { ############################################################################### -class KodiMonitor(Monitor): +class KodiMonitor(xbmc.Monitor): """ PKC implementation of the Kodi Monitor class. Invoke only once. """ def __init__(self): - self.xbmcplayer = Player() + self.xbmcplayer = xbmc.Player() self._already_slept = False - Monitor.__init__(self) + xbmc.Monitor.__init__(self) for playerid in state.PLAYER_STATES: state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) @@ -137,7 +140,15 @@ class KodiMonitor(Monitor): elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') - pass + if data.get('end'): + if state.PKC_CAUSED_STOP is True: + state.PKC_CAUSED_STOP = False + state.PKC_CAUSED_STOP_DONE = True + LOG.debug('PKC caused this playback stop - ignoring') + else: + _playback_cleanup(ended=True) + else: + _playback_cleanup() elif method == 'Playlist.OnAdd': self._playlist_onadd(data) elif method == 'Playlist.OnRemove': @@ -178,11 +189,11 @@ class KodiMonitor(Monitor): window('plex_online', value="sleep") elif method == "System.OnWake": # Allow network to wake up - sleep(10000) + xbmc.sleep(10000) window('plex_online', value="false") elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": - sleep(5000) + xbmc.sleep(5000) plex_command('RUN_LIB_SCAN', 'full') elif method == "System.OnQuit": LOG.info('Kodi OnQuit detected - shutting down') @@ -296,7 +307,7 @@ class KodiMonitor(Monitor): # start as Kodi updates this info very late!! Might get previous # element otherwise self._already_slept = True - sleep(1000) + xbmc.sleep(1000) json_item = js.get_item(playerid) LOG.debug('Kodi playing item properties: %s', json_item) return (json_item.get('id'), @@ -413,16 +424,130 @@ class SpecialMonitor(Thread): def run(self): LOG.info("----====# Starting Special Monitor #====----") # "Start from beginning", "Play from beginning" - strings = (try_encode(getLocalizedString(12021)), - try_encode(getLocalizedString(12023))) + strings = (try_encode(xbmc.getLocalizedString(12021)), + try_encode(xbmc.getLocalizedString(12023))) while not self.stopped(): - if getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): - if getInfoLabel('Control.GetLabel(1002)') in strings: + if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): + if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings: # Remember that the item IS indeed resumable control = int(Window(10106).getFocusId()) state.RESUME_PLAYBACK = True if control == 1001 else False else: # Different context menu is displayed state.RESUME_PLAYBACK = False - sleep(200) + xbmc.sleep(200) LOG.info("#====---- Special Monitor Stopped ----====#") + + +@LOCKER.lockthis +def _playback_cleanup(ended=False): + """ + PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi + completely finished playing an item (because we will get and use wrong + timing data otherwise) + """ + LOG.debug('playback_cleanup called. Active players: %s', + state.ACTIVE_PLAYERS) + # 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 playerid in state.ACTIVE_PLAYERS: + status = state.PLAYER_STATES[playerid] + # Remember the last played item later + state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status) + # Stop transcoding + if status['playmethod'] == 'Transcode': + LOG.debug('Tell the PMS to stop transcoding') + DU().downloadUrl( + '{server}/video/:/transcode/universal/stop', + parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + if playerid == 1: + # Bookmarks might not be pickup up correctly, so let's do them + # manually. Applies to addon paths, but direct paths might have + # started playback via PMS + _record_playstate(status, ended) + # Reset the player's status + state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) + # As all playback has halted, reset the players that have been active + state.ACTIVE_PLAYERS = [] + LOG.debug('Finished PKC playback cleanup') + + +def _record_playstate(status, ended): + if not status['plex_id']: + LOG.debug('No Plex id found to record playstate for status %s', status) + return + with plexdb.Get_Plex_DB() as plex_db: + kodi_db_item = plex_db.getItem_byId(status['plex_id']) + if kodi_db_item is None: + # Item not (yet) in Kodi library + LOG.debug('No playstate update due to Plex id not found: %s', status) + return + totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000 + if ended: + progress = 0.99 + time = v.IGNORE_SECONDS_AT_START + 1 + else: + time = float(kodi_time_to_millis(status['time'])) / 1000 + try: + progress = time / totaltime + except ZeroDivisionError: + progress = 0.0 + LOG.debug('Playback progress %s (%s of %s seconds)', + progress, time, totaltime) + playcount = status['playcount'] + last_played = unix_date_to_kodi(unix_timestamp()) + if playcount is None: + LOG.debug('playcount not found, looking it up in the Kodi DB') + with kodidb.GetKodiDB('video') as kodi_db: + playcount = kodi_db.get_playcount(kodi_db_item[1]) + playcount = 0 if playcount is None else playcount + if time < v.IGNORE_SECONDS_AT_START: + LOG.debug('Ignoring playback less than %s seconds', + v.IGNORE_SECONDS_AT_START) + # Annoying Plex bug - it'll reset an already watched video to unwatched + playcount = None + last_played = None + time = 0 + elif progress >= v.MARK_PLAYED_AT: + LOG.debug('Recording entirely played video since progress > %s', + v.MARK_PLAYED_AT) + playcount += 1 + time = 0 + with kodidb.GetKodiDB('video') as kodi_db: + kodi_db.addPlaystate(kodi_db_item[1], + time, + totaltime, + playcount, + last_played) + # Hack to force "in progress" widget to appear if it wasn't visible before + if (state.FORCE_RELOAD_SKIN and + xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): + LOG.debug('Refreshing skin to update widgets') + xbmc.executebuiltin('ReloadSkin()') + thread = Thread(target=_clean_file_table) + thread.setDaemon(True) + thread.start() + + +def _clean_file_table(): + """ + If we associate a playing video e.g. pointing to plugin://... to an existing + Kodi library item, Kodi will add an additional entry for this (additional) + path plugin:// in the file table. This leads to all sorts of wierd behavior. + This function tries for at most 5 seconds to clean the file table. + """ + LOG.debug('Start cleaning Kodi files table') + i = 0 + while i < 100 and not state.STOP_PKC: + with kodidb.GetKodiDB('video') as kodi_db: + files = kodi_db.obsolete_file_ids() + if files: + break + i += 1 + xbmc.sleep(50) + with kodidb.GetKodiDB('video') as kodi_db: + for file_id in files: + LOG.debug('Removing obsolete Kodi file_id %s', file_id) + kodi_db.remove_file(file_id[0], remove_orphans=False) + LOG.debug('Done cleaning up Kodi file table') diff --git a/resources/lib/player.py b/resources/lib/player.py deleted file mode 100644 index 22ab3568..00000000 --- a/resources/lib/player.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################### -from logging import getLogger -import copy - -import xbmc - -import kodidb_functions as kodidb -import plexdb_functions as plexdb -from downloadutils import DownloadUtils as DU -from plexbmchelper.subscribers import LOCKER -from utils import kodi_time_to_millis, unix_date_to_kodi, unix_timestamp -import variables as v -import state - -############################################################################### - -LOG = getLogger("PLEX." + __name__) - -############################################################################### - - -@LOCKER.lockthis -def playback_cleanup(ended=False): - """ - PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi - completely finished playing an item (because we will get and use wrong - timing data otherwise) - """ - LOG.debug('playback_cleanup called. Active players: %s', - state.ACTIVE_PLAYERS) - # 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 playerid in state.ACTIVE_PLAYERS: - status = state.PLAYER_STATES[playerid] - # Remember the last played item later - state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status) - # Stop transcoding - if status['playmethod'] == 'Transcode': - LOG.debug('Tell the PMS to stop transcoding') - DU().downloadUrl( - '{server}/video/:/transcode/universal/stop', - parameters={'session': v.PKC_MACHINE_IDENTIFIER}) - if playerid == 1: - # Bookmarks might not be pickup up correctly, so let's do them - # manually. Applies to addon paths, but direct paths might have - # started playback via PMS - _record_playstate(status, ended) - # Reset the player's status - state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) - # As all playback has halted, reset the players that have been active - state.ACTIVE_PLAYERS = [] - LOG.debug('Finished PKC playback cleanup') - - -def _record_playstate(status, ended): - if not status['plex_id']: - LOG.debug('No Plex id found to record playstate for status %s', status) - return - with plexdb.Get_Plex_DB() as plex_db: - kodi_db_item = plex_db.getItem_byId(status['plex_id']) - if kodi_db_item is None: - # Item not (yet) in Kodi library - LOG.debug('No playstate update due to Plex id not found: %s', status) - return - totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000 - if ended: - progress = 0.99 - time = v.IGNORE_SECONDS_AT_START + 1 - else: - time = float(kodi_time_to_millis(status['time'])) / 1000 - try: - progress = time / totaltime - except ZeroDivisionError: - progress = 0.0 - LOG.debug('Playback progress %s (%s of %s seconds)', - progress, time, totaltime) - playcount = status['playcount'] - last_played = unix_date_to_kodi(unix_timestamp()) - if playcount is None: - LOG.debug('playcount not found, looking it up in the Kodi DB') - with kodidb.GetKodiDB('video') as kodi_db: - playcount = kodi_db.get_playcount(kodi_db_item[1]) - playcount = 0 if playcount is None else playcount - if time < v.IGNORE_SECONDS_AT_START: - LOG.debug('Ignoring playback less than %s seconds', - v.IGNORE_SECONDS_AT_START) - # Annoying Plex bug - it'll reset an already watched video to unwatched - playcount = None - last_played = None - time = 0 - elif progress >= v.MARK_PLAYED_AT: - LOG.debug('Recording entirely played video since progress > %s', - v.MARK_PLAYED_AT) - playcount += 1 - time = 0 - with kodidb.GetKodiDB('video') as kodi_db: - kodi_db.addPlaystate(kodi_db_item[1], - time, - totaltime, - playcount, - last_played) - # Hack to force "in progress" widget to appear if it wasn't visible before - if (state.FORCE_RELOAD_SKIN and - xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): - LOG.debug('Refreshing skin to update widgets') - xbmc.executebuiltin('ReloadSkin()') - if (state.DIRECT_PATHS and - status['playmethod'] in ('DirectStream', 'Transcode')): - LOG.debug('Start cleaning Kodi files table') - with kodidb.GetKodiDB('video') as kodi_db: - kodi_db.clean_file_table() - - -class PKC_Player(xbmc.Player): - def __init__(self): - xbmc.Player.__init__(self) - LOG.info("Started playback monitor.") - - def onPlayBackStarted(self): - """ - Will be called when xbmc starts playing a file. - """ - pass - - def onPlayBackPaused(self): - """ - Will be called when playback is paused - """ - pass - - def onPlayBackResumed(self): - """ - Will be called when playback is resumed - """ - pass - - def onPlayBackSeek(self, time, seekOffset): - """ - Will be called when user seeks to a certain time during playback - """ - pass - - def onPlayBackStopped(self): - """ - Will be called when playback is stopped by the user - """ - LOG.debug("ONPLAYBACK_STOPPED") - playback_cleanup() - - def onPlayBackEnded(self): - """ - Will be called when playback ends due to the media file being finished - """ - LOG.debug("ONPLAYBACK_ENDED") - if state.PKC_CAUSED_STOP is True: - state.PKC_CAUSED_STOP = False - state.PKC_CAUSED_STOP_DONE = True - LOG.debug('PKC caused this playback stop - ignoring') - else: - playback_cleanup(ended=True)