Compare commits

...

74 Commits

Author SHA1 Message Date
croneter 6cba4a1d01 Fix playback startup for Plex Companion 2019-05-26 17:56:16 +02:00
croneter 48d288ac53 Remove some logging 2019-05-26 17:43:22 +02:00
croneter 27c4c6ac38 Replace window var plex.playlist.start with app.PLAYSTATE var 2019-05-26 17:41:17 +02:00
croneter e204ef9849 Replace window var plex.playlist.ready with app.PLAYSTATE var 2019-05-26 17:35:37 +02:00
croneter 7e676eb043 Fix playback startup 2019-05-26 17:28:05 +02:00
croneter a650c42cfd Lock playqueue activities 2019-05-26 13:13:13 +02:00
croneter 0f9e754815 Cleanup 2019-05-26 12:52:32 +02:00
croneter bb2fff5909 Fix resume when user chose to not resume 2019-05-26 12:51:04 +02:00
croneter 725416751f Revert "Fix resume when user chose to not resume"
This reverts commit 6e692d22c2.
2019-05-26 12:30:49 +02:00
croneter 6e692d22c2 Fix resume when user chose to not resume 2019-05-26 12:06:27 +02:00
croneter fb21bc7d71 Cleanup 2019-05-26 11:53:59 +02:00
croneter d3752e1958 Rename to PlayqueueError 2019-05-26 11:26:14 +02:00
croneter 3d4bde878e Cleanup 2019-05-25 20:52:41 +02:00
croneter 9d517c2c3d Refactoring 2019-05-25 20:49:29 +02:00
croneter d397fb5b20 Cleanup 2019-05-25 13:35:14 +02:00
croneter f7237d7033 Cleanup 2019-05-25 13:12:29 +02:00
croneter 9d79f78190 Fix TypeError 2019-05-21 18:08:09 +02:00
croneter 7725af5a6f Fix resume from Plex Companion 2019-05-21 17:56:48 +02:00
croneter 0ce29dc0ce Fix Plex Companion telling the wrong item is playing 2019-05-19 18:47:09 +02:00
croneter ea4a062aac Big update 2019-05-12 14:38:31 +02:00
croneter a1f4960bca Revert "Set kodi_type for PlaylistItem automatically from plex_type"
This reverts commit cef07c3598.
2019-05-05 12:11:43 +02:00
croneter 1d01f4794e Fixup 2019-05-05 11:31:00 +02:00
croneter cef07c3598 Set kodi_type for PlaylistItem automatically from plex_type 2019-05-05 11:29:43 +02:00
croneter 7616d6dc26 Fixup 2019-05-05 11:14:27 +02:00
croneter b586ac09c4 Fix moving of items in Plex playqueue 2019-05-05 11:00:41 +02:00
croneter dc56c2a6a2 Improve code 2019-05-05 10:42:44 +02:00
croneter 1123a2ee3c Fix widget playback not starting up 2019-05-05 10:29:04 +02:00
croneter 8ad6d1bcce Clear playqueue on playback startup 2019-05-05 10:21:59 +02:00
croneter 45fc9fa8be Better way to detect video widget playback 2019-05-04 16:13:26 +02:00
croneter 353cb04532 New functions 2019-05-04 14:29:18 +02:00
croneter 48cda467c3 Move method 2019-05-04 14:26:18 +02:00
croneter 6bd98fcefd Cleanup 2019-05-04 13:14:34 +02:00
croneter 1218cde0a2 Big update 2019-04-28 18:03:20 +02:00
croneter 578ced789f Fix arg 2019-04-27 13:23:01 +02:00
croneter 0d8b3b3ba7 Remove arg 2019-04-27 13:23:01 +02:00
croneter 8660b12d15 Fix position 2019-04-27 13:23:01 +02:00
croneter bbd8e18002 Increase timeout 2019-04-27 13:23:01 +02:00
croneter 7753903c05 Be more resiliant when manipulating Plex playqueues 2019-04-27 13:23:01 +02:00
croneter 4fa1f48b43 Improve logging 2019-04-27 13:23:01 +02:00
croneter 130ec674e5 Fix companion playback crashing 2019-04-27 13:23:01 +02:00
croneter 2dac26ffc4 Fix force transcoding 2019-04-27 13:23:01 +02:00
croneter 3aa5c87ca0 Skip force close connection error messages 2019-04-27 13:23:01 +02:00
croneter c63d9ad4d6 Some more switches to webservice, away from plugin playback 2019-04-27 13:23:00 +02:00
croneter 95b37b51f5 Fix resume 2019-04-27 13:23:00 +02:00
croneter d380aa8ac3 Drop filename for url arg, but add kodi_type 2019-04-27 13:23:00 +02:00
croneter 885e8dd581 Fix PKC wanting to initiate playback when it should not 2019-04-27 13:23:00 +02:00
croneter ac285467c4 Fix main movie being added as trailer 2019-04-27 13:23:00 +02:00
croneter 61ff2b72f3 Increase logging 2019-04-27 13:23:00 +02:00
croneter 0acf470343 Optimize logging 2019-04-27 13:23:00 +02:00
croneter 9a9bc9f0eb Optimize code 2019-04-27 13:23:00 +02:00
croneter 643e6171c4 Revamp monitor 2019-04-27 13:23:00 +02:00
croneter ad6c160524 Don't sleep 2019-04-27 13:23:00 +02:00
croneter b11ca48294 Enable playqueue elements comparison 2019-04-27 13:23:00 +02:00
croneter fe52efd88e Less playlist logging 2019-04-27 13:23:00 +02:00
croneter 8c614f3e47 Don't automatically look up kodi_id from Plex DB 2019-04-27 13:22:59 +02:00
croneter 0d36a2a3b9 Simplify code 2019-04-27 13:22:59 +02:00
croneter f4c3674bc2 Increase logging 2019-04-27 13:22:59 +02:00
croneter 5428dafe59 Simplify code 2019-04-27 13:22:59 +02:00
croneter 4ed17f1a5b Fix widgets not updating 2019-04-27 13:22:59 +02:00
croneter 484b03482e Fix resume flags for ListItems 2019-04-27 13:22:59 +02:00
croneter 797a58a3d5 Rewire plex.playlist.audio 2019-04-27 13:22:59 +02:00
croneter 439857a9ce Rewire autoplay flag 2019-04-27 13:22:59 +02:00
croneter 12befecc4a Rewire video resume info 2019-04-27 13:22:59 +02:00
croneter 20bffc1b41 Enable webservice playback for shows 2019-04-27 13:22:59 +02:00
croneter d7541b7f74 TO BE CHECKED: better method to delete obsolete fileIds 2019-04-27 13:22:59 +02:00
croneter dfcfa0edab Better detect videos playing for playback cleanup 2019-04-27 13:22:59 +02:00
croneter 20c1c6e502 Add missing default.py option 2019-04-27 13:22:58 +02:00
croneter 875d704e5a Don't store filename in Kodi db 2019-04-27 13:22:58 +02:00
croneter c0035c84a6 Fix monitor's playlist.onadd 2019-04-27 13:22:58 +02:00
croneter 4a3b38f5b6 Increase logging 2019-04-27 13:22:58 +02:00
croneter 16423e18ec Ignore suspends for webservice 2019-04-27 13:22:58 +02:00
croneter 059ed7a5f0 Switch to stream playback, part II 2019-04-27 13:22:58 +02:00
croneter 7c6fdad770 Fixup 2019-04-27 13:22:58 +02:00
croneter 9b4584e7df Switch to stream playback, part I 2019-04-27 13:22:58 +02:00
37 changed files with 2336 additions and 1865 deletions

View File

@ -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':

View File

@ -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

View File

@ -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):
"""

View File

@ -217,7 +217,8 @@ 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
widgets.attach_kodi_ids(xml)
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,
all_items)

View File

@ -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)

View File

@ -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,22 +449,25 @@ 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:
# 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
# filename_2 is exactly the same as filename
# so WITH plex show id!
kodi_pathid_2 = self.kodidb.add_path(path_2)
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
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)
# UPDATE THE EPISODE #####
if update_item:

View File

@ -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):
"""

View File

@ -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,42 +76,22 @@ 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)
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)
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
playcount = data.get('playcount')
@ -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
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]
self.playqueue = PQ.PLAYQUEUES[self.playerid]
LOG.debug('Current PKC playqueue: %s', self.playqueue)
item = None
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)

View File

@ -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))

View File

@ -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'],
params['offset'],
resolve=resolve)
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

View File

@ -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')

View File

@ -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:

View 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

View 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

View 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')

View File

@ -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)

View 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
View 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

View File

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

View File

@ -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):

View File

@ -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(),
transient_token=data.get('token'))
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,32 +163,35 @@ class PlexCompanion(backgroundthread.KillableThread):
"""
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
app.APP.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
app.APP.player.showSubtitles(False)
else:
try:
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
app.APP.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
data['audioStreamID'], 'audio')
app.APP.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
app.APP.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
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,23 +219,29 @@ class PlexCompanion(backgroundthread.KillableThread):
"""
LOG.debug('Processing: %s', task)
data = task['data']
if task['action'] == 'alexa':
with app.APP.lock_playqueues:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
try:
try:
if task['action'] == 'alexa':
with app.APP.lock_playqueues:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
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):
"""

View File

@ -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

View File

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

View File

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

View File

@ -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']

View File

@ -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)

View File

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

View File

@ -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
View 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

View File

@ -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)

View 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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB