1007 lines
39 KiB
Python
1007 lines
39 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Collection of functions associated with Kodi and Plex playlists and playqueues
|
|
"""
|
|
from logging import getLogger
|
|
|
|
from .plex_api import API
|
|
from .plex_db import PlexDB
|
|
from . import plex_functions as PF
|
|
from .kodi_db import kodiid_from_filename
|
|
from .downloadutils import DownloadUtils as DU
|
|
from . import utils
|
|
from .utils import cast
|
|
from . import json_rpc as js
|
|
from . import variables as v
|
|
from . import app
|
|
from .exceptions import PlaylistError
|
|
from .subtitles import accessible_plex_subtitles
|
|
|
|
|
|
LOG = getLogger('PLEX.playlist_func')
|
|
|
|
|
|
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'
|
|
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] PMS path to item; will be auto-set with plex_id
|
|
guid = None [str] Weird Plex guid
|
|
api = None [API] API of xml 1 lvl below <MediaContainer>
|
|
playmethod = None [str] either 'DirectPath', '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
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.id = None
|
|
self._plex_id = None
|
|
self.plex_type = None
|
|
self.kodi_id = None
|
|
self.kodi_type = None
|
|
self.file = None
|
|
self._uri = None
|
|
self.guid = None
|
|
self.api = None
|
|
self.playmethod = None
|
|
self.playcount = None
|
|
self.offset = None
|
|
self.playerid = None
|
|
# Transcoding quality, if needed
|
|
self.quality = None
|
|
# If Plex video consists of several parts; part number
|
|
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
|
|
# Get the Plex audio and subtitle streams in the same order as Kodi
|
|
# uses them (Kodi uses indexes to activate them, not ids like Plex)
|
|
self._streams_have_been_processed = False
|
|
self._video_streams = None
|
|
self._audio_streams = None
|
|
self._subtitle_streams = None
|
|
# Which Kodi streams are active?
|
|
self._current_kodi_video_stream = None
|
|
self._current_kodi_audio_stream = None
|
|
# Kodi subs can be turned on/off additionally!
|
|
self._current_kodi_sub_stream = None
|
|
self._current_kodi_sub_stream_enabled = None
|
|
self.streams_initialized = False
|
|
|
|
@property
|
|
def plex_id(self):
|
|
return self._plex_id
|
|
|
|
@plex_id.setter
|
|
def plex_id(self, value):
|
|
self._plex_id = value
|
|
self._uri = ('server://%s/com.plexapp.plugins.library/library/metadata/%s' %
|
|
(app.CONN.machine_identifier, value))
|
|
|
|
@property
|
|
def uri(self):
|
|
return self._uri
|
|
|
|
@property
|
|
def video_streams(self):
|
|
if not self._streams_have_been_processed:
|
|
self._process_streams()
|
|
return self._video_streams
|
|
|
|
@property
|
|
def audio_streams(self):
|
|
if not self._streams_have_been_processed:
|
|
self._process_streams()
|
|
return self._audio_streams
|
|
|
|
@property
|
|
def subtitle_streams(self):
|
|
if not self._streams_have_been_processed:
|
|
self._process_streams()
|
|
return self._subtitle_streams
|
|
|
|
@property
|
|
def current_kodi_video_stream(self):
|
|
return self._current_kodi_video_stream
|
|
|
|
@current_kodi_video_stream.setter
|
|
def current_kodi_video_stream(self, value):
|
|
if value != self._current_kodi_video_stream:
|
|
self.on_kodi_video_stream_change(value)
|
|
self._current_kodi_video_stream = value
|
|
|
|
@property
|
|
def current_kodi_audio_stream(self):
|
|
return self._current_kodi_audio_stream
|
|
|
|
@current_kodi_audio_stream.setter
|
|
def current_kodi_audio_stream(self, value):
|
|
if value != self._current_kodi_audio_stream:
|
|
self.on_kodi_audio_stream_change(value)
|
|
self._current_kodi_audio_stream = value
|
|
|
|
@property
|
|
def current_kodi_sub_stream_enabled(self):
|
|
return self._current_kodi_sub_stream_enabled
|
|
|
|
@current_kodi_sub_stream_enabled.setter
|
|
def current_kodi_sub_stream_enabled(self, value):
|
|
if value != self._current_kodi_sub_stream_enabled:
|
|
self.on_kodi_subtitle_stream_change(self.current_kodi_sub_stream,
|
|
value)
|
|
self._current_kodi_sub_stream_enabled = value
|
|
|
|
@property
|
|
def current_kodi_sub_stream(self):
|
|
return self._current_kodi_sub_stream
|
|
|
|
@current_kodi_sub_stream.setter
|
|
def current_kodi_sub_stream(self, value):
|
|
if value != self._current_kodi_sub_stream:
|
|
self.on_kodi_subtitle_stream_change(value,
|
|
self.current_kodi_sub_stream_enabled)
|
|
self._current_kodi_sub_stream = value
|
|
|
|
@property
|
|
def current_plex_video_stream(self):
|
|
return self.plex_stream_index(self.current_kodi_video_stream, 'video')
|
|
|
|
@property
|
|
def current_plex_audio_stream(self):
|
|
return self.plex_stream_index(self.current_kodi_audio_stream, 'audio')
|
|
|
|
@property
|
|
def current_plex_sub_stream(self):
|
|
return self.plex_stream_index(self.current_kodi_sub_stream, 'subtitle')
|
|
|
|
def __repr__(self):
|
|
return ("{{"
|
|
"'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}', "
|
|
"'guid': '{self.guid}', "
|
|
"'playmethod': '{self.playmethod}', "
|
|
"'playcount': {self.playcount}, "
|
|
"'resume': {self.resume},"
|
|
"'offset': {self.offset}, "
|
|
"'force_transcode': {self.force_transcode}, "
|
|
"'part': {self.part}}}".format(self=self))
|
|
|
|
def _process_streams(self):
|
|
"""
|
|
Builds audio and subtitle streams and enables matching between Plex
|
|
and Kodi using self.audio_streams and self.subtitle_streams
|
|
"""
|
|
# The playqueue response from the PMS does not contain a stream filename
|
|
# thanks Plex
|
|
self._subtitle_streams = accessible_plex_subtitles(
|
|
self.playmethod,
|
|
self.file,
|
|
self.api.plex_media_streams())
|
|
# Audio streams are much easier - they're always available and sorted
|
|
# the same in Kodi and Plex
|
|
self._audio_streams = [x for x in self.api.plex_media_streams()
|
|
if x.get('streamType') == '2']
|
|
# Same for video streams
|
|
self._video_streams = [x for x in self.api.plex_media_streams()
|
|
if x.get('streamType') == '1']
|
|
if len(self._video_streams) == 1:
|
|
# Add a selected = "1" attribute to let our logic stand!
|
|
# Missing if there is only 1 video stream present
|
|
self._video_streams[0].set('selected', '1')
|
|
self._streams_have_been_processed = True
|
|
|
|
def _get_iterator(self, stream_type):
|
|
if stream_type == 'audio':
|
|
return self.audio_streams
|
|
elif stream_type == 'subtitle':
|
|
return self.subtitle_streams
|
|
elif stream_type == 'video':
|
|
return self.video_streams
|
|
|
|
def _current_index(self, stream_type):
|
|
"""
|
|
Kodi might tell us the wrong index for any stream after playback start
|
|
Get the correct one!
|
|
"""
|
|
function = {
|
|
'audio': js.get_current_audio_stream_index,
|
|
'video': js.get_current_video_stream_index,
|
|
'subtitle': js.get_current_subtitle_stream_index
|
|
}[stream_type]
|
|
i = 0
|
|
while i < 30:
|
|
# Really annoying: Kodi might return wrong results directly after
|
|
# playback startup, e.g. a Kodi audio index of 1953718901 (!)
|
|
try:
|
|
index = function(self.playerid)
|
|
except (TypeError, IndexError, KeyError):
|
|
# No sensible reply yet
|
|
pass
|
|
else:
|
|
if index != 1953718901:
|
|
# Correct result!
|
|
return index
|
|
i += 1
|
|
app.APP.monitor.waitForAbort(0.1)
|
|
else:
|
|
raise RuntimeError('Kodi did not tell us the correct index for %s'
|
|
% stream_type)
|
|
|
|
def init_streams(self):
|
|
"""
|
|
Initializes all streams after Kodi has started playing this video
|
|
WARNING: KODI TAKES FOREVER TO INITIALIZE STREAMS AFTER PLAYBACK
|
|
STARTUP. YOU WONT GET THE CORRECT NUMBER OFAUDIO AND SUB STREAMS RIGHT
|
|
AFTER STARTUP. Seems like you need to wait a couple of seconds
|
|
"""
|
|
if not app.PLAYSTATE.item == self:
|
|
# Already stopped playback or skipped to the next one
|
|
LOG.warn('Skipping init_streams!')
|
|
return
|
|
self.init_kodi_streams()
|
|
self.switch_to_plex_stream('video')
|
|
if utils.settings('audioStreamPick') == '0':
|
|
self.switch_to_plex_stream('audio')
|
|
if utils.settings('subtitleStreamPick') == '0':
|
|
self.switch_to_plex_stream('subtitle')
|
|
self.streams_initialized = True
|
|
|
|
def init_kodi_streams(self):
|
|
self._current_kodi_video_stream = self._current_index('video')
|
|
self._current_kodi_audio_stream = self._current_index('audio')
|
|
self._current_kodi_sub_stream_enabled = js.get_subtitle_enabled(self.playerid)
|
|
self._current_kodi_sub_stream = self._current_index('subtitle')
|
|
|
|
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 [int].
|
|
stream_type: 'video', 'audio', 'subtitle'
|
|
"""
|
|
if stream_type == 'audio':
|
|
return int(self.audio_streams[kodi_stream_index].get('id'))
|
|
elif stream_type == 'video':
|
|
return int(self.video_streams[kodi_stream_index].get('id'))
|
|
elif stream_type == 'subtitle':
|
|
if self.current_kodi_sub_stream_enabled:
|
|
try:
|
|
return int(self.subtitle_streams[kodi_stream_index].get('id'))
|
|
except (IndexError, TypeError):
|
|
# A subtitle that is not available on the Plex side
|
|
# deactivating subs
|
|
return 0
|
|
else:
|
|
return 0
|
|
|
|
def kodi_stream_index(self, plex_stream_index, stream_type):
|
|
"""
|
|
Pass in the plex_stream_index [int] in order to receive the Kodi stream
|
|
index [int].
|
|
stream_type: 'video', 'audio', 'subtitle'
|
|
Raises ValueError if unsuccessful
|
|
"""
|
|
if not isinstance(plex_stream_index, int):
|
|
raise ValueError('%s plex_stream_index %s of type %s received' %
|
|
(stream_type, plex_stream_index, type(plex_stream_index)))
|
|
for i, stream in enumerate(self._get_iterator(stream_type)):
|
|
if cast(int, stream.get('id')) == plex_stream_index:
|
|
return i
|
|
raise ValueError('No %s kodi_stream_index for plex_stream_index %s' %
|
|
(stream_type, plex_stream_index))
|
|
|
|
def active_plex_stream_index(self, stream_type):
|
|
"""
|
|
Returns the following tuple for the active stream on the Plex side:
|
|
(Plex stream id [int], languageTag [str] or None)
|
|
Returns None if no stream has been selected
|
|
"""
|
|
for i, stream in enumerate(self._get_iterator(stream_type)):
|
|
if stream.get('selected') == '1':
|
|
return (int(stream.get('id')), stream.get('languageTag'))
|
|
|
|
def on_kodi_subtitle_stream_change(self, kodi_stream_index, subs_enabled):
|
|
"""
|
|
Call this method if Kodi changed its subtitle and you want Plex to
|
|
know.
|
|
"""
|
|
if subs_enabled:
|
|
try:
|
|
plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
|
|
except (IndexError, TypeError):
|
|
LOG.debug('Kodi subtitle change detected to a sub %s that is '
|
|
'NOT available on the Plex side', kodi_stream_index)
|
|
plex_stream_index = 0
|
|
else:
|
|
LOG.debug('Kodi subtitle change detected: telling Plex about '
|
|
'switch to index %s, Plex stream id %s',
|
|
kodi_stream_index, plex_stream_index)
|
|
else:
|
|
plex_stream_index = 0
|
|
LOG.debug('Kodi subtitle has been deactivated, telling Plex')
|
|
PF.change_subtitle(plex_stream_index, self.api.part_id())
|
|
|
|
def on_kodi_audio_stream_change(self, kodi_stream_index):
|
|
"""
|
|
Call this method if Kodi changed its audio stream and you want Plex to
|
|
know. kodi_stream_index [int]
|
|
"""
|
|
plex_stream_index = int(self.audio_streams[kodi_stream_index].get('id'))
|
|
LOG.debug('Changing Plex audio stream to %s, Kodi index %s',
|
|
plex_stream_index, kodi_stream_index)
|
|
PF.change_audio_stream(plex_stream_index, self.api.part_id())
|
|
|
|
def on_kodi_video_stream_change(self, kodi_stream_index):
|
|
"""
|
|
Call this method if Kodi changed its video stream and you want Plex to
|
|
know. kodi_stream_index [int]
|
|
"""
|
|
plex_stream_index = int(self.video_streams[kodi_stream_index].get('id'))
|
|
LOG.debug('Changing Plex video stream to %s, Kodi index %s',
|
|
plex_stream_index, kodi_stream_index)
|
|
PF.change_video_stream(plex_stream_index, self.api.part_id())
|
|
|
|
def _set_kodi_stream_if_different(self, kodi_index, typus):
|
|
"""Will always activate subtitles."""
|
|
if typus == 'video':
|
|
current = js.get_current_video_stream_index(self.playerid)
|
|
if current != kodi_index:
|
|
LOG.debug('Switching video stream')
|
|
app.APP.player.setVideoStream(kodi_index)
|
|
else:
|
|
LOG.debug('Not switching video stream (no change)')
|
|
elif typus == 'audio':
|
|
current = js.get_current_audio_stream_index(self.playerid)
|
|
if current != kodi_index:
|
|
LOG.debug('Switching audio stream')
|
|
app.APP.player.setAudioStream(kodi_index)
|
|
else:
|
|
LOG.debug('Not switching audio stream (no change)')
|
|
elif typus == 'subtitle':
|
|
current = js.get_current_subtitle_stream_index(self.playerid)
|
|
enabled = js.get_subtitle_enabled(self.playerid)
|
|
if current != kodi_index:
|
|
LOG.debug('Switching subtitle stream')
|
|
app.APP.player.setSubtitleStream(kodi_index)
|
|
else:
|
|
LOG.debug('Not switching subtitle stream (no change)')
|
|
if not enabled:
|
|
LOG.debug('Enabling subtitles')
|
|
app.APP.player.showSubtitles(True)
|
|
else:
|
|
raise RuntimeError('Unknown stream type %s' % typus)
|
|
|
|
def switch_to_plex_stream(self, typus):
|
|
try:
|
|
plex_index, language_tag = self.active_plex_stream_index(typus)
|
|
except TypeError:
|
|
# Only happens if Plex did not provide us with a suitable sub
|
|
# Meaning Plex tells us to deactivate subs
|
|
LOG.debug('Deactivating Kodi subtitles because the PMS '
|
|
'told us to not show any subtitles')
|
|
app.APP.player.showSubtitles(False)
|
|
self._current_kodi_sub_stream_enabled = False
|
|
return
|
|
# Rest: video, audio and activated subs
|
|
LOG.debug('The PMS wants to display %s stream with Plex id %s and '
|
|
'languageTag %s', typus, plex_index, language_tag)
|
|
try:
|
|
kodi_index = self.kodi_stream_index(plex_index, typus)
|
|
except ValueError:
|
|
kodi_index = None
|
|
LOG.debug('Leaving Kodi %s stream settings untouched since we '
|
|
'could not parse Plex %s stream with id %s to a Kodi'
|
|
' index', typus, typus, plex_index)
|
|
else:
|
|
LOG.debug('Switching to Kodi %s stream number %s because the '
|
|
'PMS told us to show stream with Plex id %s',
|
|
typus, kodi_index, plex_index)
|
|
# If we're choosing an "illegal" index, this function does
|
|
# need seem to fail nor log any errors
|
|
self._set_kodi_stream_if_different(kodi_index, typus)
|
|
if typus == 'audio':
|
|
self._current_kodi_audio_stream = kodi_index
|
|
elif typus == 'subtitle':
|
|
self._current_kodi_sub_stream_enabled = True
|
|
self._current_kodi_sub_stream = kodi_index
|
|
elif typus == 'video':
|
|
self._current_kodi_video_stream = kodi_index
|
|
|
|
def on_plex_stream_change(self, video_stream_id=None, audio_stream_id=None,
|
|
subtitle_stream_id=None):
|
|
"""
|
|
Call this method if Plex Companion wants to change streams [ints]
|
|
"""
|
|
if video_stream_id is not None:
|
|
try:
|
|
kodi_index = self.kodi_stream_index(video_stream_id, 'video')
|
|
except ValueError:
|
|
LOG.error('Unexpected Plex video_stream_id %s, not changing '
|
|
'the video stream!', video_stream_id)
|
|
return
|
|
self._set_kodi_stream_if_different(kodi_index, 'video')
|
|
self._current_kodi_video_stream = kodi_index
|
|
if audio_stream_id is not None:
|
|
try:
|
|
kodi_index = self.kodi_stream_index(audio_stream_id, 'audio')
|
|
except ValueError:
|
|
LOG.error('Unexpected Plex audio_stream_id %s, not changing '
|
|
'the video stream!', audio_stream_id)
|
|
return
|
|
self._set_kodi_stream_if_different(kodi_index, 'audio')
|
|
self._current_kodi_audio_stream = kodi_index
|
|
if subtitle_stream_id is not None:
|
|
if subtitle_stream_id == 0:
|
|
app.APP.player.showSubtitles(False)
|
|
self._current_kodi_sub_stream_enabled = False
|
|
else:
|
|
try:
|
|
kodi_index = self.kodi_stream_index(subtitle_stream_id,
|
|
'subtitle')
|
|
except ValueError:
|
|
LOG.debug('The PMS wanted to change subs, but we could not'
|
|
' match the sub with id %s to a Kodi sub',
|
|
subtitle_stream_id)
|
|
else:
|
|
app.APP.player.setSubtitleStream(kodi_index)
|
|
app.APP.player.showSubtitles(True)
|
|
self._current_kodi_sub_stream_enabled = True
|
|
self._current_kodi_sub_stream = kodi_index
|
|
|
|
|
|
def playlist_item_from_kodi(kodi_item):
|
|
"""
|
|
Turns the JSON answer from Kodi into a playlist element
|
|
|
|
Supply with data['item'] as returned from Kodi JSON-RPC interface.
|
|
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
|
|
"""
|
|
item = PlaylistItem()
|
|
item.kodi_id = kodi_item.get('id')
|
|
item.kodi_type = kodi_item.get('type')
|
|
if item.kodi_id:
|
|
with PlexDB(lock=False) as plexdb:
|
|
db_item = plexdb.item_by_kodi_id(kodi_item['id'], kodi_item['type'])
|
|
if db_item:
|
|
item.plex_id = db_item['plex_id']
|
|
item.plex_type = db_item['plex_type']
|
|
item.file = kodi_item.get('file')
|
|
if item.plex_id is None and item.file is not None:
|
|
try:
|
|
query = item.file.split('?', 1)[1]
|
|
except IndexError:
|
|
query = ''
|
|
query = dict(utils.parse_qsl(query))
|
|
item.plex_id = cast(int, query.get('plex_id'))
|
|
item.plex_type = query.get('itemType')
|
|
LOG.debug('Made playlist item from Kodi: %s', item)
|
|
return item
|
|
|
|
|
|
def verify_kodi_item(plex_id, kodi_item):
|
|
"""
|
|
Tries to lookup kodi_id and kodi_type for kodi_item (with kodi_item['file']
|
|
supplied) - if and only if plex_id is None.
|
|
|
|
Returns the kodi_item with kodi_item['id'] and kodi_item['type'] possibly
|
|
set to None if unsuccessful.
|
|
|
|
Will raise a PlaylistError if plex_id is None and kodi_item['file'] starts
|
|
with either 'plugin' or 'http'.
|
|
Will raise KeyError if neither plex_id nor kodi_id are found
|
|
"""
|
|
if plex_id is not None or kodi_item.get('id') is not None:
|
|
# Got all the info we need
|
|
return kodi_item
|
|
# Special case playlist startup - got type but no id
|
|
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
|
|
kodi_item.get('type') == v.KODI_TYPE_SONG and
|
|
kodi_item['file'].startswith('http')):
|
|
kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'],
|
|
v.KODI_TYPE_SONG)
|
|
LOG.debug('Detected song. Research results: %s', kodi_item)
|
|
return kodi_item
|
|
# Need more info since we don't have kodi_id nor type. Use file path.
|
|
if ((kodi_item['file'].startswith('plugin') and
|
|
not kodi_item['file'].startswith('plugin://%s' % v.ADDON_ID)) or
|
|
kodi_item['file'].startswith('http')):
|
|
LOG.debug('kodi_item cannot be used for Plex playback: %s', kodi_item)
|
|
raise PlaylistError('kodi_item cannot be used for Plex playback')
|
|
LOG.debug('Starting research for Kodi id since we didnt get one: %s',
|
|
kodi_item)
|
|
# Try the VIDEO DB first - will find both movies and episodes
|
|
kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'],
|
|
db_type='video')
|
|
if not kodi_id:
|
|
# No movie or episode found - try MUSIC DB now for songs
|
|
kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'],
|
|
db_type='music')
|
|
kodi_item['id'] = kodi_id
|
|
kodi_item['type'] = None if kodi_id is None else kodi_type
|
|
if plex_id is None and kodi_id is None:
|
|
raise KeyError('Neither Plex nor Kodi id found for %s' % kodi_item)
|
|
LOG.debug('Research results for kodi_item: %s', kodi_item)
|
|
return kodi_item
|
|
|
|
|
|
def playlist_item_from_plex(plex_id):
|
|
"""
|
|
Returns a playlist element providing the plex_id ("ratingKey")
|
|
|
|
Returns a Playlist_Item
|
|
"""
|
|
item = PlaylistItem()
|
|
item.plex_id = plex_id
|
|
with PlexDB(lock=False) as plexdb:
|
|
db_item = plexdb.item_by_id(plex_id)
|
|
if db_item:
|
|
item.plex_type = db_item['plex_type']
|
|
item.kodi_id = db_item['kodi_id']
|
|
item.kodi_type = db_item['kodi_type']
|
|
else:
|
|
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
|
LOG.debug('Made playlist item from plex: %s', item)
|
|
return item
|
|
|
|
|
|
def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
|
"""
|
|
Returns a playlist element for the playqueue using the Plex xml
|
|
|
|
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
|
|
"""
|
|
item = PlaylistItem()
|
|
api = API(xml_video_element)
|
|
item.plex_id = api.plex_id
|
|
item.plex_type = api.plex_type
|
|
# item.id will only be set if you passed in an xml_video_element from e.g.
|
|
# a playQueue
|
|
item.id = api.item_id()
|
|
if kodi_id is not None:
|
|
item.kodi_id = kodi_id
|
|
item.kodi_type = kodi_type
|
|
elif item.plex_type != v.PLEX_TYPE_CLIP:
|
|
with PlexDB(lock=False) as plexdb:
|
|
db_element = plexdb.item_by_id(item.plex_id,
|
|
plex_type=item.plex_type)
|
|
if db_element:
|
|
item.kodi_id = db_element['kodi_id']
|
|
item.kodi_type = db_element['kodi_type']
|
|
item.guid = api.guid_html_escaped()
|
|
item.playcount = api.viewcount()
|
|
item.offset = api.resume_point()
|
|
item.api = api
|
|
LOG.debug('Created new playlist item from xml: %s', item)
|
|
return item
|
|
|
|
|
|
def _update_playlist_version(playlist, xml):
|
|
"""
|
|
Takes a PMS xml (one level above the xml-depth where we're usually applying
|
|
API()) as input to overwrite the playlist version (e.g. Plex
|
|
playQueueVersion).
|
|
|
|
Raises PlaylistError if unsuccessful
|
|
"""
|
|
try:
|
|
playlist.version = int(xml.get('%sVersion' % playlist.kind))
|
|
except (AttributeError, TypeError):
|
|
raise PlaylistError('Could not get new playlist Version for playlist '
|
|
'%s' % playlist)
|
|
|
|
|
|
def get_playlist_details_from_xml(playlist, xml):
|
|
"""
|
|
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
|
|
playlist.id with the XML's playQueueID
|
|
|
|
Raises PlaylistError if something went wrong.
|
|
"""
|
|
if xml is None:
|
|
raise PlaylistError('No playlist received for playlist %s' % playlist)
|
|
playlist.id = cast(int, xml.get('%sID' % playlist.kind))
|
|
playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
|
|
playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
|
|
playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
|
|
% playlist.kind))
|
|
playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
|
|
% playlist.kind))
|
|
LOG.debug('Updated playlist from xml: %s', playlist)
|
|
|
|
|
|
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
|
"""
|
|
Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we
|
|
need to fetch a new playqueue
|
|
|
|
If an xml is passed in, the playlist will be overwritten with its info
|
|
|
|
Raises PlaylistError if something went wront
|
|
"""
|
|
if xml is None:
|
|
xml = get_PMS_playlist(playlist, playlist_id)
|
|
# Clear our existing playlist and the associated Kodi playlist
|
|
playlist.clear()
|
|
# Set new values
|
|
get_playlist_details_from_xml(playlist, xml)
|
|
for plex_item in xml:
|
|
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
|
|
if playlist_item is not None:
|
|
playlist.items.append(playlist_item)
|
|
|
|
|
|
def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
|
|
"""
|
|
Initializes the Plex side without changing the Kodi playlists
|
|
WILL ALSO UPDATE OUR PLAYLISTS.
|
|
|
|
Returns the first PKC playlist item or raises PlaylistError
|
|
"""
|
|
LOG.debug('Initializing the playqueue on the Plex side: %s', playlist)
|
|
verify_kodi_item(plex_id, kodi_item)
|
|
playlist.clear(kodi=False)
|
|
try:
|
|
if plex_id:
|
|
item = playlist_item_from_plex(plex_id)
|
|
else:
|
|
item = playlist_item_from_kodi(kodi_item)
|
|
params = {
|
|
'next': 0,
|
|
'type': playlist.type,
|
|
'uri': item.uri,
|
|
'includeMarkers': 1, # e.g. start + stop of intros
|
|
}
|
|
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
|
|
action_type="POST",
|
|
parameters=params)
|
|
if xml in (None, 401):
|
|
raise PlaylistError('Did not receive a valid xml from the PMS')
|
|
get_playlist_details_from_xml(playlist, xml)
|
|
# Need to get the details for the playlist item
|
|
item = playlist_item_from_xml(xml[0])
|
|
except (KeyError, IndexError, TypeError):
|
|
LOG.error('Could not init Plex playlist: plex_id %s, kodi_item %s',
|
|
plex_id, kodi_item)
|
|
raise PlaylistError
|
|
playlist.items.append(item)
|
|
LOG.debug('Initialized the playqueue on the Plex side: %s', playlist)
|
|
return item
|
|
|
|
|
|
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
|
|
kodi_type=None, plex_id=None, file=None):
|
|
"""
|
|
Adds a listitem to both the Kodi and Plex playlist at position pos [int].
|
|
|
|
If file is not None, file will overrule kodi_id!
|
|
|
|
file: str!!
|
|
"""
|
|
LOG.debug('add_listitem_to_playlist at position %s. Playlist before add: '
|
|
'%s', pos, playlist)
|
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
|
if playlist.id is None:
|
|
init_plex_playqueue(playlist, plex_id, kodi_item)
|
|
else:
|
|
add_item_to_plex_playqueue(playlist, pos, plex_id, kodi_item)
|
|
if kodi_id is None and playlist.items[pos].kodi_id:
|
|
kodi_id = playlist.items[pos].kodi_id
|
|
kodi_type = playlist.items[pos].kodi_type
|
|
if file is None:
|
|
file = playlist.items[pos].file
|
|
# Otherwise we double the item!
|
|
del playlist.items[pos]
|
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
|
add_listitem_to_Kodi_playlist(playlist,
|
|
pos,
|
|
listitem,
|
|
file,
|
|
kodi_item=kodi_item)
|
|
|
|
|
|
def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
|
plex_id=None, file=None):
|
|
"""
|
|
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
|
|
file: str!
|
|
|
|
Raises PlaylistError if something went wrong
|
|
"""
|
|
LOG.debug('add_item_to_playlist. Playlist before adding: %s', playlist)
|
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
|
if playlist.id is None:
|
|
item = init_plex_playqueue(playlist, plex_id, kodi_item)
|
|
else:
|
|
item = add_item_to_plex_playqueue(playlist, pos, plex_id, kodi_item)
|
|
params = {
|
|
'playlistid': playlist.playlistid,
|
|
'position': pos
|
|
}
|
|
if item.kodi_id is not None:
|
|
params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)}
|
|
else:
|
|
params['item'] = {'file': item.file}
|
|
reply = js.playlist_insert(params)
|
|
if reply.get('error') is not None:
|
|
raise PlaylistError('Could not add item to playlist. Kodi reply. %s'
|
|
% reply)
|
|
return item
|
|
|
|
|
|
def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
|
|
"""
|
|
Adds a new item to the playlist at position pos [int] only on the Plex
|
|
side of things (e.g. because the user changed the Kodi side)
|
|
WILL ALSO UPDATE OUR PLAYLISTS
|
|
|
|
Returns the PKC PlayList item or raises PlaylistError
|
|
"""
|
|
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" % (playlist.kind, playlist.id)
|
|
parameters = {
|
|
'uri': item.uri,
|
|
'includeMarkers': 1, # e.g. start + stop of intros
|
|
}
|
|
# Will always put the new item at the end of the Plex playlist
|
|
xml = DU().downloadUrl(url,
|
|
action_type="PUT",
|
|
parameters=parameters)
|
|
try:
|
|
xml[-1].attrib
|
|
except (TypeError, AttributeError, KeyError, IndexError):
|
|
raise PlaylistError('Could not add item %s to playlist %s'
|
|
% (kodi_item, playlist))
|
|
api = API(xml[-1])
|
|
item.api = api
|
|
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
|
|
_update_playlist_version(playlist, xml)
|
|
else:
|
|
# Move the new item to the correct position
|
|
move_playlist_item(playlist,
|
|
len(playlist.items) - 1,
|
|
pos)
|
|
LOG.debug('Successfully added item on the Plex side: %s', playlist)
|
|
return item
|
|
|
|
|
|
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
|
file=None, xml_video_element=None):
|
|
"""
|
|
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
|
|
|
|
Returns the playlist item that was just added or raises PlaylistError
|
|
|
|
file: str!
|
|
"""
|
|
LOG.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
|
|
'only at position %s for %s',
|
|
kodi_id, kodi_type, file, pos, playlist)
|
|
params = {
|
|
'playlistid': playlist.playlistid,
|
|
'position': pos
|
|
}
|
|
if kodi_id is not None:
|
|
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
|
|
else:
|
|
params['item'] = {'file': file}
|
|
reply = js.playlist_insert(params)
|
|
if reply.get('error') is not None:
|
|
raise PlaylistError('Could not add item to playlist. Kodi reply. %s',
|
|
reply)
|
|
if xml_video_element is not None:
|
|
item = playlist_item_from_xml(xml_video_element)
|
|
item.kodi_id = kodi_id
|
|
item.kodi_type = kodi_type
|
|
item.file = file
|
|
elif kodi_id is not None:
|
|
item = playlist_item_from_kodi(
|
|
{'id': kodi_id, 'type': kodi_type, 'file': file})
|
|
if item.plex_id is not None:
|
|
xml = PF.GetPlexMetadata(item.plex_id)
|
|
item.api = API(xml[-1])
|
|
playlist.items.insert(pos, item)
|
|
return item
|
|
|
|
|
|
def move_playlist_item(playlist, before_pos, after_pos):
|
|
"""
|
|
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
|
|
|
|
WILL ALSO CHANGE OUR PLAYLISTS.
|
|
"""
|
|
LOG.debug('Moving item from %s to %s on the Plex side for %s',
|
|
before_pos, after_pos, playlist)
|
|
if after_pos == 0:
|
|
url = "{server}/%ss/%s/items/%s/move?after=0" % \
|
|
(playlist.kind,
|
|
playlist.id,
|
|
playlist.items[before_pos].id)
|
|
else:
|
|
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
|
|
(playlist.kind,
|
|
playlist.id,
|
|
playlist.items[before_pos].id,
|
|
playlist.items[after_pos - 1].id)
|
|
# Tell the PMS that we're moving items around
|
|
xml = DU().downloadUrl(url, action_type="PUT")
|
|
# We need to increment the playlist version for communicating with the PMS
|
|
_update_playlist_version(playlist, 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)
|
|
|
|
|
|
def get_PMS_playlist(playlist, playlist_id=None):
|
|
"""
|
|
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
|
|
need to fetch a new playlist
|
|
|
|
Raises PlaylistError if something went wrong
|
|
"""
|
|
playlist_id = playlist_id if playlist_id else playlist.id
|
|
parameters = {'includeMarkers': 1}
|
|
if playlist.kind == 'playList':
|
|
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id,
|
|
parameters=parameters)
|
|
else:
|
|
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id,
|
|
parameters=parameters)
|
|
try:
|
|
xml.attrib
|
|
except AttributeError:
|
|
raise PlaylistError('Did not get a valid xml')
|
|
return xml
|
|
|
|
|
|
def refresh_playlist_from_PMS(playlist):
|
|
"""
|
|
Only updates the selected item from the PMS side (e.g.
|
|
playQueueSelectedItemID). Will NOT check whether items still make sense.
|
|
"""
|
|
get_playlist_details_from_xml(playlist, get_PMS_playlist(playlist))
|
|
|
|
|
|
def delete_playlist_item_from_PMS(playlist, pos):
|
|
"""
|
|
Delete the item at position pos [int] on the Plex side and our playlists
|
|
"""
|
|
LOG.debug('Deleting position %s for %s on the Plex side', pos, playlist)
|
|
try:
|
|
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
|
|
(playlist.kind,
|
|
playlist.id,
|
|
playlist.items[pos].id,
|
|
playlist.repeat),
|
|
action_type="DELETE")
|
|
except IndexError:
|
|
raise PlaylistError('Position %s out of bound for %s' % (pos, playlist))
|
|
else:
|
|
del playlist.items[pos]
|
|
_update_playlist_version(playlist, xml)
|
|
|
|
|
|
# Functions operating on the Kodi playlist objects ##########
|
|
|
|
def add_to_Kodi_playlist(playlist, xml_video_element):
|
|
"""
|
|
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
|
|
Pass in the PMS xml's video element (one level underneath MediaContainer).
|
|
|
|
Returns a Playlist_Item or raises PlaylistError
|
|
"""
|
|
item = playlist_item_from_xml(xml_video_element)
|
|
if item.kodi_id:
|
|
json_item = {'%sid' % item.kodi_type: item.kodi_id}
|
|
else:
|
|
json_item = {'file': item.file}
|
|
reply = js.playlist_add(playlist.playlistid, json_item)
|
|
if reply.get('error') is not None:
|
|
raise PlaylistError('Could not add item %s to Kodi playlist. Error: '
|
|
'%s', xml_video_element, reply)
|
|
return item
|
|
|
|
|
|
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
|
|
xml_video_element=None, kodi_item=None):
|
|
"""
|
|
Adds an xbmc listitem to the Kodi playlist.xml_video_element
|
|
|
|
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
|
|
|
file: string!
|
|
"""
|
|
LOG.debug('Insert listitem at position %s for Kodi only for %s',
|
|
pos, playlist)
|
|
# Add the item into Kodi playlist
|
|
playlist.kodi_pl.add(url=file, listitem=listitem, index=pos)
|
|
# We need to add this to our internal queue as well
|
|
if xml_video_element is not None:
|
|
item = playlist_item_from_xml(xml_video_element)
|
|
else:
|
|
item = playlist_item_from_kodi(kodi_item)
|
|
if file is not None:
|
|
item.file = file
|
|
playlist.items.insert(pos, item)
|
|
LOG.debug('Done inserting for %s', playlist)
|
|
return item
|
|
|
|
|
|
def remove_from_kodi_playlist(playlist, pos):
|
|
"""
|
|
Removes the item at position pos from the Kodi playlist using JSON.
|
|
|
|
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
|
"""
|
|
LOG.debug('Removing position %s from Kodi only from %s', pos, playlist)
|
|
reply = js.playlist_remove(playlist.playlistid, pos)
|
|
if reply.get('error') is not None:
|
|
LOG.error('Could not delete the item from the playlist. Error: %s',
|
|
reply)
|
|
return
|
|
try:
|
|
del playlist.items[pos]
|
|
except IndexError:
|
|
LOG.error('Cannot delete position %s for %s', pos, playlist)
|
|
|
|
|
|
def get_pms_playqueue(playqueue_id):
|
|
"""
|
|
Returns the Plex playqueue as an etree XML or None if unsuccessful
|
|
"""
|
|
parameters = {'includeMarkers': 1}
|
|
xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id,
|
|
parameters=parameters,
|
|
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')
|