Skip to content

Commit

Permalink
Switch to stream playback, part I
Browse files Browse the repository at this point in the history
  • Loading branch information
croneter committed Mar 25, 2019
1 parent 8d8b631 commit d60c72c
Show file tree
Hide file tree
Showing 7 changed files with 610 additions and 4 deletions.
3 changes: 2 additions & 1 deletion resources/lib/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
# Need to chain keys for navigation
widgets.KEY = key
# Process all items to show
widgets.attach_kodi_ids(xml)
if synched:
widgets.attach_kodi_ids(xml)
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
all_items)
Expand Down
255 changes: 255 additions & 0 deletions resources/lib/playstrm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import urllib

import xbmc
import xbmcgui

from .plex_api import API
from . import plex_function as PF, utils, json_rpc, variables as v, \
widgets


LOG = getLogger('PLEX.playstrm')


class PlayStrmException(Exception):
"""
Any Exception associated with playstrm
"""
pass


class PlayStrm(object):
'''
Workflow: Strm that calls our webservice in database. When played, the
webserivce returns a dummy file to play. Meanwhile, PlayStrm adds the real
listitems for items to play to the playlist.
'''
def __init__(self, params, server_id=None):
LOG.debug('Starting PlayStrm with server_id %s, params: %s',
server_id, params)
self.xml = None
self.api = None
self.start_index = None
self.index = None
self.server_id = server_id
self.plex_id = utils.cast(int, params['plex_id'])
self.plex_type = params.get('plex_type')
if params.get('synched') and params['synched'].lower() == 'false':
self.synched = False
else:
self.synched = True
self._get_xml()
self.name = self.api.title()
self.kodi_id = utils.cast(int, params.get('kodi_id'))
self.kodi_type = params.get('kodi_type')
if ((self.kodi_id is None or self.kodi_type is None) and
self.xml[0].get('pkc_db_item')):
self.kodi_id = self.xml[0].get('pkc_db_item')['kodi_id']
self.kodi_type = self.xml[0].get('pkc_db_item')['kodi_type']
self.transcode = params.get('transcode')
if self.transcode is None:
self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None
if utils.window('plex.playlist.audio.bool'):
LOG.info('Audio playlist detected')
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
else:
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)

def __repr__(self):
return ("{{"
"'name': '{self.name}', "
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"'server_id': '{self.server_id}', "
"'transcode': {self.transcode}, "
"'start_index': {self.start_index}, "
"'index': {self.index}"
"}}").format(self=self).encode('utf-8')
__str__ = __repr__

def add_to_playlist(self, kodi_id, kodi_type, index=None, playlistid=None):
playlistid = playlistid or self.kodi_playlist.getPlayListId()
LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s',
kodi_id, kodi_type, playlistid, index)
if index is None:
json_rpc.playlist_add(playlistid, {'%sid' % kodi_type: kodi_id})
else:
json_rpc.playlist_insert({'playlistid': playlistid,
'position': index,
'item': {'%sid' % kodi_type: kodi_id}})

def remove_from_playlist(self, index):
LOG.debug('Removing playlist item number %s from %s', index, self)
json_rpc.playlist_remove(self.kodi_playlist.getPlayListId(),
index)

def _get_xml(self):
self.xml = PF.GetPlexMetadata(self.plex_id)
if self.xml in (None, 401):
raise PlayStrmException('No xml received from the PMS')
if self.synched:
# Adds a new key 'pkc_db_item' to self.xml[0].attrib
widgets.attach_kodi_ids(self.xml)
else:
self.xml[0].set('pkc_db_item', None)
self.api = API(self.xml[0])

def start_playback(self, index=0):
LOG.debug('Starting playback at %s', index)
xbmc.Player().play(self.kodi_playlist, startpos=index, windowed=False)

def play(self, start_position=None, delayed=True):
'''
Create and add listitems to the Kodi playlist.
'''
if start_position is not None:
self.start_index = start_position
else:
self.start_index = max(self.kodi_playlist.getposition(), 0)
self.index = self.start_index
listitem = xbmcgui.ListItem()
self._set_playlist(listitem)
LOG.info('Initiating play for %s', self)
if not delayed:
self.start_playback(self.start_index)
return self.start_index

def play_folder(self, position=None):
'''
When an entire queue is requested, If requested from Kodi, kodi_type is
provided, add as Kodi would, otherwise queue playlist items using strm
links to setup playback later.
'''
self.start_index = position or max(self.kodi_playlist.size(), 0)
self.index = self.start_index + 1
LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index)
if self.kodi_id and self.kodi_type:
self.add_to_playlist(self.kodi_id, self.kodi_type, self.index)
else:
listitem = widgets.get_listitem(self.xml[0])
url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT
args = {
'mode': 'play',
'plex_id': self.plex_id,
'plex_type': self.api.plex_type()
}
if self.kodi_id:
args['kodi_id'] = self.kodi_id
if self.kodi_type:
args['kodi_type'] = self.kodi_type
if self.server_id:
args['server_id'] = self.server_id
if self.transcode:
args['transcode'] = True
url = '%s?%s' % (url, urllib.urlencode(args))
listitem.setPath(url)
self.kodi_playlist.add(url=url,
listitem=listitem,
index=self.index)
return self.index

def _set_playlist(self, listitem):
'''
Verify seektime, set intros, set main item and set additional parts.
Detect the seektime for video type content. Verify the default video
action set in Kodi for accurate resume behavior.
'''
seektime = self._resume()
if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and
utils.settings('enableCinema') == 'true'):
self._set_intros()

play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server'])
source = play.select_source(play.get_sources())

if not source:
raise PlayStrmException('Playback selection cancelled')

play.set_external_subs(source, listitem)
self.set_listitem(self.xml, listitem, self.kodi_id, seektime)
listitem.setPath(self.xml['PlaybackInfo']['Path'])
playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id)

self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
self.index += 1

if self.xml.get('PartCount'):
self._set_additional_parts()

def _resume(self):
'''
Resume item if available. Returns bool or raise an PlayStrmException if
resume was cancelled by user.
'''
seektime = utils.window('plex.resume')
utils.window('plex.resume', clear=True)
seektime = seektime == 'true' if seektime else None
auto_play = utils.window('plex.autoplay.bool')
if auto_play:
seektime = False
LOG.info('Skip resume for autoplay')
elif seektime is None:
resume = self.api.resume_point()
if resume:
seektime = resume_dialog(resume)
LOG.info('Resume: %s', seektime)
if seektime is None:
raise PlayStrmException('User backed out of resume dialog.')
# Todo: Probably need to have a look here
utils.window('plex.autoplay.bool', value='true')
return seektime

def _set_intros(self):
'''
if we have any play them when the movie/show is not being resumed.
'''
if self.info['Intros']['Items']:
enabled = True

if utils.settings('askCinema') == 'true':

resp = dialog('yesno', heading='{emby}', line1=_(33016))
if not resp:

enabled = False
LOG.info('Skip trailers.')

if enabled:
for intro in self.info['Intros']['Items']:

listitem = xbmcgui.ListItem()
LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name'])

play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['Server'])
source = play.select_source(play.get_sources())
self.set_listitem(intro, listitem, intro=True)
listitem.setPath(intro['PlaybackInfo']['Path'])
playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id)

self.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
self.index += 1

utils.window('plex.skip.%s' % intro['plex_id'], value='true')

def _set_additional_parts(self):
''' Create listitems and add them to the stack of playlist.
'''
for part in self.info['AdditionalParts']['Items']:

listitem = xbmcgui.ListItem()
LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name'])

play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server'])
source = play.select_source(play.get_sources())
play.set_external_subs(source, listitem)
self.set_listitem(part, listitem)
listitem.setPath(part['PlaybackInfo']['Path'])
playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id)

self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
self.index += 1
15 changes: 15 additions & 0 deletions resources/lib/plex_db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@

class PlexDB(PlexDBBase, TVShows, Movies, Music, Playlists, Sections):
pass


def kodi_from_plex(plex_id, plex_type=None):
"""
Returns the tuple (kodi_id, kodi_type) for plex_id. Faster, if plex_type
is provided
Returns (None, None) if unsuccessful
"""
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item:
return (db_item['kodi_id'], db_item['kodi_type'])
else:
return None, None
9 changes: 9 additions & 0 deletions resources/lib/service_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import kodimonitor
from . import sync, library_sync
from . import websocket_client
from . import webservice
from . import plex_companion
from . import plex_functions as PF, playqueue as PQ
from . import playback_starter
Expand Down Expand Up @@ -403,6 +404,7 @@ def ServiceEntryPoint(self):
self.setup.setup()

# Initialize important threads
self.webservice = webservice.WebService()
self.ws = websocket_client.PMS_Websocket()
self.alexa = websocket_client.Alexa_Websocket()
self.sync = sync.Sync()
Expand Down Expand Up @@ -462,6 +464,12 @@ def ServiceEntryPoint(self):
xbmc.sleep(100)
continue

if self.webservice is not None and not self.webservice.is_alive():
LOG.info('Restarting webservice')
self.webservice.abort()
self.webservice = webservice.WebService()
self.webservice.start()

# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
Expand Down Expand Up @@ -491,6 +499,7 @@ def ServiceEntryPoint(self):
continue
elif not self.startup_completed:
self.startup_completed = True
self.webservice.start()
self.ws.start()
self.sync.start()
self.plexcompanion.start()
Expand Down
3 changes: 3 additions & 0 deletions resources/lib/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ def try_decode(string, encoding='utf-8'):

COMPANION_PORT = int(_ADDON.getSetting('companionPort'))

# Port for the PKC webservice
WEBSERVICE_PORT = 57578

# Unique ID for this Plex client; also see clientinfo.py
PKC_MACHINE_IDENTIFIER = None

Expand Down

0 comments on commit d60c72c

Please sign in to comment.