Merge pull request #1021 from croneter/fix-resume
Fix resume not working in some cases
This commit is contained in:
commit
265b2dcf6e
7 changed files with 107 additions and 168 deletions
|
@ -17,14 +17,15 @@ from .plex_api import API
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
from . import kodi_db
|
from . import kodi_db
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils, timing, plex_functions as PF, playback
|
from . import utils, timing, plex_functions as PF
|
||||||
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||||
from . import backgroundthread, app, variables as v
|
from . import backgroundthread, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodimonitor')
|
LOG = getLogger('PLEX.kodimonitor')
|
||||||
|
|
||||||
# "Start from beginning", "Play from beginning"
|
# "Start from beginning", "Play from beginning"
|
||||||
STRINGS = (utils.lang(12021).encode('utf-8'), utils.lang(12023).encode('utf-8'))
|
STRINGS = (utils.lang(12021).encode('utf-8'),
|
||||||
|
utils.lang(12023).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
class KodiMonitor(xbmc.Monitor):
|
class KodiMonitor(xbmc.Monitor):
|
||||||
|
@ -33,7 +34,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._already_slept = False
|
self._already_slept = False
|
||||||
self.hack_replay = None
|
|
||||||
xbmc.Monitor.__init__(self)
|
xbmc.Monitor.__init__(self)
|
||||||
for playerid in app.PLAYSTATE.player_states:
|
for playerid in app.PLAYSTATE.player_states:
|
||||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
|
@ -57,9 +57,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
Monitor the PKC settings for changes made by the user
|
Monitor the PKC settings for changes made by the user
|
||||||
"""
|
"""
|
||||||
LOG.debug('PKC settings change detected')
|
LOG.debug('PKC settings change detected')
|
||||||
# Assume that the user changed something so we can try to reconnect
|
|
||||||
# app.APP.suspend = False
|
|
||||||
# app.APP.resume_threads(block=False)
|
|
||||||
|
|
||||||
def onNotification(self, sender, method, data):
|
def onNotification(self, sender, method, data):
|
||||||
"""
|
"""
|
||||||
|
@ -69,28 +66,12 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
data = loads(data, 'utf-8')
|
data = loads(data, 'utf-8')
|
||||||
LOG.debug("Method: %s Data: %s", method, data)
|
LOG.debug("Method: %s Data: %s", method, data)
|
||||||
|
|
||||||
# Hack
|
|
||||||
if not method == 'Player.OnStop':
|
|
||||||
self.hack_replay = None
|
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
# Should refresh our video nodes, e.g. on deck
|
with app.APP.lock_playqueues:
|
||||||
# xbmc.executebuiltin('ReloadSkin()')
|
_playback_cleanup(ended=data.get('end'))
|
||||||
if (self.hack_replay and not data.get('end') and
|
|
||||||
self.hack_replay == data['item']):
|
|
||||||
# Hack for add-on paths
|
|
||||||
self.hack_replay = None
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
self._hack_addon_paths_replay_video()
|
|
||||||
elif data.get('end'):
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
_playback_cleanup(ended=True)
|
|
||||||
else:
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
_playback_cleanup()
|
|
||||||
elif method == 'Playlist.OnAdd':
|
elif method == 'Playlist.OnAdd':
|
||||||
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
||||||
# Hitting the "browse" button on tv show info dialog
|
# Hitting the "browse" button on tv show info dialog
|
||||||
|
@ -126,39 +107,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
|
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
|
||||||
self._start_next_episode(data)
|
self._start_next_episode(data)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _hack_addon_paths_replay_video():
|
|
||||||
"""
|
|
||||||
Hack we need for RESUMABLE items because Kodi lost the path of the
|
|
||||||
last played item that is now being replayed (see playback.py's
|
|
||||||
Player().play()) Also see playqueue.py _compare_playqueues()
|
|
||||||
|
|
||||||
Needed if user re-starts the same video from the library using addon
|
|
||||||
paths. (Video is only added to playqueue, then immediately stoppen.
|
|
||||||
There is no playback initialized by Kodi.) Log excerpts:
|
|
||||||
Method: Playlist.OnAdd Data:
|
|
||||||
{u'item': {u'type': u'movie', u'id': 4},
|
|
||||||
u'playlistid': 1,
|
|
||||||
u'position': 0}
|
|
||||||
Now we would hack!
|
|
||||||
Method: Player.OnStop Data:
|
|
||||||
{u'item': {u'type': u'movie', u'id': 4},
|
|
||||||
u'end': False}
|
|
||||||
(within the same micro-second!)
|
|
||||||
"""
|
|
||||||
LOG.info('Detected re-start of playback of last item')
|
|
||||||
old = app.PLAYSTATE.old_player_states[1]
|
|
||||||
kwargs = {
|
|
||||||
'plex_id': old['plex_id'],
|
|
||||||
'plex_type': old['plex_type'],
|
|
||||||
'path': old['file'],
|
|
||||||
'resolve': False
|
|
||||||
}
|
|
||||||
task = backgroundthread.FunctionAsTask(playback.playback_triage,
|
|
||||||
None,
|
|
||||||
**kwargs)
|
|
||||||
backgroundthread.BGThreader.addTasksToFront([task])
|
|
||||||
|
|
||||||
def _playlist_onadd(self, data):
|
def _playlist_onadd(self, data):
|
||||||
"""
|
"""
|
||||||
Called if an item is added to a Kodi playlist. Example data dict:
|
Called if an item is added to a Kodi playlist. Example data dict:
|
||||||
|
@ -171,15 +119,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
}
|
}
|
||||||
Will NOT be called if playback initiated by Kodi widgets
|
Will NOT be called if playback initiated by Kodi widgets
|
||||||
"""
|
"""
|
||||||
if 'id' not in data['item']:
|
pass
|
||||||
return
|
|
||||||
old = app.PLAYSTATE.old_player_states[data['playlistid']]
|
|
||||||
if (not app.SYNC.direct_paths and
|
|
||||||
data['position'] == 0 and data['playlistid'] == 1 and
|
|
||||||
not PQ.PLAYQUEUES[data['playlistid']].items and
|
|
||||||
data['item']['type'] == old['kodi_type'] and
|
|
||||||
data['item']['id'] == old['kodi_id']):
|
|
||||||
self.hack_replay = data['item']
|
|
||||||
|
|
||||||
def _playlist_onremove(self, data):
|
def _playlist_onremove(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -451,7 +391,7 @@ def _playback_cleanup(ended=False):
|
||||||
app.PLAYSTATE.active_players = set()
|
app.PLAYSTATE.active_players = set()
|
||||||
app.PLAYSTATE.item = None
|
app.PLAYSTATE.item = None
|
||||||
utils.delete_temporary_subtitles()
|
utils.delete_temporary_subtitles()
|
||||||
LOG.info('Finished PKC playback cleanup')
|
LOG.debug('Finished PKC playback cleanup')
|
||||||
|
|
||||||
|
|
||||||
def _record_playstate(status, ended):
|
def _record_playstate(status, ended):
|
||||||
|
|
|
@ -30,7 +30,8 @@ RESOLVE = True
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
|
||||||
|
resume=False):
|
||||||
"""
|
"""
|
||||||
Hit this function for addon path playback, Plex trailers, etc.
|
Hit this function for addon path playback, Plex trailers, etc.
|
||||||
Will setup playback first, then on second call complete playback.
|
Will setup playback first, then on second call complete playback.
|
||||||
|
@ -47,7 +48,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
service.py Python instance
|
service.py Python instance
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_playback_triage(plex_id, plex_type, path, resolve)
|
_playback_triage(plex_id, plex_type, path, resolve, resume)
|
||||||
finally:
|
finally:
|
||||||
# Reset some playback variables the user potentially set to init
|
# Reset some playback variables the user potentially set to init
|
||||||
# playback
|
# playback
|
||||||
|
@ -56,10 +57,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
app.PLAYSTATE.resume_playback = None
|
app.PLAYSTATE.resume_playback = None
|
||||||
|
|
||||||
|
|
||||||
def _playback_triage(plex_id, plex_type, path, resolve):
|
def _playback_triage(plex_id, plex_type, path, resolve, resume):
|
||||||
plex_id = utils.cast(int, plex_id)
|
plex_id = utils.cast(int, plex_id)
|
||||||
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
||||||
'resolve %s', plex_id, plex_type, path, resolve)
|
'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
|
||||||
global RESOLVE
|
global RESOLVE
|
||||||
# If started via Kodi context menu, we never resolve
|
# If started via Kodi context menu, we never resolve
|
||||||
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
||||||
|
@ -85,12 +86,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
|
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
|
||||||
# add-on paths
|
# add-on paths
|
||||||
LOG.info('No position returned from player! Assuming playlist')
|
LOG.debug('No position returned from player! Assuming playlist')
|
||||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
||||||
try:
|
try:
|
||||||
pos = js.get_position(playqueue.playlistid)
|
pos = js.get_position(playqueue.playlistid)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.info('Assuming video instead of audio playlist playback')
|
LOG.debug('Assuming video instead of audio playlist playback')
|
||||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
|
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
|
||||||
try:
|
try:
|
||||||
pos = js.get_position(playqueue.playlistid)
|
pos = js.get_position(playqueue.playlistid)
|
||||||
|
@ -108,12 +109,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
try:
|
try:
|
||||||
item = items[pos]
|
item = items[pos]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
LOG.info('Could not apply playlist hack! Probably Widget playback')
|
LOG.debug('Could not apply playlist hack! Probably Widget playback')
|
||||||
else:
|
else:
|
||||||
if ('id' not in item and
|
if ('id' not in item and
|
||||||
item.get('type') == 'unknown' and item.get('title') == ''):
|
item.get('type') == 'unknown' and item.get('title') == ''):
|
||||||
LOG.info('Kodi playlist play detected')
|
LOG.debug('Kodi playlist play detected')
|
||||||
_playlist_playback(plex_id, plex_type)
|
_playlist_playback(plex_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Can return -1 (as in "no playlist")
|
# Can return -1 (as in "no playlist")
|
||||||
|
@ -127,7 +128,7 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
initiate = True
|
initiate = True
|
||||||
else:
|
else:
|
||||||
if item.plex_id != plex_id:
|
if item.plex_id != plex_id:
|
||||||
LOG.debug('Received new plex_id %s, expected %s',
|
LOG.debug('Received new plex_id%s, expected %s',
|
||||||
plex_id, item.plex_id)
|
plex_id, item.plex_id)
|
||||||
initiate = True
|
initiate = True
|
||||||
else:
|
else:
|
||||||
|
@ -136,13 +137,14 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
LOG.debug('Detected re-playing of the same item')
|
LOG.debug('Detected re-playing of the same item')
|
||||||
initiate = True
|
initiate = True
|
||||||
if initiate:
|
if initiate:
|
||||||
_playback_init(plex_id, plex_type, playqueue, pos)
|
_playback_init(plex_id, plex_type, playqueue, pos, resume)
|
||||||
else:
|
else:
|
||||||
# kick off playback on second pass
|
# kick off playback on second pass, resume was already set on first
|
||||||
|
# pass (threaded_playback will seek to resume)
|
||||||
_conclude_playback(playqueue, pos)
|
_conclude_playback(playqueue, pos)
|
||||||
|
|
||||||
|
|
||||||
def _playlist_playback(plex_id, plex_type):
|
def _playlist_playback(plex_id):
|
||||||
"""
|
"""
|
||||||
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
||||||
where, causing Playlist.onAdd to fire for each item like this:
|
where, causing Playlist.onAdd to fire for each item like this:
|
||||||
|
@ -175,11 +177,11 @@ def _playlist_playback(plex_id, plex_type):
|
||||||
_conclude_playback(playqueue, pos=0)
|
_conclude_playback(playqueue, pos=0)
|
||||||
|
|
||||||
|
|
||||||
def _playback_init(plex_id, plex_type, playqueue, pos):
|
def _playback_init(plex_id, plex_type, playqueue, pos, force_resume):
|
||||||
"""
|
"""
|
||||||
Playback setup if Kodi starts playing an item for the first time.
|
Playback setup if Kodi starts playing an item for the first time.
|
||||||
"""
|
"""
|
||||||
LOG.info('Initializing PKC playback')
|
LOG.debug('Initializing PKC playback')
|
||||||
# Stop playback so we don't get an error message that the last item of the
|
# Stop playback so we don't get an error message that the last item of the
|
||||||
# queue failed to play
|
# queue failed to play
|
||||||
app.APP.player.stop()
|
app.APP.player.stop()
|
||||||
|
@ -210,19 +212,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
# playqueues
|
# playqueues
|
||||||
# Release default.py
|
# Release default.py
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
api = API(xml[0])
|
LOG.debug('Using force_resume %s', force_resume)
|
||||||
if api.resume_point() and (app.SYNC.direct_paths or
|
resume = force_resume or False
|
||||||
app.PLAYSTATE.context_menu_play):
|
|
||||||
# Since Kodi won't ask if user wants to resume playback -
|
|
||||||
# we need to ask ourselves
|
|
||||||
resume = resume_dialog(int(api.resume_point()))
|
|
||||||
if resume is None:
|
|
||||||
LOG.info('User cancelled resume dialog')
|
|
||||||
return
|
|
||||||
elif app.SYNC.direct_paths:
|
|
||||||
resume = False
|
|
||||||
else:
|
|
||||||
resume = app.PLAYSTATE.resume_playback or False
|
|
||||||
trailers = False
|
trailers = False
|
||||||
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
||||||
utils.settings('enableCinema') == "true"):
|
utils.settings('enableCinema') == "true"):
|
||||||
|
@ -251,11 +242,17 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
stack = _prep_playlist_stack(xml, resume)
|
stack = _prep_playlist_stack(xml, resume)
|
||||||
_process_stack(playqueue, stack)
|
_process_stack(playqueue, stack)
|
||||||
|
offset = _use_kodi_db_offset(playqueue.items[pos].plex_id,
|
||||||
|
playqueue.items[pos].plex_type,
|
||||||
|
playqueue.items[pos].offset) if resume else 0
|
||||||
# New thread to release this one sooner (e.g. harddisk spinning up)
|
# New thread to release this one sooner (e.g. harddisk spinning up)
|
||||||
thread = Thread(target=threaded_playback,
|
thread = Thread(target=threaded_playback,
|
||||||
args=(playqueue.kodi_pl, pos, None))
|
args=(playqueue.kodi_pl, pos, offset))
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
LOG.info('Done initializing playback, starting Kodi player at pos %s', pos)
|
LOG.debug('Done initializing playback, starting Kodi player at pos %s and '
|
||||||
|
'offset %s', pos, offset)
|
||||||
|
# Ensure that PKC playqueue monitor ignores the changes we just made
|
||||||
|
playqueue.pkc_edit = True
|
||||||
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
||||||
# caches paths like our plugin://pkc. If we use Player().play() between
|
# caches paths like our plugin://pkc. If we use Player().play() between
|
||||||
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
||||||
|
@ -263,8 +260,6 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
||||||
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
||||||
thread.start()
|
thread.start()
|
||||||
# Ensure that PKC playqueue monitor ignores the changes we just made
|
|
||||||
playqueue.pkc_edit = True
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_resolve(abort=False):
|
def _ensure_resolve(abort=False):
|
||||||
|
@ -376,7 +371,7 @@ def _prep_playlist_stack(xml, resume):
|
||||||
'part': part,
|
'part': part,
|
||||||
'playcount': api.viewcount(),
|
'playcount': api.viewcount(),
|
||||||
'offset': api.resume_point(),
|
'offset': api.resume_point(),
|
||||||
'resume': resume if i + 1 == len(xml) and part == 0 else False,
|
'resume': resume if part == 0 and i + 1 == len(xml) else None,
|
||||||
'id': api.item_id()
|
'id': api.item_id()
|
||||||
})
|
})
|
||||||
return stack
|
return stack
|
||||||
|
@ -413,33 +408,20 @@ def _process_stack(playqueue, stack):
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
|
|
||||||
def _set_resume(listitem, item, api):
|
def _use_kodi_db_offset(plex_id, plex_type, plex_offset):
|
||||||
if item.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
|
"""
|
||||||
return
|
Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
|
||||||
if item.resume is True:
|
have gotten the last resume point)
|
||||||
# Do NOT use item.offset directly but get it from the DB
|
"""
|
||||||
# (user might have initiated same video twice)
|
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
|
||||||
with PlexDB(lock=False) as plexdb:
|
return plex_offset
|
||||||
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
|
with PlexDB(lock=False) as plexdb:
|
||||||
if db_item:
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||||
file_id = db_item['kodi_fileid']
|
if db_item:
|
||||||
with KodiVideoDB(lock=False) as kodidb:
|
with KodiVideoDB(lock=False) as kodidb:
|
||||||
item.offset = kodidb.get_resume(file_id)
|
return kodidb.get_resume(db_item['kodi_fileid'])
|
||||||
LOG.info('Resuming playback at %s', item.offset)
|
else:
|
||||||
if v.KODIVERSION >= 18 and api:
|
return plex_offset
|
||||||
# Kodi 18 Alpha 3 broke StartOffset
|
|
||||||
try:
|
|
||||||
percent = (item.offset or api.resume_point()) / api.runtime() * 100.0
|
|
||||||
except ZeroDivisionError:
|
|
||||||
percent = 0.0
|
|
||||||
LOG.debug('Resuming at %s percent', percent)
|
|
||||||
listitem.setProperty('StartPercent', str(percent))
|
|
||||||
else:
|
|
||||||
listitem.setProperty('StartOffset', str(item.offset))
|
|
||||||
listitem.setProperty('resumetime', str(item.offset))
|
|
||||||
elif v.KODIVERSION >= 18:
|
|
||||||
# Make sure that the video starts from the beginning
|
|
||||||
listitem.setProperty('StartPercent', '0')
|
|
||||||
|
|
||||||
|
|
||||||
def _conclude_playback(playqueue, pos):
|
def _conclude_playback(playqueue, pos):
|
||||||
|
@ -457,19 +439,14 @@ def _conclude_playback(playqueue, pos):
|
||||||
start playback
|
start playback
|
||||||
return PKC listitem attached to result
|
return PKC listitem attached to result
|
||||||
"""
|
"""
|
||||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
LOG.debug('Concluding playback for playqueue position %s', pos)
|
||||||
item = playqueue.items[pos]
|
item = playqueue.items[pos]
|
||||||
if item.xml is not None:
|
api = API(item.xml)
|
||||||
# Got a Plex element
|
api.part = item.part or 0
|
||||||
api = API(item.xml)
|
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||||
api.part = item.part or 0
|
set_playurl(api, item)
|
||||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
|
||||||
set_playurl(api, item)
|
|
||||||
else:
|
|
||||||
listitem = transfer.PKCListItem()
|
|
||||||
api = None
|
|
||||||
if not item.file:
|
if not item.file:
|
||||||
LOG.info('Did not get a playurl, aborting playback silently')
|
LOG.debug('Did not get a playurl, aborting playback silently')
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
return
|
return
|
||||||
listitem.setPath(item.file.encode('utf-8'))
|
listitem.setPath(item.file.encode('utf-8'))
|
||||||
|
@ -478,9 +455,8 @@ def _conclude_playback(playqueue, pos):
|
||||||
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
|
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
|
||||||
v.PLAYBACK_METHOD_TRANSCODE):
|
v.PLAYBACK_METHOD_TRANSCODE):
|
||||||
audio_subtitle_prefs(api, listitem)
|
audio_subtitle_prefs(api, listitem)
|
||||||
_set_resume(listitem, item, api)
|
|
||||||
transfer.send(listitem)
|
transfer.send(listitem)
|
||||||
LOG.info('Done concluding playback')
|
LOG.debug('Done concluding playback')
|
||||||
|
|
||||||
|
|
||||||
def process_indirect(key, offset, resolve=True):
|
def process_indirect(key, offset, resolve=True):
|
||||||
|
@ -494,8 +470,8 @@ def process_indirect(key, offset, resolve=True):
|
||||||
Set resolve to False if playback should be kicked off directly, not via
|
Set resolve to False if playback should be kicked off directly, not via
|
||||||
setResolvedUrl
|
setResolvedUrl
|
||||||
"""
|
"""
|
||||||
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
|
LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
|
||||||
key, offset, resolve)
|
key, offset, resolve)
|
||||||
global RESOLVE
|
global RESOLVE
|
||||||
RESOLVE = resolve
|
RESOLVE = resolve
|
||||||
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
||||||
|
@ -513,7 +489,7 @@ def process_indirect(key, offset, resolve=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
|
@ -552,7 +528,7 @@ def process_indirect(key, offset, resolve=True):
|
||||||
args={'item': utils.try_encode(playurl),
|
args={'item': utils.try_encode(playurl),
|
||||||
'listitem': listitem})
|
'listitem': listitem})
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
LOG.info('Done initializing PKC playback, starting Kodi player')
|
LOG.debug('Done initializing PKC playback, starting Kodi player')
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
@ -563,9 +539,9 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
Either supply the ratingKey of the starting Plex element. Or set
|
Either supply the ratingKey of the starting Plex element. Or set
|
||||||
playqueue.selectedItemID
|
playqueue.selectedItemID
|
||||||
"""
|
"""
|
||||||
offset = int(offset) if offset else None
|
offset = int(offset) / 1000 if offset else None
|
||||||
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
LOG.debug("play_xml called with offset %s, start_plex_id %s",
|
||||||
offset, start_plex_id)
|
offset, start_plex_id)
|
||||||
start_item = start_plex_id if start_plex_id is not None \
|
start_item = start_plex_id if start_plex_id is not None \
|
||||||
else playqueue.selectedItemID
|
else playqueue.selectedItemID
|
||||||
for startpos, video in enumerate(xml):
|
for startpos, video in enumerate(xml):
|
||||||
|
@ -581,21 +557,38 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||||
thread = Thread(target=threaded_playback,
|
thread = Thread(target=threaded_playback,
|
||||||
args=(playqueue.kodi_pl, startpos, offset))
|
args=(playqueue.kodi_pl, startpos, offset))
|
||||||
LOG.info('Done play_xml, starting Kodi player at position %s', startpos)
|
LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
def threaded_playback(kodi_playlist, startpos, offset):
|
def threaded_playback(kodi_playlist, startpos, offset):
|
||||||
"""
|
"""
|
||||||
Seek immediately after kicking off playback is not reliable.
|
Seek immediately after kicking off playback is not reliable. We even seek
|
||||||
|
to 0 (starting position) in case Kodi wants to resume but we want to start
|
||||||
|
over.
|
||||||
|
|
||||||
|
offset: resume position in seconds [int/float]
|
||||||
"""
|
"""
|
||||||
|
LOG.debug('threaded_playback with startpos %s, offset %s',
|
||||||
|
startpos, offset)
|
||||||
app.APP.player.play(kodi_playlist, None, False, startpos)
|
app.APP.player.play(kodi_playlist, None, False, startpos)
|
||||||
if offset and offset != '0':
|
if offset:
|
||||||
i = 0
|
i = 0
|
||||||
while not app.APP.is_playing or not js.get_player_ids():
|
while not app.APP.is_playing or not js.get_player_ids():
|
||||||
app.APP.monitor.waitForAbort(0.1)
|
app.APP.monitor.waitForAbort(0.1)
|
||||||
i += 1
|
i += 1
|
||||||
if i > 100:
|
if i > 200:
|
||||||
LOG.error('Could not seek to %s', offset)
|
LOG.error('Could not seek to %s', offset)
|
||||||
return
|
return
|
||||||
js.seek_to(int(offset))
|
i = 0
|
||||||
|
answ = js.seek_to(offset * 1000)
|
||||||
|
while 'error' in answ:
|
||||||
|
# Kodi sometimes returns {u'message': u'Failed to execute method.',
|
||||||
|
# u'code': -32100} if user quickly switches videos
|
||||||
|
i += 1
|
||||||
|
if i > 10:
|
||||||
|
LOG.error('Failed to seek to %s', offset)
|
||||||
|
return
|
||||||
|
app.APP.monitor.waitForAbort(0.1)
|
||||||
|
answ = js.seek_to(offset * 1000)
|
||||||
|
LOG.debug('Seek to offset %s successful', offset)
|
||||||
|
|
|
@ -37,10 +37,15 @@ class PlaybackTask(backgroundthread.Task):
|
||||||
resolve = False if params.get('handle') == '-1' else True
|
resolve = False if params.get('handle') == '-1' else True
|
||||||
LOG.debug('Received mode: %s, params: %s', mode, params)
|
LOG.debug('Received mode: %s, params: %s', mode, params)
|
||||||
if mode == 'play':
|
if mode == 'play':
|
||||||
|
if params.get('resume'):
|
||||||
|
resume = params.get('resume') == '1'
|
||||||
|
else:
|
||||||
|
resume = None
|
||||||
playback.playback_triage(plex_id=params.get('plex_id'),
|
playback.playback_triage(plex_id=params.get('plex_id'),
|
||||||
plex_type=params.get('plex_type'),
|
plex_type=params.get('plex_type'),
|
||||||
path=params.get('path'),
|
path=params.get('path'),
|
||||||
resolve=resolve)
|
resolve=resolve,
|
||||||
|
resume=resume)
|
||||||
elif mode == 'plex_node':
|
elif mode == 'plex_node':
|
||||||
playback.process_indirect(params['key'],
|
playback.process_indirect(params['key'],
|
||||||
params['offset'],
|
params['offset'],
|
||||||
|
|
|
@ -213,6 +213,7 @@ class PlaylistItem(object):
|
||||||
"'guid': '{self.guid}', "
|
"'guid': '{self.guid}', "
|
||||||
"'playmethod': '{self.playmethod}', "
|
"'playmethod': '{self.playmethod}', "
|
||||||
"'playcount': {self.playcount}, "
|
"'playcount': {self.playcount}, "
|
||||||
|
"'resume': {self.resume},"
|
||||||
"'offset': {self.offset}, "
|
"'offset': {self.offset}, "
|
||||||
"'force_transcode': {self.force_transcode}, "
|
"'force_transcode': {self.force_transcode}, "
|
||||||
"'part': {self.part}".format(self=self))
|
"'part': {self.part}".format(self=self))
|
||||||
|
|
|
@ -605,11 +605,16 @@ class Base(object):
|
||||||
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def listitem(self, listitem=xbmcgui.ListItem):
|
def listitem(self, listitem=xbmcgui.ListItem, resume=True):
|
||||||
"""
|
"""
|
||||||
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
|
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
|
||||||
|
|
||||||
|
Pass resume=False in order to NOT set a resume point (but let Kodi
|
||||||
|
automatically handle it)
|
||||||
"""
|
"""
|
||||||
item = widgets.generate_item(self)
|
item = widgets.generate_item(self)
|
||||||
|
if not resume and 'resume' in item:
|
||||||
|
del item['resume']
|
||||||
item = widgets.prepare_listitem(item)
|
item = widgets.prepare_listitem(item)
|
||||||
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
|
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
|
||||||
|
|
||||||
|
|
|
@ -151,11 +151,10 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
playback.play_xml(playqueue, xml, offset)
|
playback.play_xml(playqueue, xml, offset)
|
||||||
else:
|
else:
|
||||||
app.CONN.plex_transient_token = data.get('token')
|
app.CONN.plex_transient_token = data.get('token')
|
||||||
if data.get('offset') != '0':
|
|
||||||
app.PLAYSTATE.resume_playback = True
|
|
||||||
playback.playback_triage(api.plex_id,
|
playback.playback_triage(api.plex_id,
|
||||||
api.plex_type,
|
api.plex_type,
|
||||||
resolve=False)
|
resolve=False,
|
||||||
|
resume=data.get('offset') not in ('0', None))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_node(data):
|
def _process_node(data):
|
||||||
|
|
|
@ -63,25 +63,21 @@ def kodi_now():
|
||||||
|
|
||||||
def millis_to_kodi_time(milliseconds):
|
def millis_to_kodi_time(milliseconds):
|
||||||
"""
|
"""
|
||||||
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
|
Converts time in milliseconds [int or float] to the time dict used by the
|
||||||
|
Kodi JSON RPC:
|
||||||
{
|
{
|
||||||
'hours': [int],
|
'hours': [int],
|
||||||
'minutes': [int],
|
'minutes': [int],
|
||||||
'seconds'[int],
|
'seconds'[int],
|
||||||
'milliseconds': [int]
|
'milliseconds': [int]
|
||||||
}
|
}
|
||||||
Pass in the time in milliseconds as an int
|
|
||||||
"""
|
"""
|
||||||
seconds = int(milliseconds / 1000)
|
seconds = int(milliseconds / 1000)
|
||||||
minutes = int(seconds / 60)
|
minutes = int(seconds / 60)
|
||||||
seconds = seconds % 60
|
return {'hours': int(minutes / 60),
|
||||||
hours = int(minutes / 60)
|
'minutes': int(minutes % 60),
|
||||||
minutes = minutes % 60
|
'seconds': int(seconds % 60),
|
||||||
milliseconds = milliseconds % 1000
|
'milliseconds': int(milliseconds % 1000)}
|
||||||
return {'hours': hours,
|
|
||||||
'minutes': minutes,
|
|
||||||
'seconds': seconds,
|
|
||||||
'milliseconds': milliseconds}
|
|
||||||
|
|
||||||
|
|
||||||
def kodi_time_to_millis(time):
|
def kodi_time_to_millis(time):
|
||||||
|
|
Loading…
Reference in a new issue