Incorporate PKC player in kodimonitor module

This commit is contained in:
Croneter 2018-04-09 08:13:54 +02:00
parent 74c0b32440
commit 76e721b78a
4 changed files with 160 additions and 217 deletions

View file

@ -7,7 +7,7 @@ from Queue import Empty
from socket import SHUT_RDWR from socket import SHUT_RDWR
from urllib import urlencode 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 utils import settings, thread_methods, language as lang, dialog
from plexbmchelper import listener, plexgdm, subscribers, httppersist 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 get_playlist_details_from_xml
from playback import playback_triage, play_xml from playback import playback_triage, play_xml
import json_rpc as js import json_rpc as js
import player
import variables as v import variables as v
import state import state
import playqueue as PQ import playqueue as PQ
@ -43,7 +42,7 @@ class PlexCompanion(Thread):
self.client.clientDetails() self.client.clientDetails()
LOG.debug("Registration string is:\n%s", self.client.getClientDetails()) LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
# kodi player instance # kodi player instance
self.player = player.PKC_Player() self.player = Player()
self.httpd = False self.httpd = False
self.subscription_manager = None self.subscription_manager = None
Thread.__init__(self) Thread.__init__(self)

View file

@ -5,8 +5,6 @@ from logging import getLogger
from ntpath import dirname from ntpath import dirname
from sqlite3 import IntegrityError from sqlite3 import IntegrityError
import xbmc
import artwork import artwork
from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi
import variables as v import variables as v
@ -206,39 +204,20 @@ class KodiDBMethods(object):
self.cursor.execute(query, (file_id, path_id, filename, date_added)) self.cursor.execute(query, (file_id, path_id, filename, date_added))
return file_id 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 Returns a list of (idFile,) tuples (ints) of all Kodi file ids that do
but without a dateAdded entry. This method cleans up all file entries not have a dateAdded set (dateAdded is NULL) and the filename start with
without a dateAdded entry - to be called after playback has ended. 'plugin://plugin.video.plexkodiconnect'
These entries should be deleted as they're created falsely by Kodi.
""" """
query = ''' query = '''
SELECT idFile FROM files SELECT idFile FROM files
WHERE dateAdded IS NULL WHERE dateAdded IS NULL
AND strFilename LIKE \'plugin://plugin.video.plexkodiconnect%\' AND strFilename LIKE \'plugin://plugin.video.plexkodiconnect%\'
''' '''
i = 0
while i < 100:
self.cursor.execute(query) self.cursor.execute(query)
files = self.cursor.fetchall() return 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],))
def show_id_from_path(self, path): def show_id_from_path(self, path):
""" """
@ -258,10 +237,12 @@ class KodiDBMethods(object):
show_id = None show_id = None
return show_id 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 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', self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1',
(file_id,)) (file_id,))
@ -279,9 +260,10 @@ class KodiDBMethods(object):
(file_id,)) (file_id,))
self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?',
(file_id,)) (file_id,))
if remove_orphans:
# Delete orphaned path entry # Delete orphaned path entry
self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? LIMIT 1', query = 'SELECT idFile FROM files WHERE idPath = ? LIMIT 1'
(path_id,)) self.cursor.execute(query, (path_id,))
if self.cursor.fetchone() is None: if self.cursor.fetchone() is None:
self.cursor.execute('DELETE FROM path WHERE idPath = ?', self.cursor.execute('DELETE FROM path WHERE idPath = ?',
(path_id,)) (path_id,))

View file

@ -6,13 +6,15 @@ from json import loads
from threading import Thread from threading import Thread
import copy import copy
from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ import xbmc
getLocalizedString
from xbmcgui import Window from xbmcgui import Window
import plexdb_functions as plexdb 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 from PlexFunctions import scrobble
import downloadutils as DU
from kodidb_functions import kodiid_from_filename from kodidb_functions import kodiid_from_filename
from plexbmchelper.subscribers import LOCKER from plexbmchelper.subscribers import LOCKER
from playback import playback_triage from playback import playback_triage
@ -21,6 +23,7 @@ import playqueue as PQ
import json_rpc as js import json_rpc as js
import playlist_func as PL import playlist_func as PL
import state 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. PKC implementation of the Kodi Monitor class. Invoke only once.
""" """
def __init__(self): def __init__(self):
self.xbmcplayer = Player() self.xbmcplayer = xbmc.Player()
self._already_slept = False self._already_slept = False
Monitor.__init__(self) xbmc.Monitor.__init__(self)
for playerid in state.PLAYER_STATES: for playerid in state.PLAYER_STATES:
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
state.OLD_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": elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck # Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()') # 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': elif method == 'Playlist.OnAdd':
self._playlist_onadd(data) self._playlist_onadd(data)
elif method == 'Playlist.OnRemove': elif method == 'Playlist.OnRemove':
@ -178,11 +189,11 @@ class KodiMonitor(Monitor):
window('plex_online', value="sleep") window('plex_online', value="sleep")
elif method == "System.OnWake": elif method == "System.OnWake":
# Allow network to wake up # Allow network to wake up
sleep(10000) xbmc.sleep(10000)
window('plex_online', value="false") window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated": elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true": if settings('dbSyncScreensaver') == "true":
sleep(5000) xbmc.sleep(5000)
plex_command('RUN_LIB_SCAN', 'full') plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit": elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down') 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 # start as Kodi updates this info very late!! Might get previous
# element otherwise # element otherwise
self._already_slept = True self._already_slept = True
sleep(1000) xbmc.sleep(1000)
json_item = js.get_item(playerid) json_item = js.get_item(playerid)
LOG.debug('Kodi playing item properties: %s', json_item) LOG.debug('Kodi playing item properties: %s', json_item)
return (json_item.get('id'), return (json_item.get('id'),
@ -413,16 +424,130 @@ class SpecialMonitor(Thread):
def run(self): def run(self):
LOG.info("----====# Starting Special Monitor #====----") LOG.info("----====# Starting Special Monitor #====----")
# "Start from beginning", "Play from beginning" # "Start from beginning", "Play from beginning"
strings = (try_encode(getLocalizedString(12021)), strings = (try_encode(xbmc.getLocalizedString(12021)),
try_encode(getLocalizedString(12023))) try_encode(xbmc.getLocalizedString(12023)))
while not self.stopped(): while not self.stopped():
if getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
if getInfoLabel('Control.GetLabel(1002)') in strings: if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
# Remember that the item IS indeed resumable # Remember that the item IS indeed resumable
control = int(Window(10106).getFocusId()) control = int(Window(10106).getFocusId())
state.RESUME_PLAYBACK = True if control == 1001 else False state.RESUME_PLAYBACK = True if control == 1001 else False
else: else:
# Different context menu is displayed # Different context menu is displayed
state.RESUME_PLAYBACK = False state.RESUME_PLAYBACK = False
sleep(200) xbmc.sleep(200)
LOG.info("#====---- Special Monitor Stopped ----====#") 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')

View file

@ -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)