Big update

This commit is contained in:
croneter 2019-04-28 18:03:20 +02:00
parent 578ced789f
commit 1218cde0a2
10 changed files with 634 additions and 403 deletions

View file

@ -219,16 +219,20 @@ class KodiMonitor(xbmc.Monitor):
{
u'playlistid': 1,
}
Let's NOT use this as Kodi's responses when e.g. playing an entire
folder are NOT threadsafe: Playlist.OnAdd might be added first, then
Playlist.OnClear might be received LATER
"""
if self.playlistid == data['playlistid']:
LOG.debug('Resetting autoplay')
app.PLAYSTATE.autoplay = False
playqueue = PQ.PLAYQUEUES[data['playlistid']]
if not playqueue.is_pkc_clear():
playqueue.pkc_edit = True
playqueue.clear(kodi=False)
else:
LOG.debug('Detected PKC clear - ignoring')
return
# playqueue = PQ.PLAYQUEUES[data['playlistid']]
# if not playqueue.is_pkc_clear():
# playqueue.pkc_edit = True
# playqueue.clear(kodi=False)
# else:
# LOG.debug('Detected PKC clear - ignoring')
@staticmethod
def _get_ids(kodi_id, kodi_type, path):
@ -311,7 +315,7 @@ class KodiMonitor(xbmc.Monitor):
def _check_playing_item(self, data):
"""
Returns a PF.Playlist_Item() for the currently playing item
Returns a PF.PlaylistItem() for the currently playing item
Raises MonitorError or IndexError if we need to init the PKC playqueue
"""
info = js.get_player_props(self.playerid)
@ -320,26 +324,13 @@ class KodiMonitor(xbmc.Monitor):
kodi_playlist = js.playlist_get_items(self.playerid)
LOG.debug('Current Kodi playlist: %s', kodi_playlist)
kodi_item = PL.playlist_item_from_kodi(kodi_playlist[position])
if (position == 1 and
len(kodi_playlist) == len(self.playqueue.items) + 1 and
kodi_playlist[0].get('type') == 'unknown' and
kodi_playlist[0].get('file') and
kodi_playlist[0].get('file').startswith('http://127.0.0.1')):
if kodi_item == self.playqueue.items[0]:
# Delete the very first item that we used to start playback:
# {
# u'title': u'',
# u'type': u'unknown',
# u'file': u'http://127.0.0.1:57578/plex/kodi/....',
# u'label': u''
# }
if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy):
# Get rid of the very first element in the queue that Kodi marked
# as unplayed (the one to init the queue)
LOG.debug('Deleting the very first playqueue item')
js.playlist_remove(self.playqueue.playlistid, 0)
del self.playqueue.items[0]
position = 0
else:
LOG.debug('Different item in PKC playlist: %s vs. %s',
self.playqueue.items[0], kodi_item)
raise MonitorError()
elif kodi_item != self.playqueue.items[position]:
LOG.debug('Different playqueue items: %s vs. %s ',
kodi_item, self.playqueue.items[position])
@ -349,7 +340,7 @@ class KodiMonitor(xbmc.Monitor):
def _load_playerstate(self, item):
"""
Pass in a PF.Playlist_Item(). Will then set the currently playing
Pass in a PF.PlaylistItem(). Will then set the currently playing
state with app.PLAYSTATE.player_states[self.playerid]
"""
if self.playqueue.id:
@ -431,6 +422,7 @@ def _playback_cleanup(ended=False):
# We might have saved a transient token from a user flinging media via
# Companion (if we could not use the playqueue to store the token)
app.CONN.plex_transient_token = None
LOG.debug('Playstate is: %s', app.PLAYSTATE.player_states)
for playerid in app.PLAYSTATE.active_players:
status = app.PLAYSTATE.player_states[playerid]
# Remember the last played item later
@ -498,6 +490,9 @@ def _record_playstate(status, ended):
playcount += 1
time = 0
with kodi_db.KodiVideoDB() as kodidb:
LOG.error('Setting file_id %s, time %s, totaltime %s, playcount %s, '
'last_played %s',
db_item['kodi_fileid'], time, totaltime, playcount, last_played)
kodidb.set_resume(db_item['kodi_fileid'],
time,
totaltime,

View file

@ -464,7 +464,7 @@ def process_indirect(key, offset, resolve=True):
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
playqueue.clear()
item = PL.Playlist_Item()
item = PL.PlaylistItem()
item.xml = xml[0]
item.offset = offset
item.plex_type = v.PLEX_TYPE_CLIP

View file

@ -6,15 +6,16 @@ Collection of functions associated with Kodi and Plex playlists and playqueues
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from .plex_api import API
from .plex_db import PlexDB
from . import plex_functions as PF
from .kodi_db import kodiid_from_filename
from .playutils import PlayUtils
from .kodi_db import kodiid_from_filename, KodiVideoDB
from .downloadutils import DownloadUtils as DU
from . import utils
from . import json_rpc as js
from . import variables as v
from . import app
from . import utils, json_rpc as js, variables as v, app, widgets
from .windows.resume import resume_dialog
###############################################################################
@ -30,14 +31,14 @@ class PlaylistError(Exception):
pass
class Playqueue_Object(object):
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 Playlist_Items
items = [] [list] of PlaylistItem
id = None [str] Plex playQueueID, unique Plex identifier
version = None [int] Plex version of the playQueue
selectedItemID = None
@ -74,8 +75,11 @@ class Playqueue_Object(object):
# 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 __repr__(self):
def __unicode__(self):
return ("{{"
"'playlistid': {self.playlistid}, "
"'id': {self.id}, "
@ -89,10 +93,14 @@ class Playqueue_Object(object):
"'kodi_playlist_playback': {self.kodi_playlist_playback}, "
"'pkc_edit': {self.pkc_edit}, "
"}}").format(**{
'items': [x.plex_id for x in self.items or []],
'items': ['%s/%s: %s' % (x.plex_id, x.id, x.name)
for x in self.items],
'self': self
}).encode('utf-8')
__str__ = __repr__
})
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def is_pkc_clear(self):
"""
@ -127,10 +135,319 @@ class Playqueue_Object(object):
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, plex_id, plex_type=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
"""
LOG.error('Current Kodi playlist: %s',
js.playlist_get_items(self.playlistid))
if position is not None:
self.index = position
else:
# Do NOT use kodi_pl.getposition() as that appears to be buggy
self.index = max(js.get_position(self.playlistid), 0)
LOG.debug('Initializing with plex_id %s, plex_type %s, position %s, '
'synched %s, force_transcode %s, index %s', plex_id,
plex_type, position, synched, force_transcode, self.index)
LOG.error('Actual start: %s', js.get_position(self.playlistid))
if self.kodi_pl.size() != len(self.items):
# 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
LOG.debug('Detected Kodi playlist size %s to be off for PKC: %s',
self.kodi_pl.size(), len(self.items))
while len(self.items) < self.kodi_pl.size():
LOG.debug('Adding a dummy item to our playqueue')
playlistitem = PlaylistItemDummy()
self.items.insert(0, playlistitem)
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):
raise PlaylistError('Could not get Plex metadata %s', plex_id)
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)
raise PlaylistError('Could not get playqueue')
# 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)
class Playlist_Item(object):
@staticmethod
def _resume_playback(db_item=None, xml=None):
'''
Pass in either db_item or xml
Resume item if available. Returns bool or raise an PlayStrmException if
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:
raise PlaylistError('User backed out of resume dialog')
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())
self._kodi_add_xml(intro, api)
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)
self._kodi_add_xml(xml[0], api)
def _kodi_add_xml(self, xml, api, resume=False):
playlistitem = PlaylistItem(xml_video_element=xml)
playlistitem.part = api.part
playlistitem.force_transcode = self.force_transcode
listitem = widgets.get_listitem(xml, resume=True)
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
Raises PlaylistError
"""
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
Raises PlaylistError
"""
if not isinstance(item, PlaylistItem):
raise PlaylistError('Wrong item %s of type %s received'
% (item, type(item)))
if pos > len(self.items):
raise PlaylistError('Position %s too large for playlist length %s'
% (pos, len(self.items)))
LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item)
if 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:
raise PlaylistError('Kodi did not add item to playlist: %s',
answ)
else:
if not listitem:
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):
raise PlaylistError('Could not get metadata for %s', item)
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': self.plex_id,
'plex_type': self.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=listitem.getPath(),
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
Raises PlaylistError
"""
if not isinstance(item, PlaylistItem) or not item.uri:
raise PlaylistError('Wrong item %s of type %s received'
% (item, type(item)))
if pos > len(self.items):
raise PlaylistError('Position %s too large for playlist length %s'
% (pos, len(self.items)))
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):
raise PlaylistError('Could not add item %s to playlist %s'
% (item, self))
if len(xml) != len(self.items) + 1:
raise PlaylistError('Could not add item %s to playlist %s - wrong'
' length received' % (item, self))
for actual_pos, xml_video_element in enumerate(xml):
api = API(xml_video_element)
if api.plex_id() == item.plex_id:
break
else:
raise PlaylistError('Something went wrong - Plex id not found')
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)
answ = js.playlist_remove(self.playlistid, pos)
if 'error' in answ:
raise PlaylistError('Could not remove item: %s' % answ)
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):
raise PlaylistError('Original position %s larger than current '
'playlist length %s',
before, len(self.items))
elif after > len(self.items):
raise PlaylistError('Desired position %s larger than current '
'playlist length %s',
after, len(self.items))
elif after == before:
raise PlaylistError('Desired position and original position are '
'identical: %s', after)
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)
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):
raise PlaylistError('Could not move playlist item from %s to %s '
'for %s' % (before, after, self))
self.update_details_from_xml(xml)
self.items.insert(after, self.items.pop(before))
LOG.debug('Done moving items for %s', self)
def start_playback(self, pos=0):
LOG.info('Starting playback at %s for %s', pos, self)
xbmc.Player().play(self.kodi_pl, startpos=pos, windowed=False)
class PlaylistItem(object):
"""
Object to fill our playqueues and playlists with.
@ -150,110 +467,75 @@ class Playlist_Item(object):
part = 0 [int] part number if Plex video consists of mult. parts
force_transcode [bool] defaults to False
Playlist_items compare as equal, if they
PlaylistItem compare as equal, if they
- have the same plex_id
- OR: have the same kodi_id AND kodi_type
- OR: have the same file
"""
def __init__(self):
self._id = None
self._plex_id = None
self.plex_type = None
def __init__(self, plex_id=None, plex_type=None, xml_video_element=None,
kodi_id=None, kodi_type=None, grab_xml=False,
lookup_kodi=True):
"""
Pass grab_xml=True in order to get Plex metadata from the PMS while
passing a plex_id.
Pass lookup_kodi=False to NOT check the plex.db for kodi_id and
kodi_type if they're missing (won't be done for clips anyway)
"""
self.name = None
self.id = None
self.plex_id = plex_id
self.plex_type = plex_type
self.plex_uuid = None
self._kodi_id = None
self.kodi_type = None
self.kodi_id = kodi_id
self.kodi_type = kodi_type
self.file = None
self.uri = None
self.guid = None
self.xml = None
self.playmethod = None
self._playcount = None
self._offset = None
# If Plex video consists of several parts; part number
self._part = 0
self.playcount = None
self.offset = None
self.part = 0
self.force_transcode = False
if grab_xml and plex_id is not None and xml_video_element is None:
xml_video_element = PF.GetPlexMetadata(plex_id)
try:
xml_video_element = xml_video_element[0]
except (TypeError, IndexError):
xml_video_element = None
if xml_video_element is not None:
self.from_xml(xml_video_element)
if (lookup_kodi and (kodi_id is None or kodi_type is None) and
self.plex_type != v.PLEX_TYPE_CLIP):
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item is not None:
self.kodi_id = db_item['kodi_id']
self.kodi_type = db_item['kodi_type']
self.plex_uuid = db_item['section_uuid']
self.set_uri()
def __eq__(self, item):
if self.plex_id is not None and item.plex_id is not None:
return self.plex_id == item.plex_id
elif (self.kodi_id is not None and item.kodi_id is not None and
self.kodi_type and item.kodi_type):
return (self.kodi_id == item.kodi_id and
self.kodi_type == item.kodi_type)
elif self.file and item.file:
return self.file == item.file
raise RuntimeError('playlist items not fully defined: %s, %s' %
(self, item))
def __eq__(self, other):
if self.plex_id is not None and other.plex_id is not None:
return self.plex_id == other.plex_id
elif (self.kodi_id is not None and other.kodi_id is not None and
self.kodi_type and other.kodi_type):
return (self.kodi_id == other.kodi_id and
self.kodi_type == other.kodi_type)
elif self.file and other.file:
return self.file == other.file
raise RuntimeError('PlaylistItems not fully defined: %s, %s' %
(self, other))
def __ne__(self, item):
return not self == item
def __ne__(self, other):
return not self == other
@property
def plex_id(self):
return self._plex_id
@plex_id.setter
def plex_id(self, value):
if not isinstance(value, int) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._plex_id = value
@property
def id(self):
return self._id
@id.setter
def id(self, value):
if not isinstance(value, int) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._id = value
@property
def kodi_id(self):
return self._kodi_id
@kodi_id.setter
def kodi_id(self, value):
if not isinstance(value, int) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._kodi_id = value
@property
def playcount(self):
return self._playcount
@playcount.setter
def playcount(self, value):
if not isinstance(value, int) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._playcount = value
@property
def offset(self):
return self._offset
@offset.setter
def offset(self, value):
if not isinstance(value, (int, float)) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._offset = value
@property
def part(self):
return self._part
@part.setter
def part(self, value):
if not isinstance(value, int) and value is not None:
raise TypeError('Passed %s instead of int!' % type(value))
self._part = value
def __repr__(self):
answ = ("{{"
def __unicode__(self):
return ("{{"
"'name': '{self.name}', "
"'id': {self.id}, "
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'plex_uuid': '{self.plex_uuid}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"'file': '{self.file}', "
@ -263,13 +545,71 @@ class Playlist_Item(object):
"'playcount': {self.playcount}, "
"'offset': {self.offset}, "
"'force_transcode': {self.force_transcode}, "
"'part': {self.part}, ".format(self=self))
answ = answ.encode('utf-8')
# etree xml.__repr__() could return string, not unicode
return answ + b"'xml': \"{self.xml}\"}}".format(self=self)
"'part': {self.part}"
"}}".format(self=self))
def __str__(self):
return self.__repr__()
return unicode(self).encode('utf-8')
__repr__ = __str__
def from_xml(self, xml_video_element):
"""
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
item.id will only be set if you passed in an xml_video_element from
e.g. a playQueue
"""
api = API(xml_video_element)
self.name = api.title()
self.plex_id = api.plex_id()
self.plex_type = api.plex_type()
self.id = api.item_id()
self.guid = api.guid_html_escaped()
self.playcount = api.viewcount()
self.offset = api.resume_point()
self.xml = xml_video_element
def from_kodi(self, playlist_item):
"""
playlist_item: dict contains keys 'id', 'type', 'file' (if applicable)
Will thus set the attributes kodi_id, kodi_type, file, if applicable
If kodi_id & kodi_type are provided, plex_id and plex_type will be
looked up (if not already set)
"""
self.kodi_id = playlist_item.get('id')
self.kodi_type = playlist_item.get('type')
self.file = playlist_item.get('file')
if self.plex_id is None and self.kodi_id is not None and self.kodi_type:
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type)
if db_item:
self.plex_id = db_item['plex_id']
self.plex_type = db_item['plex_type']
self.plex_uuid = db_item['section_uuid']
if self.plex_id is None and self.file is not None:
try:
query = self.file.split('?', 1)[1]
except IndexError:
query = ''
query = dict(utils.parse_qsl(query))
self.plex_id = utils.cast(int, query.get('plex_id'))
self.plex_type = query.get('itemType')
self.set_uri()
LOG.debug('Made playlist item from Kodi: %s', self)
def set_uri(self):
if self.plex_id is None and self.file is not None:
self.uri = ('library://whatever/item/%s'
% utils.quote(self.file, safe=''))
elif self.plex_id is not None and self.plex_uuid is not None:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(self.plex_uuid, self.plex_id))
elif self.plex_id is not None:
self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(self.plex_id, self.plex_id))
else:
self.uri = None
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
@ -327,6 +667,17 @@ class Playlist_Item(object):
count += 1
class PlaylistItemDummy(PlaylistItem):
"""
Let e.g. Kodimonitor detect that this is a dummy item
"""
def __init__(self, *args, **kwargs):
super(PlaylistItemDummy, self).__init__(*args, **kwargs)
self.name = 'dummy item'
self.id = 0
self.plex_id = 0
def playlist_item_from_kodi(kodi_item):
"""
Turns the JSON answer from Kodi into a playlist element
@ -334,7 +685,7 @@ def playlist_item_from_kodi(kodi_item):
Supply with data['item'] as returned from Kodi JSON-RPC interface.
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
"""
item = Playlist_Item()
item = PlaylistItem()
item.kodi_id = kodi_item.get('id')
item.kodi_type = kodi_item.get('type')
if item.kodi_id:
@ -343,7 +694,7 @@ def playlist_item_from_kodi(kodi_item):
if db_item:
item.plex_id = db_item['plex_id']
item.plex_type = db_item['plex_type']
item.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-)
item.plex_uuid = db_item['section_uuid']
item.file = kodi_item.get('file')
if item.plex_id is None and item.file is not None:
try:
@ -413,7 +764,7 @@ def playlist_item_from_plex(plex_id):
Returns a Playlist_Item
"""
item = Playlist_Item()
item = PlaylistItem()
item.plex_id = plex_id
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id)
@ -436,7 +787,7 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
"""
item = Playlist_Item()
item = PlaylistItem()
api = API(xml_video_element)
item.plex_id = api.plex_id()
item.plex_type = api.plex_type()
@ -612,12 +963,14 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
Returns the PKC PlayList item or raises PlaylistError
"""
LOG.debug('Adding item to Plex playqueue with plex id %s, kodi_item %s at '
'position %s', plex_id, kodi_item, pos)
verify_kodi_item(plex_id, kodi_item)
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri)
url = '{server}/%ss/%s?uri=%s' % (playlist.kind, playlist.id, item.uri)
# Will always put the new item at the end of the Plex playlist
xml = DU().downloadUrl(url, action_type="PUT")
try:
@ -625,21 +978,27 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
except (TypeError, AttributeError, KeyError, IndexError):
raise PlaylistError('Could not add item %s to playlist %s'
% (kodi_item, playlist))
api = API(xml[-1])
item.xml = xml[-1]
if len(xml) != len(playlist.items) + 1:
raise PlaylistError('Couldnt add item %s to playlist %s - wrong length'
% (kodi_item, playlist))
for actual_pos, xml_video_element in enumerate(xml):
api = API(xml_video_element)
if api.plex_id() == item.plex_id:
break
else:
raise PlaylistError('Something went terribly wrong!')
utils.dump_xml(xml)
LOG.debug('Plex added the new item at position %s', actual_pos)
item.xml = xml[actual_pos]
item.id = api.item_id()
item.guid = api.guid_html_escaped()
item.offset = api.resume_point()
item.playcount = api.viewcount()
playlist.items.append(item)
if pos == len(playlist.items) - 1:
# Item was added at the end
playlist.items.insert(actual_pos, item)
_get_playListVersion_from_xml(playlist, xml)
else:
if actual_pos != pos:
# Move the new item to the correct position
move_playlist_item(playlist,
len(playlist.items) - 1,
pos)
move_playlist_item(playlist, actual_pos, pos)
LOG.debug('Successfully added item on the Plex side: %s', playlist)
return item
@ -710,6 +1069,7 @@ def move_playlist_item(playlist, before_pos, after_pos):
LOG.error('Could not move playlist item')
return
_get_playListVersion_from_xml(playlist, xml)
utils.dump_xml(xml)
# Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
LOG.debug('Done moving for %s', playlist)

View file

@ -18,7 +18,7 @@ LOG = getLogger('PLEX.playqueue')
PLUGIN = 'plugin://%s' % v.ADDON_ID
# Our PKC playqueues (3 instances of Playqueue_Object())
# Our PKC playqueues (3 instances PlayQueue())
PLAYQUEUES = []
###############################################################################
@ -38,7 +38,7 @@ def init_playqueues():
for queue in js.get_playlists():
if queue['playlistid'] != i:
continue
playqueue = PL.Playqueue_Object()
playqueue = PL.PlayQueue()
playqueue.playlistid = i
playqueue.type = queue['type']
# Initialize each Kodi playlist
@ -206,6 +206,8 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
with app.APP.lock_playqueues:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
playqueue.old_kodi_pl = list(kodi_pl)
continue
if playqueue.old_kodi_pl != kodi_pl:
if playqueue.id is None and (not app.SYNC.direct_paths or
app.PLAYSTATE.context_menu_play):
@ -215,5 +217,4 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
else:
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl)
app.APP.monitor.waitForAbort(0.2)

View file

@ -2,13 +2,8 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from .plex_api import API
from .playutils import PlayUtils
from .windows.resume import resume_dialog
from . import app, plex_functions as PF, utils, json_rpc, variables as v, \
widgets, playlist_func as PL, playqueue as PQ
from . import app, utils, json_rpc, variables as v, playlist_func as PL, \
playqueue as PQ
LOG = getLogger('PLEX.playstrm')
@ -27,15 +22,8 @@ class PlayStrm(object):
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.playqueue_item = None
self.api = None
self.start_index = None
self.index = None
self.server_id = server_id
def __init__(self, params):
LOG.debug('Starting PlayStrm with params: %s', params)
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':
@ -44,97 +32,46 @@ class PlayStrm(object):
self.synched = True
self.kodi_id = utils.cast(int, params.get('kodi_id'))
self.kodi_type = params.get('kodi_type')
self._get_xml()
self.name = self.api.title()
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
self.force_transcode = params.get('transcode') == 'true'
if app.PLAYSTATE.audioplaylist:
LOG.debug('Audio playlist detected')
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
else:
LOG.debug('Video playlist detected')
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
self.kodi_playlist = self.playqueue.kodi_pl
def __repr__(self):
def __unicode__(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__
"}}").format(self=self)
def playlist_add_json(self):
playlistid = self.kodi_playlist.getPlayListId()
LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s',
self.kodi_id, self.kodi_type, playlistid, self.index)
if self.index is None:
json_rpc.playlist_add(playlistid,
{'%sid' % self.kodi_type: self.kodi_id})
else:
json_rpc.playlist_insert({'playlistid': playlistid,
'position': self.index,
'item': {'%sid' % self.kodi_type: self.kodi_id}})
def playlist_add(self, url, listitem):
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
self.playqueue_item.file = url.decode('utf-8')
self.playqueue.items.insert(self.index, self.playqueue_item)
self.index += 1
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 set_playqueue_item(self, xml, kodi_id, kodi_type):
self.playqueue_item = PL.playlist_item_from_xml(xml,
kodi_id=kodi_id,
kodi_type=kodi_type)
self.playqueue_item.force_transcode = self.transcode
def start_playback(self, index=0):
LOG.debug('Starting playback at %s', index)
xbmc.Player().play(self.kodi_playlist, startpos=index, windowed=False)
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def play(self, start_position=None, delayed=True):
'''
Create and add listitems to the Kodi playlist.
Create and add a single listitem to the Kodi playlist, potentially
with trailers and different file-parts
'''
LOG.debug('play called with start_position %s, delayed %s',
start_position, delayed)
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
self._set_playlist()
LOG.debug('Kodi playlist BEFORE: %s',
json_rpc.playlist_get_items(self.playqueue.playlistid))
self.playqueue.init(self.plex_id,
plex_type=self.plex_type,
position=start_position,
synched=self.synched,
force_transcode=self.force_transcode)
LOG.info('Initiating play for %s', self)
LOG.debug('Kodi playlist AFTER: %s',
json_rpc.playlist_get_items(self.playqueue.playlistid))
if not delayed:
self.start_playback(self.start_index)
return self.index
self.playqueue.start_playback(start_position)
return self.playqueue.index
def play_folder(self, position=None):
'''
@ -142,136 +79,13 @@ class PlayStrm(object):
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.playlist_add_json()
self.index += 1
else:
listitem = widgets.get_listitem(self.xml[0], resume=True)
url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT
args = {
'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 = utils.extend_url(url, args).encode('utf-8')
listitem.setPath(url)
self.playlist_add(url, listitem)
return self.index - 1
def _set_playlist(self):
'''
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()
trailers = False
if (not seektime and self.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(self.plex_id,
self.xml.get('librarySectionUUID'),
mediatype=self.plex_type,
trailers=trailers)
if xml is None:
LOG.error('Could not get playqueue for UUID %s for %s',
self.xml.get('librarySectionUUID'), self)
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.resume_playback = False
return
PL.get_playlist_details_from_xml(self.playqueue, xml)
# See that we add trailers, if they exist in the xml return
self._add_intros(xml)
# Add the main item
if seektime:
listitem = widgets.get_listitem(self.xml[0], resume=True)
else:
listitem = widgets.get_listitem(self.xml[0], resume=False)
listitem.setSubtitles(self.api.cache_external_subs())
self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type)
play = PlayUtils(self.api, self.playqueue_item)
url = play.getPlayUrl().encode('utf-8')
listitem.setPath(url)
self.playlist_add(url, listitem)
# Add additional file parts, if any exist
self._add_additional_parts()
def _resume(self):
'''
Resume item if available. Returns bool or raise an PlayStrmException if
resume was cancelled by user.
'''
seektime = app.PLAYSTATE.resume_playback
app.PLAYSTATE.resume_playback = None
if app.PLAYSTATE.autoplay:
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('User chose resume: %s', seektime)
if seektime is None:
raise PlayStrmException('User backed out of resume dialog.')
app.PLAYSTATE.autoplay = True
return seektime
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 intro in xml:
api = API(intro)
if not api.plex_type() == v.PLEX_TYPE_CLIP:
# E.g. the main item we're looking at - skip!
continue
LOG.debug('Adding trailer: %s', api.title())
listitem = widgets.get_listitem(intro, resume=False)
self.set_playqueue_item(intro, None, None)
play = PlayUtils(api, self.playqueue_item)
url = play.getPlayUrl().encode('utf-8')
listitem.setPath(url)
self.playlist_add(url, listitem)
def _add_additional_parts(self):
''' Create listitems and add them to the stack of playlist.
'''
for part, _ in enumerate(self.xml[0][0]):
if part == 0:
# The first part that we've already added
continue
self.api.set_part_number(part)
LOG.debug('Adding addional part %s', part)
self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type)
self.playqueue_item.part = part
listitem = widgets.get_listitem(self.xml[0], resume=False)
listitem.setSubtitles(self.api.cache_external_subs())
playqueue_item = PL.playlist_item_from_xml(self.xml[0])
play = PlayUtils(self.api, playqueue_item)
url = play.getPlayUrl().encode('utf-8')
listitem.setPath(url)
self.playlist_add(url, listitem)
start_position = position or max(self.playqueue.kodi_pl.size(), 0)
index = start_position + 1
LOG.info('Play folder plex_id %s, index: %s', self.plex_id, index)
item = PL.PlaylistItem(plex_id=self.plex_id,
plex_type=self.plex_type,
kodi_id=self.kodi_id,
kodi_type=self.kodi_type)
self.playqueue.add_item(item, index)
index += 1
return index - 1

View file

@ -14,13 +14,13 @@ LOG = getLogger('PLEX.playutils')
class PlayUtils():
def __init__(self, api, playqueue_item):
def __init__(self, api, playlistitem):
"""
init with api (PlexAPI wrapper of the PMS xml element) and
playqueue_item (Playlist_Item())
playlistitem [PlaylistItem()]
"""
self.api = api
self.item = playqueue_item
self.item = playlistitem
def getPlayUrl(self):
"""

View file

@ -20,7 +20,7 @@ class Playlists(object):
def delete_playlist(self, playlist):
"""
Removes the entry for playlist [Playqueue_Object] from the Plex
Removes the entry for playlist [PlayQueue] from the Plex
playlists table.
Be sure to either set playlist.id or playlist.kodi_path
"""

View file

@ -820,14 +820,14 @@ def get_plex_sections():
return xml
def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie',
def init_plex_playqueue(plex_id, librarySectionUUID, plex_type='movie',
trailers=False):
"""
Returns raw API metadata XML dump for a playlist with e.g. trailers.
"""
url = "{server}/playQueues"
args = {
'type': mediatype,
'type': plex_type,
'uri': ('library://{0}/item/%2Flibrary%2Fmetadata%2F{1}'.format(
librarySectionUUID, plex_id)),
'includeChapters': '1',

View file

@ -69,6 +69,16 @@ def getGlobalProperty(key):
'Window(10000).Property(plugin.video.plexkodiconnect.{0})'.format(key))
def dump_xml(xml):
tree = etree.ElementTree(xml)
i = 0
while path_ops.exists(path_ops.path.join(v.ADDON_PROFILE, 'xml%s.xml' % i)):
i += 1
tree.write(path_ops.path.join(v.ADDON_PROFILE, 'xml%s.xml' % i),
encoding='utf-8')
LOG.debug('Dumped to xml: %s', 'xml%s.xml' % i)
def reboot_kodi(message=None):
"""
Displays an OK prompt with 'Kodi will now restart to apply the changes'

View file

@ -13,8 +13,8 @@ import Queue
import xbmc
import xbmcvfs
from . import backgroundthread, utils, variables as v, app
from .playstrm import PlayStrm
from . import backgroundthread, utils, variables as v, app, playqueue as PQ
from . import playlist_func as PL, json_rpc as js
LOG = getLogger('PLEX.webservice')
@ -270,58 +270,110 @@ class QueuePlay(backgroundthread.KillableThread):
def __init__(self, server):
self.server = server
self.plex_id = None
self.plex_type = None
self.kodi_id = None
self.kodi_type = None
self.synched = None
self.force_transcode = None
super(QueuePlay, self).__init__()
def load_params(self, params):
self.plex_id = utils.cast(int, params['plex_id'])
self.plex_type = params.get('plex_type')
self.kodi_id = utils.cast(int, params.get('kodi_id'))
self.kodi_type = params.get('kodi_type')
if params.get('synched') and params['synched'].lower() == 'false':
self.synched = False
else:
self.synched = True
if params.get('transcode') and params['transcode'].lower() == 'true':
self.force_transcode = True
else:
self.force_transcode = False
def run(self):
LOG.info('##===---- Starting QueuePlay ----===##')
LOG.debug('##===---- Starting QueuePlay ----===##')
if app.PLAYSTATE.audioplaylist:
LOG.debug('Audio playlist detected')
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
else:
LOG.debug('Video playlist detected')
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
abort = False
play_folder = False
play = None
start_position = None
position = None
# Let Kodi catch up
# Position to start playback from (!!)
# Do NOT use kodi_pl.getposition() as that appears to be buggy
start_position = max(js.get_position(playqueue.playlistid), 0)
# Position to add next element to queue - we're doing this at the end
# of our playqueue
position = playqueue.kodi_pl.size()
LOG.debug('start %s, position %s for current playqueue: %s',
start_position, position, playqueue)
# Make sure we got at least 2 items in the queue - ugly
# TODO: find a better solution
xbmc.sleep(200)
while True:
try:
try:
params = self.server.queue.get(timeout=0.1)
except Queue.Empty:
count = 20
count = 50
while not utils.window('plex.playlist.ready'):
xbmc.sleep(50)
if not count:
LOG.info('Playback aborted')
raise Exception('PlaybackAborted')
raise Exception('Playback aborted')
count -= 1
LOG.info('Starting playback at position: %s', start_position)
if play_folder:
LOG.info('Start playing folder')
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
play.start_playback()
playqueue.start_playback(start_position)
else:
# TODO - do we need to do anything here?
# Originally, 1st failable item should have been removed
utils.window('plex.playlist.play', value='true')
# xbmc.sleep(1000)
play.remove_from_playlist(start_position)
# playqueue.kodi_remove_item(start_position)
break
play = PlayStrm(params, params.get('ServerId'))
if start_position is None:
start_position = max(play.kodi_playlist.getposition(), 0)
position = start_position + 1
self.load_params(params)
if play_folder:
position = play.play_folder(position)
# position = play.play_folder(position)
item = PL.PlaylistItem(plex_id=self.plex_id,
plex_type=self.plex_type,
kodi_id=self.kodi_id,
kodi_type=self.kodi_type)
item.force_transcode = self.force_transcode
playqueue.add_item(item, position)
position += 1
else:
if self.server.pending.count(params['plex_id']) != len(self.server.pending):
LOG.debug('Folder playback detected')
play_folder = True
utils.window('plex.playlist.start', str(start_position))
position = play.play(position)
playqueue.init(self.plex_id,
plex_type=self.plex_type,
position=position,
synched=self.synched,
force_transcode=self.force_transcode)
# Do NOT start playback here - because Kodi already started
# it!
# playqueue.start_playback(position)
position = playqueue.index
if play_folder:
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
except PL.PlaylistError as error:
abort = True
LOG.warn('Not playing due to the following: %s', error)
except Exception:
abort = True
utils.ERROR()
play.kodi_playlist.clear()
try:
self.server.queue.task_done()
except ValueError:
# "task_done() called too many times"
pass
if abort:
playqueue.clear()
xbmc.Player().stop()
self.server.queue.queue.clear()
if play_folder:
@ -329,11 +381,10 @@ class QueuePlay(backgroundthread.KillableThread):
else:
utils.window('plex.playlist.aborted', value='true')
break
self.server.queue.task_done()
utils.window('plex.playlist.ready', clear=True)
utils.window('plex.playlist.start', clear=True)
app.PLAYSTATE.audioplaylist = None
self.server.threads.remove(self)
self.server.pending = []
LOG.info('##===---- QueuePlay Stopped ----===##')
LOG.debug('##===---- QueuePlay Stopped ----===##')