PlexKodiConnect/resources/lib/playback.py

403 lines
15 KiB
Python
Raw Normal View History

2018-01-08 03:50:30 +11:00
"""
Used to kick off Kodi playback
"""
2018-01-11 06:14:05 +11:00
from logging import getLogger
2018-01-21 23:42:22 +11:00
from threading import Thread
2018-01-11 06:14:05 +11:00
from urllib import urlencode
2018-01-21 23:42:22 +11:00
from xbmc import Player, sleep
2018-01-11 06:14:05 +11:00
2018-01-08 03:50:30 +11:00
from PlexAPI import API
2018-01-11 06:14:05 +11:00
from PlexFunctions import GetPlexMetadata, init_plex_playqueue
2018-01-29 03:21:28 +11:00
from downloadutils import DownloadUtils as DU
2018-01-11 06:14:05 +11:00
import plexdb_functions as plexdb
import playlist_func as PL
2018-01-08 03:50:30 +11:00
import playqueue as PQ
from playutils import PlayUtils
2018-01-11 06:14:05 +11:00
from PKC_listitem import PKC_ListItem
from pickler import pickle_me, Playback_Successful
import json_rpc as js
2018-02-03 22:45:48 +11:00
from utils import settings, dialog, language as lang, tryEncode
2018-01-21 23:42:22 +11:00
from plexbmchelper.subscribers import LOCKER
2018-01-11 06:14:05 +11:00
import variables as v
import state
###############################################################################
2018-01-08 03:50:30 +11:00
2018-01-11 06:14:05 +11:00
LOG = getLogger("PLEX." + __name__)
2018-01-08 03:50:30 +11:00
2018-01-11 06:14:05 +11:00
###############################################################################
@LOCKER.lockthis
2018-01-26 03:15:38 +11:00
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
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-01-21 23:42:22 +11:00
Will set Playback_Successful() with potentially a PKC_ListItem() attached
(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
"""
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s',
plex_id, plex_type, path)
if not state.AUTHENTICATED:
LOG.error('Not yet authenticated for PMS, abort starting playback')
2018-01-28 23:26:25 +11:00
if resolve is True:
# Release default.py
pickle_me(Playback_Successful())
2018-01-11 06:14:05 +11:00
# "Unauthorized for PMS"
dialog('notification', lang(29999), lang(30017))
2018-01-21 23:42:22 +11:00
return
2018-01-11 06:14:05 +11:00
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
pos = js.get_position(playqueue.playlistid)
2018-01-21 23:42:22 +11:00
# Can return -1 (as in "no playlist")
2018-01-11 06:14:05 +11:00
pos = pos if pos != -1 else 0
2018-02-03 23:54:39 +11:00
LOG.debug('playQueue position: %s for %s', pos, playqueue)
2018-01-11 06:14:05 +11:00
# Have we already initiated playback?
try:
2018-01-21 23:42:22 +11:00
playqueue.items[pos]
2018-01-11 06:14:05 +11:00
except IndexError:
# Release our default.py before starting our own Kodi player instance
2018-01-26 03:15:38 +11:00
if resolve is True:
result = Playback_Successful()
2018-01-28 23:24:42 +11:00
result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails')
2018-01-26 03:15:38 +11:00
pickle_me(result)
2018-01-21 23:42:22 +11:00
playback_init(plex_id, plex_type, playqueue)
2018-01-11 06:14:05 +11:00
else:
2018-01-21 23:42:22 +11:00
# kick off playback on second pass
2018-01-11 06:14:05 +11:00
conclude_playback(playqueue, pos)
2018-01-22 21:20:37 +11:00
def play_resume(playqueue, xml, stack):
"""
If there exists a resume point, Kodi will ask the user whether to continue
playback. We thus need to use setResolvedUrl "correctly". Mind that there
might be several parts!
"""
result = Playback_Successful()
listitem = PKC_ListItem()
# Only get the very first item of our playqueue (i.e. the very first part)
stack_item = stack.pop(0)
api = API(xml[0])
item = PL.playlist_item_from_xml(playqueue,
xml[0],
kodi_id=stack_item['kodi_id'],
kodi_type=stack_item['kodi_type'])
api.setPartNumber(item.part)
item.playcount = stack_item['playcount']
item.offset = stack_item['offset']
item.part = stack_item['part']
item.init_done = True
api.CreateListItemFromPlexItem(listitem)
playutils = PlayUtils(api, item)
playurl = playutils.getPlayUrl()
2018-01-29 01:24:41 +11:00
listitem.setPath(tryEncode(playurl))
2018-01-22 21:20:37 +11:00
if item.playmethod in ('DirectStream', 'DirectPlay'):
listitem.setSubtitles(api.externalSubs())
else:
playutils.audio_subtitle_prefs(listitem)
result.listitem = listitem
# Add to our playlist
playqueue.items.append(item)
# This will release default.py with setResolvedUrl
pickle_me(result)
# Add remaining parts to the playlist, if any
if stack:
_process_stack(playqueue, stack)
2018-01-21 23:42:22 +11:00
def playback_init(plex_id, plex_type, playqueue):
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
"""
2018-01-21 23:42:22 +11:00
LOG.info('Initializing PKC playback')
2018-01-11 06:14:05 +11:00
xml = GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
2018-01-21 23:42:22 +11:00
dialog('notification', lang(29999), lang(30128), icon='{error}')
2018-01-11 06:14:05 +11:00
return
2018-01-08 03:50:30 +11:00
trailers = False
api = API(xml[0])
if (plex_type == v.PLEX_TYPE_MOVIE and not api.getResume() and
2018-01-08 03:50:30 +11:00
settings('enableCinema') == "true"):
if settings('askCinema') == "true":
2018-01-11 06:14:05 +11:00
# "Play trailers?"
trailers = dialog('yesno', lang(29999), lang(33016))
2018-01-08 03:50:30 +11:00
trailers = True if trailers else False
else:
trailers = True
2018-01-21 23:42:22 +11:00
LOG.info('Playing trailers: %s', trailers)
# Post to the PMS to create a playqueue - in any case due to Plex Companion
2018-01-08 03:50:30 +11:00
xml = init_plex_playqueue(plex_id,
2018-01-11 06:14:05 +11:00
xml.attrib.get('librarySectionUUID'),
mediatype=plex_type,
2018-01-08 03:50:30 +11:00
trailers=trailers)
2018-01-11 06:14:05 +11:00
if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
plex_id, xml.attrib.get('librarySectionUUID'))
# "Play error"
2018-01-21 23:42:22 +11:00
dialog('notification', lang(29999), lang(30128), icon='{error}')
2018-01-11 06:14:05 +11:00
return
2018-01-21 23:42:22 +11:00
# Should already be empty, but just in case
2018-01-11 06:14:05 +11:00
playqueue.clear()
PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml)
2018-01-21 23:42:22 +11:00
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
2018-01-28 23:31:35 +11:00
sleep(200)
2018-01-21 23:42:22 +11:00
_process_stack(playqueue, stack)
2018-02-03 22:45:48 +11:00
# Reset some playback variables
state.CONTEXT_MENU_PLAY = False
state.FORCE_TRANSCODE = False
2018-01-21 23:42:22 +11:00
# New thread to release this one sooner (e.g. harddisk spinning up)
thread = Thread(target=Player().play,
args=(playqueue.kodi_pl, ))
thread.setDaemon(True)
LOG.info('Done initializing PKC playback, starting Kodi player')
# 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>"
2018-01-21 23:42:22 +11:00
thread.start()
2018-01-08 03:50:30 +11:00
2018-01-11 06:14:05 +11:00
def _prep_playlist_stack(xml):
stack = []
for item in xml:
api = API(item)
2018-02-04 00:04:17 +11:00
if (state.CONTEXT_MENU_PLAY is False and
2018-02-03 22:45:48 +11:00
api.getType() != v.PLEX_TYPE_CLIP):
# If user chose to play via PMS or force transcode, do not
# use the item path stored in the Kodi DB
2018-01-21 23:42:22 +11:00
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byId(api.getRatingKey())
kodi_id = plex_dbitem[0] if plex_dbitem else None
kodi_type = plex_dbitem[4] if plex_dbitem else None
else:
# We will never store clips (trailers) in the Kodi DB
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]):
api.setPartNumber(part)
if kodi_id is None:
2018-01-11 06:14:05 +11:00
# Need to redirect again to PKC to conclude playback
params = {
'mode': 'play',
'plex_id': api.getRatingKey(),
'plex_type': api.getType()
}
path = ('plugin://plugin.video.plexkodiconnect?%s'
2018-01-21 23:42:22 +11:00
% urlencode(params))
2018-01-11 06:14:05 +11:00
listitem = api.CreateListItemFromPlexItem()
2018-01-29 01:24:41 +11:00
listitem.setPath(tryEncode(path))
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,
'playcount': api.getViewCount(),
'offset': api.getResume(),
'id': api.getItemId()
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-02-03 22:45:48 +11:00
playlist_item.force_transcode = state.FORCE_TRANSCODE
2018-01-11 06:14:05 +11:00
playlist_item.init_done = True
2018-01-21 23:42:22 +11:00
pos += 1
2018-01-11 06:14:05 +11:00
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
"""
2018-01-22 21:20:37 +11:00
LOG.info('Concluding playback for playqueue position %s', pos)
2018-01-08 03:50:30 +11:00
result = Playback_Successful()
listitem = PKC_ListItem()
item = playqueue.items[pos]
2018-01-11 06:14:05 +11:00
if item.xml is not None:
# Got a Plex element
api = API(item.xml)
api.setPartNumber(item.part)
api.CreateListItemFromPlexItem(listitem)
2018-01-08 03:50:30 +11:00
playutils = PlayUtils(api, item)
playurl = playutils.getPlayUrl()
2018-01-11 06:14:05 +11:00
else:
playurl = item.file
2018-01-29 01:24:41 +11:00
listitem.setPath(tryEncode(playurl))
2018-01-22 21:20:37 +11:00
if item.playmethod in ('DirectStream', 'DirectPlay'):
2018-01-08 03:50:30 +11:00
listitem.setSubtitles(api.externalSubs())
else:
playutils.audio_subtitle_prefs(listitem)
2018-01-22 21:20:37 +11:00
if state.RESUME_PLAYBACK is True:
state.RESUME_PLAYBACK = False
LOG.info('Resuming playback at %s', item.offset)
listitem.setProperty('StartOffset', str(item.offset))
listitem.setProperty('resumetime', str(item.offset))
# Reset the resumable flag
state.RESUMABLE = False
2018-01-08 03:50:30 +11:00
result.listitem = listitem
2018-01-11 06:14:05 +11:00
pickle_me(result)
2018-01-22 21:20:37 +11:00
LOG.info('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
"""
LOG.info('process_indirect called with key: %s, offset: %s', key, offset)
result = Playback_Successful()
if key.startswith('http') or key.startswith('{server}'):
xml = DU().downloadUrl(key)
elif key.startswith('/system/services'):
xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key)
else:
xml = DU().downloadUrl('{server}%s' % key)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download PMS metadata')
if resolve is True:
# Release default.py
pickle_me(result)
return
if offset != '0':
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
# Todo: implement offset
api = API(xml[0])
listitem = PKC_ListItem()
api.CreateListItemFromPlexItem(listitem)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
playqueue.clear()
item = PL.Playlist_Item()
item.xml = xml[0]
item.offset = int(offset)
item.plex_type = v.PLEX_TYPE_CLIP
item.playmethod = 'DirectStream'
item.init_done = True
# Need to get yet another xml to get the final playback url
xml = DU().downloadUrl('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'])
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download last xml for playurl')
if resolve is True:
# Release default.py
pickle_me(result)
return
playurl = xml[0].attrib['key']
item.file = playurl
listitem.setPath(tryEncode(playurl))
playqueue.items.append(item)
if resolve is True:
result.listitem = listitem
pickle_me(result)
else:
thread = Thread(target=Player().play,
2018-02-03 22:45:48 +11:00
args={'item': tryEncode(playurl),
'listitem': listitem})
thread.setDaemon(True)
LOG.info('Done initializing PKC playback, starting Kodi player')
thread.start()
def play_xml(playqueue, xml, offset=None):
"""
Play all items contained in the xml passed in. Called by Plex Companion.
"""
LOG.info("play_xml called")
stack = _prep_playlist_stack(xml)
_process_stack(playqueue, stack)
LOG.debug('Playqueue after play_xml update: %s', playqueue)
for startpos, item in enumerate(playqueue.items):
if item.id == playqueue.selectedItemID:
break
else:
startpos = 0
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, startpos, offset))
LOG.info('Done play_xml, starting Kodi player at position %s', startpos)
thread.start()
def threaded_playback(kodi_playlist, startpos, offset):
"""
Seek immediately after kicking off playback is not reliable.
"""
player = Player()
player.play(kodi_playlist, None, False, startpos)
2018-02-01 17:16:09 +11:00
if offset and offset != '0':
i = 0
while not player.isPlaying():
sleep(100)
i += 1
if i > 100:
LOG.error('Could not seek to %s', offset)
return
js.seek_to(int(offset))