PlexKodiConnect/resources/lib/playback.py

615 lines
24 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# -*- coding: utf-8 -*-
2018-01-08 03:50:30 +11:00
"""
Used to kick off Kodi playback
"""
from __future__ import absolute_import, division, unicode_literals
2018-01-11 06:14:05 +11:00
from logging import getLogger
2018-01-21 23:42:22 +11:00
from threading import Thread
import datetime
2018-01-11 06:14:05 +11:00
import xbmc
2018-06-22 03:24:37 +10:00
from .plex_api import API
2018-10-25 02:17:02 +11:00
from .plex_db import PlexDB
2018-06-22 03:24:37 +10:00
from . import plex_functions as PF
from . import utils
2018-11-09 07:22:16 +11:00
from .kodi_db import KodiVideoDB
2018-06-22 03:24:37 +10:00
from . import playlist_func as PL
from . import playqueue as PQ
from . import json_rpc as js
from . import transfer
2019-10-05 20:44:40 +10:00
from .playback_decision import set_playurl, audio_subtitle_prefs
2018-06-22 03:24:37 +10:00
from . import variables as v
2018-11-19 00:59:17 +11:00
from . import app
2018-01-11 06:14:05 +11:00
###############################################################################
2018-06-22 03:24:37 +10:00
LOG = getLogger('PLEX.playback')
# Do we need to return ultimately with a setResolvedUrl?
RESOLVE = True
TRY_TO_SEEK_FOR = 300 # =30 seconds
IGNORE_SECONDS_AT_START = 15
2018-01-11 06:14:05 +11:00
###############################################################################
2019-10-26 02:06:50 +11:00
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
resume=False):
2018-01-08 03:50:30 +11:00
"""
2018-01-11 06:14:05 +11:00
Hit this function for addon path playback, Plex trailers, etc.
Will setup playback first, then on second call complete playback.
2018-01-08 03:50:30 +11:00
2018-06-22 03:24:37 +10:00
Will set Playback_Successful() with potentially a PKCListItem() attached
2018-01-21 23:42:22 +11:00
(to be consumed by setResolvedURL in default.py)
If trailers or additional (movie-)parts are added, default.py is released
and a completely new player instance is called with a new playlist. This
circumvents most issues with Kodi & playqueues
2018-01-26 03:15:38 +11:00
Set resolve to False if you do not want setResolvedUrl to be called on
the first pass - e.g. if you're calling this function from the original
service.py Python instance
2018-01-11 06:14:05 +11:00
"""
try:
2019-10-26 02:06:50 +11:00
_playback_triage(plex_id, plex_type, path, resolve, resume)
finally:
# Reset some playback variables the user potentially set to init
# playback
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
2019-10-26 02:06:50 +11:00
def _playback_triage(plex_id, plex_type, path, resolve, resume):
plex_id = utils.cast(int, plex_id)
2019-10-26 02:06:50 +11:00
LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
global RESOLVE
# If started via Kodi context menu, we never resolve
2018-11-19 00:59:17 +11:00
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
if not app.CONN.online or not app.ACCOUNT.authenticated:
if not app.CONN.online:
LOG.error('PMS not online for playback')
# "{0} offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(app.CONN.server_name),
icon='{plex}')
else:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
_ensure_resolve(abort=True)
2018-01-21 23:42:22 +11:00
return
2018-11-19 00:59:17 +11:00
with app.APP.lock_playqueues:
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# add-on paths
2019-10-26 02:06:50 +11:00
LOG.debug('No position returned from player! Assuming playlist')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
2019-10-26 02:06:50 +11:00
LOG.debug('Assuming video instead of audio playlist playback')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
LOG.error('Still no position - abort')
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
# HACK to detect playback of playlists for add-on paths
items = js.playlist_get_items(playqueue.playlistid)
try:
item = items[pos]
except IndexError:
2019-10-26 02:06:50 +11:00
LOG.debug('Could not apply playlist hack! Probably Widget playback')
else:
if ('id' not in item and
item.get('type') == 'unknown' and item.get('title') == ''):
2019-10-26 02:06:50 +11:00
LOG.debug('Kodi playlist play detected')
_playlist_playback(plex_id)
return
# Can return -1 (as in "no playlist")
pos = pos if pos != -1 else 0
LOG.debug('playQueue position %s for %s', pos, playqueue)
# Have we already initiated playback?
try:
item = playqueue.items[pos]
except IndexError:
LOG.debug('PKC playqueue yet empty, need to initialize playback')
2018-06-22 03:24:37 +10:00
initiate = True
else:
if item.plex_id != plex_id:
2019-10-26 02:06:50 +11:00
LOG.debug('Received new plex_id%s, expected %s',
plex_id, item.plex_id)
initiate = True
else:
initiate = False
2018-10-06 21:30:43 +10:00
if initiate:
2019-10-26 02:06:50 +11:00
_playback_init(plex_id, plex_type, playqueue, pos, resume)
2018-10-06 21:30:43 +10:00
else:
2019-10-26 02:06:50 +11:00
# kick off playback on second pass, resume was already set on first
# pass (threaded_playback will seek to resume)
2018-10-06 21:30:43 +10:00
_conclude_playback(playqueue, pos)
2019-10-26 02:06:50 +11:00
def _playlist_playback(plex_id):
"""
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
where, causing Playlist.onAdd to fire for each item like this:
Playlist.OnAdd Data: {u'item': {u'type': u'episode', u'id': 164},
u'playlistid': 0,
u'position': 2}
2018-06-15 00:27:13 +10:00
This does NOT work for Addon paths, type and id will be unknown:
{u'item': {u'type': u'unknown'},
u'playlistid': 0,
u'position': 7}
At the end, only the element being played actually shows up in the Kodi
playqueue.
Hence: if we fail the first addon paths call, Kodi will start playback
for the next item in line :-)
2018-06-15 00:27:13 +10:00
(by the way: trying to get active Kodi player id will return [])
"""
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
_ensure_resolve(abort=True)
return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
# has actually started. Need to tell Kodimonitor
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
playqueue.clear(kodi=False)
# Set the flag for the potentially WRONG audio playlist so Kodimonitor
# can pick up on it
playqueue.kodi_playlist_playback = True
playlist_item = PL.playlist_item_from_xml(xml[0])
playqueue.items.append(playlist_item)
_conclude_playback(playqueue, pos=0)
2018-01-11 06:14:05 +11:00
def _playback_init(plex_id, plex_type, playqueue, pos, resume):
2018-01-11 06:14:05 +11:00
"""
2018-01-21 23:42:22 +11:00
Playback setup if Kodi starts playing an item for the first time.
2018-01-08 03:50:30 +11:00
"""
2019-10-26 02:06:50 +11:00
LOG.debug('Initializing PKC playback')
# Stop playback so we don't get an error message that the last item of the
# queue failed to play
app.APP.player.stop()
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
2018-01-11 06:14:05 +11:00
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
_ensure_resolve(abort=True)
2018-01-11 06:14:05 +11:00
return
if (xbmc.getCondVisibility('Window.IsVisible(Home.xml)') and
plex_type in v.PLEX_VIDEOTYPES and
playqueue.kodi_pl.size() > 1):
# playqueue.kodi_pl.size() could return more than one - since playback
# was initiated from the audio queue!
LOG.debug('Detected widget playback for videos')
elif playqueue.kodi_pl.size() > 1:
# Special case - we already got a filled Kodi playqueue
try:
_init_existing_kodi_playlist(playqueue, pos)
except PL.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed')
_ensure_resolve(abort=True)
return
# Now we need to use setResolvedUrl for the item at position ZERO
# playqueue.py will pick up the missing items
_conclude_playback(playqueue, 0)
return
# "Usual" case - consider trailers and parts and build both Kodi and Plex
# playqueues
# Release default.py
2018-02-23 23:23:49 +11:00
_ensure_resolve()
api = API(xml[0])
if (app.PLAYSTATE.context_menu_play and
api.resume_point() and
api.plex_type in v.PLEX_VIDEOTYPES):
# User chose to either play via PMS or to force transcode
# Need to prompt whether we should resume_playback
resume = resume_dialog(int(api.resume_point()))
if resume is None:
# User cancelled dialog
return
LOG.debug('Using resume %s', resume)
resume = resume or False
trailers = False
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
2018-06-22 03:24:37 +10:00
utils.settings('enableCinema') == "true"):
if utils.settings('askCinema') == "true":
2018-01-11 06:14:05 +11:00
# "Play trailers?"
2018-09-19 00:26:40 +10:00
trailers = utils.yesno_dialog(utils.lang(29999), utils.lang(33016))
2018-01-08 03:50:30 +11:00
else:
trailers = True
2019-06-02 21:30:23 +10:00
LOG.debug('Resuming: %s. Playing trailers: %s', resume, trailers)
2018-01-11 06:14:05 +11:00
playqueue.clear()
2018-02-07 06:12:44 +11:00
if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion
xml = PF.init_plex_playqueue(plex_id,
plex_type,
xml.get('librarySectionUUID'),
trailers=trailers)
2018-02-07 06:12:44 +11:00
if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s', plex_id)
2018-02-07 06:12:44 +11:00
# "Play error"
2018-06-22 03:24:37 +10:00
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
# Do NOT use _ensure_resolve() because we resolved above already
2018-02-07 06:12:44 +11:00
return
PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml, resume)
_process_stack(playqueue, stack)
2019-10-26 02:06:50 +11:00
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)
thread = Thread(target=threaded_playback,
2019-10-26 02:06:50 +11:00
args=(playqueue.kodi_pl, pos, offset))
thread.setDaemon(True)
2019-10-26 02:06:50 +11:00
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
# caches paths like our plugin://pkc. If we use Player().play() between
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
# cache will have been flushed for some reason. Hence the 2nd call for
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
thread.start()
2018-01-08 03:50:30 +11:00
def _ensure_resolve(abort=False):
"""
Will check whether RESOLVE=True and if so, fail Kodi playback startup
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
pickling)
This way we're making sure that other Python instances (calling default.py)
will be destroyed.
"""
if RESOLVE:
# Releases the other Python thread without a ListItem
transfer.send(True)
# Wait for default.py to have completed xbmcplugin.setResolvedUrl()
transfer.wait_for_transfer(source='default')
if abort:
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30128),
icon='{error}',
time=3000)
def resume_dialog(resume):
"""
Pass the resume [int] point in seconds. Returns True if user chose to
resume. Returns None if user cancelled
"""
# "Resume from {0:s}"
# "Start from beginning"
resume = datetime.timedelta(seconds=resume)
LOG.debug('Showing PKC resume dialog for resume: %s', resume)
answ = utils.dialog('contextmenu',
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
utils.lang(12021)])
if answ == -1:
return
return answ == 0
def _init_existing_kodi_playlist(playqueue, pos):
"""
Will take the playqueue's kodi_pl with MORE than 1 element and initiate
playback (without adding trailers)
"""
LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size())
kodi_items = js.playlist_get_items(playqueue.playlistid)
if not kodi_items:
2018-05-15 04:43:48 +10:00
LOG.error('No Kodi items returned')
raise PL.PlaylistError('No Kodi items returned')
2018-05-02 02:08:31 +10:00
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
2018-11-19 00:59:17 +11:00
item.force_transcode = app.PLAYSTATE.force_transcode
# playqueue.py will add the rest - this will likely put the PMS under
# a LOT of strain if the following Kodi setting is enabled:
# Settings -> Player -> Videos -> Play next video automatically
LOG.debug('Done init_existing_kodi_playlist')
def _prep_playlist_stack(xml, resume):
"""
resume [bool] will set the resume point of the LAST item of the stack, for
part 1 only
"""
2018-01-11 06:14:05 +11:00
stack = []
for i, item in enumerate(xml):
2018-01-11 06:14:05 +11:00
api = API(item)
2018-11-19 00:59:17 +11:00
if (app.PLAYSTATE.context_menu_play is False and
2019-06-11 05:29:42 +10:00
api.plex_type not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
2018-02-03 22:45:48 +11:00
# If user chose to play via PMS or force transcode, do not
# use the item path stored in the Kodi DB
with PlexDB(lock=False) as plexdb:
2019-06-11 05:29:42 +10:00
db_item = plexdb.item_by_id(api.plex_id, api.plex_type)
2018-10-25 02:17:02 +11:00
kodi_id = db_item['kodi_id'] if db_item else None
kodi_type = db_item['kodi_type'] if db_item else None
2018-01-21 23:42:22 +11:00
else:
# We will never store clips (trailers) in the Kodi DB.
# Also set kodi_id to None for playback via PMS, so that we're
# using add-on paths.
# Also do NOT associate episodes with library items for addon paths
# as artwork lookup is broken (episode path does not link back to
# season and show)
2018-01-11 06:14:05 +11:00
kodi_id = None
kodi_type = None
2018-01-21 23:42:22 +11:00
for part, _ in enumerate(item[0]):
2019-06-11 05:29:42 +10:00
api.part = part
2018-01-21 23:42:22 +11:00
if kodi_id is None:
2018-01-11 06:14:05 +11:00
# Need to redirect again to PKC to conclude playback
path = api.path(force_addon=True, force_first_media=True)
# Using different paths than the ones saved in the Kodi DB
# fixes Kodi immediately resuming the video if one restarts
# the same video again after playback
# WARNING: This fixes startup, but renders Kodi unstable
# path = path.replace('plugin.video.plexkodiconnect.tvshows',
# 'plugin.video.plexkodiconnect', 1)
# path = path.replace('plugin.video.plexkodiconnect.movies',
# 'plugin.video.plexkodiconnect', 1)
2019-06-11 05:29:42 +10:00
listitem = api.listitem()
listitem.setPath(path.encode('utf-8'))
2018-01-21 23:42:22 +11:00
else:
# Will add directly via the Kodi DB
path = None
listitem = None
2018-01-11 06:14:05 +11:00
stack.append({
'kodi_id': kodi_id,
'kodi_type': kodi_type,
'file': path,
'xml_video_element': item,
'listitem': listitem,
2018-01-22 04:31:49 +11:00
'part': part,
2018-02-12 00:42:49 +11:00
'playcount': api.viewcount(),
'offset': api.resume_point(),
2019-10-26 02:06:50 +11:00
'resume': resume if part == 0 and i + 1 == len(xml) else None,
2018-02-12 00:42:49 +11:00
'id': api.item_id()
2018-01-11 06:14:05 +11:00
})
return stack
def _process_stack(playqueue, stack):
2018-01-11 06:14:05 +11:00
"""
Takes our stack and adds the items to the PKC and Kodi playqueues.
"""
2018-01-21 23:42:22 +11:00
# getposition() can return -1
pos = max(playqueue.kodi_pl.getposition(), 0) + 1
for item in stack:
if item['kodi_id'] is None:
2018-01-11 06:14:05 +11:00
playlist_item = PL.add_listitem_to_Kodi_playlist(
playqueue,
2018-01-21 23:42:22 +11:00
pos,
2018-01-11 06:14:05 +11:00
item['listitem'],
file=item['file'],
xml_video_element=item['xml_video_element'])
2018-01-21 23:42:22 +11:00
else:
# Directly add element so we have full metadata
playlist_item = PL.add_item_to_kodi_playlist(
playqueue,
pos,
kodi_id=item['kodi_id'],
kodi_type=item['kodi_type'],
xml_video_element=item['xml_video_element'])
2018-01-22 04:31:49 +11:00
playlist_item.playcount = item['playcount']
playlist_item.offset = item['offset']
2018-01-21 23:42:22 +11:00
playlist_item.part = item['part']
playlist_item.id = item['id']
2018-11-19 00:59:17 +11:00
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
playlist_item.resume = item['resume']
2018-01-21 23:42:22 +11:00
pos += 1
2018-01-11 06:14:05 +11:00
2019-10-26 02:06:50 +11:00
def _use_kodi_db_offset(plex_id, plex_type, plex_offset):
"""
Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
have gotten the last resume point)
"""
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
return plex_offset
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item:
with KodiVideoDB(lock=False) as kodidb:
return kodidb.get_resume(db_item['kodi_fileid'])
else:
return plex_offset
def _conclude_playback(playqueue, pos):
2018-01-08 03:50:30 +11:00
"""
ONLY if actually being played (e.g. at 5th position of a playqueue).
Decide on direct play, direct stream, transcoding
path to
direct paths: file itself
PMS URL
Web URL
audiostream (e.g. let user choose)
subtitle stream (e.g. let user choose)
Init Kodi Playback (depending on situation):
start playback
return PKC listitem attached to result
"""
2019-10-26 02:06:50 +11:00
LOG.debug('Concluding playback for playqueue position %s', pos)
2018-01-08 03:50:30 +11:00
item = playqueue.items[pos]
2019-10-26 02:06:50 +11:00
api = API(item.xml)
api.part = item.part or 0
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
set_playurl(api, item)
if not item.file:
2019-10-26 02:06:50 +11:00
LOG.debug('Did not get a playurl, aborting playback silently')
_ensure_resolve()
return
listitem.setPath(item.file.encode('utf-8'))
if item.playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY:
2018-02-12 00:42:49 +11:00
listitem.setSubtitles(api.cache_external_subs())
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
v.PLAYBACK_METHOD_TRANSCODE):
2019-10-05 20:44:40 +10:00
audio_subtitle_prefs(api, listitem)
transfer.send(listitem)
2019-10-26 02:06:50 +11:00
LOG.debug('Done concluding playback')
def process_indirect(key, offset, resolve=True):
"""
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
set.
Will release default.py with setResolvedUrl
Set resolve to False if playback should be kicked off directly, not via
setResolvedUrl
"""
2019-10-26 02:06:50 +11:00
LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
if key.startswith('http') or key.startswith('{server}'):
xml = PF.get_playback_xml(key, app.CONN.server_name)
elif key.startswith('/system/services'):
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
else:
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
if xml is None:
_ensure_resolve(abort=True)
return
api = API(xml[0])
2019-10-26 02:06:50 +11:00
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
playqueue = PQ.get_playqueue_from_type(
2019-06-11 05:29:42 +10:00
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear()
2019-08-09 23:39:23 +10:00
item = PL.playlist_item_from_xml(xml[0])
item.offset = offset
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
# Need to get yet another xml to get the final playback url
try:
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'],
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
except (TypeError, IndexError, AttributeError):
LOG.error('XML malformed: %s', xml.attrib)
xml = None
if xml is None:
_ensure_resolve(abort=True)
return
try:
playurl = xml[0].attrib['key']
except (TypeError, IndexError, AttributeError):
LOG.error('Last xml malformed: %s', xml.attrib)
_ensure_resolve(abort=True)
return
item.file = playurl
2018-06-22 03:24:37 +10:00
listitem.setPath(utils.try_encode(playurl))
playqueue.items.append(item)
if resolve is True:
transfer.send(listitem)
else:
2018-11-23 18:41:05 +11:00
thread = Thread(target=app.APP.player.play,
2018-06-22 03:24:37 +10:00
args={'item': utils.try_encode(playurl),
2018-02-03 22:45:48 +11:00
'listitem': listitem})
thread.setDaemon(True)
2019-10-26 02:06:50 +11:00
LOG.debug('Done initializing PKC playback, starting Kodi player')
thread.start()
2018-02-08 21:16:39 +11:00
def play_xml(playqueue, xml, offset=None, start_plex_id=None):
"""
Play all items contained in the xml passed in. Called by Plex Companion.
2018-02-08 21:16:39 +11:00
Either supply the ratingKey of the starting Plex element. Or set
playqueue.selectedItemID
"""
2019-10-29 04:01:49 +11:00
offset = int(offset) / 1000 if offset else None
LOG.debug("play_xml called with offset %s, start_plex_id %s",
offset, start_plex_id)
start_item = start_plex_id if start_plex_id is not None \
else playqueue.selectedItemID
for startpos, video in enumerate(xml):
api = API(video)
2019-06-11 05:29:42 +10:00
if api.plex_id == start_item:
break
else:
startpos = 0
stack = _prep_playlist_stack(xml, resume=False)
if offset:
stack[startpos]['resume'] = True
_process_stack(playqueue, stack)
LOG.debug('Playqueue after play_xml update: %s', playqueue)
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, startpos, offset))
2019-10-26 02:06:50 +11:00
LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
thread.start()
def threaded_playback(kodi_playlist, startpos, offset):
"""
2019-10-26 02:06:50 +11:00
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]
"""
2019-10-26 02:06:50 +11:00
LOG.debug('threaded_playback with startpos %s, offset %s',
startpos, offset)
2018-11-23 18:41:05 +11:00
app.APP.player.play(kodi_playlist, None, False, startpos)
offset = offset if offset else 0
i = 0
while not app.APP.is_playing or not js.get_player_ids():
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
i += 1
if i > TRY_TO_SEEK_FOR:
LOG.error('Could not seek to %s', offset)
return
try:
if offset == 0 and app.APP.player.getTime() < IGNORE_SECONDS_AT_START:
LOG.debug('Avoiding small jump to the very start of the video')
return
except RuntimeError:
# RuntimeError: XBMC is not playing any media file
pass
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
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
i += 1
if i > TRY_TO_SEEK_FOR:
LOG.error('Failed to seek to %s. Error: %s', offset, answ)
return
2019-10-26 02:06:50 +11:00
answ = js.seek_to(offset * 1000)
LOG.debug('Seek to offset %s successful', offset)