Compare commits
74 commits
master
...
webserver-
Author | SHA1 | Date | |
---|---|---|---|
|
6cba4a1d01 | ||
|
48d288ac53 | ||
|
27c4c6ac38 | ||
|
e204ef9849 | ||
|
7e676eb043 | ||
|
a650c42cfd | ||
|
0f9e754815 | ||
|
bb2fff5909 | ||
|
725416751f | ||
|
6e692d22c2 | ||
|
fb21bc7d71 | ||
|
d3752e1958 | ||
|
3d4bde878e | ||
|
9d517c2c3d | ||
|
d397fb5b20 | ||
|
f7237d7033 | ||
|
9d79f78190 | ||
|
7725af5a6f | ||
|
0ce29dc0ce | ||
|
ea4a062aac | ||
|
a1f4960bca | ||
|
1d01f4794e | ||
|
cef07c3598 | ||
|
7616d6dc26 | ||
|
b586ac09c4 | ||
|
dc56c2a6a2 | ||
|
1123a2ee3c | ||
|
8ad6d1bcce | ||
|
45fc9fa8be | ||
|
353cb04532 | ||
|
48cda467c3 | ||
|
6bd98fcefd | ||
|
1218cde0a2 | ||
|
578ced789f | ||
|
0d8b3b3ba7 | ||
|
8660b12d15 | ||
|
bbd8e18002 | ||
|
7753903c05 | ||
|
4fa1f48b43 | ||
|
130ec674e5 | ||
|
2dac26ffc4 | ||
|
3aa5c87ca0 | ||
|
c63d9ad4d6 | ||
|
95b37b51f5 | ||
|
d380aa8ac3 | ||
|
885e8dd581 | ||
|
ac285467c4 | ||
|
61ff2b72f3 | ||
|
0acf470343 | ||
|
9a9bc9f0eb | ||
|
643e6171c4 | ||
|
ad6c160524 | ||
|
b11ca48294 | ||
|
fe52efd88e | ||
|
8c614f3e47 | ||
|
0d36a2a3b9 | ||
|
f4c3674bc2 | ||
|
5428dafe59 | ||
|
4ed17f1a5b | ||
|
484b03482e | ||
|
797a58a3d5 | ||
|
439857a9ce | ||
|
12befecc4a | ||
|
20bffc1b41 | ||
|
d7541b7f74 | ||
|
dfcfa0edab | ||
|
20c1c6e502 | ||
|
875d704e5a | ||
|
c0035c84a6 | ||
|
4a3b38f5b6 | ||
|
16423e18ec | ||
|
059ed7a5f0 | ||
|
7c6fdad770 | ||
|
9b4584e7df |
37 changed files with 2336 additions and 1865 deletions
16
default.py
16
default.py
|
@ -39,7 +39,21 @@ class Main():
|
|||
mode = params.get('mode', '')
|
||||
itemid = params.get('id', '')
|
||||
|
||||
if mode == 'play':
|
||||
if mode == 'playstrm':
|
||||
while not utils.window('plex.playlist.play'):
|
||||
xbmc.sleep(25)
|
||||
if utils.window('plex.playlist.aborted'):
|
||||
LOG.info("playback aborted")
|
||||
break
|
||||
else:
|
||||
LOG.info("Playback started")
|
||||
xbmcplugin.setResolvedUrl(int(argv[1]),
|
||||
False,
|
||||
xbmcgui.ListItem())
|
||||
utils.window('plex.playlist.play', clear=True)
|
||||
utils.window('plex.playlist.aborted', clear=True)
|
||||
|
||||
elif mode == 'play':
|
||||
self.play()
|
||||
|
||||
elif mode == 'plex_node':
|
||||
|
|
|
@ -53,12 +53,25 @@ class PlayState(object):
|
|||
}
|
||||
self.played_info = {}
|
||||
|
||||
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
||||
# beginning?
|
||||
self.resume_playback = False
|
||||
# Set by SpecialMonitor - did user choose to resume playback or start
|
||||
# from the beginning?
|
||||
# Do set to None if NO resume dialog is displayed! True/False otherwise
|
||||
self.resume_playback = None
|
||||
# Don't ask user whether to resume but immediatly resume
|
||||
self.autoplay = False
|
||||
# Was the playback initiated by the user using the Kodi context menu?
|
||||
self.context_menu_play = False
|
||||
# Set by context menu - shall we force-transcode the next playing item?
|
||||
self.force_transcode = False
|
||||
# Which Kodi player is/has been active? (either int 1, 2 or 3)
|
||||
# Which Kodi player is/has been active? (either int 0, 1, 2)
|
||||
self.active_players = set()
|
||||
# Have we initiated playback via Plex Companion or Alexa - so from the
|
||||
# Plex side of things?
|
||||
self.initiated_by_plex = False
|
||||
# PKC adds/replaces items in the playqueue. We need to use
|
||||
# xbmcplugin.setResolvedUrl() AFTER an item has successfully been added
|
||||
# This flag is set by Kodimonitor/xbmc.Monitor() and the Playlist.OnAdd
|
||||
# signal only when the currently playing item that called the
|
||||
# webservice has successfully been processed
|
||||
self.playlist_ready = False
|
||||
# Flag for Kodimonitor to check when the correct item has been
|
||||
# processed and the Playlist.OnAdd signal has been received
|
||||
self.playlist_start_pos = None
|
||||
|
|
|
@ -7,7 +7,7 @@ import xbmcgui
|
|||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import context, plex_functions as PF, playqueue as PQ
|
||||
from . import context, plex_functions as PF
|
||||
from . import utils, variables as v, app
|
||||
|
||||
###############################################################################
|
||||
|
@ -112,8 +112,7 @@ class ContextMenu(object):
|
|||
"""
|
||||
selected = self._selected_option
|
||||
if selected == OPTIONS['Transcode']:
|
||||
app.PLAYSTATE.force_transcode = True
|
||||
self._PMS_play()
|
||||
self._PMS_play(transcode=True)
|
||||
elif selected == OPTIONS['PMS_Play']:
|
||||
self._PMS_play()
|
||||
elif selected == OPTIONS['Extras']:
|
||||
|
@ -139,17 +138,21 @@ class ContextMenu(object):
|
|||
if PF.delete_item_from_pms(self.plex_id) is False:
|
||||
utils.dialog("ok", heading="{plex}", line1=utils.lang(30414))
|
||||
|
||||
def _PMS_play(self):
|
||||
def _PMS_play(self, transcode=False):
|
||||
"""
|
||||
For using direct paths: Initiates playback using the PMS
|
||||
"""
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
||||
playqueue.clear()
|
||||
app.PLAYSTATE.context_menu_play = True
|
||||
handle = self.api.path(force_first_media=False, force_addon=True)
|
||||
handle = 'RunPlugin(%s)' % handle
|
||||
xbmc.executebuiltin(handle.encode('utf-8'))
|
||||
path = ('http://127.0.0.1:%s/plex/play/file.strm?plex_id=%s'
|
||||
% (v.WEBSERVICE_PORT, self.plex_id))
|
||||
if self.plex_type:
|
||||
path += '&plex_type=%s' % self.plex_type
|
||||
if self.kodi_id:
|
||||
path += '&kodi_id=%s' % self.kodi_id
|
||||
if self.kodi_type:
|
||||
path += '&kodi_type=%s' % self.kodi_type
|
||||
if transcode:
|
||||
path += '&transcode=true'
|
||||
xbmc.executebuiltin(('PlayMedia(%s)' % path).encode('utf-8'))
|
||||
|
||||
def _extras(self):
|
||||
"""
|
||||
|
|
|
@ -217,6 +217,7 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
|
|||
# Need to chain keys for navigation
|
||||
widgets.KEY = key
|
||||
# Process all items to show
|
||||
if synched:
|
||||
widgets.attach_kodi_ids(xml)
|
||||
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
|
||||
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
|
||||
|
|
|
@ -72,10 +72,12 @@ class Movie(ItemBase):
|
|||
scraper='metadata.local')
|
||||
if do_indirect:
|
||||
# Set plugin path and media flags using real filename
|
||||
filename = api.file_name(force_first_media=True)
|
||||
path = 'plugin://%s.movies/' % v.ADDON_ID
|
||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||
% (path, plex_id, v.PLEX_TYPE_MOVIE, filename))
|
||||
path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
|
||||
filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}'
|
||||
filename = filename.format(plex_id,
|
||||
kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
v.PLEX_TYPE_MOVIE)
|
||||
playurl = filename
|
||||
kodi_pathid = self.kodidb.get_path(path)
|
||||
|
||||
|
|
|
@ -180,7 +180,8 @@ class Show(TvShowMixin, ItemBase):
|
|||
scraper='metadata.local')
|
||||
else:
|
||||
# Set plugin path
|
||||
toplevelpath = "plugin://%s.tvshows/" % v.ADDON_ID
|
||||
toplevelpath = ('http://127.0.0.1:%s/plex/kodi/shows/'
|
||||
% v.WEBSERVICE_PORT)
|
||||
path = "%s%s/" % (toplevelpath, plex_id)
|
||||
# Do NOT set a parent id because addon-path cannot be "stacked"
|
||||
toppathid = None
|
||||
|
@ -448,19 +449,22 @@ class Episode(TvShowMixin, ItemBase):
|
|||
if do_indirect:
|
||||
# Set plugin path - do NOT use "intermediate" paths for the show
|
||||
# as with direct paths!
|
||||
filename = api.file_name(force_first_media=True)
|
||||
path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, show_id)
|
||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
|
||||
# Set plugin path and media flags using real filename
|
||||
path = ('http://127.0.0.1:%s/plex/kodi/shows/%s/'
|
||||
% (v.WEBSERVICE_PORT, show_id))
|
||||
filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}'
|
||||
filename = filename.format(plex_id,
|
||||
kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
v.PLEX_TYPE_EPISODE)
|
||||
playurl = filename
|
||||
# Root path tvshows/ already saved in Kodi DB
|
||||
kodi_pathid = self.kodidb.add_path(path)
|
||||
if not app.SYNC.direct_paths:
|
||||
kodi_pathid = self.kodidb.get_path(path)
|
||||
# HACK
|
||||
# need to set a 2nd file entry for a path without plex show id
|
||||
# This fixes e.g. context menu and widgets working as they
|
||||
# should
|
||||
# A dirty hack, really
|
||||
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
|
||||
path_2 = 'http://127.0.0.1:%s/plex/kodi/shows/' % v.WEBSERVICE_PORT
|
||||
# filename_2 is exactly the same as filename
|
||||
# so WITH plex show id!
|
||||
kodi_pathid_2 = self.kodidb.add_path(path_2)
|
||||
|
|
|
@ -5,12 +5,12 @@ from logging import getLogger
|
|||
from sqlite3 import IntegrityError
|
||||
|
||||
from . import common
|
||||
from .. import path_ops, timing, variables as v, app
|
||||
from .. import path_ops, timing, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.kodi_db.video')
|
||||
|
||||
MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID
|
||||
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID
|
||||
MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
|
||||
SHOW_PATH = 'http://127.0.0.1:%s/plex/kodi/shows/' % v.WEBSERVICE_PORT
|
||||
|
||||
|
||||
class KodiVideoDB(common.KodiDBBase):
|
||||
|
@ -174,15 +174,17 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
def obsolete_file_ids(self):
|
||||
"""
|
||||
Returns a generator for idFile of all Kodi file ids that do not have a
|
||||
dateAdded set (dateAdded NULL) and the filename start with
|
||||
'plugin://plugin.video.plexkodiconnect'
|
||||
These entries should be deleted as they're created falsely by Kodi.
|
||||
dateAdded set (dateAdded NULL) and the associated path entry has
|
||||
a field noUpdate of NULL as well as dateAdded of NULL
|
||||
"""
|
||||
return (x[0] for x in self.cursor.execute('''
|
||||
SELECT idFile FROM files
|
||||
WHERE dateAdded IS NULL
|
||||
AND strFilename LIKE \'plugin://plugin.video.plexkodiconnect%\'
|
||||
'''))
|
||||
return (x[0] for x in self.cursor.execute("""
|
||||
SELECT files.idFile
|
||||
FROM files
|
||||
LEFT JOIN path ON path.idPath = files.idPath
|
||||
WHERE files.dateAdded IS NULL
|
||||
AND path.noUpdate IS NULL
|
||||
AND path.dateAdded IS NULL
|
||||
"""))
|
||||
|
||||
def show_id_from_path(self, path):
|
||||
"""
|
||||
|
|
|
@ -14,15 +14,21 @@ 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, playback
|
||||
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')
|
||||
|
||||
# "Start from beginning", "Play from beginning"
|
||||
STRINGS = (utils.try_encode(utils.lang(12021)),
|
||||
utils.try_encode(utils.lang(12023)))
|
||||
STRINGS = (utils.lang(12021).encode('utf-8'),
|
||||
utils.lang(12023).encode('utf-8'))
|
||||
|
||||
|
||||
class MonitorError(Exception):
|
||||
"""
|
||||
Exception we raise for all errors associated with xbmc.Monitor
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class KodiMonitor(xbmc.Monitor):
|
||||
|
@ -31,11 +37,14 @@ class KodiMonitor(xbmc.Monitor):
|
|||
"""
|
||||
def __init__(self):
|
||||
self._already_slept = False
|
||||
self.hack_replay = None
|
||||
xbmc.Monitor.__init__(self)
|
||||
# Info to the currently playing item
|
||||
self.playerid = None
|
||||
self.playlistid = None
|
||||
self.playqueue = None
|
||||
for playerid in app.PLAYSTATE.player_states:
|
||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||
xbmc.Monitor.__init__(self)
|
||||
LOG.info("Kodi monitor started.")
|
||||
|
||||
def onScanStarted(self, library):
|
||||
|
@ -67,41 +76,21 @@ class KodiMonitor(xbmc.Monitor):
|
|||
data = loads(data, 'utf-8')
|
||||
LOG.debug("Method: %s Data: %s", method, data)
|
||||
|
||||
# Hack
|
||||
if not method == 'Player.OnStop':
|
||||
self.hack_replay = None
|
||||
|
||||
if method == "Player.OnPlay":
|
||||
with app.APP.lock_playqueues:
|
||||
self.PlayBackStart(data)
|
||||
self.on_play(data)
|
||||
elif method == "Player.OnStop":
|
||||
# Should refresh our video nodes, e.g. on deck
|
||||
# xbmc.executebuiltin('ReloadSkin()')
|
||||
if (self.hack_replay and not data.get('end') and
|
||||
self.hack_replay == data['item']):
|
||||
# Hack for add-on paths
|
||||
self.hack_replay = None
|
||||
with app.APP.lock_playqueues:
|
||||
self._hack_addon_paths_replay_video()
|
||||
elif data.get('end'):
|
||||
if data.get('end'):
|
||||
with app.APP.lock_playqueues:
|
||||
_playback_cleanup(ended=True)
|
||||
else:
|
||||
with app.APP.lock_playqueues:
|
||||
_playback_cleanup()
|
||||
elif method == 'Playlist.OnAdd':
|
||||
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
||||
# Hitting the "browse" button on tv show info dialog
|
||||
# Hence show the tv show directly
|
||||
xbmc.executebuiltin("Dialog.Close(all, true)")
|
||||
js.activate_window('videos',
|
||||
'videodb://tvshows/titles/%s/' % data['item']['id'])
|
||||
with app.APP.lock_playqueues:
|
||||
self._playlist_onadd(data)
|
||||
elif method == 'Playlist.OnRemove':
|
||||
self._playlist_onremove(data)
|
||||
elif method == 'Playlist.OnClear':
|
||||
with app.APP.lock_playqueues:
|
||||
self._playlist_onclear(data)
|
||||
elif method == "VideoLibrary.OnUpdate":
|
||||
# Manually marking as watched/unwatched
|
||||
|
@ -144,60 +133,24 @@ class KodiMonitor(xbmc.Monitor):
|
|||
LOG.info('Kodi OnQuit detected - shutting down')
|
||||
app.APP.stop_pkc = True
|
||||
|
||||
@staticmethod
|
||||
def _hack_addon_paths_replay_video():
|
||||
"""
|
||||
Hack we need for RESUMABLE items because Kodi lost the path of the
|
||||
last played item that is now being replayed (see playback.py's
|
||||
Player().play()) Also see playqueue.py _compare_playqueues()
|
||||
|
||||
Needed if user re-starts the same video from the library using addon
|
||||
paths. (Video is only added to playqueue, then immediately stoppen.
|
||||
There is no playback initialized by Kodi.) Log excerpts:
|
||||
Method: Playlist.OnAdd Data:
|
||||
{u'item': {u'type': u'movie', u'id': 4},
|
||||
u'playlistid': 1,
|
||||
u'position': 0}
|
||||
Now we would hack!
|
||||
Method: Player.OnStop Data:
|
||||
{u'item': {u'type': u'movie', u'id': 4},
|
||||
u'end': False}
|
||||
(within the same micro-second!)
|
||||
"""
|
||||
LOG.info('Detected re-start of playback of last item')
|
||||
old = app.PLAYSTATE.old_player_states[1]
|
||||
kwargs = {
|
||||
'plex_id': old['plex_id'],
|
||||
'plex_type': old['plex_type'],
|
||||
'path': old['file'],
|
||||
'resolve': False
|
||||
}
|
||||
task = backgroundthread.FunctionAsTask(playback.playback_triage,
|
||||
None,
|
||||
**kwargs)
|
||||
backgroundthread.BGThreader.addTasksToFront([task])
|
||||
|
||||
def _playlist_onadd(self, data):
|
||||
"""
|
||||
Called if an item is added to a Kodi playlist. Example data dict:
|
||||
{
|
||||
u'item': {
|
||||
u'type': u'movie',
|
||||
u'id': 2},
|
||||
u'playlistid': 1,
|
||||
u'position': 0
|
||||
}
|
||||
Will NOT be called if playback initiated by Kodi widgets
|
||||
"""
|
||||
if 'id' not in data['item']:
|
||||
'''
|
||||
Called when a new item is added to a Kodi playqueue
|
||||
'''
|
||||
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
||||
# Hitting the "browse" button on tv show info dialog
|
||||
# Hence show the tv show directly
|
||||
xbmc.executebuiltin("Dialog.Close(all, true)")
|
||||
js.activate_window('videos',
|
||||
'videodb://tvshows/titles/%s/' % data['item']['id'])
|
||||
return
|
||||
old = app.PLAYSTATE.old_player_states[data['playlistid']]
|
||||
if (not app.SYNC.direct_paths and
|
||||
data['position'] == 0 and data['playlistid'] == 1 and
|
||||
not PQ.PLAYQUEUES[data['playlistid']].items and
|
||||
data['item']['type'] == old['kodi_type'] and
|
||||
data['item']['id'] == old['kodi_id']):
|
||||
self.hack_replay = data['item']
|
||||
|
||||
if data['position'] == 0:
|
||||
self.playlistid = data['playlistid']
|
||||
if app.PLAYSTATE.playlist_start_pos == data['position']:
|
||||
LOG.debug('Playlist ready')
|
||||
app.PLAYSTATE.playlist_ready = True
|
||||
app.PLAYSTATE.playlist_start_pos = None
|
||||
|
||||
def _playlist_onremove(self, data):
|
||||
"""
|
||||
|
@ -209,20 +162,25 @@ class KodiMonitor(xbmc.Monitor):
|
|||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _playlist_onclear(data):
|
||||
def _playlist_onclear(self, data):
|
||||
"""
|
||||
Called if a Kodi playlist is cleared. Example data dict:
|
||||
{
|
||||
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
|
||||
"""
|
||||
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')
|
||||
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')
|
||||
|
||||
@staticmethod
|
||||
def _get_ids(kodi_id, kodi_type, path):
|
||||
|
@ -243,24 +201,6 @@ class KodiMonitor(xbmc.Monitor):
|
|||
plex_type = db_item['plex_type']
|
||||
return plex_id, plex_type
|
||||
|
||||
@staticmethod
|
||||
def _add_remaining_items_to_playlist(playqueue):
|
||||
"""
|
||||
Adds all but the very first item of the Kodi playlist to the Plex
|
||||
playqueue
|
||||
"""
|
||||
items = js.playlist_get_items(playqueue.playlistid)
|
||||
if not items:
|
||||
LOG.error('Could not retrieve Kodi playlist items')
|
||||
return
|
||||
# Remove first item
|
||||
items.pop(0)
|
||||
try:
|
||||
for i, item in enumerate(items):
|
||||
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
|
||||
except PL.PlaylistError:
|
||||
LOG.info('Could not build Plex playlist for: %s', items)
|
||||
|
||||
def _json_item(self, playerid):
|
||||
"""
|
||||
Uses JSON RPC to get the playing item's info and returns the tuple
|
||||
|
@ -283,7 +223,73 @@ class KodiMonitor(xbmc.Monitor):
|
|||
json_item.get('type'),
|
||||
json_item.get('file'))
|
||||
|
||||
def PlayBackStart(self, data):
|
||||
def _get_playerid(self, data):
|
||||
"""
|
||||
Sets self.playerid with an int 0, 1 [or 2] or raises MonitorError
|
||||
0: usually video
|
||||
1: usually audio
|
||||
"""
|
||||
try:
|
||||
self.playerid = data['player']['playerid']
|
||||
except (TypeError, KeyError):
|
||||
LOG.info('Aborting playback report - data invalid for updates: %s',
|
||||
data)
|
||||
raise MonitorError()
|
||||
if self.playerid == -1:
|
||||
# Kodi might return -1 for "last player"
|
||||
try:
|
||||
self.playerid = js.get_player_ids()[0]
|
||||
except IndexError:
|
||||
LOG.error('Coud not get playerid for data: %s', data)
|
||||
raise MonitorError()
|
||||
|
||||
def _check_playing_item(self, data):
|
||||
"""
|
||||
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)
|
||||
LOG.debug('Current info for player %s: %s', self.playerid, info)
|
||||
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 = 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')
|
||||
position = 1
|
||||
elif playlistitem != self.playqueue.items[position]:
|
||||
LOG.debug('Different playqueue items: %s vs. %s ',
|
||||
playlistitem, self.playqueue.items[position])
|
||||
raise MonitorError()
|
||||
# Return the PKC playqueue item - contains more info
|
||||
return self.playqueue.items[position]
|
||||
|
||||
def _load_playerstate(self, item):
|
||||
"""
|
||||
Pass in a PF.PlaylistItem(). Will then set the currently playing
|
||||
state with app.PLAYSTATE.player_states[self.playerid]
|
||||
"""
|
||||
if self.playqueue.id:
|
||||
container_key = '/playQueues/%s' % self.playqueue.id
|
||||
else:
|
||||
container_key = '/library/metadata/%s' % item.plex_id
|
||||
status = app.PLAYSTATE.player_states[self.playerid]
|
||||
# Remember that this player has been active
|
||||
app.PLAYSTATE.active_players.add(self.playerid)
|
||||
status.update(js.get_player_props(self.playerid))
|
||||
status['container_key'] = container_key
|
||||
status['file'] = item.file
|
||||
status['kodi_id'] = item.kodi_id
|
||||
status['kodi_type'] = item.kodi_type
|
||||
status['plex_id'] = item.plex_id
|
||||
status['plex_type'] = item.plex_type
|
||||
status['playmethod'] = item.playmethod
|
||||
status['playcount'] = item.playcount
|
||||
LOG.debug('Set player state for player %s: %s', self.playerid, status)
|
||||
|
||||
def on_play(self, data):
|
||||
"""
|
||||
Called whenever playback is started. Example data:
|
||||
{
|
||||
|
@ -292,87 +298,25 @@ class KodiMonitor(xbmc.Monitor):
|
|||
}
|
||||
Unfortunately when using Widgets, Kodi doesn't tell us shit
|
||||
"""
|
||||
# Some init
|
||||
self._already_slept = False
|
||||
self.playerid = None
|
||||
# Get the type of media we're playing
|
||||
try:
|
||||
playerid = data['player']['playerid']
|
||||
except (TypeError, KeyError):
|
||||
LOG.info('Aborting playback report - item invalid for updates %s',
|
||||
data)
|
||||
self._get_playerid(data)
|
||||
except MonitorError:
|
||||
return
|
||||
kodi_id = data['item'].get('id') if 'item' in data else None
|
||||
kodi_type = data['item'].get('type') if 'item' in data else None
|
||||
path = data['item'].get('file') if 'item' in data else None
|
||||
if playerid == -1:
|
||||
# Kodi might return -1 for "last player"
|
||||
# Getting the playerid is really a PITA
|
||||
self.playqueue = PQ.PLAYQUEUES[self.playerid]
|
||||
LOG.debug('Current PKC playqueue: %s', self.playqueue)
|
||||
item = None
|
||||
try:
|
||||
playerid = js.get_player_ids()[0]
|
||||
except IndexError:
|
||||
# E.g. Kodi 18 doesn't tell us anything useful
|
||||
if kodi_type in v.KODI_VIDEOTYPES:
|
||||
playlist_type = v.KODI_TYPE_VIDEO_PLAYLIST
|
||||
elif kodi_type in v.KODI_AUDIOTYPES:
|
||||
playlist_type = v.KODI_TYPE_AUDIO_PLAYLIST
|
||||
else:
|
||||
LOG.error('Unexpected type %s, data %s', kodi_type, data)
|
||||
return
|
||||
playerid = js.get_playlist_id(playlist_type)
|
||||
if not playerid:
|
||||
LOG.error('Coud not get playerid for data %s', data)
|
||||
return
|
||||
playqueue = PQ.PLAYQUEUES[playerid]
|
||||
info = js.get_player_props(playerid)
|
||||
if playqueue.kodi_playlist_playback:
|
||||
# Kodi will tell us the wrong position - of the playlist, not the
|
||||
# playqueue, when user starts playing from a playlist :-(
|
||||
pos = 0
|
||||
LOG.debug('Detected playback from a Kodi playlist')
|
||||
else:
|
||||
pos = info['position'] if info['position'] != -1 else 0
|
||||
LOG.debug('Detected position %s for %s', pos, playqueue)
|
||||
status = app.PLAYSTATE.player_states[playerid]
|
||||
try:
|
||||
item = playqueue.items[pos]
|
||||
LOG.debug('PKC playqueue item is: %s', item)
|
||||
except IndexError:
|
||||
# PKC playqueue not yet initialized
|
||||
LOG.debug('Position %s not in PKC playqueue yet', pos)
|
||||
initialize = True
|
||||
else:
|
||||
if not kodi_id:
|
||||
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||
if kodi_id and item.kodi_id:
|
||||
if item.kodi_id != kodi_id or item.kodi_type != kodi_type:
|
||||
LOG.debug('Detected different Kodi id')
|
||||
initialize = True
|
||||
else:
|
||||
initialize = False
|
||||
else:
|
||||
# E.g. clips set-up previously with no Kodi DB entry
|
||||
if not path:
|
||||
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||
if path == '':
|
||||
LOG.debug('Detected empty path: aborting playback report')
|
||||
return
|
||||
if item.file != path:
|
||||
# Clips will get a new path
|
||||
LOG.debug('Detected different path')
|
||||
try:
|
||||
tmp_plex_id = int(utils.REGEX_PLEX_ID.findall(path)[0])
|
||||
except IndexError:
|
||||
LOG.debug('No Plex id in path, need to init playqueue')
|
||||
initialize = True
|
||||
else:
|
||||
if tmp_plex_id == item.plex_id:
|
||||
LOG.debug('Detected different path for the same id')
|
||||
initialize = False
|
||||
else:
|
||||
LOG.debug('Different Plex id, need to init playqueue')
|
||||
initialize = True
|
||||
else:
|
||||
initialize = False
|
||||
if initialize:
|
||||
item = self._check_playing_item(data)
|
||||
except (MonitorError, IndexError):
|
||||
LOG.debug('Detected that we need to initialize the PKC playqueue')
|
||||
|
||||
if not item:
|
||||
# Initialize the PKC playqueue
|
||||
# Yet TODO
|
||||
LOG.debug('Need to initialize Plex and PKC playqueue')
|
||||
if not kodi_id or not kodi_type:
|
||||
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||
|
@ -381,8 +325,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:
|
||||
|
@ -392,29 +338,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
container_key = '/playQueues/%s' % container_key
|
||||
elif plex_id is not None:
|
||||
container_key = '/library/metadata/%s' % plex_id
|
||||
else:
|
||||
LOG.debug('No need to initialize playqueues')
|
||||
kodi_id = item.kodi_id
|
||||
kodi_type = item.kodi_type
|
||||
plex_id = item.plex_id
|
||||
plex_type = item.plex_type
|
||||
if playqueue.id:
|
||||
container_key = '/playQueues/%s' % playqueue.id
|
||||
else:
|
||||
container_key = '/library/metadata/%s' % plex_id
|
||||
# Remember that this player has been active
|
||||
app.PLAYSTATE.active_players.add(playerid)
|
||||
status.update(info)
|
||||
LOG.debug('Set the Plex container_key to: %s', container_key)
|
||||
status['container_key'] = container_key
|
||||
status['file'] = path
|
||||
status['kodi_id'] = kodi_id
|
||||
status['kodi_type'] = kodi_type
|
||||
status['plex_id'] = plex_id
|
||||
status['plex_type'] = plex_type
|
||||
status['playmethod'] = item.playmethod
|
||||
status['playcount'] = item.playcount
|
||||
LOG.debug('Set the player state: %s', status)
|
||||
self._load_playerstate(item)
|
||||
|
||||
|
||||
def _playback_cleanup(ended=False):
|
||||
|
@ -428,6 +352,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
|
||||
|
@ -438,8 +363,8 @@ def _playback_cleanup(ended=False):
|
|||
DU().downloadUrl(
|
||||
'{server}/video/:/transcode/universal/stop',
|
||||
parameters={'session': v.PKC_MACHINE_IDENTIFIER})
|
||||
if playerid == 1:
|
||||
# Bookmarks might not be pickup up correctly, so let's do them
|
||||
if status['plex_type'] in v.PLEX_VIDEOTYPES:
|
||||
# Bookmarks are not be pickup up correctly, so let's do them
|
||||
# manually. Applies to addon paths, but direct paths might have
|
||||
# started playback via PMS
|
||||
_record_playstate(status, ended)
|
||||
|
@ -507,13 +432,23 @@ def _record_playstate(status, ended):
|
|||
totaltime,
|
||||
playcount,
|
||||
last_played)
|
||||
# We might need to reconsider cleaning the file/path table in the future
|
||||
# _clean_file_table()
|
||||
# Update the current view to show e.g. an up-to-date progress bar and use
|
||||
# the latest resume point info
|
||||
if xbmc.getCondVisibility('Container.Content(musicvideos)'):
|
||||
# Prevent cursor from moving
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
else:
|
||||
# Update widgets
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
if xbmc.getCondVisibility('Window.IsMedia'):
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
# Hack to force "in progress" widget to appear if it wasn't visible before
|
||||
if (app.APP.force_reload_skin and
|
||||
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
|
||||
LOG.debug('Refreshing skin to update widgets')
|
||||
xbmc.executebuiltin('ReloadSkin()')
|
||||
task = backgroundthread.FunctionAsTask(_clean_file_table, None)
|
||||
backgroundthread.BGThreader.addTasksToFront([task])
|
||||
|
||||
|
||||
def _clean_file_table():
|
||||
|
@ -524,13 +459,13 @@ def _clean_file_table():
|
|||
This function tries for at most 5 seconds to clean the file table.
|
||||
"""
|
||||
LOG.debug('Start cleaning Kodi files table')
|
||||
app.APP.monitor.waitForAbort(2)
|
||||
# app.APP.monitor.waitForAbort(1)
|
||||
try:
|
||||
with kodi_db.KodiVideoDB() as kodidb_1:
|
||||
with kodi_db.KodiVideoDB(lock=False) as kodidb_2:
|
||||
for file_id in kodidb_1.obsolete_file_ids():
|
||||
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
|
||||
kodidb_2.remove_file(file_id, remove_orphans=False)
|
||||
with kodi_db.KodiVideoDB() as kodidb:
|
||||
file_ids = list(kodidb.obsolete_file_ids())
|
||||
LOG.debug('Obsolete kodi file_ids: %s', file_ids)
|
||||
for file_id in file_ids:
|
||||
kodidb.remove_file(file_id)
|
||||
except utils.OperationalError:
|
||||
LOG.debug('Database was locked, unable to clean file table')
|
||||
else:
|
||||
|
@ -566,5 +501,5 @@ class ContextMonitor(backgroundthread.KillableThread):
|
|||
app.PLAYSTATE.resume_playback = True if control == 1001 else False
|
||||
else:
|
||||
# Different context menu is displayed
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
app.PLAYSTATE.resume_playback = None
|
||||
xbmc.sleep(100)
|
||||
|
|
|
@ -1,551 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Used to kick off Kodi playback
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import plex_functions as PF
|
||||
from . import utils
|
||||
from .kodi_db import KodiVideoDB
|
||||
from . import playlist_func as PL
|
||||
from . import playqueue as PQ
|
||||
from . import json_rpc as js
|
||||
from . import transfer
|
||||
from .playutils import PlayUtils
|
||||
from . import variables as v
|
||||
from . import app
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playback')
|
||||
# Do we need to return ultimately with a setResolvedUrl?
|
||||
RESOLVE = True
|
||||
###############################################################################
|
||||
|
||||
|
||||
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||
"""
|
||||
Hit this function for addon path playback, Plex trailers, etc.
|
||||
Will setup playback first, then on second call complete playback.
|
||||
|
||||
Will set Playback_Successful() with potentially a PKCListItem() attached
|
||||
(to be consumed by setResolvedURL in default.py)
|
||||
|
||||
If trailers or additional (movie-)parts are added, default.py is released
|
||||
and a completely new player instance is called with a new playlist. This
|
||||
circumvents most issues with Kodi & playqueues
|
||||
|
||||
Set resolve to False if you do not want setResolvedUrl to be called on
|
||||
the first pass - e.g. if you're calling this function from the original
|
||||
service.py Python instance
|
||||
"""
|
||||
plex_id = utils.cast(int, plex_id)
|
||||
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
||||
'resolve %s', plex_id, plex_type, path, resolve)
|
||||
global RESOLVE
|
||||
# If started via Kodi context menu, we never resolve
|
||||
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
||||
if not app.CONN.online or not app.ACCOUNT.authenticated:
|
||||
if not app.CONN.online:
|
||||
LOG.error('PMS not online for playback')
|
||||
# "{0} offline"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(39213).format(app.CONN.server_name),
|
||||
icon='{plex}')
|
||||
else:
|
||||
LOG.error('Not yet authenticated for PMS, abort starting playback')
|
||||
# "Unauthorized for PMS"
|
||||
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
with app.APP.lock_playqueues:
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
||||
try:
|
||||
pos = js.get_position(playqueue.playlistid)
|
||||
except KeyError:
|
||||
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
|
||||
# add-on paths
|
||||
LOG.info('No position returned from player! Assuming playlist')
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
||||
try:
|
||||
pos = js.get_position(playqueue.playlistid)
|
||||
except KeyError:
|
||||
LOG.info('Assuming video instead of audio playlist playback')
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
|
||||
try:
|
||||
pos = js.get_position(playqueue.playlistid)
|
||||
except KeyError:
|
||||
LOG.error('Still no position - abort')
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
# HACK to detect playback of playlists for add-on paths
|
||||
items = js.playlist_get_items(playqueue.playlistid)
|
||||
try:
|
||||
item = items[pos]
|
||||
except IndexError:
|
||||
LOG.info('Could not apply playlist hack! Probably Widget playback')
|
||||
else:
|
||||
if ('id' not in item and
|
||||
item.get('type') == 'unknown' and item.get('title') == ''):
|
||||
LOG.info('Kodi playlist play detected')
|
||||
_playlist_playback(plex_id, plex_type)
|
||||
return
|
||||
|
||||
# Can return -1 (as in "no playlist")
|
||||
pos = pos if pos != -1 else 0
|
||||
LOG.debug('playQueue position %s for %s', pos, playqueue)
|
||||
# Have we already initiated playback?
|
||||
try:
|
||||
item = playqueue.items[pos]
|
||||
except IndexError:
|
||||
LOG.debug('PKC playqueue yet empty, need to initialize playback')
|
||||
initiate = True
|
||||
else:
|
||||
if item.plex_id != plex_id:
|
||||
LOG.debug('Received new plex_id %s, expected %s',
|
||||
plex_id, item.plex_id)
|
||||
initiate = True
|
||||
else:
|
||||
initiate = False
|
||||
if initiate:
|
||||
_playback_init(plex_id, plex_type, playqueue, pos)
|
||||
else:
|
||||
# kick off playback on second pass
|
||||
_conclude_playback(playqueue, pos)
|
||||
|
||||
|
||||
def _playlist_playback(plex_id, plex_type):
|
||||
"""
|
||||
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
||||
where, causing Playlist.onAdd to fire for each item like this:
|
||||
Playlist.OnAdd Data: {u'item': {u'type': u'episode', u'id': 164},
|
||||
u'playlistid': 0,
|
||||
u'position': 2}
|
||||
This does NOT work for Addon paths, type and id will be unknown:
|
||||
{u'item': {u'type': u'unknown'},
|
||||
u'playlistid': 0,
|
||||
u'position': 7}
|
||||
At the end, only the element being played actually shows up in the Kodi
|
||||
playqueue.
|
||||
Hence: if we fail the first addon paths call, Kodi will start playback
|
||||
for the next item in line :-)
|
||||
(by the way: trying to get active Kodi player id will return [])
|
||||
"""
|
||||
xml = PF.GetPlexMetadata(plex_id, reraise=True)
|
||||
if xml in (None, 401):
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
|
||||
# has actually started. Need to tell Kodimonitor
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
||||
playqueue.clear(kodi=False)
|
||||
# Set the flag for the potentially WRONG audio playlist so Kodimonitor
|
||||
# can pick up on it
|
||||
playqueue.kodi_playlist_playback = True
|
||||
playlist_item = PL.playlist_item_from_xml(xml[0])
|
||||
playqueue.items.append(playlist_item)
|
||||
_conclude_playback(playqueue, pos=0)
|
||||
|
||||
|
||||
def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||
"""
|
||||
Playback setup if Kodi starts playing an item for the first time.
|
||||
"""
|
||||
LOG.info('Initializing PKC playback')
|
||||
xml = PF.GetPlexMetadata(plex_id, reraise=True)
|
||||
if xml in (None, 401):
|
||||
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
if playqueue.kodi_pl.size() > 1:
|
||||
# Special case - we already got a filled Kodi playqueue
|
||||
try:
|
||||
_init_existing_kodi_playlist(playqueue, pos)
|
||||
except PL.PlaylistError:
|
||||
LOG.error('Playback_init for existing Kodi playlist failed')
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
# Now we need to use setResolvedUrl for the item at position ZERO
|
||||
# playqueue.py will pick up the missing items
|
||||
_conclude_playback(playqueue, 0)
|
||||
return
|
||||
# "Usual" case - consider trailers and parts and build both Kodi and Plex
|
||||
# playqueues
|
||||
# Pass dummy PKC video with 0 length so Kodi immediately stops playback
|
||||
# and we can build our own playqueue.
|
||||
_ensure_resolve()
|
||||
api = API(xml[0])
|
||||
trailers = False
|
||||
if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and
|
||||
utils.settings('enableCinema') == "true"):
|
||||
if utils.settings('askCinema') == "true":
|
||||
# "Play trailers?"
|
||||
trailers = utils.yesno_dialog(utils.lang(29999), utils.lang(33016))
|
||||
else:
|
||||
trailers = True
|
||||
LOG.debug('Playing trailers: %s', trailers)
|
||||
playqueue.clear()
|
||||
if plex_type != v.PLEX_TYPE_CLIP:
|
||||
# Post to the PMS to create a playqueue - in any case due to Companion
|
||||
xml = PF.init_plex_playqueue(plex_id,
|
||||
xml.attrib.get('librarySectionUUID'),
|
||||
mediatype=plex_type,
|
||||
trailers=trailers)
|
||||
if xml is None:
|
||||
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
|
||||
plex_id, xml.attrib.get('librarySectionUUID'))
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
# Do NOT use _ensure_resolve() because we resolved above already
|
||||
app.PLAYSTATE.context_menu_play = False
|
||||
app.PLAYSTATE.force_transcode = False
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
return
|
||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||
stack = _prep_playlist_stack(xml)
|
||||
_process_stack(playqueue, stack)
|
||||
# Always resume if playback initiated via PMS and there IS a resume
|
||||
# point
|
||||
offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None
|
||||
# Reset some playback variables
|
||||
app.PLAYSTATE.context_menu_play = False
|
||||
app.PLAYSTATE.force_transcode = False
|
||||
# New thread to release this one sooner (e.g. harddisk spinning up)
|
||||
thread = Thread(target=threaded_playback,
|
||||
args=(playqueue.kodi_pl, pos, offset))
|
||||
thread.setDaemon(True)
|
||||
LOG.info('Done initializing playback, starting Kodi player at pos %s and '
|
||||
'resume point %s', pos, offset)
|
||||
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
||||
# caches paths like our plugin://pkc. If we use Player().play() between
|
||||
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
||||
# cache will have been flushed for some reason. Hence the 2nd call for
|
||||
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
||||
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
||||
thread.start()
|
||||
# Ensure that PKC playqueue monitor ignores the changes we just made
|
||||
playqueue.pkc_edit = True
|
||||
|
||||
|
||||
def _ensure_resolve(abort=False):
|
||||
"""
|
||||
Will check whether RESOLVE=True and if so, fail Kodi playback startup
|
||||
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
|
||||
pickling)
|
||||
|
||||
This way we're making sure that other Python instances (calling default.py)
|
||||
will be destroyed.
|
||||
"""
|
||||
if RESOLVE:
|
||||
# Releases the other Python thread without a ListItem
|
||||
transfer.send(True)
|
||||
# Shows PKC error message
|
||||
# transfer.send(None)
|
||||
if abort:
|
||||
# Reset some playback variables
|
||||
app.PLAYSTATE.context_menu_play = False
|
||||
app.PLAYSTATE.force_transcode = False
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
|
||||
|
||||
def _init_existing_kodi_playlist(playqueue, pos):
|
||||
"""
|
||||
Will take the playqueue's kodi_pl with MORE than 1 element and initiate
|
||||
playback (without adding trailers)
|
||||
"""
|
||||
LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size())
|
||||
kodi_items = js.playlist_get_items(playqueue.playlistid)
|
||||
if not kodi_items:
|
||||
LOG.error('No Kodi items returned')
|
||||
raise PL.PlaylistError('No Kodi items returned')
|
||||
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
||||
item.force_transcode = app.PLAYSTATE.force_transcode
|
||||
# playqueue.py will add the rest - this will likely put the PMS under
|
||||
# a LOT of strain if the following Kodi setting is enabled:
|
||||
# Settings -> Player -> Videos -> Play next video automatically
|
||||
LOG.debug('Done init_existing_kodi_playlist')
|
||||
|
||||
|
||||
def _prep_playlist_stack(xml):
|
||||
stack = []
|
||||
for item in xml:
|
||||
api = API(item)
|
||||
if (app.PLAYSTATE.context_menu_play is False and
|
||||
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
||||
# If user chose to play via PMS or force transcode, do not
|
||||
# use the item path stored in the Kodi DB
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_item = plexdb.item_by_id(api.plex_id(), api.plex_type())
|
||||
kodi_id = db_item['kodi_id'] if db_item else None
|
||||
kodi_type = db_item['kodi_type'] if db_item else None
|
||||
else:
|
||||
# We will never store clips (trailers) in the Kodi DB.
|
||||
# Also set kodi_id to None for playback via PMS, so that we're
|
||||
# using add-on paths.
|
||||
# Also do NOT associate episodes with library items for addon paths
|
||||
# as artwork lookup is broken (episode path does not link back to
|
||||
# season and show)
|
||||
kodi_id = None
|
||||
kodi_type = None
|
||||
for part, _ in enumerate(item[0]):
|
||||
api.set_part_number(part)
|
||||
if kodi_id is None:
|
||||
# Need to redirect again to PKC to conclude playback
|
||||
path = api.path()
|
||||
listitem = api.create_listitem()
|
||||
listitem.setPath(utils.try_encode(path))
|
||||
else:
|
||||
# Will add directly via the Kodi DB
|
||||
path = None
|
||||
listitem = None
|
||||
stack.append({
|
||||
'kodi_id': kodi_id,
|
||||
'kodi_type': kodi_type,
|
||||
'file': path,
|
||||
'xml_video_element': item,
|
||||
'listitem': listitem,
|
||||
'part': part,
|
||||
'playcount': api.viewcount(),
|
||||
'offset': api.resume_point(),
|
||||
'id': api.item_id()
|
||||
})
|
||||
return stack
|
||||
|
||||
|
||||
def _process_stack(playqueue, stack):
|
||||
"""
|
||||
Takes our stack and adds the items to the PKC and Kodi playqueues.
|
||||
"""
|
||||
# getposition() can return -1
|
||||
pos = max(playqueue.kodi_pl.getposition(), 0) + 1
|
||||
for item in stack:
|
||||
if item['kodi_id'] is None:
|
||||
playlist_item = PL.add_listitem_to_Kodi_playlist(
|
||||
playqueue,
|
||||
pos,
|
||||
item['listitem'],
|
||||
file=item['file'],
|
||||
xml_video_element=item['xml_video_element'])
|
||||
else:
|
||||
# Directly add element so we have full metadata
|
||||
playlist_item = PL.add_item_to_kodi_playlist(
|
||||
playqueue,
|
||||
pos,
|
||||
kodi_id=item['kodi_id'],
|
||||
kodi_type=item['kodi_type'],
|
||||
xml_video_element=item['xml_video_element'])
|
||||
playlist_item.playcount = item['playcount']
|
||||
playlist_item.offset = item['offset']
|
||||
playlist_item.part = item['part']
|
||||
playlist_item.id = item['id']
|
||||
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
|
||||
pos += 1
|
||||
|
||||
|
||||
def _conclude_playback(playqueue, pos):
|
||||
"""
|
||||
ONLY if actually being played (e.g. at 5th position of a playqueue).
|
||||
|
||||
Decide on direct play, direct stream, transcoding
|
||||
path to
|
||||
direct paths: file itself
|
||||
PMS URL
|
||||
Web URL
|
||||
audiostream (e.g. let user choose)
|
||||
subtitle stream (e.g. let user choose)
|
||||
Init Kodi Playback (depending on situation):
|
||||
start playback
|
||||
return PKC listitem attached to result
|
||||
"""
|
||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
||||
listitem = transfer.PKCListItem()
|
||||
item = playqueue.items[pos]
|
||||
if item.xml is not None:
|
||||
# Got a Plex element
|
||||
api = API(item.xml)
|
||||
api.set_part_number(item.part)
|
||||
api.create_listitem(listitem)
|
||||
playutils = PlayUtils(api, item)
|
||||
playurl = playutils.getPlayUrl()
|
||||
else:
|
||||
api = None
|
||||
playurl = item.file
|
||||
if not playurl:
|
||||
LOG.info('Did not get a playurl, aborting playback silently')
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
transfer.send(True)
|
||||
return
|
||||
listitem.setPath(utils.try_encode(playurl))
|
||||
if item.playmethod == 'DirectStream':
|
||||
listitem.setSubtitles(api.cache_external_subs())
|
||||
elif item.playmethod == 'Transcode':
|
||||
playutils.audio_subtitle_prefs(listitem)
|
||||
|
||||
if app.PLAYSTATE.resume_playback is True:
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
if item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
|
||||
# Do NOT use item.offset directly but get it from the DB
|
||||
# (user might have initiated same video twice)
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
|
||||
file_id = db_item['kodi_fileid'] if db_item else None
|
||||
with KodiVideoDB(lock=False) as kodidb:
|
||||
item.offset = kodidb.get_resume(file_id)
|
||||
LOG.info('Resuming playback at %s', item.offset)
|
||||
if v.KODIVERSION >= 18 and api:
|
||||
# Kodi 18 Alpha 3 broke StartOffset
|
||||
try:
|
||||
percent = (item.offset or api.resume_point()) / api.runtime() * 100.0
|
||||
except ZeroDivisionError:
|
||||
percent = 0.0
|
||||
LOG.debug('Resuming at %s percent', percent)
|
||||
listitem.setProperty('StartPercent', str(percent))
|
||||
else:
|
||||
listitem.setProperty('StartOffset', str(item.offset))
|
||||
listitem.setProperty('resumetime', str(item.offset))
|
||||
elif v.KODIVERSION >= 18:
|
||||
listitem.setProperty('StartPercent', '0')
|
||||
# Reset the resumable flag
|
||||
transfer.send(listitem)
|
||||
LOG.info('Done concluding playback')
|
||||
|
||||
|
||||
def process_indirect(key, offset, resolve=True):
|
||||
"""
|
||||
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
|
||||
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
|
||||
set.
|
||||
|
||||
Will release default.py with setResolvedUrl
|
||||
|
||||
Set resolve to False if playback should be kicked off directly, not via
|
||||
setResolvedUrl
|
||||
"""
|
||||
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
|
||||
key, offset, resolve)
|
||||
global RESOLVE
|
||||
RESOLVE = resolve
|
||||
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
||||
if key.startswith('http') or key.startswith('{server}'):
|
||||
xml = PF.get_playback_xml(key, app.CONN.server_name)
|
||||
elif key.startswith('/system/services'):
|
||||
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
|
||||
'plexapp.com',
|
||||
authenticate=False,
|
||||
token=app.ACCOUNT.plex_token)
|
||||
else:
|
||||
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
|
||||
if xml is None:
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
api = API(xml[0])
|
||||
listitem = transfer.PKCListItem()
|
||||
api.create_listitem(listitem)
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
playqueue.clear()
|
||||
item = PL.Playlist_Item()
|
||||
item.xml = xml[0]
|
||||
item.offset = offset
|
||||
item.plex_type = v.PLEX_TYPE_CLIP
|
||||
item.playmethod = 'DirectStream'
|
||||
|
||||
# Need to get yet another xml to get the final playback url
|
||||
try:
|
||||
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
|
||||
% xml[0][0][0].attrib['key'],
|
||||
'plexapp.com',
|
||||
authenticate=False,
|
||||
token=app.ACCOUNT.plex_token)
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('XML malformed: %s', xml.attrib)
|
||||
xml = None
|
||||
if xml is None:
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
try:
|
||||
playurl = xml[0].attrib['key']
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Last xml malformed: %s', xml.attrib)
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
item.file = playurl
|
||||
listitem.setPath(utils.try_encode(playurl))
|
||||
playqueue.items.append(item)
|
||||
if resolve is True:
|
||||
transfer.send(listitem)
|
||||
else:
|
||||
thread = Thread(target=app.APP.player.play,
|
||||
args={'item': utils.try_encode(playurl),
|
||||
'listitem': listitem})
|
||||
thread.setDaemon(True)
|
||||
LOG.info('Done initializing PKC playback, starting Kodi player')
|
||||
thread.start()
|
||||
|
||||
|
||||
def play_xml(playqueue, xml, offset=None, start_plex_id=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
|
||||
"""
|
||||
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
||||
offset, start_plex_id)
|
||||
stack = _prep_playlist_stack(xml)
|
||||
_process_stack(playqueue, stack)
|
||||
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||
if start_plex_id is not None:
|
||||
for startpos, item in enumerate(playqueue.items):
|
||||
if item.plex_id == start_plex_id:
|
||||
break
|
||||
else:
|
||||
startpos = 0
|
||||
else:
|
||||
for startpos, item in enumerate(playqueue.items):
|
||||
if item.id == playqueue.selectedItemID:
|
||||
break
|
||||
else:
|
||||
startpos = 0
|
||||
thread = Thread(target=threaded_playback,
|
||||
args=(playqueue.kodi_pl, startpos, offset))
|
||||
LOG.info('Done play_xml, starting Kodi player at position %s', startpos)
|
||||
thread.start()
|
||||
|
||||
|
||||
def threaded_playback(kodi_playlist, startpos, offset):
|
||||
"""
|
||||
Seek immediately after kicking off playback is not reliable.
|
||||
"""
|
||||
app.APP.player.play(kodi_playlist, None, False, startpos)
|
||||
if offset and offset != '0':
|
||||
i = 0
|
||||
while not app.APP.is_playing:
|
||||
app.APP.monitor.waitForAbort(0.1)
|
||||
i += 1
|
||||
if i > 100:
|
||||
LOG.error('Could not seek to %s', offset)
|
||||
return
|
||||
js.seek_to(int(offset))
|
|
@ -3,7 +3,9 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import utils, playback, context_entry, transfer, backgroundthread
|
||||
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
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -34,16 +36,102 @@ class PlaybackTask(backgroundthread.Task):
|
|||
mode = params.get('mode')
|
||||
resolve = False if params.get('handle') == '-1' else True
|
||||
LOG.debug('Received mode: %s, params: %s', mode, params)
|
||||
if mode == 'play':
|
||||
playback.playback_triage(plex_id=params.get('plex_id'),
|
||||
plex_type=params.get('plex_type'),
|
||||
path=params.get('path'),
|
||||
resolve=resolve)
|
||||
elif mode == 'plex_node':
|
||||
playback.process_indirect(params['key'],
|
||||
if mode == 'plex_node':
|
||||
process_indirect(params['key'],
|
||||
params['offset'],
|
||||
resolve=resolve)
|
||||
elif mode == 'context_menu':
|
||||
context_entry.ContextMenu(kodi_id=params.get('kodi_id'),
|
||||
kodi_type=params.get('kodi_type'))
|
||||
LOG.debug('Finished PlaybackTask')
|
||||
|
||||
|
||||
def process_indirect(key, offset, resolve=True):
|
||||
"""
|
||||
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
|
||||
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
|
||||
set.
|
||||
|
||||
Will release default.py with setResolvedUrl
|
||||
|
||||
Set resolve to False if playback should be kicked off directly, not via
|
||||
setResolvedUrl
|
||||
"""
|
||||
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
|
||||
key, offset, resolve)
|
||||
global RESOLVE
|
||||
RESOLVE = resolve
|
||||
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
||||
if key.startswith('http') or key.startswith('{server}'):
|
||||
xml = PF.get_playback_xml(key, app.CONN.server_name)
|
||||
elif key.startswith('/system/services'):
|
||||
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
|
||||
'plexapp.com',
|
||||
authenticate=False,
|
||||
token=app.ACCOUNT.plex_token)
|
||||
else:
|
||||
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
|
||||
if xml is None:
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
api = API(xml[0])
|
||||
listitem = transfer.PKCListItem()
|
||||
api.create_listitem(listitem)
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
playqueue.clear()
|
||||
item = PQ.PlaylistItem(xml_video_element=xml[0])
|
||||
item.offset = offset
|
||||
item.playmethod = 'DirectStream'
|
||||
|
||||
# Need to get yet another xml to get the final playback url
|
||||
try:
|
||||
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
|
||||
% xml[0][0][0].attrib['key'],
|
||||
'plexapp.com',
|
||||
authenticate=False,
|
||||
token=app.ACCOUNT.plex_token)
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('XML malformed: %s', xml.attrib)
|
||||
xml = None
|
||||
if xml is None:
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
try:
|
||||
playurl = xml[0].attrib['key']
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Last xml malformed: %s\n%s', xml.tag, xml.attrib)
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
item.file = playurl
|
||||
listitem.setPath(playurl.encode('utf-8'))
|
||||
playqueue.items.append(item)
|
||||
if resolve is True:
|
||||
transfer.send(listitem)
|
||||
else:
|
||||
LOG.info('Done initializing PKC playback, starting Kodi player')
|
||||
app.APP.player.play(item=playurl.encode('utf-8'),
|
||||
listitem=listitem)
|
||||
|
||||
|
||||
def _ensure_resolve(abort=False):
|
||||
"""
|
||||
Will check whether RESOLVE=True and if so, fail Kodi playback startup
|
||||
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
|
||||
pickling)
|
||||
|
||||
This way we're making sure that other Python instances (calling default.py)
|
||||
will be destroyed.
|
||||
"""
|
||||
if RESOLVE:
|
||||
# Releases the other Python thread without a ListItem
|
||||
transfer.send(True)
|
||||
# Shows PKC error message
|
||||
# transfer.send(None)
|
||||
if abort:
|
||||
# Reset some playback variables
|
||||
app.PLAYSTATE.context_menu_play = False
|
||||
app.PLAYSTATE.force_transcode = False
|
||||
app.PLAYSTATE.resume_playback = False
|
||||
|
|
|
@ -1,839 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Collection of functions associated with Kodi and Plex playlists and playqueues
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
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 . import json_rpc as js
|
||||
from . import variables as v
|
||||
from . import app
|
||||
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger('PLEX.playlist_func')
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
class PlaylistError(Exception):
|
||||
"""
|
||||
Exception for our playlist constructs
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Playqueue_Object(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
|
||||
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
|
||||
|
||||
def __repr__(self):
|
||||
answ = ("{{"
|
||||
"'playlistid': {self.playlistid}, "
|
||||
"'id': {self.id}, "
|
||||
"'version': {self.version}, "
|
||||
"'type': '{self.type}', "
|
||||
"'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(self=self))
|
||||
answ = answ.encode('utf-8')
|
||||
# Since list.__repr__ will return string, not unicode
|
||||
return answ + b"'items': {self.items}}}".format(self=self)
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
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
|
||||
LOG.debug('Playlist cleared: %s', self)
|
||||
|
||||
|
||||
class Playlist_Item(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
|
||||
"""
|
||||
def __init__(self):
|
||||
self._id = None
|
||||
self._plex_id = None
|
||||
self.plex_type = None
|
||||
self.plex_uuid = None
|
||||
self._kodi_id = None
|
||||
self.kodi_type = None
|
||||
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.force_transcode = False
|
||||
|
||||
@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 = ("{{"
|
||||
"'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}', "
|
||||
"'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))
|
||||
answ = answ.encode('utf-8')
|
||||
# etree xml.__repr__() could return string, not unicode
|
||||
return answ + b"'xml': \"{self.xml}\"}}".format(self=self)
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 = Playlist_Item()
|
||||
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.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-)
|
||||
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 = utils.cast(int, query.get('plex_id'))
|
||||
item.plex_type = query.get('itemType')
|
||||
if item.plex_id is None and item.file is not None:
|
||||
item.uri = ('library://whatever/item/%s'
|
||||
% utils.quote(item.file, safe=''))
|
||||
else:
|
||||
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||
(item.plex_uuid, item.plex_id))
|
||||
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'
|
||||
"""
|
||||
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.info('kodi_item %s cannot be used for Plex playback', kodi_item)
|
||||
raise PlaylistError
|
||||
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
|
||||
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 = Playlist_Item()
|
||||
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)
|
||||
item.plex_uuid = plex_id
|
||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||
(item.plex_uuid, 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 = Playlist_Item()
|
||||
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_id is not None and item.plex_type != v.PLEX_TYPE_CLIP:
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_element = plexdb.item_by_id(item.plex_id)
|
||||
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.xml = xml_video_element
|
||||
LOG.debug('Created new playlist item from xml: %s', item)
|
||||
return item
|
||||
|
||||
|
||||
def _get_playListVersion_from_xml(playlist, xml):
|
||||
"""
|
||||
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
||||
playQueueVersion).
|
||||
|
||||
Raises PlaylistError if unsuccessful
|
||||
"""
|
||||
playlist.version = utils.cast(int,
|
||||
xml.get('%sVersion' % playlist.kind))
|
||||
if playlist.version is None:
|
||||
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.
|
||||
"""
|
||||
playlist.id = utils.cast(int,
|
||||
xml.get('%sID' % playlist.kind))
|
||||
playlist.version = utils.cast(int,
|
||||
xml.get('%sVersion' % playlist.kind))
|
||||
playlist.shuffled = utils.cast(int,
|
||||
xml.get('%sShuffled' % playlist.kind))
|
||||
playlist.selectedItemID = utils.cast(int,
|
||||
xml.get('%sSelectedItemID'
|
||||
% playlist.kind))
|
||||
playlist.selectedItemOffset = utils.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
|
||||
"""
|
||||
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)
|
||||
playlist.clear(kodi=False)
|
||||
verify_kodi_item(plex_id, kodi_item)
|
||||
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
|
||||
}
|
||||
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
|
||||
action_type="POST",
|
||||
parameters=params)
|
||||
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?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:
|
||||
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.xml = xml[-1]
|
||||
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
|
||||
_get_playListVersion_from_xml(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.xml = 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)
|
||||
# We need to increment the playlistVersion
|
||||
_get_playListVersion_from_xml(
|
||||
playlist, DU().downloadUrl(url, action_type="PUT"))
|
||||
# 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
|
||||
|
||||
Returns None if something went wrong
|
||||
"""
|
||||
playlist_id = playlist_id if playlist_id else playlist.id
|
||||
if 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 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)
|
||||
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
|
||||
(playlist.kind,
|
||||
playlist.id,
|
||||
playlist.items[pos].id,
|
||||
playlist.repeat),
|
||||
action_type="DELETE")
|
||||
_get_playListVersion_from_xml(playlist, xml)
|
||||
del playlist.items[pos]
|
||||
|
||||
|
||||
# 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
|
||||
"""
|
||||
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')
|
|
@ -61,7 +61,8 @@ def get_playlist(path=None, kodi_hash=None, plex_id=None):
|
|||
|
||||
def _m3u_iterator(text):
|
||||
"""
|
||||
Yields e.g. plugin://plugin.video.plexkodiconnect.movies/?plex_id=xxx
|
||||
Yields e.g.
|
||||
http://127.0.0.1:<port>/plex/kodi/movies/file.strm?plex_id=...
|
||||
"""
|
||||
lines = iter(text.split('\n'))
|
||||
for line in lines:
|
||||
|
|
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, PlayqueueError, 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
|
302
resources/lib/playqueue/common.py
Normal file
302
resources/lib/playqueue/common.py
Normal file
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from ..plex_db import PlexDB
|
||||
from ..plex_api import API
|
||||
from .. import plex_functions as PF, utils, kodi_db, variables as v, app
|
||||
|
||||
# Our PKC playqueues (3 instances of PlayQueue())
|
||||
PLAYQUEUES = []
|
||||
|
||||
|
||||
class PlayqueueError(Exception):
|
||||
"""
|
||||
Exception for our playqueue 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 = utils.cast(int, 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:
|
||||
self._from_plex_db()
|
||||
if grab_xml and self.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_type = db_item['plex_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._from_plex_db()
|
||||
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_plex_db(self):
|
||||
"""
|
||||
Uses self.kodi_id and self.kodi_type to look up the item in the Plex
|
||||
DB. Thus potentially sets self.plex_id, plex_type, plex_uuid
|
||||
"""
|
||||
if self.kodi_id is None or not self.kodi_type:
|
||||
return
|
||||
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']
|
||||
|
||||
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
|
||||
if self.kodi_id is None or not self.kodi_type:
|
||||
self._from_plex_db()
|
||||
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 = utils.cast(int, 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:
|
||||
self._from_plex_db()
|
||||
if self.plex_id is None and self.file:
|
||||
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()
|
||||
|
||||
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:
|
||||
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 self.file is set, will try to guess kodi_id and kodi_type from the
|
||||
filename and path using the Kodi video and music databases
|
||||
"""
|
||||
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)
|
||||
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
|
||||
# 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
|
||||
|
||||
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 = 'PKC Dummy playqueue 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,90 +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 of Playqueue_Object())
|
||||
PLAYQUEUES = []
|
||||
###############################################################################
|
||||
from .common import PlayqueueError, 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_Object()
|
||||
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 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):
|
||||
|
@ -146,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 PlayqueueError:
|
||||
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 PlayqueueError:
|
||||
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
|
||||
|
@ -183,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 PlayqueueError:
|
||||
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')
|
||||
|
@ -206,6 +125,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 +136,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)
|
604
resources/lib/playqueue/playqueue.py
Normal file
604
resources/lib/playqueue/playqueue.py
Normal file
|
@ -0,0 +1,604 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import threading
|
||||
|
||||
from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError
|
||||
|
||||
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 PlayqueueError()
|
||||
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 PlayqueueError('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 PlayqueueError('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 PlayqueueError('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 a PlayqueueError 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 PlayqueueError('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, resume=False)
|
||||
|
||||
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, resume=False)
|
||||
|
||||
def _kodi_add_xml(self, xml, api, resume, playlistitem=None):
|
||||
"""
|
||||
Be careful what you pass as resume:
|
||||
False: do not resume, do not subsequently ask user
|
||||
True: do resume, do not subsequently ask user
|
||||
"""
|
||||
if not playlistitem:
|
||||
playlistitem = PlaylistItem(xml_video_element=xml)
|
||||
playlistitem.part = api.part
|
||||
playlistitem.force_transcode = self.force_transcode
|
||||
playlistitem.resume = resume
|
||||
listitem = widgets.get_listitem(xml, resume=resume)
|
||||
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 PlayqueueError
|
||||
"""
|
||||
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 PlayqueueError
|
||||
"""
|
||||
if not isinstance(item, PlaylistItem):
|
||||
raise PlayqueueError('Wrong item %s of type %s received'
|
||||
% (item, type(item)))
|
||||
if pos > len(self.items):
|
||||
raise PlayqueueError('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 PlayqueueError('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 PlayqueueError('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 PlayqueueError
|
||||
"""
|
||||
if not isinstance(item, PlaylistItem) or not item.uri:
|
||||
raise PlayqueueError('Wrong item %s of type %s received'
|
||||
% (item, type(item)))
|
||||
if pos > len(self.items):
|
||||
raise PlayqueueError('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 PlayqueueError('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 PlayqueueError('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 PlayqueueError('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 PlayqueueError()
|
||||
|
||||
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 PlayqueueError('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 PlayqueueError('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)
|
90
resources/lib/playstrm.py
Normal file
90
resources/lib/playstrm.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import app, utils, json_rpc, variables as v, playqueue as PQ
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.playstrm')
|
||||
|
||||
|
||||
class PlayStrmException(Exception):
|
||||
"""
|
||||
Any Exception associated with playstrm
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlayStrm(object):
|
||||
'''
|
||||
Workflow: Strm that calls our webservice in database. When played, the
|
||||
webserivce returns a dummy file to play. Meanwhile, PlayStrm adds the real
|
||||
listitems for items to play to the playlist.
|
||||
'''
|
||||
def __init__(self, params):
|
||||
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':
|
||||
self.synched = False
|
||||
else:
|
||||
self.synched = True
|
||||
self.kodi_id = utils.cast(int, params.get('kodi_id'))
|
||||
self.kodi_type = params.get('kodi_type')
|
||||
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)
|
||||
|
||||
def __unicode__(self):
|
||||
return ("{{"
|
||||
"'plex_id': {self.plex_id}, "
|
||||
"'plex_type': '{self.plex_type}', "
|
||||
"'kodi_id': {self.kodi_id}, "
|
||||
"'kodi_type': '{self.kodi_type}', "
|
||||
"}}").format(self=self)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
__repr__ = __str__
|
||||
|
||||
def play(self, start_position=None, delayed=True):
|
||||
'''
|
||||
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)
|
||||
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.playqueue.start_playback(start_position)
|
||||
return self.playqueue.index
|
||||
|
||||
def play_folder(self, position=None):
|
||||
'''
|
||||
When an entire queue is requested, If requested from Kodi, kodi_type is
|
||||
provided, add as Kodi would, otherwise queue playlist items using strm
|
||||
links to setup playback later.
|
||||
'''
|
||||
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 = PQ.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
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -133,12 +133,14 @@ class API(object):
|
|||
# Set plugin path and media flags using real filename
|
||||
if self.plex_type() == v.PLEX_TYPE_EPISODE:
|
||||
# need to include the plex show id in the path
|
||||
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
|
||||
% self.grandparent_id())
|
||||
else:
|
||||
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type()]
|
||||
path = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||
% (path, self.plex_id(), self.plex_type(), filename))
|
||||
path = ('http://127.0.0.1:%s/plex/kodi/shows/%s'
|
||||
% (v.WEBSERVICE_PORT, self.grandparent_id()))
|
||||
elif self.plex_type() in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_CLIP):
|
||||
path = 'http://127.0.0.1:%s/plex/kodi/movies' % v.WEBSERVICE_PORT
|
||||
elif self.plex_type() == v.PLEX_TYPE_SONG:
|
||||
path = 'http://127.0.0.1:%s/plex/kodi/music' % v.WEBSERVICE_PORT
|
||||
path = '{0}/{1}/file.strm?plex_id={1}&plex_type={2}'.format(
|
||||
path, self.plex_id(), self.plex_type())
|
||||
else:
|
||||
# Direct paths is set the Kodi way
|
||||
path = self.validate_playurl(filename,
|
||||
|
@ -810,8 +812,8 @@ class API(object):
|
|||
elif not url:
|
||||
url = extra.get('ratingKey')
|
||||
if url:
|
||||
url = ('plugin://%s.movies/?plex_id=%s&plex_type=%s&mode=play'
|
||||
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
||||
url = 'http://127.0.0.1:{0}/plex/kodi/movies/{1}/file.strm?plex_id={1}&plex_type={2}'.format(
|
||||
v.WEBSERVICE_PORT, url, v.PLEX_TYPE_CLIP)
|
||||
return url
|
||||
|
||||
def mediastreams(self):
|
||||
|
|
|
@ -14,8 +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 playback
|
||||
from . import json_rpc as js
|
||||
from . import playqueue as PQ
|
||||
from . import variables as v
|
||||
|
@ -40,6 +38,8 @@ def update_playqueue_from_PMS(playqueue,
|
|||
|
||||
repeat = 0, 1, 2
|
||||
offset = time offset in Plextime (milliseconds)
|
||||
|
||||
Will (re)start playback
|
||||
"""
|
||||
LOG.info('New playqueue %s received from Plex companion with offset '
|
||||
'%s, repeat %s', playqueue_id, offset, repeat)
|
||||
|
@ -47,21 +47,15 @@ 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(playqueue, playqueue_id)
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
xml = PQ.get_PMS_playlist(playlist_id=playqueue_id)
|
||||
if xml is None:
|
||||
LOG.error('Could now download playqueue %s', playqueue_id)
|
||||
return
|
||||
playqueue.clear()
|
||||
try:
|
||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||
except PL.PlaylistError:
|
||||
LOG.error('Could not get playqueue ID %s', playqueue_id)
|
||||
return
|
||||
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||
playqueue.plex_transient_token = transient_token
|
||||
playback.play_xml(playqueue, xml, offset)
|
||||
raise PQ.PlayqueueError()
|
||||
app.PLAYSTATE.initiated_by_plex = True
|
||||
playqueue.init_from_xml(xml,
|
||||
offset=offset,
|
||||
repeat=0 if not repeat else int(repeat),
|
||||
transient_token=transient_token)
|
||||
|
||||
|
||||
class PlexCompanion(backgroundthread.KillableThread):
|
||||
|
@ -81,45 +75,47 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
|
||||
@staticmethod
|
||||
def _process_alexa(data):
|
||||
app.PLAYSTATE.initiated_by_plex = True
|
||||
xml = PF.GetPlexMetadata(data['key'])
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
LOG.error('Could not download Plex metadata for: %s', data)
|
||||
return
|
||||
raise PQ.PlayqueueError()
|
||||
api = API(xml[0])
|
||||
if api.plex_type() == v.PLEX_TYPE_ALBUM:
|
||||
LOG.debug('Plex music album detected')
|
||||
PQ.init_playqueue_from_plex_children(
|
||||
api.plex_id(),
|
||||
xml = PF.GetAllPlexChildren(api.plex_id())
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not download the album xml for %s', data)
|
||||
raise PQ.PlayqueueError()
|
||||
playqueue = PQ.get_playqueue_from_type('audio')
|
||||
playqueue.init_from_xml(xml,
|
||||
transient_token=data.get('token'))
|
||||
elif data['containerKey'].startswith('/playQueues/'):
|
||||
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
|
||||
xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
|
||||
if xml is None:
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
return
|
||||
LOG.error('Could not get playqueue for %s', data)
|
||||
raise PQ.PlayqueueError()
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
playqueue.clear()
|
||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||
playqueue.plex_transient_token = data.get('token')
|
||||
if data.get('offset') != '0':
|
||||
offset = float(data['offset']) / 1000.0
|
||||
else:
|
||||
offset = None
|
||||
playback.play_xml(playqueue, xml, offset)
|
||||
offset = utils.cast(float, data.get('offset')) or None
|
||||
if offset:
|
||||
offset = offset / 1000.0
|
||||
playqueue.init_from_xml(xml,
|
||||
offset=offset,
|
||||
transient_token=data.get('token'))
|
||||
else:
|
||||
app.CONN.plex_transient_token = data.get('token')
|
||||
if data.get('offset') != '0':
|
||||
if utils.cast(float, data.get('offset')):
|
||||
app.PLAYSTATE.resume_playback = True
|
||||
playback.playback_triage(api.plex_id(),
|
||||
api.plex_type(),
|
||||
resolve=False)
|
||||
path = ('http://127.0.0.1:%s/plex/play/file.strm?plex_id=%s'
|
||||
% (v.WEBSERVICE_PORT, api.plex_id()))
|
||||
path += '&plex_type=%s' % api.plex_type()
|
||||
executebuiltin(('PlayMedia(%s)' % path).encode('utf-8'))
|
||||
|
||||
@staticmethod
|
||||
def _process_node(data):
|
||||
|
@ -150,14 +146,14 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
xml[0].attrib
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
LOG.error('Could not download Plex metadata')
|
||||
return
|
||||
raise PQ.PlayqueueError()
|
||||
api = API(xml[0])
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||
update_playqueue_from_PMS(playqueue,
|
||||
playqueue_id=container_key,
|
||||
repeat=query.get('repeat'),
|
||||
offset=data.get('offset'),
|
||||
offset=utils.cast(float, data.get('offset')) or None,
|
||||
transient_token=data.get('token'))
|
||||
|
||||
@staticmethod
|
||||
|
@ -167,6 +163,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
"""
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
|
||||
try:
|
||||
pos = js.get_position(playqueue.playlistid)
|
||||
if 'audioStreamID' in data:
|
||||
index = playqueue.items[pos].kodi_stream_index(
|
||||
|
@ -181,18 +178,20 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
app.APP.player.setSubtitleStream(index)
|
||||
else:
|
||||
LOG.error('Unknown setStreams command: %s', data)
|
||||
except KeyError:
|
||||
LOG.warn('Could not process stream data: %s', data)
|
||||
|
||||
@staticmethod
|
||||
def _process_refresh(data):
|
||||
"""
|
||||
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(
|
||||
|
@ -220,6 +219,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
"""
|
||||
LOG.debug('Processing: %s', task)
|
||||
data = task['data']
|
||||
try:
|
||||
if task['action'] == 'alexa':
|
||||
with app.APP.lock_playqueues:
|
||||
self._process_alexa(data)
|
||||
|
@ -233,10 +233,15 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
with app.APP.lock_playqueues:
|
||||
self._process_refresh(data)
|
||||
elif task['action'] == 'setStreams':
|
||||
try:
|
||||
self._process_streams(data)
|
||||
except KeyError:
|
||||
pass
|
||||
except PQ.PlayqueueError:
|
||||
LOG.error('Could not process companion data: %s', data)
|
||||
# "Play Error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
app.PLAYSTATE.initiated_by_plex = False
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
|
|
|
@ -12,3 +12,18 @@ from .sections import Sections
|
|||
|
||||
class PlexDB(PlexDBBase, TVShows, Movies, Music, Playlists, Sections):
|
||||
pass
|
||||
|
||||
|
||||
def kodi_from_plex(plex_id, plex_type=None):
|
||||
"""
|
||||
Returns the tuple (kodi_id, kodi_type) for plex_id. Faster, if plex_type
|
||||
is provided
|
||||
|
||||
Returns (None, None) if unsuccessful
|
||||
"""
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||
if db_item:
|
||||
return (db_item['kodi_id'], db_item['kodi_type'])
|
||||
else:
|
||||
return None, None
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -159,18 +159,17 @@ class SubscriptionMgr(object):
|
|||
v.PLEX_PLAYLIST_TYPE_AUDIO: None,
|
||||
v.PLEX_PLAYLIST_TYPE_PHOTO: None
|
||||
}
|
||||
for typus in timelines:
|
||||
if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
|
||||
for plex_type in timelines:
|
||||
kodi_type = v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[plex_type]
|
||||
if players.get(kodi_type) is None:
|
||||
timeline = {
|
||||
'controllable': CONTROLLABLE[typus],
|
||||
'type': typus,
|
||||
'controllable': CONTROLLABLE[plex_type],
|
||||
'type': plex_type,
|
||||
'state': 'stopped'
|
||||
}
|
||||
else:
|
||||
timeline = self._timeline_dict(players[
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]],
|
||||
typus)
|
||||
timelines[typus] = self._dict_to_xml(timeline)
|
||||
timeline = self._timeline_dict(players[kodi_type], plex_type)
|
||||
timelines[plex_type] = self._dict_to_xml(timeline)
|
||||
timelines.update({'command_id': '{command_id}',
|
||||
'location': self.location})
|
||||
return answ.format(**timelines)
|
||||
|
@ -302,7 +301,7 @@ class SubscriptionMgr(object):
|
|||
playqueue = PQ.PLAYQUEUES[playerid]
|
||||
info = app.PLAYSTATE.player_states[playerid]
|
||||
position = self._get_correct_position(info, playqueue)
|
||||
if info[STREAM_DETAILS[stream_type]] == -1:
|
||||
if info[STREAM_DETAILS[stream_type]] in (-1, None):
|
||||
kodi_stream_index = -1
|
||||
else:
|
||||
kodi_stream_index = info[STREAM_DETAILS[stream_type]]['index']
|
||||
|
|
|
@ -10,6 +10,7 @@ from . import initialsetup
|
|||
from . import kodimonitor
|
||||
from . import sync, library_sync
|
||||
from . import websocket_client
|
||||
from . import webservice
|
||||
from . import plex_companion
|
||||
from . import plex_functions as PF, playqueue as PQ
|
||||
from . import playback_starter
|
||||
|
@ -433,6 +434,7 @@ class Service(object):
|
|||
self.setup.setup()
|
||||
|
||||
# Initialize important threads
|
||||
self.webservice = webservice.WebService()
|
||||
self.ws = websocket_client.PMS_Websocket()
|
||||
self.alexa = websocket_client.Alexa_Websocket()
|
||||
self.sync = sync.Sync()
|
||||
|
@ -494,6 +496,14 @@ class Service(object):
|
|||
xbmc.sleep(100)
|
||||
continue
|
||||
|
||||
if self.webservice is not None and not self.webservice.is_alive():
|
||||
# TODO: Emby completely restarts Emby for Kodi at this point
|
||||
# Check if this is really necessary
|
||||
LOG.info('Restarting webservice')
|
||||
self.webservice.abort()
|
||||
self.webservice = webservice.WebService()
|
||||
self.webservice.start()
|
||||
|
||||
# Before proceeding, need to make sure:
|
||||
# 1. Server is online
|
||||
# 2. User is set
|
||||
|
@ -523,12 +533,15 @@ class Service(object):
|
|||
continue
|
||||
elif not self.startup_completed:
|
||||
self.startup_completed = True
|
||||
LOG.debug('Starting service threads')
|
||||
self.webservice.start()
|
||||
self.ws.start()
|
||||
self.sync.start()
|
||||
self.plexcompanion.start()
|
||||
self.playqueue.start()
|
||||
if utils.settings('enable_alexa') == 'true':
|
||||
self.alexa.start()
|
||||
LOG.debug('Service threads started')
|
||||
|
||||
xbmc.sleep(100)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -92,6 +92,9 @@ DEVICENAME = DEVICENAME.replace(' ', "")
|
|||
|
||||
COMPANION_PORT = int(_ADDON.getSetting('companionPort'))
|
||||
|
||||
# Port for the PKC webservice
|
||||
WEBSERVICE_PORT = 57578
|
||||
|
||||
# Unique ID for this Plex client; also see clientinfo.py
|
||||
PKC_MACHINE_IDENTIFIER = None
|
||||
|
||||
|
|
455
resources/lib/webservice.py
Normal file
455
resources/lib/webservice.py
Normal file
|
@ -0,0 +1,455 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
PKC-dedicated webserver. Listens to Kodi starting playback; will then hand-over
|
||||
playback to plugin://video.plexkodiconnect
|
||||
'''
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import BaseHTTPServer
|
||||
import httplib
|
||||
import socket
|
||||
import Queue
|
||||
|
||||
import xbmc
|
||||
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 json_rpc as js, plex_functions as PF
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.webservice')
|
||||
|
||||
|
||||
class WebService(backgroundthread.KillableThread):
|
||||
''' Run a webservice to trigger playback.
|
||||
'''
|
||||
def is_alive(self):
|
||||
''' Called to see if the webservice is still responding.
|
||||
'''
|
||||
alive = True
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(('127.0.0.1', v.WEBSERVICE_PORT))
|
||||
s.sendall('')
|
||||
except Exception as error:
|
||||
LOG.error('is_alive error: %s', error)
|
||||
if 'Errno 61' in str(error):
|
||||
alive = False
|
||||
s.close()
|
||||
return alive
|
||||
|
||||
def abort(self):
|
||||
''' Called when the thread needs to stop
|
||||
'''
|
||||
try:
|
||||
conn = httplib.HTTPConnection('127.0.0.1:%d' % v.WEBSERVICE_PORT)
|
||||
conn.request('QUIT', '/')
|
||||
conn.getresponse()
|
||||
except Exception as error:
|
||||
xbmc.log('PLEX.webservice abort error: %s' % error, xbmc.LOGWARNING)
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
Called when thread needs to suspend - let's not do anything and keep
|
||||
webservice up
|
||||
"""
|
||||
self.suspend_reached = True
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Called when thread needs to resume - let's not do anything and keep
|
||||
webservice up
|
||||
"""
|
||||
self.suspend_reached = False
|
||||
|
||||
def run(self):
|
||||
''' Called to start the webservice.
|
||||
'''
|
||||
LOG.info('----===## Starting WebService on port %s ##===----',
|
||||
v.WEBSERVICE_PORT)
|
||||
app.APP.register_thread(self)
|
||||
try:
|
||||
server = HttpServer(('127.0.0.1', v.WEBSERVICE_PORT),
|
||||
RequestHandler)
|
||||
LOG.info('Serving http on %s', server.socket.getsockname())
|
||||
server.serve_forever()
|
||||
except Exception as error:
|
||||
LOG.error('Error encountered: %s', error)
|
||||
if '10053' not in error: # ignore host diconnected errors
|
||||
utils.ERROR()
|
||||
finally:
|
||||
app.APP.deregister_thread(self)
|
||||
LOG.info('##===---- WebService stopped ----===##')
|
||||
|
||||
|
||||
class HttpServer(BaseHTTPServer.HTTPServer):
|
||||
''' Http server that reacts to self.stop flag.
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stop = False
|
||||
self.pending = []
|
||||
self.threads = []
|
||||
self.queue = Queue.Queue()
|
||||
BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
|
||||
|
||||
def serve_forever(self):
|
||||
|
||||
''' Handle one request at a time until stopped.
|
||||
'''
|
||||
while not self.stop:
|
||||
self.handle_request()
|
||||
|
||||
|
||||
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
'''
|
||||
Http request handler. Do not use LOG here, it will hang requests in Kodi >
|
||||
show information dialog.
|
||||
'''
|
||||
timeout = 0.5
|
||||
|
||||
def log_message(self, format, *args):
|
||||
''' Mute the webservice requests.
|
||||
'''
|
||||
pass
|
||||
|
||||
def handle(self):
|
||||
''' To quiet socket errors with 404.
|
||||
'''
|
||||
try:
|
||||
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
|
||||
except Exception as error:
|
||||
if '10054' in error:
|
||||
# Silence "[Errno 10054] An existing connection was forcibly
|
||||
# closed by the remote host"
|
||||
return
|
||||
xbmc.log('PLEX.webservice handle error: %s' % error, xbmc.LOGWARNING)
|
||||
|
||||
def do_QUIT(self):
|
||||
''' send 200 OK response, and set server.stop to True
|
||||
'''
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.server.stop = True
|
||||
|
||||
def get_params(self):
|
||||
''' Get the params as a dict
|
||||
'''
|
||||
try:
|
||||
path = self.path[1:].decode('utf-8')
|
||||
except IndexError:
|
||||
path = ''
|
||||
params = {}
|
||||
if '?' in path:
|
||||
path = path.split('?', 1)[1]
|
||||
params = dict(utils.parse_qsl(path))
|
||||
if 'plex_id' not in params:
|
||||
LOG.error('No plex_id received for path %s', path)
|
||||
return
|
||||
|
||||
if 'plex_type' in params and params['plex_type'].lower() == 'none':
|
||||
del params['plex_type']
|
||||
if 'plex_type' not in params:
|
||||
LOG.debug('Need to look-up plex_type')
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
db_item = plexdb.item_by_id(params['plex_id'])
|
||||
if db_item:
|
||||
params['plex_type'] = db_item['plex_type']
|
||||
else:
|
||||
LOG.debug('No plex_type found, using Kodi player id')
|
||||
players = js.get_players()
|
||||
if players:
|
||||
params['plex_type'] = v.PLEX_TYPE_CLIP if 'video' in players \
|
||||
else v.PLEX_TYPE_SONG
|
||||
LOG.debug('Using the following plex_type: %s',
|
||||
params['plex_type'])
|
||||
else:
|
||||
xml = PF.GetPlexMetadata(params['plex_id'])
|
||||
if xml in (None, 401):
|
||||
LOG.error('Could not get metadata for %s', params)
|
||||
return
|
||||
api = API(xml[0])
|
||||
params['plex_type'] = api.plex_type()
|
||||
LOG.debug('Got metadata, using plex_type %s',
|
||||
params['plex_type'])
|
||||
return params
|
||||
|
||||
def do_HEAD(self):
|
||||
''' Called on HEAD requests
|
||||
'''
|
||||
self.handle_request(True)
|
||||
|
||||
def do_GET(self):
|
||||
''' Called on GET requests
|
||||
'''
|
||||
self.handle_request()
|
||||
|
||||
def handle_request(self, headers_only=False):
|
||||
'''Send headers and reponse
|
||||
'''
|
||||
xbmc.log('PLEX.webservice handle_request called. headers %s, path: %s'
|
||||
% (headers_only, self.path), xbmc.LOGDEBUG)
|
||||
try:
|
||||
if b'extrafanart' in self.path or b'extrathumbs' in self.path:
|
||||
raise Exception('unsupported artwork request')
|
||||
|
||||
if headers_only:
|
||||
self.send_response(200)
|
||||
self.send_header(b'Content-type', b'text/html')
|
||||
self.end_headers()
|
||||
|
||||
elif b'file.strm' not in self.path:
|
||||
self.images()
|
||||
else:
|
||||
self.strm()
|
||||
|
||||
except Exception as error:
|
||||
self.send_error(500,
|
||||
b'PLEX.webservice: Exception occurred: %s' % error)
|
||||
|
||||
def strm(self):
|
||||
''' Return a dummy video and and queue real items.
|
||||
'''
|
||||
xbmc.log('PLEX.webservice: starting strm', xbmc.LOGDEBUG)
|
||||
self.send_response(200)
|
||||
self.send_header(b'Content-type', b'text/html')
|
||||
self.end_headers()
|
||||
|
||||
params = self.get_params()
|
||||
|
||||
if b'kodi/movies' in self.path:
|
||||
params['kodi_type'] = v.KODI_TYPE_MOVIE
|
||||
elif b'kodi/tvshows' in self.path:
|
||||
params['kodi_type'] = v.KODI_TYPE_EPISODE
|
||||
# elif 'kodi/musicvideos' in self.path:
|
||||
# params['MediaType'] = 'musicvideo'
|
||||
|
||||
if utils.settings('pluginSingle.bool'):
|
||||
path = 'plugin://plugin.video.plexkodiconnect?mode=playsingle&plex_id=%s' % params['plex_id']
|
||||
if params.get('server'):
|
||||
path += '&server=%s' % params['server']
|
||||
if params.get('transcode'):
|
||||
path += '&transcode=true'
|
||||
if params.get('kodi_id'):
|
||||
path += '&kodi_id=%s' % params['kodi_id']
|
||||
if params.get('kodi_type'):
|
||||
path += '&kodi_type=%s' % params['kodi_type']
|
||||
self.wfile.write(bytes(path))
|
||||
return
|
||||
|
||||
path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id']
|
||||
self.wfile.write(bytes(path.encode('utf-8')))
|
||||
if params['plex_id'] not in self.server.pending:
|
||||
self.server.pending.append(params['plex_id'])
|
||||
self.server.queue.put(params)
|
||||
if not len(self.server.threads):
|
||||
queue = QueuePlay(self.server, params['plex_type'])
|
||||
queue.start()
|
||||
self.server.threads.append(queue)
|
||||
|
||||
def images(self):
|
||||
''' Return a dummy image for unwanted images requests over the webservice.
|
||||
Required to prevent freezing of widget playback if the file url has no
|
||||
local textures cached yet.
|
||||
'''
|
||||
image = xbmc.translatePath(
|
||||
'special://home/addons/plugin.video.plexkodiconnect/icon.png').decode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header(b'Content-type', b'image/png')
|
||||
modified = xbmcvfs.Stat(image).st_mtime()
|
||||
self.send_header(b'Last-Modified', b'%s' % modified)
|
||||
image = xbmcvfs.File(image)
|
||||
size = image.size()
|
||||
self.send_header(b'Content-Length', str(size))
|
||||
self.end_headers()
|
||||
self.wfile.write(image.readBytes())
|
||||
image.close()
|
||||
|
||||
|
||||
class QueuePlay(backgroundthread.KillableThread):
|
||||
''' Workflow for new playback:
|
||||
|
||||
Queue up strm playback that was called in the webservice. Called
|
||||
playstrm in default.py which will wait for our signal here. Downloads
|
||||
plex information. Add content to the playlist after the strm file that
|
||||
initiated playback from db. Start playback by telling playstrm waiting.
|
||||
It will fail playback of the current strm and move to the next entry for
|
||||
us. If play folder, playback starts here.
|
||||
|
||||
Required delay for widgets, custom skin containers and non library
|
||||
windows. Otherwise Kodi will freeze if no artwork textures are cached
|
||||
yet in Textures13.db Will be skipped if the player already has media and
|
||||
is playing.
|
||||
|
||||
Why do all this instead of using plugin? Strms behaves better than
|
||||
plugin in database. Allows to load chapter images with direct play.
|
||||
Allows to have proper artwork for intros. Faster than resolving using
|
||||
plugin, especially on low powered devices. Cons: Can't use external
|
||||
players with this method.
|
||||
'''
|
||||
|
||||
def __init__(self, server, plex_type):
|
||||
self.server = server
|
||||
self.plex_type = plex_type
|
||||
self.plex_id = None
|
||||
self.kodi_id = None
|
||||
self.kodi_type = None
|
||||
self.synched = True
|
||||
self.force_transcode = False
|
||||
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')
|
||||
# Some cleanup
|
||||
if params.get('transcode'):
|
||||
self.force_transcode = params['transcode'].lower() == 'true'
|
||||
if params.get('server') and params['server'].lower() == 'none':
|
||||
self.server = None
|
||||
if params.get('synched'):
|
||||
self.synched = not params['synched'].lower() == 'false'
|
||||
|
||||
def _get_playqueue(self):
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
|
||||
if ((self.plex_type in v.PLEX_VIDEOTYPES and
|
||||
not app.PLAYSTATE.initiated_by_plex and
|
||||
xbmc.getCondVisibility('Window.IsVisible(Home.xml)'))):
|
||||
# Video launched from a widget - which starts a Kodi AUDIO playlist
|
||||
# We will empty everything and start with a fresh VIDEO playlist
|
||||
LOG.debug('Widget video playback detected')
|
||||
video_widget_playback = True
|
||||
# Release default.py
|
||||
utils.window('plex.playlist.play', value='true')
|
||||
# The playlist will be ready anyway
|
||||
app.PLAYSTATE.playlist_ready = True
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
|
||||
playqueue.clear()
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
|
||||
playqueue.clear()
|
||||
# Wait for Kodi to catch up - xbmcplugin.setResolvedUrl() needs to
|
||||
# have run its course and thus the original item needs to have
|
||||
# failed before we start playback anew
|
||||
xbmc.sleep(200)
|
||||
else:
|
||||
video_widget_playback = False
|
||||
if self.plex_type in v.PLEX_VIDEOTYPES:
|
||||
LOG.debug('Video playback detected')
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
|
||||
else:
|
||||
LOG.debug('Audio playback detected')
|
||||
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
|
||||
return playqueue, video_widget_playback
|
||||
|
||||
def run(self):
|
||||
with app.APP.lock_playqueues:
|
||||
LOG.debug('##===---- Starting QueuePlay ----===##')
|
||||
try:
|
||||
self._run()
|
||||
finally:
|
||||
app.PLAYSTATE.playlist_ready = False
|
||||
app.PLAYSTATE.playlist_start_pos = None
|
||||
app.PLAYSTATE.initiated_by_plex = False
|
||||
self.server.threads.remove(self)
|
||||
self.server.pending = []
|
||||
LOG.debug('##===---- QueuePlay Stopped ----===##')
|
||||
|
||||
def _run(self):
|
||||
abort = False
|
||||
play_folder = False
|
||||
playqueue, video_widget_playback = self._get_playqueue()
|
||||
# Position to start playback from (!!)
|
||||
# Do NOT use kodi_pl.getposition() as that appears to be buggy
|
||||
try:
|
||||
start_position = max(js.get_position(playqueue.playlistid), 0)
|
||||
except KeyError:
|
||||
# Widgets: Since we've emptied the entire playlist, we won't get a
|
||||
# position
|
||||
start_position = 0
|
||||
# Position to add next element to queue - we're doing this at the end
|
||||
# of our current playqueue
|
||||
position = playqueue.kodi_pl.size()
|
||||
# Set to start_position + 1 because first item will fail
|
||||
app.PLAYSTATE.playlist_start_pos = start_position + 1
|
||||
LOG.debug('start_position %s, position %s for current playqueue: %s',
|
||||
start_position, position, playqueue)
|
||||
while True:
|
||||
try:
|
||||
try:
|
||||
# We cannot know when Kodi will send the last item, e.g.
|
||||
# when playing an entire folder
|
||||
params = self.server.queue.get(timeout=0.01)
|
||||
except Queue.Empty:
|
||||
LOG.debug('Wrapping up')
|
||||
if xbmc.getCondVisibility('VideoPlayer.Content(livetv)'):
|
||||
# avoid issues with ongoing Live TV playback
|
||||
app.APP.player.stop()
|
||||
count = 50
|
||||
while not app.PLAYSTATE.playlist_ready:
|
||||
xbmc.sleep(50)
|
||||
if not count:
|
||||
LOG.info('Playback aborted')
|
||||
raise Exception('Playback aborted')
|
||||
count -= 1
|
||||
if play_folder:
|
||||
LOG.info('Start playing folder')
|
||||
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||
playqueue.start_playback(start_position)
|
||||
elif video_widget_playback:
|
||||
LOG.info('Start widget video playback')
|
||||
playqueue.start_playback()
|
||||
else:
|
||||
LOG.info('Start normal playback')
|
||||
# Release default.py
|
||||
utils.window('plex.playlist.play', value='true')
|
||||
if not app.PLAYSTATE.initiated_by_plex:
|
||||
# Remove the playlist element we just added with the
|
||||
# right path
|
||||
xbmc.sleep(1000)
|
||||
playqueue.kodi_remove_item(start_position)
|
||||
del playqueue.items[start_position]
|
||||
LOG.debug('Done wrapping up')
|
||||
break
|
||||
self.load_params(params)
|
||||
if play_folder:
|
||||
playlistitem = PQ.PlaylistItem(plex_id=self.plex_id,
|
||||
plex_type=self.plex_type,
|
||||
kodi_id=self.kodi_id,
|
||||
kodi_type=self.kodi_type)
|
||||
playlistitem.force_transcode = self.force_transcode
|
||||
playqueue.add_item(playlistitem, position)
|
||||
position += 1
|
||||
else:
|
||||
if self.server.pending.count(params['plex_id']) != len(self.server.pending):
|
||||
# E.g. when selecting "play" for an entire video genre
|
||||
LOG.debug('Folder playback detected')
|
||||
play_folder = True
|
||||
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
|
||||
playqueue.play(self.plex_id,
|
||||
plex_type=self.plex_type,
|
||||
startpos=start_position,
|
||||
position=position,
|
||||
synched=self.synched,
|
||||
force_transcode=self.force_transcode)
|
||||
# Do NOT start playback here - because Kodi already started
|
||||
# it!
|
||||
position = playqueue.index
|
||||
except Exception:
|
||||
abort = True
|
||||
utils.ERROR(notify=True)
|
||||
try:
|
||||
self.server.queue.task_done()
|
||||
except ValueError:
|
||||
# "task_done() called too many times" when aborting
|
||||
pass
|
||||
if abort:
|
||||
app.APP.player.stop()
|
||||
playqueue.clear()
|
||||
self.server.queue.queue.clear()
|
||||
if play_folder:
|
||||
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||
else:
|
||||
utils.window('plex.playlist.aborted', value='true')
|
||||
break
|
|
@ -29,11 +29,22 @@ PLEX_TYPE = None
|
|||
SECTION_ID = None
|
||||
APPEND_SHOW_TITLE = None
|
||||
APPEND_SXXEXX = None
|
||||
SYNCHED = True
|
||||
# Need to chain the PMS keys
|
||||
KEY = None
|
||||
|
||||
|
||||
def get_listitem(xml_element, resume=True):
|
||||
"""
|
||||
Returns a valid xbmcgui.ListItem() for xml_element. Pass in resume=False
|
||||
to NOT set a resume point for this listitem
|
||||
"""
|
||||
item = generate_item(xml_element)
|
||||
if not resume and 'resume' in item:
|
||||
del item['resume']
|
||||
prepare_listitem(item)
|
||||
return create_listitem(item, as_tuple=False)
|
||||
|
||||
|
||||
def process_method_on_list(method_to_run, items):
|
||||
"""
|
||||
helper method that processes a method on each listitem with pooling if the
|
||||
|
@ -246,8 +257,6 @@ def attach_kodi_ids(xml):
|
|||
"""
|
||||
Attaches the kodi db_item to the xml's children, attribute 'pkc_db_item'
|
||||
"""
|
||||
if not SYNCHED:
|
||||
return
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
for child in xml:
|
||||
api = API(child)
|
||||
|
|
81
resources/lib/windows/resume.py
Normal file
81
resources/lib/windows/resume.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from datetime import timedelta
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.windows.resume')
|
||||
|
||||
XML_PATH = (xbmcaddon.Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
|
||||
"default",
|
||||
"1080i")
|
||||
|
||||
ACTION_PARENT_DIR = 9
|
||||
ACTION_PREVIOUS_MENU = 10
|
||||
ACTION_BACK = 92
|
||||
RESUME = 3010
|
||||
START_BEGINNING = 3011
|
||||
|
||||
|
||||
class ResumeDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
_resume_point = None
|
||||
selected_option = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
|
||||
def set_resume_point(self, time):
|
||||
self._resume_point = time
|
||||
|
||||
def is_selected(self):
|
||||
return True if self.selected_option is not None else False
|
||||
|
||||
def get_selected(self):
|
||||
return self.selected_option
|
||||
|
||||
def onInit(self):
|
||||
|
||||
self.getControl(RESUME).setLabel(self._resume_point)
|
||||
self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021))
|
||||
|
||||
def onAction(self, action):
|
||||
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||
self.close()
|
||||
|
||||
def onClick(self, controlID):
|
||||
if controlID == RESUME:
|
||||
self.selected_option = 1
|
||||
self.close()
|
||||
if controlID == START_BEGINNING:
|
||||
self.selected_option = 0
|
||||
self.close()
|
||||
|
||||
|
||||
def resume_dialog(seconds):
|
||||
'''
|
||||
Base resume dialog based on Kodi settings
|
||||
Returns True if PKC should resume, False if not, None if user backed out
|
||||
of the dialog
|
||||
'''
|
||||
LOG.info("Resume dialog called")
|
||||
dialog = ResumeDialog("script-plex-resume.xml", *XML_PATH)
|
||||
dialog.set_resume_point("Resume from %s"
|
||||
% unicode(timedelta(seconds=seconds)).split(".")[0])
|
||||
dialog.doModal()
|
||||
|
||||
if dialog.is_selected():
|
||||
if not dialog.get_selected():
|
||||
# Start from beginning selected
|
||||
return False
|
||||
else:
|
||||
# User backed out
|
||||
LOG.info("User exited without a selection")
|
||||
return
|
||||
return True
|
112
resources/skins/default/1080i/script-plex-resume.xml
Normal file
112
resources/skins/default/1080i/script-plex-resume.xml
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<window id="3301" type="dialog">
|
||||
<defaultcontrol always="true">100</defaultcontrol>
|
||||
<controls>
|
||||
<control type="group">
|
||||
<control type="image">
|
||||
<top>0</top>
|
||||
<bottom>0</bottom>
|
||||
<left>0</left>
|
||||
<right>0</right>
|
||||
<texture colordiffuse="CC000000">white.png</texture>
|
||||
<aspectratio>stretch</aspectratio>
|
||||
<animation effect="fade" end="100" time="200">WindowOpen</animation>
|
||||
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
|
||||
</control>
|
||||
<control type="group">
|
||||
<animation effect="slide" time="0" end="0,-15" condition="true">Conditional</animation>
|
||||
<animation type="WindowOpen" reversible="false">
|
||||
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
|
||||
<effect type="fade" delay="160" end="100" time="240" />
|
||||
</animation>
|
||||
<animation type="WindowClose" reversible="false">
|
||||
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
|
||||
<effect type="fade" start="100" end="0" time="240" />
|
||||
</animation>
|
||||
<centerleft>50%</centerleft>
|
||||
<centertop>50%</centertop>
|
||||
<width>20%</width>
|
||||
<height>90%</height>
|
||||
<control type="grouplist" id="100">
|
||||
<orientation>vertical</orientation>
|
||||
<left>0</left>
|
||||
<right>0</right>
|
||||
<height>auto</height>
|
||||
<align>center</align>
|
||||
<itemgap>0</itemgap>
|
||||
<onright>close</onright>
|
||||
<onleft>close</onleft>
|
||||
<usecontrolcoords>true</usecontrolcoords>
|
||||
<control type="group">
|
||||
<height>30</height>
|
||||
<control type="image">
|
||||
<left>20</left>
|
||||
<width>100%</width>
|
||||
<height>25</height>
|
||||
<texture>logo-white.png</texture>
|
||||
<aspectratio align="left">keep</aspectratio>
|
||||
</control>
|
||||
<control type="image">
|
||||
<right>20</right>
|
||||
<width>100%</width>
|
||||
<height>25</height>
|
||||
<aspectratio align="right">keep</aspectratio>
|
||||
<texture diffuse="user_image.png">$INFO[Window(Home).Property(EmbyUserImage)]</texture>
|
||||
<visible>!String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
|
||||
</control>
|
||||
<control type="image">
|
||||
<right>20</right>
|
||||
<width>100%</width>
|
||||
<height>25</height>
|
||||
<aspectratio align="right">keep</aspectratio>
|
||||
<texture diffuse="user_image.png">userflyoutdefault.png</texture>
|
||||
<visible>String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
|
||||
</control>
|
||||
</control>
|
||||
<control type="image">
|
||||
<width>100%</width>
|
||||
<height>10</height>
|
||||
<texture border="5" colordiffuse="ff222326">dialogs/menu_top.png</texture>
|
||||
</control>
|
||||
<control type="button" id="3010">
|
||||
<width>100%</width>
|
||||
<height>65</height>
|
||||
<align>left</align>
|
||||
<aligny>center</aligny>
|
||||
<textoffsetx>20</textoffsetx>
|
||||
<font>font13</font>
|
||||
<textcolor>ffe1e1e1</textcolor>
|
||||
<focusedcolor>ffe1e1e1</focusedcolor>
|
||||
<shadowcolor>66000000</shadowcolor>
|
||||
<disabledcolor>FF404040</disabledcolor>
|
||||
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
|
||||
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
|
||||
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
|
||||
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
|
||||
</control>
|
||||
<control type="button" id="3011">
|
||||
<width>100%</width>
|
||||
<height>65</height>
|
||||
<align>left</align>
|
||||
<aligny>center</aligny>
|
||||
<textoffsetx>20</textoffsetx>
|
||||
<font>font13</font>
|
||||
<textcolor>ffe1e1e1</textcolor>
|
||||
<focusedcolor>ffe1e1e1</focusedcolor>
|
||||
<shadowcolor>66000000</shadowcolor>
|
||||
<disabledcolor>FF404040</disabledcolor>
|
||||
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
|
||||
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
|
||||
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
|
||||
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
|
||||
</control>
|
||||
<control type="image">
|
||||
<width>100%</width>
|
||||
<height>10</height>
|
||||
<texture border="5" colordiffuse="ff222326">dialogs/menu_bottom.png</texture>
|
||||
</control>
|
||||
</control>
|
||||
</control>
|
||||
</control>
|
||||
</controls>
|
||||
</window>
|
BIN
resources/skins/default/media/dialogs/dialog_back.png
Normal file
BIN
resources/skins/default/media/dialogs/dialog_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
resources/skins/default/media/dialogs/menu_back.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
resources/skins/default/media/dialogs/menu_bottom.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_bottom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
resources/skins/default/media/dialogs/menu_top.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
resources/skins/default/media/dialogs/white.jpg
Normal file
BIN
resources/skins/default/media/dialogs/white.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Loading…
Reference in a new issue