2019-05-26 04:49:29 +10:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import absolute_import, division, unicode_literals
|
|
|
|
from logging import getLogger
|
|
|
|
import threading
|
|
|
|
|
2019-05-26 19:26:14 +10:00
|
|
|
from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError
|
2019-05-26 04:49:29 +10:00
|
|
|
|
|
|
|
from ..downloadutils import DownloadUtils as DU
|
|
|
|
from ..plex_api import API
|
|
|
|
from ..plex_db import PlexDB
|
|
|
|
from ..kodi_db import KodiVideoDB
|
|
|
|
from ..playutils import PlayUtils
|
|
|
|
from ..windows.resume import resume_dialog
|
|
|
|
from .. import plex_functions as PF, utils, widgets, variables as v, app
|
|
|
|
from .. import json_rpc as js
|
|
|
|
|
|
|
|
|
|
|
|
LOG = getLogger('PLEX.playqueue')
|
|
|
|
|
|
|
|
|
|
|
|
class PlayQueue(object):
|
|
|
|
"""
|
|
|
|
PKC object to represent PMS playQueues and Kodi playlist for queueing
|
|
|
|
|
|
|
|
playlistid = None [int] Kodi playlist id (0, 1, 2)
|
|
|
|
type = None [str] Kodi type: 'audio', 'video', 'picture'
|
|
|
|
kodi_pl = None Kodi xbmc.PlayList object
|
|
|
|
items = [] [list] of PlaylistItem
|
|
|
|
id = None [str] Plex playQueueID, unique Plex identifier
|
|
|
|
version = None [int] Plex version of the playQueue
|
|
|
|
selectedItemID = None
|
|
|
|
[str] Plex selectedItemID, playing element in queue
|
|
|
|
selectedItemOffset = None
|
|
|
|
[str] Offset of the playing element in queue
|
|
|
|
shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
|
|
|
|
repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
|
|
|
|
|
|
|
|
If Companion playback is initiated by another user:
|
|
|
|
plex_transient_token = None
|
|
|
|
"""
|
|
|
|
kind = 'playQueue'
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.id = None
|
|
|
|
self.type = None
|
|
|
|
self.playlistid = None
|
|
|
|
self.kodi_pl = None
|
|
|
|
self.items = []
|
|
|
|
self.version = None
|
|
|
|
self.selectedItemID = None
|
|
|
|
self.selectedItemOffset = None
|
|
|
|
self.shuffled = 0
|
|
|
|
self.repeat = 0
|
|
|
|
self.plex_transient_token = None
|
|
|
|
# Need a hack for detecting swaps of elements
|
|
|
|
self.old_kodi_pl = []
|
|
|
|
# Did PKC itself just change the playqueue so the PKC playqueue monitor
|
|
|
|
# should not pick up any changes?
|
|
|
|
self.pkc_edit = False
|
|
|
|
# Workaround to avoid endless loops of detecting PL clears
|
|
|
|
self._clear_list = []
|
|
|
|
# To keep track if Kodi playback was initiated from a Kodi playlist
|
|
|
|
# There are a couple of pitfalls, unfortunately...
|
|
|
|
self.kodi_playlist_playback = False
|
|
|
|
# Playlist position/index used when initiating the playqueue
|
|
|
|
self.index = None
|
|
|
|
self.force_transcode = None
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return ("{{"
|
|
|
|
"'playlistid': {self.playlistid}, "
|
|
|
|
"'id': {self.id}, "
|
|
|
|
"'version': {self.version}, "
|
|
|
|
"'type': '{self.type}', "
|
|
|
|
"'items': {items}, "
|
|
|
|
"'selectedItemID': {self.selectedItemID}, "
|
|
|
|
"'selectedItemOffset': {self.selectedItemOffset}, "
|
|
|
|
"'shuffled': {self.shuffled}, "
|
|
|
|
"'repeat': {self.repeat}, "
|
|
|
|
"'kodi_playlist_playback': {self.kodi_playlist_playback}, "
|
|
|
|
"'pkc_edit': {self.pkc_edit}, "
|
|
|
|
"}}").format(**{
|
|
|
|
'items': ['%s/%s: %s' % (x.plex_id, x.id, x.name)
|
|
|
|
for x in self.items],
|
|
|
|
'self': self
|
|
|
|
})
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return unicode(self).encode('utf-8')
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
|
|
def is_pkc_clear(self):
|
|
|
|
"""
|
|
|
|
Returns True if PKC has cleared the Kodi playqueue just recently.
|
|
|
|
Then this clear will be ignored from now on
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self._clear_list.pop()
|
|
|
|
except IndexError:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
def clear(self, kodi=True):
|
|
|
|
"""
|
|
|
|
Resets the playlist object to an empty playlist.
|
|
|
|
|
|
|
|
Pass kodi=False in order to NOT clear the Kodi playqueue
|
|
|
|
"""
|
|
|
|
# kodi monitor's on_clear method will only be called if there were some
|
|
|
|
# items to begin with
|
|
|
|
if kodi and self.kodi_pl.size() != 0:
|
|
|
|
self._clear_list.append(None)
|
|
|
|
self.kodi_pl.clear() # Clear Kodi playlist object
|
|
|
|
self.items = []
|
|
|
|
self.id = None
|
|
|
|
self.version = None
|
|
|
|
self.selectedItemID = None
|
|
|
|
self.selectedItemOffset = None
|
|
|
|
self.shuffled = 0
|
|
|
|
self.repeat = 0
|
|
|
|
self.plex_transient_token = None
|
|
|
|
self.old_kodi_pl = []
|
|
|
|
self.kodi_playlist_playback = False
|
|
|
|
self.index = None
|
|
|
|
self.force_transcode = None
|
|
|
|
LOG.debug('Playlist cleared: %s', self)
|
|
|
|
|
|
|
|
def init(self, playlistitem):
|
|
|
|
"""
|
|
|
|
Hit if Kodi initialized playback and we need to catch up on the PKC
|
|
|
|
and Plex side; e.g. for direct paths.
|
|
|
|
|
|
|
|
Kodi side will NOT be changed, e.g. no trailers will be added, but Kodi
|
|
|
|
playqueue taken as-is
|
|
|
|
"""
|
|
|
|
LOG.debug('Playqueue init called')
|
|
|
|
self.clear(kodi=False)
|
|
|
|
if not isinstance(playlistitem, PlaylistItem) or playlistitem.uri is None:
|
|
|
|
raise RuntimeError('Didnt receive a valid PlaylistItem but %s: %s'
|
|
|
|
% (type(playlistitem), playlistitem))
|
|
|
|
try:
|
|
|
|
params = {
|
|
|
|
'next': 0,
|
|
|
|
'type': self.type,
|
|
|
|
'uri': playlistitem.uri
|
|
|
|
}
|
|
|
|
xml = DU().downloadUrl(url="{server}/%ss" % self.kind,
|
|
|
|
action_type="POST",
|
|
|
|
parameters=params)
|
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
# Need to update the details for the playlist item
|
|
|
|
playlistitem.from_xml(xml[0])
|
|
|
|
except (KeyError, IndexError, TypeError):
|
|
|
|
LOG.error('Could not init Plex playlist with %s', playlistitem)
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError()
|
2019-05-26 04:49:29 +10:00
|
|
|
self.items.append(playlistitem)
|
|
|
|
LOG.debug('Initialized the playqueue on the Plex side: %s', self)
|
|
|
|
|
|
|
|
def play(self, plex_id, plex_type=None, startpos=None, position=None,
|
|
|
|
synched=True, force_transcode=None):
|
|
|
|
"""
|
|
|
|
Initializes the playQueue with e.g. trailers and additional file parts
|
|
|
|
Pass synched=False if you're sure that this item has not been synched
|
|
|
|
to Kodi
|
|
|
|
|
|
|
|
Or resolves webservice paths to actual paths
|
|
|
|
|
|
|
|
Hit by webservice.py
|
|
|
|
"""
|
|
|
|
LOG.debug('Play called with plex_id %s, plex_type %s, position %s, '
|
|
|
|
'synched %s, force_transcode %s, startpos %s', plex_id,
|
|
|
|
plex_type, position, synched, force_transcode, startpos)
|
|
|
|
resolve = False
|
|
|
|
try:
|
|
|
|
if plex_id == self.items[startpos].plex_id:
|
|
|
|
resolve = True
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
if resolve:
|
|
|
|
LOG.info('Resolving playback')
|
|
|
|
self._resolve(plex_id, startpos)
|
|
|
|
else:
|
|
|
|
LOG.info('Initializing playback')
|
|
|
|
self._init(plex_id,
|
|
|
|
plex_type,
|
|
|
|
startpos,
|
|
|
|
position,
|
|
|
|
synched,
|
|
|
|
force_transcode)
|
|
|
|
|
|
|
|
def _resolve(self, plex_id, startpos):
|
|
|
|
"""
|
|
|
|
The Plex playqueue has already been initialized. We resolve the path
|
|
|
|
from original webservice http://127.0.0.1 to the "correct" Plex one
|
|
|
|
"""
|
|
|
|
playlistitem = self.items[startpos]
|
|
|
|
# Add an additional item with the resolved path after the current one
|
|
|
|
self.index = startpos + 1
|
|
|
|
xml = PF.GetPlexMetadata(plex_id)
|
|
|
|
if xml in (None, 401):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not get Plex metadata %s for %s',
|
2019-05-26 19:53:59 +10:00
|
|
|
plex_id, self.items[startpos])
|
2019-05-26 04:49:29 +10:00
|
|
|
api = API(xml[0])
|
|
|
|
if playlistitem.resume is None:
|
|
|
|
# Potentially ask user to resume
|
|
|
|
resume = self._resume_playback(None, xml[0])
|
|
|
|
else:
|
|
|
|
# Do NOT ask user
|
|
|
|
resume = playlistitem.resume
|
|
|
|
# Use the original playlistitem to retain all info!
|
|
|
|
self._kodi_add_xml(xml[0],
|
|
|
|
api,
|
|
|
|
resume,
|
|
|
|
playlistitem=playlistitem)
|
|
|
|
# Add additional file parts, if any exist
|
|
|
|
self._add_additional_parts(xml)
|
|
|
|
# Note: the CURRENT playlistitem will be deleted through webservice.py
|
|
|
|
# once the path resolution has completed
|
|
|
|
|
|
|
|
def _init(self, plex_id, plex_type=None, startpos=None, position=None,
|
|
|
|
synched=True, force_transcode=None):
|
|
|
|
"""
|
|
|
|
Initializes the Plex and PKC playqueue for playback. Possibly adds
|
|
|
|
additionals trailers
|
|
|
|
"""
|
|
|
|
self.index = position
|
|
|
|
while len(self.items) < self.kodi_pl.size():
|
|
|
|
# The original item that Kodi put into the playlist, e.g.
|
|
|
|
# {
|
|
|
|
# u'title': u'',
|
|
|
|
# u'type': u'unknown',
|
|
|
|
# u'file': u'http://127.0.0.1:57578/plex/kodi/....',
|
|
|
|
# u'label': u''
|
|
|
|
# }
|
|
|
|
# We CANNOT delete that item right now - so let's add a dummy
|
|
|
|
# on the PKC side to keep all indicees lined up.
|
|
|
|
# The failing item will be deleted in webservice.py
|
|
|
|
LOG.debug('Adding a dummy item to our playqueue')
|
|
|
|
self.items.insert(0, PlaylistItemDummy())
|
|
|
|
self.force_transcode = force_transcode
|
|
|
|
if synched:
|
|
|
|
with PlexDB(lock=False) as plexdb:
|
|
|
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
|
|
|
else:
|
|
|
|
db_item = None
|
|
|
|
if db_item:
|
|
|
|
xml = None
|
|
|
|
section_uuid = db_item['section_uuid']
|
|
|
|
plex_type = db_item['plex_type']
|
|
|
|
else:
|
|
|
|
xml = PF.GetPlexMetadata(plex_id)
|
|
|
|
if xml in (None, 401):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not get Plex metadata %s', plex_id)
|
2019-05-26 04:49:29 +10:00
|
|
|
section_uuid = xml.get('librarySectionUUID')
|
|
|
|
api = API(xml[0])
|
|
|
|
plex_type = api.plex_type()
|
|
|
|
resume = self._resume_playback(db_item, xml)
|
|
|
|
trailers = False
|
|
|
|
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
|
|
|
utils.settings('enableCinema') == 'true'):
|
|
|
|
if utils.settings('askCinema') == "true":
|
|
|
|
# "Play trailers?"
|
|
|
|
trailers = utils.yesno_dialog(utils.lang(29999),
|
|
|
|
utils.lang(33016)) or False
|
|
|
|
else:
|
|
|
|
trailers = True
|
|
|
|
LOG.debug('Playing trailers: %s', trailers)
|
|
|
|
xml = PF.init_plex_playqueue(plex_id,
|
|
|
|
section_uuid,
|
|
|
|
plex_type=plex_type,
|
|
|
|
trailers=trailers)
|
|
|
|
if xml is None:
|
|
|
|
LOG.error('Could not get playqueue for plex_id %s UUID %s for %s',
|
|
|
|
plex_id, section_uuid, self)
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not get playqueue')
|
2019-05-26 04:49:29 +10:00
|
|
|
# See that we add trailers, if they exist in the xml return
|
|
|
|
self._add_intros(xml)
|
|
|
|
# Add the main item after the trailers
|
|
|
|
# Look at the LAST item
|
|
|
|
api = API(xml[-1])
|
|
|
|
self._kodi_add_xml(xml[-1], api, resume)
|
|
|
|
# Add additional file parts, if any exist
|
|
|
|
self._add_additional_parts(xml)
|
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _resume_playback(db_item=None, xml=None):
|
|
|
|
'''
|
|
|
|
Pass in either db_item or xml
|
2019-05-26 20:51:04 +10:00
|
|
|
Resume item if available. Returns bool or raise a PlayqueueError if
|
2019-05-26 04:49:29 +10:00
|
|
|
resume was cancelled by user.
|
|
|
|
'''
|
|
|
|
resume = app.PLAYSTATE.resume_playback
|
|
|
|
app.PLAYSTATE.resume_playback = None
|
|
|
|
if app.PLAYSTATE.autoplay:
|
|
|
|
resume = False
|
|
|
|
LOG.info('Skip resume for autoplay')
|
|
|
|
elif resume is None:
|
|
|
|
if db_item:
|
|
|
|
with KodiVideoDB(lock=False) as kodidb:
|
|
|
|
resume = kodidb.get_resume(db_item['kodi_fileid'])
|
|
|
|
else:
|
|
|
|
api = API(xml)
|
|
|
|
resume = api.resume_point()
|
|
|
|
if resume:
|
|
|
|
resume = resume_dialog(resume)
|
|
|
|
LOG.info('User chose resume: %s', resume)
|
|
|
|
if resume is None:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('User backed out of resume dialog')
|
2019-05-26 04:49:29 +10:00
|
|
|
app.PLAYSTATE.autoplay = True
|
|
|
|
return resume
|
|
|
|
|
|
|
|
def _add_intros(self, xml):
|
|
|
|
'''
|
|
|
|
if we have any play them when the movie/show is not being resumed.
|
|
|
|
'''
|
|
|
|
if not len(xml) > 1:
|
|
|
|
LOG.debug('No trailers returned from the PMS')
|
|
|
|
return
|
|
|
|
for i, intro in enumerate(xml):
|
|
|
|
if i + 1 == len(xml):
|
|
|
|
# The main item we're looking at - skip!
|
|
|
|
break
|
|
|
|
api = API(intro)
|
|
|
|
LOG.debug('Adding trailer: %s', api.title())
|
2019-05-26 20:51:04 +10:00
|
|
|
self._kodi_add_xml(intro, api, resume=False)
|
2019-05-26 04:49:29 +10:00
|
|
|
|
|
|
|
def _add_additional_parts(self, xml):
|
|
|
|
''' Create listitems and add them to the stack of playlist.
|
|
|
|
'''
|
|
|
|
api = API(xml[0])
|
|
|
|
for part, _ in enumerate(xml[0][0]):
|
|
|
|
if part == 0:
|
|
|
|
# The first part that we've already added
|
|
|
|
continue
|
|
|
|
api.set_part_number(part)
|
|
|
|
LOG.debug('Adding addional part for %s: %s', api.title(), part)
|
2019-05-26 20:51:04 +10:00
|
|
|
self._kodi_add_xml(xml[0], api, resume=False)
|
2019-05-26 04:49:29 +10:00
|
|
|
|
2019-05-26 20:51:04 +10:00
|
|
|
def _kodi_add_xml(self, xml, api, resume, playlistitem=None):
|
|
|
|
"""
|
|
|
|
Be careful what you pass as resume:
|
|
|
|
False: do not resume, do not subsequently ask user
|
|
|
|
True: do resume, do not subsequently ask user
|
|
|
|
"""
|
2019-05-26 04:49:29 +10:00
|
|
|
if not playlistitem:
|
|
|
|
playlistitem = PlaylistItem(xml_video_element=xml)
|
|
|
|
playlistitem.part = api.part
|
|
|
|
playlistitem.force_transcode = self.force_transcode
|
2019-05-26 20:51:04 +10:00
|
|
|
playlistitem.resume = resume
|
|
|
|
listitem = widgets.get_listitem(xml, resume=resume)
|
2019-05-26 04:49:29 +10:00
|
|
|
listitem.setSubtitles(api.cache_external_subs())
|
|
|
|
play = PlayUtils(api, playlistitem)
|
|
|
|
url = play.getPlayUrl()
|
|
|
|
listitem.setPath(url.encode('utf-8'))
|
|
|
|
self.kodi_add_item(playlistitem, self.index, listitem)
|
|
|
|
self.items.insert(self.index, playlistitem)
|
|
|
|
self.index += 1
|
|
|
|
|
|
|
|
def update_details_from_xml(self, xml):
|
|
|
|
"""
|
|
|
|
Updates the playlist details from the xml provided
|
|
|
|
"""
|
|
|
|
self.id = utils.cast(int, xml.get('%sID' % self.kind))
|
|
|
|
self.version = utils.cast(int, xml.get('%sVersion' % self.kind))
|
|
|
|
self.shuffled = utils.cast(int, xml.get('%sShuffled' % self.kind))
|
|
|
|
self.selectedItemID = utils.cast(int,
|
|
|
|
xml.get('%sSelectedItemID' % self.kind))
|
|
|
|
self.selectedItemOffset = utils.cast(int,
|
|
|
|
xml.get('%sSelectedItemOffset'
|
|
|
|
% self.kind))
|
|
|
|
LOG.debug('Updated playlist from xml: %s', self)
|
|
|
|
|
|
|
|
def add_item(self, item, pos, listitem=None):
|
|
|
|
"""
|
|
|
|
Adds a PlaylistItem to both Kodi and Plex at position pos [int]
|
|
|
|
Also changes self.items
|
2019-05-26 19:26:14 +10:00
|
|
|
Raises PlayqueueError
|
2019-05-26 04:49:29 +10:00
|
|
|
"""
|
|
|
|
self.kodi_add_item(item, pos, listitem)
|
|
|
|
self.plex_add_item(item, pos)
|
|
|
|
|
|
|
|
def kodi_add_item(self, item, pos, listitem=None):
|
|
|
|
"""
|
|
|
|
Adds a PlaylistItem to Kodi only. Will not change self.items
|
2019-05-26 19:26:14 +10:00
|
|
|
Raises PlayqueueError
|
2019-05-26 04:49:29 +10:00
|
|
|
"""
|
|
|
|
if not isinstance(item, PlaylistItem):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Wrong item %s of type %s received'
|
2019-05-26 19:53:59 +10:00
|
|
|
% (item, type(item)))
|
2019-05-26 04:49:29 +10:00
|
|
|
if pos > len(self.items):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Position %s too large for playlist length %s'
|
2019-05-26 19:53:59 +10:00
|
|
|
% (pos, len(self.items)))
|
2019-05-26 04:49:29 +10:00
|
|
|
LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item)
|
|
|
|
if listitem:
|
|
|
|
self.kodi_pl.add(url=listitem.getPath(),
|
|
|
|
listitem=listitem,
|
|
|
|
index=pos)
|
|
|
|
elif item.kodi_id is not None and item.kodi_type is not None:
|
|
|
|
# This method ensures we have full Kodi metadata, potentially
|
|
|
|
# with more artwork, for example, than Plex provides
|
|
|
|
if pos == len(self.items):
|
|
|
|
answ = js.playlist_add(self.playlistid,
|
|
|
|
{'%sid' % item.kodi_type: item.kodi_id})
|
|
|
|
else:
|
|
|
|
answ = js.playlist_insert({'playlistid': self.playlistid,
|
|
|
|
'position': pos,
|
|
|
|
'item': {'%sid' % item.kodi_type: item.kodi_id}})
|
|
|
|
if 'error' in answ:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Kodi did not add item to playlist: %s',
|
2019-05-26 19:53:59 +10:00
|
|
|
answ)
|
2019-05-26 04:49:29 +10:00
|
|
|
else:
|
|
|
|
if item.xml is None:
|
|
|
|
LOG.debug('Need to get metadata for item %s', item)
|
|
|
|
item.xml = PF.GetPlexMetadata(item.plex_id)
|
|
|
|
if item.xml in (None, 401):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not get metadata for %s', item)
|
2019-05-26 04:49:29 +10:00
|
|
|
api = API(item.xml[0])
|
|
|
|
listitem = widgets.get_listitem(item.xml, resume=True)
|
|
|
|
url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT
|
|
|
|
args = {
|
|
|
|
'plex_id': item.plex_id,
|
|
|
|
'plex_type': api.plex_type()
|
|
|
|
}
|
|
|
|
if item.force_transcode:
|
|
|
|
args['transcode'] = 'true'
|
|
|
|
url = utils.extend_url(url, args)
|
|
|
|
item.file = url
|
|
|
|
listitem.setPath(url.encode('utf-8'))
|
|
|
|
self.kodi_pl.add(url=url.encode('utf-8'),
|
|
|
|
listitem=listitem,
|
|
|
|
index=pos)
|
|
|
|
|
|
|
|
def plex_add_item(self, item, pos):
|
|
|
|
"""
|
|
|
|
Adds a new PlaylistItem to the playlist at position pos [int] only on
|
|
|
|
the Plex side of things. Also changes self.items
|
2019-05-26 19:26:14 +10:00
|
|
|
Raises PlayqueueError
|
2019-05-26 04:49:29 +10:00
|
|
|
"""
|
|
|
|
if not isinstance(item, PlaylistItem) or not item.uri:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Wrong item %s of type %s received'
|
2019-05-26 19:53:59 +10:00
|
|
|
% (item, type(item)))
|
2019-05-26 04:49:29 +10:00
|
|
|
if pos > len(self.items):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Position %s too large for playlist length %s'
|
2019-05-26 19:53:59 +10:00
|
|
|
% (pos, len(self.items)))
|
2019-05-26 04:49:29 +10:00
|
|
|
LOG.debug('Adding item to Plex playlist at position %s: %s', pos, item)
|
|
|
|
url = '{server}/%ss/%s?uri=%s' % (self.kind, self.id, item.uri)
|
|
|
|
# Will usually put the new item at the end of the Plex playlist
|
|
|
|
xml = DU().downloadUrl(url, action_type='PUT')
|
|
|
|
try:
|
|
|
|
xml[0].attrib
|
|
|
|
except (TypeError, AttributeError, KeyError, IndexError):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not add item %s to playlist %s'
|
2019-05-26 19:53:59 +10:00
|
|
|
% (item, self))
|
2019-05-26 04:49:29 +10:00
|
|
|
for actual_pos, xml_video_element in enumerate(xml):
|
|
|
|
api = API(xml_video_element)
|
|
|
|
if api.plex_id() == item.plex_id:
|
|
|
|
break
|
|
|
|
else:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Something went wrong - Plex id not found')
|
2019-05-26 04:49:29 +10:00
|
|
|
item.from_xml(xml[actual_pos])
|
|
|
|
self.items.insert(actual_pos, item)
|
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
if actual_pos != pos:
|
|
|
|
self.plex_move_item(actual_pos, pos)
|
|
|
|
LOG.debug('Added item %s on Plex side: %s', item, self)
|
|
|
|
|
|
|
|
def kodi_remove_item(self, pos):
|
|
|
|
"""
|
|
|
|
Only manipulates the Kodi playlist. Won't change self.items
|
|
|
|
"""
|
|
|
|
LOG.debug('Removing position %s on the Kodi side for %s', pos, self)
|
2019-05-26 20:51:04 +10:00
|
|
|
LOG.error('Current Kodi playlist: %s', js.playlist_get_items(self.playlistid))
|
2019-05-26 04:49:29 +10:00
|
|
|
answ = js.playlist_remove(self.playlistid, pos)
|
|
|
|
if 'error' in answ:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not remove item: %s' % answ['error'])
|
2019-05-26 04:49:29 +10:00
|
|
|
|
|
|
|
def plex_remove_item(self, pos):
|
|
|
|
"""
|
|
|
|
Removes an item from Plex as well as our self.items item list
|
|
|
|
"""
|
|
|
|
LOG.debug('Deleting position %s on the Plex side for: %s', pos, self)
|
|
|
|
try:
|
|
|
|
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
|
|
|
|
(self.kind,
|
|
|
|
self.id,
|
|
|
|
self.items[pos].id,
|
|
|
|
self.repeat),
|
|
|
|
action_type="DELETE")
|
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
del self.items[pos]
|
|
|
|
except IndexError:
|
|
|
|
LOG.error('Could not delete item at position %s on the Plex side',
|
|
|
|
pos)
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError()
|
2019-05-26 04:49:29 +10:00
|
|
|
|
|
|
|
def plex_move_item(self, before, after):
|
|
|
|
"""
|
|
|
|
Moves playlist item from before [int] to after [int] for Plex only.
|
|
|
|
|
|
|
|
Will also change self.items
|
|
|
|
"""
|
|
|
|
if before > len(self.items) or after > len(self.items) or after == before:
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Illegal original position %s and/or desired '
|
2019-05-26 19:53:59 +10:00
|
|
|
'position %s for playlist length %s' %
|
|
|
|
(before, after, len(self.items)))
|
2019-05-26 04:49:29 +10:00
|
|
|
LOG.debug('Moving item from %s to %s on the Plex side for %s',
|
|
|
|
before, after, self)
|
|
|
|
if after == 0:
|
|
|
|
url = "{server}/%ss/%s/items/%s/move?after=0" % \
|
|
|
|
(self.kind,
|
|
|
|
self.id,
|
|
|
|
self.items[before].id)
|
|
|
|
elif after > before:
|
|
|
|
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
|
|
|
|
(self.kind,
|
|
|
|
self.id,
|
|
|
|
self.items[before].id,
|
|
|
|
self.items[after].id)
|
|
|
|
else:
|
|
|
|
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
|
|
|
|
(self.kind,
|
|
|
|
self.id,
|
|
|
|
self.items[before].id,
|
|
|
|
self.items[after - 1].id)
|
|
|
|
xml = DU().downloadUrl(url, action_type="PUT")
|
|
|
|
try:
|
|
|
|
xml[0].attrib
|
|
|
|
except (TypeError, IndexError, AttributeError):
|
2019-05-26 19:26:14 +10:00
|
|
|
raise PlayqueueError('Could not move playlist item from %s to %s '
|
2019-05-26 19:53:59 +10:00
|
|
|
'for %s' % (before, after, self))
|
2019-05-26 04:49:29 +10:00
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
self.items.insert(after, self.items.pop(before))
|
|
|
|
LOG.debug('Done moving items for %s', self)
|
|
|
|
|
|
|
|
def init_from_xml(self, xml, offset=None, start_plex_id=None, repeat=None,
|
|
|
|
transient_token=None):
|
|
|
|
"""
|
|
|
|
Play all items contained in the xml passed in. Called by Plex Companion.
|
|
|
|
Either supply the ratingKey of the starting Plex element. Or set
|
|
|
|
playqueue.selectedItemID
|
|
|
|
|
|
|
|
offset [float]: will seek to position offset after playback start
|
|
|
|
start_plex_id [int]: the plex_id of the element that should be
|
|
|
|
played
|
|
|
|
repeat [int]: 0: don't repear
|
|
|
|
1: repeat item
|
|
|
|
2: repeat everything
|
|
|
|
transient_token [unicode]: temporary token received from the PMS
|
|
|
|
|
|
|
|
Will stop current playback and start playback at the end
|
|
|
|
"""
|
|
|
|
LOG.debug("init_from_xml called with offset %s, start_plex_id %s",
|
|
|
|
offset, start_plex_id)
|
|
|
|
app.APP.player.stop()
|
|
|
|
self.clear()
|
|
|
|
self.update_details_from_xml(xml)
|
|
|
|
self.repeat = 0 if not repeat else repeat
|
|
|
|
self.plex_transient_token = transient_token
|
|
|
|
for pos, xml_video_element in enumerate(xml):
|
|
|
|
playlistitem = PlaylistItem(xml_video_element=xml_video_element)
|
|
|
|
self.kodi_add_item(playlistitem, pos)
|
|
|
|
self.items.append(playlistitem)
|
|
|
|
# Where do we start playback?
|
|
|
|
if start_plex_id is not None:
|
|
|
|
for startpos, item in enumerate(self.items):
|
|
|
|
if item.plex_id == start_plex_id:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
startpos = 0
|
|
|
|
else:
|
|
|
|
for startpos, item in enumerate(self.items):
|
|
|
|
if item.id == self.selectedItemID:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
startpos = 0
|
|
|
|
# Set resume for the item we should play - do NOT ask user since we
|
|
|
|
# initiated from the other Companion client
|
|
|
|
self.items[startpos].resume = True if offset else False
|
|
|
|
self.start_playback(pos=startpos, offset=offset)
|
|
|
|
|
|
|
|
def start_playback(self, pos=0, offset=0):
|
|
|
|
"""
|
|
|
|
Seek immediately after kicking off playback is not reliable.
|
|
|
|
Threaded, since we need to return BEFORE seeking
|
|
|
|
"""
|
|
|
|
LOG.info('Starting playback at %s offset %s for %s', pos, offset, self)
|
|
|
|
thread = threading.Thread(target=self._threaded_playback,
|
|
|
|
args=(self.kodi_pl, pos, offset))
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _threaded_playback(kodi_playlist, pos, offset):
|
|
|
|
app.APP.player.play(kodi_playlist, startpos=pos, windowed=False)
|
|
|
|
if offset:
|
|
|
|
i = 0
|
|
|
|
while not app.APP.is_playing:
|
|
|
|
app.APP.monitor.waitForAbort(0.1)
|
|
|
|
i += 1
|
|
|
|
if i > 50:
|
|
|
|
LOG.warn('Could not seek to %s', offset)
|
|
|
|
return
|
|
|
|
js.seek_to(offset)
|