Refactoring
This commit is contained in:
parent
d397fb5b20
commit
9d517c2c3d
12 changed files with 1116 additions and 1478 deletions
|
@ -14,9 +14,8 @@ import xbmcgui
|
|||
from .plex_db import PlexDB
|
||||
from . import kodi_db
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import utils, timing, plex_functions as PF
|
||||
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||
from . import backgroundthread, app, variables as v
|
||||
from . import utils, timing, plex_functions as PF, json_rpc as js
|
||||
from . import playqueue as PQ, backgroundthread, app, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.kodimonitor')
|
||||
|
||||
|
@ -256,8 +255,8 @@ class KodiMonitor(xbmc.Monitor):
|
|||
position = info['position'] if info['position'] != -1 else 0
|
||||
kodi_playlist = js.playlist_get_items(self.playerid)
|
||||
LOG.debug('Current Kodi playlist: %s', kodi_playlist)
|
||||
playlistitem = PL.PlaylistItem(kodi_item=kodi_playlist[position])
|
||||
if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy):
|
||||
playlistitem = PQ.PlaylistItem(kodi_item=kodi_playlist[position])
|
||||
if isinstance(self.playqueue.items[0], PQ.PlaylistItemDummy):
|
||||
# This dummy item will be deleted by webservice soon - it won't
|
||||
# play
|
||||
LOG.debug('Dummy item detected')
|
||||
|
@ -328,8 +327,10 @@ class KodiMonitor(xbmc.Monitor):
|
|||
LOG.debug('No Plex id obtained - aborting playback report')
|
||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||
return
|
||||
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
||||
item.file = path
|
||||
playlistitem = PQ.PlaylistItem(plex_id=plex_id,
|
||||
grab_xml=True)
|
||||
playlistitem.file = path
|
||||
self.playqueue.init(playlistitem)
|
||||
# Set the Plex container key (e.g. using the Plex playqueue)
|
||||
container_key = None
|
||||
if info['playlistid'] != -1:
|
||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
|||
|
||||
from .plex_api import API
|
||||
from . import utils, context_entry, transfer, backgroundthread, variables as v
|
||||
from . import app, plex_functions as PF, playqueue as PQ, playlist_func as PL
|
||||
from . import app, plex_functions as PF, playqueue as PQ
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -81,7 +81,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.PlaylistItem(xml_video_element=xml[0])
|
||||
item = PQ.PlaylistItem(xml_video_element=xml[0])
|
||||
item.offset = offset
|
||||
item.playmethod = 'DirectStream'
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
14
resources/lib/playqueue/__init__.py
Normal file
14
resources/lib/playqueue/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from .common import PlaylistItem, PlaylistItemDummy, PlaylistError, PLAYQUEUES
|
||||
from .playqueue import PlayQueue
|
||||
from .monitor import PlayqueueMonitor
|
||||
from .functions import init_playqueues, get_playqueue_from_type, \
|
||||
playqueue_from_plextype, playqueue_from_id, get_PMS_playlist, \
|
||||
init_playqueue_from_plex_children, get_pms_playqueue, \
|
||||
get_plextype_from_xml
|
301
resources/lib/playqueue/common.py
Normal file
301
resources/lib/playqueue/common.py
Normal file
|
@ -0,0 +1,301 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from ..plex_db import PlexDB
|
||||
from ..plex_api import API
|
||||
from .. import plex_functions as PF, utils, kodi_db, variables as v, app
|
||||
|
||||
LOG = getLogger('PLEX.playqueue')
|
||||
|
||||
# Our PKC playqueues (3 instances PlayQueue())
|
||||
PLAYQUEUES = []
|
||||
|
||||
|
||||
class PlaylistError(Exception):
|
||||
"""
|
||||
Exception for our playlist constructs
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistItem(object):
|
||||
"""
|
||||
Object to fill our playqueues and playlists with.
|
||||
|
||||
id = None [int] Plex playlist/playqueue id, e.g. playQueueItemID
|
||||
plex_id = None [int] Plex unique item id, "ratingKey"
|
||||
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
|
||||
plex_uuid = None [str] Plex librarySectionUUID
|
||||
kodi_id = None [int] Kodi unique kodi id (unique only within type!)
|
||||
kodi_type = None [str] Kodi type: 'movie'
|
||||
file = None [str] Path to the item's file. STRING!!
|
||||
uri = None [str] Weird Plex uri path involving plex_uuid. STRING!
|
||||
guid = None [str] Weird Plex guid
|
||||
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
|
||||
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
|
||||
playcount = None [int] how many times the item has already been played
|
||||
offset = None [int] the item's view offset UPON START in Plex time
|
||||
part = 0 [int] part number if Plex video consists of mult. parts
|
||||
force_transcode [bool] defaults to False
|
||||
|
||||
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, plex_id=None, plex_type=None, xml_video_element=None,
|
||||
kodi_id=None, kodi_type=None, kodi_item=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 = kodi_id
|
||||
self.kodi_type = kodi_type
|
||||
self.file = None
|
||||
if kodi_item:
|
||||
self.kodi_id = kodi_item.get('id')
|
||||
self.kodi_type = kodi_item.get('type')
|
||||
self.file = kodi_item.get('file')
|
||||
self.uri = None
|
||||
self.guid = None
|
||||
self.xml = None
|
||||
self.playmethod = None
|
||||
self.playcount = None
|
||||
self.offset = None
|
||||
self.part = 0
|
||||
self.force_transcode = False
|
||||
# Shall we ask user to resume this item?
|
||||
# None: ask user to resume
|
||||
# False: do NOT resume, don't ask user
|
||||
# True: do resume, don't ask user
|
||||
self.resume = None
|
||||
if (self.plex_id is None and
|
||||
(self.kodi_id is not None and self.kodi_type is not None)):
|
||||
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 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 (self.kodi_id is None or self.kodi_type is None) and
|
||||
self.plex_type != v.PLEX_TYPE_CLIP):
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_item = plexdb.item_by_id(self.plex_id, self.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']
|
||||
if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and
|
||||
self.plex_type != v.PLEX_TYPE_CLIP):
|
||||
self._guess_id_from_file()
|
||||
self.set_uri()
|
||||
|
||||
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, other):
|
||||
return not self == other
|
||||
|
||||
def __unicode__(self):
|
||||
return ("{{"
|
||||
"'name': '{self.name}', "
|
||||
"'id': {self.id}, "
|
||||
"'plex_id': {self.plex_id}, "
|
||||
"'plex_type': '{self.plex_type}', "
|
||||
"'kodi_id': {self.kodi_id}, "
|
||||
"'kodi_type': '{self.kodi_type}', "
|
||||
"'file': '{self.file}', "
|
||||
"'uri': '{self.uri}', "
|
||||
"'guid': '{self.guid}', "
|
||||
"'playmethod': '{self.playmethod}', "
|
||||
"'playcount': {self.playcount}, "
|
||||
"'offset': {self.offset}, "
|
||||
"'force_transcode': {self.force_transcode}, "
|
||||
"'part': {self.part}"
|
||||
"}}".format(self=self))
|
||||
|
||||
def __str__(self):
|
||||
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
|
||||
self.set_uri()
|
||||
|
||||
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 _guess_id_from_file(self):
|
||||
"""
|
||||
"""
|
||||
if not self.file:
|
||||
return
|
||||
# Special case playlist startup - got type but no id
|
||||
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
|
||||
self.kodi_type == v.KODI_TYPE_SONG and
|
||||
self.file.startswith('http')):
|
||||
self.kodi_id, _ = kodi_db.kodiid_from_filename(self.file,
|
||||
v.KODI_TYPE_SONG)
|
||||
LOG.debug('Detected song. Research results: %s', self)
|
||||
return
|
||||
# Need more info since we don't have kodi_id nor type. Use file path.
|
||||
if (self.file.startswith('plugin') or
|
||||
(self.file.startswith('http') and not
|
||||
self.file.startswith('http://127.0.0.1:%s' % v.WEBSERVICE_PORT))):
|
||||
return
|
||||
LOG.debug('Starting research for Kodi id since we didnt get one')
|
||||
# Try the VIDEO DB first - will find both movies and episodes
|
||||
self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file,
|
||||
db_type='video')
|
||||
if self.kodi_id is None:
|
||||
# No movie or episode found - try MUSIC DB now for songs
|
||||
self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file,
|
||||
db_type='music')
|
||||
self.kodi_type = None if self.kodi_id is None else self.kodi_type
|
||||
LOG.debug('Research results for guessing Kodi id: %s', self)
|
||||
|
||||
def plex_stream_index(self, kodi_stream_index, stream_type):
|
||||
"""
|
||||
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||
index.
|
||||
|
||||
stream_type: 'video', 'audio', 'subtitle'
|
||||
|
||||
Returns None if unsuccessful
|
||||
"""
|
||||
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
||||
count = 0
|
||||
if kodi_stream_index == -1:
|
||||
# Kodi telling us "it's the last one"
|
||||
iterator = list(reversed(self.xml[0][self.part]))
|
||||
kodi_stream_index = 0
|
||||
else:
|
||||
iterator = self.xml[0][self.part]
|
||||
# Kodi indexes differently than Plex
|
||||
for stream in iterator:
|
||||
if (stream.attrib['streamType'] == stream_type and
|
||||
'key' in stream.attrib):
|
||||
if count == kodi_stream_index:
|
||||
return stream.attrib['id']
|
||||
count += 1
|
||||
for stream in iterator:
|
||||
if (stream.attrib['streamType'] == stream_type and
|
||||
'key' not in stream.attrib):
|
||||
if count == kodi_stream_index:
|
||||
return stream.attrib['id']
|
||||
count += 1
|
||||
|
||||
def kodi_stream_index(self, plex_stream_index, stream_type):
|
||||
"""
|
||||
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||
index.
|
||||
|
||||
stream_type: 'video', 'audio', 'subtitle'
|
||||
|
||||
Returns None if unsuccessful
|
||||
"""
|
||||
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
||||
count = 0
|
||||
for stream in self.xml[0][self.part]:
|
||||
if (stream.attrib['streamType'] == stream_type and
|
||||
'key' in stream.attrib):
|
||||
if stream.attrib['id'] == plex_stream_index:
|
||||
return count
|
||||
count += 1
|
||||
for stream in self.xml[0][self.part]:
|
||||
if (stream.attrib['streamType'] == stream_type and
|
||||
'key' not in stream.attrib):
|
||||
if stream.attrib['id'] == plex_stream_index:
|
||||
return count
|
||||
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
|
164
resources/lib/playqueue/functions.py
Normal file
164
resources/lib/playqueue/functions.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
import xbmc
|
||||
|
||||
from .common import PLAYQUEUES, PlaylistItem
|
||||
from .playqueue import PlayQueue
|
||||
|
||||
from ..downloadutils import DownloadUtils as DU
|
||||
from .. import json_rpc as js, app, variables as v, plex_functions as PF
|
||||
from .. import utils
|
||||
|
||||
LOG = getLogger('PLEX.playqueue_functions')
|
||||
|
||||
|
||||
def init_playqueues():
|
||||
"""
|
||||
Call this once on startup to initialize the PKC playqueue objects in
|
||||
the list PLAYQUEUES
|
||||
"""
|
||||
if PLAYQUEUES:
|
||||
LOG.debug('Playqueues have already been initialized')
|
||||
return
|
||||
# Initialize Kodi playqueues
|
||||
with app.APP.lock_playqueues:
|
||||
for i in (0, 1, 2):
|
||||
# Just in case the Kodi response is not sorted correctly
|
||||
for queue in js.get_playlists():
|
||||
if queue['playlistid'] != i:
|
||||
continue
|
||||
playqueue = PlayQueue()
|
||||
playqueue.playlistid = i
|
||||
playqueue.type = queue['type']
|
||||
# Initialize each Kodi playlist
|
||||
if playqueue.type == v.KODI_TYPE_AUDIO:
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
||||
elif playqueue.type == v.KODI_TYPE_VIDEO:
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
else:
|
||||
# Currently, only video or audio playqueues available
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
# Overwrite 'picture' with 'photo'
|
||||
playqueue.type = v.KODI_TYPE_PHOTO
|
||||
PLAYQUEUES.append(playqueue)
|
||||
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
|
||||
|
||||
|
||||
def get_playqueue_from_type(kodi_playlist_type):
|
||||
"""
|
||||
Returns the playqueue according to the kodi_playlist_type ('video',
|
||||
'audio', 'picture') passed in
|
||||
"""
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.type == kodi_playlist_type:
|
||||
break
|
||||
else:
|
||||
raise ValueError('Wrong playlist type passed in: %s'
|
||||
% kodi_playlist_type)
|
||||
return playqueue
|
||||
|
||||
|
||||
def playqueue_from_plextype(plex_type):
|
||||
if plex_type in v.PLEX_VIDEOTYPES:
|
||||
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
|
||||
elif plex_type in v.PLEX_AUDIOTYPES:
|
||||
plex_type = v.PLEX_TYPE_AUDIO_PLAYLIST
|
||||
else:
|
||||
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.type == plex_type:
|
||||
break
|
||||
return playqueue
|
||||
|
||||
|
||||
def playqueue_from_id(kodi_playlist_id):
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.playlistid == kodi_playlist_id:
|
||||
break
|
||||
else:
|
||||
raise ValueError('Wrong playlist id passed in: %s of type %s'
|
||||
% (kodi_playlist_id, type(kodi_playlist_id)))
|
||||
return playqueue
|
||||
|
||||
|
||||
def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||
"""
|
||||
Init a new playqueue e.g. from an album. Alexa does this
|
||||
|
||||
Returns the playqueue
|
||||
"""
|
||||
xml = PF.GetAllPlexChildren(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not download the PMS xml for %s', plex_id)
|
||||
return
|
||||
playqueue = get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
||||
playqueue.clear()
|
||||
for i, child in enumerate(xml):
|
||||
playlistitem = PlaylistItem(xml_video_element=child)
|
||||
playqueue.add_item(playlistitem, i)
|
||||
playqueue.plex_transient_token = transient_token
|
||||
LOG.debug('Firing up Kodi player')
|
||||
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
|
||||
return playqueue
|
||||
|
||||
|
||||
def get_PMS_playlist(playlist=None, playlist_id=None):
|
||||
"""
|
||||
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
|
||||
need to fetch a new playlist
|
||||
|
||||
Returns None if something went wrong
|
||||
"""
|
||||
playlist_id = playlist_id if playlist_id else playlist.id
|
||||
if playlist and playlist.kind == 'playList':
|
||||
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id)
|
||||
else:
|
||||
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id)
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
xml = None
|
||||
return xml
|
||||
|
||||
|
||||
def get_pms_playqueue(playqueue_id):
|
||||
"""
|
||||
Returns the Plex playqueue as an etree XML or None if unsuccessful
|
||||
"""
|
||||
xml = DU().downloadUrl(
|
||||
"{server}/playQueues/%s" % playqueue_id,
|
||||
headerOptions={'Accept': 'application/xml'})
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
LOG.error('Could not download Plex playqueue %s', playqueue_id)
|
||||
xml = None
|
||||
return xml
|
||||
|
||||
|
||||
def get_plextype_from_xml(xml):
|
||||
"""
|
||||
Needed if PMS returns an empty playqueue. Will get the Plex type from the
|
||||
empty playlist playQueueSourceURI. Feed with (empty) etree xml
|
||||
|
||||
returns None if unsuccessful
|
||||
"""
|
||||
try:
|
||||
plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
|
||||
xml.attrib['playQueueSourceURI'])[0]
|
||||
except IndexError:
|
||||
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
|
||||
return
|
||||
new_xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
new_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not get plex metadata for plex id %s', plex_id)
|
||||
return
|
||||
return new_xml[0].attrib.get('type').decode('utf-8')
|
|
@ -7,113 +7,11 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from logging import getLogger
|
||||
import copy
|
||||
|
||||
import xbmc
|
||||
|
||||
from .plex_api import API
|
||||
from . import playlist_func as PL, plex_functions as PF
|
||||
from . import backgroundthread, utils, json_rpc as js, app, variables as v
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playqueue')
|
||||
|
||||
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
||||
|
||||
# Our PKC playqueues (3 instances PlayQueue())
|
||||
PLAYQUEUES = []
|
||||
###############################################################################
|
||||
from .common import PlaylistError, PlaylistItem, PLAYQUEUES
|
||||
from .. import backgroundthread, json_rpc as js, utils, app
|
||||
|
||||
|
||||
def init_playqueues():
|
||||
"""
|
||||
Call this once on startup to initialize the PKC playqueue objects in
|
||||
the list PLAYQUEUES
|
||||
"""
|
||||
if PLAYQUEUES:
|
||||
LOG.debug('Playqueues have already been initialized')
|
||||
return
|
||||
# Initialize Kodi playqueues
|
||||
with app.APP.lock_playqueues:
|
||||
for i in (0, 1, 2):
|
||||
# Just in case the Kodi response is not sorted correctly
|
||||
for queue in js.get_playlists():
|
||||
if queue['playlistid'] != i:
|
||||
continue
|
||||
playqueue = PL.PlayQueue()
|
||||
playqueue.playlistid = i
|
||||
playqueue.type = queue['type']
|
||||
# Initialize each Kodi playlist
|
||||
if playqueue.type == v.KODI_TYPE_AUDIO:
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
||||
elif playqueue.type == v.KODI_TYPE_VIDEO:
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
else:
|
||||
# Currently, only video or audio playqueues available
|
||||
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
# Overwrite 'picture' with 'photo'
|
||||
playqueue.type = v.KODI_TYPE_PHOTO
|
||||
PLAYQUEUES.append(playqueue)
|
||||
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
|
||||
|
||||
|
||||
def get_playqueue_from_type(kodi_playlist_type):
|
||||
"""
|
||||
Returns the playqueue according to the kodi_playlist_type ('video',
|
||||
'audio', 'picture') passed in
|
||||
"""
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.type == kodi_playlist_type:
|
||||
break
|
||||
else:
|
||||
raise ValueError('Wrong playlist type passed in: %s'
|
||||
% kodi_playlist_type)
|
||||
return playqueue
|
||||
|
||||
|
||||
def playqueue_from_plextype(plex_type):
|
||||
if plex_type in v.PLEX_VIDEOTYPES:
|
||||
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
|
||||
elif plex_type in v.PLEX_AUDIOTYPES:
|
||||
plex_type = v.PLEX_TYPE_AUDIO_PLAYLIST
|
||||
else:
|
||||
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.type == plex_type:
|
||||
break
|
||||
return playqueue
|
||||
|
||||
|
||||
def playqueue_from_id(kodi_playlist_id):
|
||||
for playqueue in PLAYQUEUES:
|
||||
if playqueue.playlistid == kodi_playlist_id:
|
||||
break
|
||||
else:
|
||||
raise ValueError('Wrong playlist id passed in: %s of type %s'
|
||||
% (kodi_playlist_id, type(kodi_playlist_id)))
|
||||
return playqueue
|
||||
|
||||
|
||||
def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||
"""
|
||||
Init a new playqueue e.g. from an album. Alexa does this
|
||||
|
||||
Returns the playqueue
|
||||
"""
|
||||
xml = PF.GetAllPlexChildren(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not download the PMS xml for %s', plex_id)
|
||||
return
|
||||
playqueue = get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
||||
playqueue.clear()
|
||||
for i, child in enumerate(xml):
|
||||
api = API(child)
|
||||
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
|
||||
playqueue.plex_transient_token = transient_token
|
||||
LOG.debug('Firing up Kodi player')
|
||||
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
|
||||
return playqueue
|
||||
LOG = getLogger('PLEX.playqueue_monitor')
|
||||
|
||||
|
||||
class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||
|
@ -169,26 +67,24 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
LOG.debug('Playqueue item %s moved to position %s',
|
||||
i + j, i)
|
||||
try:
|
||||
PL.move_playlist_item(playqueue, i + j, i)
|
||||
except PL.PlaylistError:
|
||||
playqueue.plex_move_item(i + j, i)
|
||||
except PlaylistError:
|
||||
LOG.error('Could not modify playqueue positions')
|
||||
LOG.error('This is likely caused by mixing audio and '
|
||||
'video tracks in the Kodi playqueue')
|
||||
del old[j], index[j]
|
||||
break
|
||||
else:
|
||||
playlistitem = PlaylistItem(kodi_item=new_item)
|
||||
LOG.debug('Detected new Kodi element at position %s: %s ',
|
||||
i, new_item)
|
||||
i, playlistitem)
|
||||
try:
|
||||
if playqueue.id is None:
|
||||
PL.init_plex_playqueue(playqueue, kodi_item=new_item)
|
||||
playqueue.init(playlistitem)
|
||||
else:
|
||||
PL.add_item_to_plex_playqueue(playqueue,
|
||||
i,
|
||||
kodi_item=new_item)
|
||||
except PL.PlaylistError:
|
||||
# Could not add the element
|
||||
pass
|
||||
playqueue.plex_add_item(playlistitem, i)
|
||||
except PlaylistError:
|
||||
LOG.warn('Couldnt add new item to Plex: %s', playlistitem)
|
||||
except IndexError:
|
||||
# This is really a hack - happens when using Addon Paths
|
||||
# and repeatedly starting the same element. Kodi will then
|
||||
|
@ -206,8 +102,8 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
return
|
||||
LOG.debug('Detected deletion of playqueue element at pos %s', i)
|
||||
try:
|
||||
PL.delete_playlist_item_from_PMS(playqueue, i)
|
||||
except PL.PlaylistError:
|
||||
playqueue.plex_remove_item(i)
|
||||
except PlaylistError:
|
||||
LOG.error('Could not delete PMS element from position %s', i)
|
||||
LOG.error('This is likely caused by mixing audio and '
|
||||
'video tracks in the Kodi playqueue')
|
601
resources/lib/playqueue/playqueue.py
Normal file
601
resources/lib/playqueue/playqueue.py
Normal file
|
@ -0,0 +1,601 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import threading
|
||||
|
||||
from .common import PlaylistItem, PlaylistItemDummy, PlaylistError
|
||||
|
||||
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)
|
||||
raise PlaylistError()
|
||||
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):
|
||||
raise PlaylistError('Could not get Plex metadata %s for %s',
|
||||
plex_id, self.items[startpos])
|
||||
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):
|
||||
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)
|
||||
|
||||
@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=None):
|
||||
if not playlistitem:
|
||||
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 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:
|
||||
raise PlaylistError('Kodi did not add item to playlist: %s',
|
||||
answ)
|
||||
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):
|
||||
raise PlaylistError('Could not get metadata for %s', item)
|
||||
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
|
||||
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))
|
||||
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['error'])
|
||||
|
||||
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)
|
||||
raise PlaylistError()
|
||||
|
||||
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:
|
||||
raise PlaylistError('Illegal original position %s and/or desired '
|
||||
'position %s for playlist length %s' %
|
||||
(before, after, len(self.items)))
|
||||
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):
|
||||
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 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)
|
0
resources/lib/playqueue/queue.py
Normal file
0
resources/lib/playqueue/queue.py
Normal file
|
@ -2,8 +2,7 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import app, utils, json_rpc, variables as v, playlist_func as PL, \
|
||||
playqueue as PQ
|
||||
from . import app, utils, json_rpc, variables as v, playqueue as PQ
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.playstrm')
|
||||
|
@ -82,7 +81,7 @@ class PlayStrm(object):
|
|||
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,
|
||||
item = PQ.PlaylistItem(plex_id=self.plex_id,
|
||||
plex_type=self.plex_type,
|
||||
kodi_id=self.kodi_id,
|
||||
kodi_type=self.kodi_type)
|
||||
|
|
|
@ -14,7 +14,6 @@ from .plexbmchelper import listener, plexgdm, subscribers, httppersist
|
|||
from .plex_api import API
|
||||
from . import utils
|
||||
from . import plex_functions as PF
|
||||
from . import playlist_func as PL
|
||||
from . import json_rpc as js
|
||||
from . import playqueue as PQ
|
||||
from . import variables as v
|
||||
|
@ -48,10 +47,10 @@ def update_playqueue_from_PMS(playqueue,
|
|||
if transient_token is None:
|
||||
transient_token = playqueue.plex_transient_token
|
||||
with app.APP.lock_playqueues:
|
||||
xml = PL.get_PMS_playlist(playlist_id=playqueue_id)
|
||||
xml = PQ.get_PMS_playlist(playlist_id=playqueue_id)
|
||||
if xml is None:
|
||||
LOG.error('Could now download playqueue %s', playqueue_id)
|
||||
raise PL.PlaylistError()
|
||||
raise PQ.PlaylistError()
|
||||
app.PLAYSTATE.initiated_by_plex = True
|
||||
playqueue.init_from_xml(xml,
|
||||
offset=offset,
|
||||
|
@ -82,7 +81,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
xml[0].attrib
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
LOG.error('Could not download Plex metadata for: %s', data)
|
||||
raise PL.PlaylistError()
|
||||
raise PQ.PlaylistError()
|
||||
api = API(xml[0])
|
||||
if api.plex_type() == v.PLEX_TYPE_ALBUM:
|
||||
LOG.debug('Plex music album detected')
|
||||
|
@ -91,7 +90,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not download the album xml for %s', data)
|
||||
raise PL.PlaylistError()
|
||||
raise PQ.PlaylistError()
|
||||
playqueue = PQ.get_playqueue_from_type('audio')
|
||||
playqueue.init_from_xml(xml,
|
||||
transient_token=data.get('token'))
|
||||
|
@ -100,7 +99,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
|
||||
if xml is None:
|
||||
LOG.error('Could not get playqueue for %s', data)
|
||||
raise PL.PlaylistError()
|
||||
raise PQ.PlaylistError()
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
offset = utils.cast(float, data.get('offset')) or None
|
||||
|
@ -147,7 +146,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
xml[0].attrib
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
LOG.error('Could not download Plex metadata')
|
||||
raise PL.PlaylistError()
|
||||
raise PQ.PlaylistError()
|
||||
api = API(xml[0])
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
|
@ -187,12 +186,12 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
"""
|
||||
example data: {'playQueueID': '8475', 'commandID': '11'}
|
||||
"""
|
||||
xml = PL.get_pms_playqueue(data['playQueueID'])
|
||||
xml = PQ.get_pms_playqueue(data['playQueueID'])
|
||||
if xml is None:
|
||||
return
|
||||
if len(xml) == 0:
|
||||
LOG.debug('Empty playqueue received - clearing playqueue')
|
||||
plex_type = PL.get_plextype_from_xml(xml)
|
||||
plex_type = PQ.get_plextype_from_xml(xml)
|
||||
if plex_type is None:
|
||||
return
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
|
@ -235,7 +234,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
self._process_refresh(data)
|
||||
elif task['action'] == 'setStreams':
|
||||
self._process_streams(data)
|
||||
except PL.PlaylistError:
|
||||
except PQ.PlaylistError:
|
||||
LOG.error('Could not process companion data: %s', data)
|
||||
# "Play Error"
|
||||
utils.dialog('notification',
|
||||
|
|
|
@ -16,7 +16,7 @@ import xbmcvfs
|
|||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import backgroundthread, utils, variables as v, app, playqueue as PQ
|
||||
from . import playlist_func as PL, json_rpc as js, plex_functions as PF
|
||||
from . import json_rpc as js, plex_functions as PF
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.webservice')
|
||||
|
@ -416,7 +416,7 @@ class QueuePlay(backgroundthread.KillableThread):
|
|||
break
|
||||
self.load_params(params)
|
||||
if play_folder:
|
||||
playlistitem = PL.PlaylistItem(plex_id=self.plex_id,
|
||||
playlistitem = PQ.PlaylistItem(plex_id=self.plex_id,
|
||||
plex_type=self.plex_type,
|
||||
kodi_id=self.kodi_id,
|
||||
kodi_type=self.kodi_type)
|
||||
|
|
Loading…
Reference in a new issue