From 9b4584e7df5ac3ce7103c1f696caf9041738f394 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 25 Mar 2019 17:15:18 +0100 Subject: [PATCH 01/74] Switch to stream playback, part I --- resources/lib/entrypoint.py | 3 +- resources/lib/playstrm.py | 255 ++++++++++++++++++++++++ resources/lib/plex_db/__init__.py | 15 ++ resources/lib/service_entry.py | 9 + resources/lib/variables.py | 3 + resources/lib/webservice.py | 319 ++++++++++++++++++++++++++++++ resources/lib/widgets.py | 12 +- 7 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 resources/lib/playstrm.py create mode 100644 resources/lib/webservice.py diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 0c41266be..5f14b7e22 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -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) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py new file mode 100644 index 000000000..385cae6a6 --- /dev/null +++ b/resources/lib/playstrm.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger +import urllib + +import xbmc +import xbmcgui + +from .plex_api import API +from . import plex_function as PF, utils, json_rpc, variables as v, \ + widgets + + +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, server_id=None): + LOG.debug('Starting PlayStrm with server_id %s, params: %s', + server_id, params) + self.xml = None + self.api = None + self.start_index = None + self.index = None + self.server_id = server_id + 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._get_xml() + self.name = self.api.title() + self.kodi_id = utils.cast(int, params.get('kodi_id')) + self.kodi_type = params.get('kodi_type') + if ((self.kodi_id is None or self.kodi_type is None) and + self.xml[0].get('pkc_db_item')): + self.kodi_id = self.xml[0].get('pkc_db_item')['kodi_id'] + self.kodi_type = self.xml[0].get('pkc_db_item')['kodi_type'] + self.transcode = params.get('transcode') + if self.transcode is None: + self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None + if utils.window('plex.playlist.audio.bool'): + LOG.info('Audio playlist detected') + self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + else: + self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + def __repr__(self): + return ("{{" + "'name': '{self.name}', " + "'plex_id': {self.plex_id}, " + "'plex_type': '{self.plex_type}', " + "'kodi_id': {self.kodi_id}, " + "'kodi_type': '{self.kodi_type}', " + "'server_id': '{self.server_id}', " + "'transcode': {self.transcode}, " + "'start_index': {self.start_index}, " + "'index': {self.index}" + "}}").format(self=self).encode('utf-8') + __str__ = __repr__ + + def add_to_playlist(self, kodi_id, kodi_type, index=None, playlistid=None): + playlistid = playlistid or self.kodi_playlist.getPlayListId() + LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s', + kodi_id, kodi_type, playlistid, index) + if index is None: + json_rpc.playlist_add(playlistid, {'%sid' % kodi_type: kodi_id}) + else: + json_rpc.playlist_insert({'playlistid': playlistid, + 'position': index, + 'item': {'%sid' % kodi_type: kodi_id}}) + + def remove_from_playlist(self, index): + LOG.debug('Removing playlist item number %s from %s', index, self) + json_rpc.playlist_remove(self.kodi_playlist.getPlayListId(), + index) + + def _get_xml(self): + self.xml = PF.GetPlexMetadata(self.plex_id) + if self.xml in (None, 401): + raise PlayStrmException('No xml received from the PMS') + if self.synched: + # Adds a new key 'pkc_db_item' to self.xml[0].attrib + widgets.attach_kodi_ids(self.xml) + else: + self.xml[0].set('pkc_db_item', None) + self.api = API(self.xml[0]) + + def start_playback(self, index=0): + LOG.debug('Starting playback at %s', index) + xbmc.Player().play(self.kodi_playlist, startpos=index, windowed=False) + + def play(self, start_position=None, delayed=True): + ''' + Create and add listitems to the Kodi playlist. + ''' + if start_position is not None: + self.start_index = start_position + else: + self.start_index = max(self.kodi_playlist.getposition(), 0) + self.index = self.start_index + listitem = xbmcgui.ListItem() + self._set_playlist(listitem) + LOG.info('Initiating play for %s', self) + if not delayed: + self.start_playback(self.start_index) + return self.start_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. + ''' + self.start_index = position or max(self.kodi_playlist.size(), 0) + self.index = self.start_index + 1 + LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index) + if self.kodi_id and self.kodi_type: + self.add_to_playlist(self.kodi_id, self.kodi_type, self.index) + else: + listitem = widgets.get_listitem(self.xml[0]) + url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT + args = { + 'mode': 'play', + 'plex_id': self.plex_id, + 'plex_type': self.api.plex_type() + } + if self.kodi_id: + args['kodi_id'] = self.kodi_id + if self.kodi_type: + args['kodi_type'] = self.kodi_type + if self.server_id: + args['server_id'] = self.server_id + if self.transcode: + args['transcode'] = True + url = '%s?%s' % (url, urllib.urlencode(args)) + listitem.setPath(url) + self.kodi_playlist.add(url=url, + listitem=listitem, + index=self.index) + return self.index + + def _set_playlist(self, listitem): + ''' + Verify seektime, set intros, set main item and set additional parts. + Detect the seektime for video type content. Verify the default video + action set in Kodi for accurate resume behavior. + ''' + seektime = self._resume() + if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and + utils.settings('enableCinema') == 'true'): + self._set_intros() + + play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server']) + source = play.select_source(play.get_sources()) + + if not source: + raise PlayStrmException('Playback selection cancelled') + + play.set_external_subs(source, listitem) + self.set_listitem(self.xml, listitem, self.kodi_id, seektime) + listitem.setPath(self.xml['PlaybackInfo']['Path']) + playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id) + + self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + self.index += 1 + + if self.xml.get('PartCount'): + self._set_additional_parts() + + def _resume(self): + ''' + Resume item if available. Returns bool or raise an PlayStrmException if + resume was cancelled by user. + ''' + seektime = utils.window('plex.resume') + utils.window('plex.resume', clear=True) + seektime = seektime == 'true' if seektime else None + auto_play = utils.window('plex.autoplay.bool') + if auto_play: + seektime = False + LOG.info('Skip resume for autoplay') + elif seektime is None: + resume = self.api.resume_point() + if resume: + seektime = resume_dialog(resume) + LOG.info('Resume: %s', seektime) + if seektime is None: + raise PlayStrmException('User backed out of resume dialog.') + # Todo: Probably need to have a look here + utils.window('plex.autoplay.bool', value='true') + return seektime + + def _set_intros(self): + ''' + if we have any play them when the movie/show is not being resumed. + ''' + if self.info['Intros']['Items']: + enabled = True + + if utils.settings('askCinema') == 'true': + + resp = dialog('yesno', heading='{emby}', line1=_(33016)) + if not resp: + + enabled = False + LOG.info('Skip trailers.') + + if enabled: + for intro in self.info['Intros']['Items']: + + listitem = xbmcgui.ListItem() + LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name']) + + play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['Server']) + source = play.select_source(play.get_sources()) + self.set_listitem(intro, listitem, intro=True) + listitem.setPath(intro['PlaybackInfo']['Path']) + playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) + + self.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + self.index += 1 + + utils.window('plex.skip.%s' % intro['plex_id'], value='true') + + def _set_additional_parts(self): + ''' Create listitems and add them to the stack of playlist. + ''' + for part in self.info['AdditionalParts']['Items']: + + listitem = xbmcgui.ListItem() + LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name']) + + play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server']) + source = play.select_source(play.get_sources()) + play.set_external_subs(source, listitem) + self.set_listitem(part, listitem) + listitem.setPath(part['PlaybackInfo']['Path']) + playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) + + self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + self.index += 1 diff --git a/resources/lib/plex_db/__init__.py b/resources/lib/plex_db/__init__.py index 53196e641..ee847db68 100644 --- a/resources/lib/plex_db/__init__.py +++ b/resources/lib/plex_db/__init__.py @@ -12,3 +12,18 @@ 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 diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index b00e879b6..eda2dfed7 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -10,6 +10,7 @@ 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 @@ def ServiceEntryPoint(self): 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,12 @@ def ServiceEntryPoint(self): xbmc.sleep(100) continue + if self.webservice is not None and not self.webservice.is_alive(): + 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,6 +531,7 @@ def ServiceEntryPoint(self): continue elif not self.startup_completed: self.startup_completed = True + self.webservice.start() self.ws.start() self.sync.start() self.plexcompanion.start() diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 46b808fba..70ec64823 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -92,6 +92,9 @@ def try_decode(string, encoding='utf-8'): 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 diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py new file mode 100644 index 000000000..7f35427c9 --- /dev/null +++ b/resources/lib/webservice.py @@ -0,0 +1,319 @@ +# -*- 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 urlparse +import socket +import Queue + +import xbmc +import xbmcvfs + +from . import backgroundthread, utils, variables as v, app +from .playstrm import PlayStrm + + +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(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: + pass + + def run(self): + ''' Called to start the webservice. + ''' + LOG.info('----===## Starting Webserver on port %s ##===----', + v.WEBSERVICE_PORT) + app.APP.register_thread(self) + try: + server = HttpServer(('127.0.0.1', v.WEBSERVICE_PORT), + RequestHandler) + server.serve_forever() + except Exception as error: + if '10053' not in error: # ignore host diconnected errors + utils.ERROR() + finally: + app.APP.deregister_thread(self) + LOG.info('##===---- Webserver 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() + super(HttpServer, self).__init__(*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: + pass + + 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: + params = {} + if '?' in path: + path = path.split('?', 1)[1] + params = dict(urlparse.parse_qsl(path)) + + if params.get('transcode'): + params['transcode'] = params['transcode'].lower() == 'true' + if params.get('server') and params['server'].lower() == 'none': + params['server'] = None + + 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 + ''' + 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() + elif b'file.strm' in self.path: + self.strm() + else: + xbmc.log(str(self.path), xbmc.LOGWARNING) + + except Exception as error: + self.send_error(500, + b'PLEX.webservice: Exception occurred: %s' % error) + xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING) + + def strm(self): + ''' Return a dummy video and and queue real items. + ''' + 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('Name'): + path += '&filename=%s' % params['Name'] + self.wfile.write(bytes(path)) + return + + path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id'] + self.wfile.write(bytes(path)) + if params['plex_id'] not in self.server.pending: + xbmc.log('PLEX.webserver: %s: path: %s params: %s' + % (str(id(self)), str(self.path), str(params)), + xbmc.LOGWARNING) + + self.server.pending.append(params['plex_id']) + self.server.queue.put(params) + if not len(self.server.threads): + queue = QueuePlay(self.server) + 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): + self.server = server + super(QueuePlay, self).__init__() + + def run(self): + LOG.info('##===---- Starting QueuePlay ----===##') + play_folder = False + play = None + start_position = None + position = None + + # Let Kodi catch up + xbmc.sleep(200) + + while True: + + try: + try: + params = self.server.queue.get(timeout=0.01) + except Queue.Empty: + count = 20 + while not utils.window('plex.playlist.ready.bool'): + xbmc.sleep(50) + if not count: + LOG.info('Playback aborted') + raise Exception('PlaybackAborted') + count -= 1 + LOG.info('Starting playback at position: %s', start_position) + if play_folder: + LOG.info('Start playing folder') + xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + play.start_playback() + else: + utils.window('plex.playlist.play.bool', True) + xbmc.sleep(1000) + play.remove_from_playlist(start_position) + break + play = PlayStrm(params, params.get('ServerId')) + + if start_position is None: + start_position = max(play.info['KodiPlaylist'].getposition(), 0) + position = start_position + 1 + if play_folder: + position = play.play_folder(position) + else: + if self.server.pending.count(params['plex_id']) != len(self.server.pending): + play_folder = True + utils.window('plex.playlist.start', str(start_position)) + position = play.play(position) + if play_folder: + xbmc.executebuiltin('Activateutils.window(busydialognocancel)') + except Exception: + utils.ERROR() + play.info['KodiPlaylist'].clear() + xbmc.Player().stop() + self.server.queue.queue.clear() + if play_folder: + xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + else: + utils.window('plex.playlist.aborted.bool', True) + break + self.server.queue.task_done() + + utils.window('plex.playlist.ready', clear=True) + utils.window('plex.playlist.start', clear=True) + utils.window('plex.playlist.audio', clear=True) + self.server.threads.remove(self) + self.server.pending = [] + LOG.info('##===---- QueuePlay Stopped ----===##') diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 757b66faf..de3e91680 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -29,11 +29,19 @@ 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): + """ + Returns a valid xbmcgui.ListItem() for xml_element + """ + item = generate_item(xml_element) + prepare_listitem(item) + return create_listitem(item) + + def process_method_on_list(method_to_run, items): """ helper method that processes a method on each listitem with pooling if the @@ -246,8 +254,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) From 7c6fdad7705fe3dcad557a8ddb220e8c85f0af21 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 30 Mar 2019 18:05:34 +0100 Subject: [PATCH 02/74] Fixup --- resources/lib/playstrm.py | 3 +-- resources/lib/webservice.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 385cae6a6..cfc768446 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -import urllib import xbmc import xbmcgui @@ -146,7 +145,7 @@ def play_folder(self, position=None): args['server_id'] = self.server_id if self.transcode: args['transcode'] = True - url = '%s?%s' % (url, urllib.urlencode(args)) + url = utils.extend_url(url, args).encode('utf-8') listitem.setPath(url) self.kodi_playlist.add(url=url, listitem=listitem, diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 7f35427c9..7120a2cd6 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -7,7 +7,6 @@ from logging import getLogger import BaseHTTPServer import httplib -import urlparse import socket import Queue @@ -121,7 +120,7 @@ def get_params(self): params = {} if '?' in path: path = path.split('?', 1)[1] - params = dict(urlparse.parse_qsl(path)) + params = dict(utils.parse_qsl(path)) if params.get('transcode'): params['transcode'] = params['transcode'].lower() == 'true' From 059ed7a5f0589ee14d8d4d654d3eb269e65cfd1e Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 10:26:01 +0200 Subject: [PATCH 03/74] Switch to stream playback, part II --- resources/lib/itemtypes/movies.py | 11 +- resources/lib/kodi_db/video.py | 4 +- resources/lib/kodimonitor.py | 17 +++ resources/lib/playstrm.py | 138 ++++++++++-------- resources/lib/webservice.py | 32 ++-- resources/lib/widgets.py | 2 +- resources/lib/windows/resume.py | 81 ++++++++++ .../default/1080i/script-plex-resume.xml | 112 ++++++++++++++ .../default/media/dialogs/dialog_back.png | Bin 0 -> 15141 bytes .../skins/default/media/dialogs/menu_back.png | Bin 0 -> 15358 bytes .../default/media/dialogs/menu_bottom.png | Bin 0 -> 14781 bytes .../skins/default/media/dialogs/menu_top.png | Bin 0 -> 14768 bytes .../skins/default/media/dialogs/white.jpg | Bin 0 -> 8060 bytes 13 files changed, 311 insertions(+), 86 deletions(-) create mode 100644 resources/lib/windows/resume.py create mode 100644 resources/skins/default/1080i/script-plex-resume.xml create mode 100644 resources/skins/default/media/dialogs/dialog_back.png create mode 100644 resources/skins/default/media/dialogs/menu_back.png create mode 100644 resources/skins/default/media/dialogs/menu_bottom.png create mode 100644 resources/skins/default/media/dialogs/menu_top.png create mode 100644 resources/skins/default/media/dialogs/white.jpg diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 409534d0a..203f4ef8f 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -72,10 +72,13 @@ def add_update(self, xml, section, children=None): 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}&name={4}' + filename = filename.format(plex_id, + kodi_id, + v.KODI_TYPE_MOVIE, + v.PLEX_TYPE_MOVIE, + api.file_name(force_first_media=True)) playurl = filename kodi_pathid = self.kodidb.get_path(path) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 98fd92fb5..1e959938c 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -5,11 +5,11 @@ 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 +MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index f5ea2288b..f35db30b7 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -449,6 +449,23 @@ def _playback_cleanup(ended=False): app.PLAYSTATE.active_players = set() LOG.info('Finished PKC playback cleanup') + def Playlist_OnAdd(self, server, data, *args, **kwargs): + ''' + Detect widget playback. Widget for some reason, use audio playlists. + ''' + LOG.debug('Playlist_OnAdd: %s, %s', server, data) + if data['position'] == 0: + if data['playlistid'] == 0: + utils.window('plex.playlist.audio', value='true') + else: + utils.window('plex.playlist.audio', clear=True) + self.playlistid = data['playlistid'] + if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: + + LOG.info("--[ playlist ready ]") + utils.window('plex.playlist.ready', value='true') + utils.window('plex.playlist.start', clear=True) + def _record_playstate(status, ended): if not status['plex_id']: diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index cfc768446..08fa690d2 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -3,11 +3,12 @@ from logging import getLogger import xbmc -import xbmcgui from .plex_api import API -from . import plex_function as PF, utils, json_rpc, variables as v, \ - widgets +from .playutils import PlayUtils +from .windows.resume import resume_dialog +from . import app, plex_functions as PF, utils, json_rpc, variables as v, \ + widgets, playlist_func as PL, playqueue as PQ LOG = getLogger('PLEX.playstrm') @@ -51,11 +52,13 @@ def __init__(self, params, server_id=None): self.transcode = params.get('transcode') if self.transcode is None: self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None - if utils.window('plex.playlist.audio.bool'): - LOG.info('Audio playlist detected') - self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + if utils.window('plex.playlist.audio'): + LOG.debug('Audio playlist detected') + self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) else: - self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + LOG.debug('Video playlist detected') + self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO) + self.kodi_playlist = self.playqueue.kodi_pl def __repr__(self): return ("{{" @@ -97,6 +100,7 @@ def _get_xml(self): else: self.xml[0].set('pkc_db_item', None) self.api = API(self.xml[0]) + self.playqueue_item = PL.playlist_item_from_xml(self.xml[0]) def start_playback(self, index=0): LOG.debug('Starting playback at %s', index) @@ -111,7 +115,7 @@ def play(self, start_position=None, delayed=True): else: self.start_index = max(self.kodi_playlist.getposition(), 0) self.index = self.start_index - listitem = xbmcgui.ListItem() + listitem = widgets.get_listitem(self.xml[0]) self._set_playlist(listitem) LOG.info('Initiating play for %s', self) if not delayed: @@ -159,24 +163,41 @@ def _set_playlist(self, listitem): action set in Kodi for accurate resume behavior. ''' seektime = self._resume() + trailers = False if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and utils.settings('enableCinema') == 'true'): - self._set_intros() - - play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - - if not source: - raise PlayStrmException('Playback selection cancelled') - - play.set_external_subs(source, listitem) - self.set_listitem(self.xml, listitem, self.kodi_id, seektime) - listitem.setPath(self.xml['PlaybackInfo']['Path']) - playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + if utils.settings('askCinema') == "true": + # "Play trailers?" + trailers = utils.yesno_dialog(utils.lang(29999), + utils.lang(33016)) or False + else: + trailers = True + LOG.debug('Playing trailers: %s', trailers) + xml = PF.init_plex_playqueue(self.plex_id, + self.xml.get('librarySectionUUID'), + mediatype=self.plex_type, + trailers=trailers) + if xml is None: + LOG.error('Could not get playqueue for UUID %s for %s', + self.xml.get('librarySectionUUID'), self) + # "Play error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') + app.PLAYSTATE.context_menu_play = False + app.PLAYSTATE.force_transcode = False + app.PLAYSTATE.resume_playback = False + return + PL.get_playlist_details_from_xml(self.playqueue, xml) + # See that we add trailers, if they exist in the xml return + self._set_intros(xml) + listitem.setSubtitles(self.api.cache_external_subs()) + play = PlayUtils(self.api, self.playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) self.index += 1 - if self.xml.get('PartCount'): self._set_additional_parts() @@ -203,52 +224,41 @@ def _resume(self): utils.window('plex.autoplay.bool', value='true') return seektime - def _set_intros(self): + def _set_intros(self, xml): ''' if we have any play them when the movie/show is not being resumed. ''' - if self.info['Intros']['Items']: - enabled = True - - if utils.settings('askCinema') == 'true': - - resp = dialog('yesno', heading='{emby}', line1=_(33016)) - if not resp: - - enabled = False - LOG.info('Skip trailers.') - - if enabled: - for intro in self.info['Intros']['Items']: - - listitem = xbmcgui.ListItem() - LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name']) - - play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - self.set_listitem(intro, listitem, intro=True) - listitem.setPath(intro['PlaybackInfo']['Path']) - playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index) - self.index += 1 - - utils.window('plex.skip.%s' % intro['plex_id'], value='true') + if not len(xml) > 1: + LOG.debug('No trailers returned from the PMS') + return + for intro in xml: + if utils.cast(int, xml.get('ratingKey')) == self.plex_id: + # The main item we're looking at - skip! + continue + api = API(intro) + listitem = widgets.get_listitem(intro) + listitem.setSubtitles(api.cache_external_subs()) + playqueue_item = PL.playlist_item_from_xml(intro) + play = PlayUtils(api, playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) + self.index += 1 + utils.window('plex.skip.%s' % api.plex_id(), value='true') def _set_additional_parts(self): ''' Create listitems and add them to the stack of playlist. ''' - for part in self.info['AdditionalParts']['Items']: - - listitem = xbmcgui.ListItem() - LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name']) - - play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - play.set_external_subs(source, listitem) - self.set_listitem(part, listitem) - listitem.setPath(part['PlaybackInfo']['Path']) - playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + for part, _ in enumerate(self.xml[0][0]): + if part == 0: + # The first part that we've already added + continue + self.api.set_part_number(part) + listitem = widgets.get_listitem(self.xml[0]) + listitem.setSubtitles(self.api.cache_external_subs()) + playqueue_item = PL.playlist_item_from_xml(self.xml[0]) + play = PlayUtils(self.api, playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) self.index += 1 diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 7120a2cd6..382fb6ae2 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -33,7 +33,7 @@ def is_alive(self): s.connect(('127.0.0.1', v.WEBSERVICE_PORT)) s.sendall('') except Exception as error: - LOG.error(error) + LOG.error('is_alive error: %s', error) if 'Errno 61' in str(error): alive = False s.close() @@ -47,12 +47,12 @@ def abort(self): conn.request('QUIT', '/') conn.getresponse() except Exception: - pass + utils.ERROR() def run(self): ''' Called to start the webservice. ''' - LOG.info('----===## Starting Webserver on port %s ##===----', + LOG.info('----===## Starting WebService on port %s ##===----', v.WEBSERVICE_PORT) app.APP.register_thread(self) try: @@ -60,11 +60,12 @@ def run(self): RequestHandler) 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('##===---- Webserver stopped ----===##') + LOG.info('##===---- WebService stopped ----===##') class HttpServer(BaseHTTPServer.HTTPServer): @@ -75,7 +76,7 @@ def __init__(self, *args, **kwargs): self.pending = [] self.threads = [] self.queue = Queue.Queue() - super(HttpServer, self).__init__(*args, **kwargs) + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) def serve_forever(self): @@ -86,8 +87,9 @@ def serve_forever(self): class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - ''' Http request handler. Do not use LOG here, - it will hang requests in Kodi > show information dialog. + ''' + Http request handler. Do not use LOG here, it will hang requests in Kodi > + show information dialog. ''' timeout = 0.5 @@ -101,8 +103,8 @@ def handle(self): ''' try: BaseHTTPServer.BaseHTTPRequestHandler.handle(self) - except Exception: - pass + except Exception as error: + xbmc.log('Plex.WebService handle error: %s' % error, xbmc.LOGWARNING) def do_QUIT(self): ''' send 200 OK response, and set server.stop to True @@ -142,6 +144,7 @@ def do_GET(self): def handle_request(self, headers_only=False): '''Send headers and reponse ''' + xbmc.log('Plex.WebService handle_request called. path: %s ]' % self.path, xbmc.LOGWARNING) try: if b'extrafanart' in self.path or b'extrathumbs' in self.path: raise Exception('unsupported artwork request') @@ -161,7 +164,6 @@ def handle_request(self, headers_only=False): except Exception as error: self.send_error(500, b'PLEX.webservice: Exception occurred: %s' % error) - xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING) def strm(self): ''' Return a dummy video and and queue real items. @@ -268,7 +270,7 @@ def run(self): params = self.server.queue.get(timeout=0.01) except Queue.Empty: count = 20 - while not utils.window('plex.playlist.ready.bool'): + while not utils.window('plex.playlist.ready'): xbmc.sleep(50) if not count: LOG.info('Playback aborted') @@ -280,14 +282,14 @@ def run(self): xbmc.executebuiltin('Dialog.Close(busydialognocancel)') play.start_playback() else: - utils.window('plex.playlist.play.bool', True) + utils.window('plex.playlist.play', value='true') xbmc.sleep(1000) play.remove_from_playlist(start_position) break play = PlayStrm(params, params.get('ServerId')) if start_position is None: - start_position = max(play.info['KodiPlaylist'].getposition(), 0) + start_position = max(play.kodi_playlist.getposition(), 0) position = start_position + 1 if play_folder: position = play.play_folder(position) @@ -300,13 +302,13 @@ def run(self): xbmc.executebuiltin('Activateutils.window(busydialognocancel)') except Exception: utils.ERROR() - play.info['KodiPlaylist'].clear() + play.kodi_playlist.clear() xbmc.Player().stop() self.server.queue.queue.clear() if play_folder: xbmc.executebuiltin('Dialog.Close(busydialognocancel)') else: - utils.window('plex.playlist.aborted.bool', True) + utils.window('plex.playlist.aborted', value='true') break self.server.queue.task_done() diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index de3e91680..8cfadf6dd 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -39,7 +39,7 @@ def get_listitem(xml_element): """ item = generate_item(xml_element) prepare_listitem(item) - return create_listitem(item) + return create_listitem(item, as_tuple=False) def process_method_on_list(method_to_run, items): diff --git a/resources/lib/windows/resume.py b/resources/lib/windows/resume.py new file mode 100644 index 000000000..3f242646a --- /dev/null +++ b/resources/lib/windows/resume.py @@ -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.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 diff --git a/resources/skins/default/1080i/script-plex-resume.xml b/resources/skins/default/1080i/script-plex-resume.xml new file mode 100644 index 000000000..43240c979 --- /dev/null +++ b/resources/skins/default/1080i/script-plex-resume.xml @@ -0,0 +1,112 @@ + + + 100 + + + + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose + + + Conditional + + + + + + + + + 50% + 50% + 20% + 90% + + vertical + 0 + 0 + auto + center + 0 + close + close + true + + 30 + + 20 + 100% + 25 + logo-white.png + keep + + + 20 + 100% + 25 + keep + $INFO[Window(Home).Property(EmbyUserImage)] + !String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + 20 + 100% + 25 + keep + userflyoutdefault.png + String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + + 100% + 10 + dialogs/menu_top.png + + + 100% + 65 + left + center + 20 + font13 + ffe1e1e1 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 65 + left + center + 20 + font13 + ffe1e1e1 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 10 + dialogs/menu_bottom.png + + + + + + diff --git a/resources/skins/default/media/dialogs/dialog_back.png b/resources/skins/default/media/dialogs/dialog_back.png new file mode 100644 index 0000000000000000000000000000000000000000..6422ff5a20ab4177b062c58afb805a7254d76689 GIT binary patch literal 15141 zcmeI3e{37&8OPsHC|$Z_Xsjg-K^!J6AZp*89VhnHap>YUafy?taT`}7YtMJ*^E)B_@!*Qu^xH~8e?-bmkrOjLINpUbiOfq;X6^kl5muj`- z@^UaXn`sM`lNdW&E$hvKR4C9(wX1Q7a@v}$0_$*5Ep8j@bi3?LYbiU!Hq(roW}B=G z%Q0?_VX4B$;;n{}CoV>~&AyF=a`0EHWymlzj;52zq%G;NsqsOYb-Ud(W2fzQD^ytZ zv|{intD@hWPcoOsC+R|5)(lxyC^Ih~Rue|6#bOpJehT*$(~5-@y}%Aqq*J^`vo?mV zWDE%u@!yVkZP#yz%D-#XV3m2+p3#>aKZ+;Odz zxh+>b#ENH>>B;R}ju*_+%qy51LV|$jwU&lebQWy#|2u*C{D^(=8p$C^xzvWrr^=}o zPok?4Bgx05^@DItT+Uw4XPs{=Pw%14(?2TDpNM?x{P~$%u?y$ZV;*W8Tnlhqd~Oa{ z551tRR5`B?nR6OVZ~j>5 zgAWOkx7q1*I6ZWEbGf5ePD#;_Plo$fH&>+--s1fNX81r+5{6Ei4 zWxf?YEx-pAy7;6rf0m0o;VTYGck~q}M$?fZC=F~(aA6Ul0)h*rfsF|+EFx4uaG^A? zF~NmJgbD~Qlm<2?xUh&&0l|gRz{Ug@77;2SxKJ9{nBc-9LIngDN&_1cTv$Y?fZ#%D zU}J&{iwG4ETqq4}OmJZlp#p*nrGbqJE-WHcKyaZnura}fMT80nE|dl~Cb+POPyxY( z(!j<97ZwpJAh=K(*qGqLB0>cO7fJ&g6I@tCsDR)?X<%c53yTO95L_q?Y)o)r5upNt z3#EaL2`(%mR6uZ{G_Wzjg++u42riTcHohdT>e9P^k^*1&OTst%w%($ggKq^=Lg(fH z0K=;R7`+#O|IEVcM*t)k0RFiH0B#=u*QuH9FK-0kiebNReK0kBc6z-seMjBy_fD|> z`UigY>V^N_@}Xo|u?hUu_F2!W-jlc9ckbsM^*?4liL5-hn{D5<>e%mY1he56>il;4 z*wJ?rABUcpt<9=VvCOMk@Ol5(l4Z4&3z(PDZ0SO|a=&sHQviPzb&JU_S2wn# zm~Sp^|I!aa!lAXtYq|#dr^fUy``b0^&RqHK@n`qjk9>GId-&AZtFAuEYo81lu($L4 z!=f-bdi|+jU+suCeeNGW6(__PYzX!D)y5uiG_KmaHse{dZB4rSd`8^z*p1(N@U3s8 zI@Zmeesd}^`8VgEKHYWTxj%k%$AKAb`lX3IuaCU^hue*8>rBn3FT8&!aQ=yR-fP(V zn$@MBKGk;gnaLCT#A_O|_hv7=;+X0BcW-R`ot~#2U%qML6{JNRRGM+E&<=V@X#J==JgHci>&?)-M(kPy<_Zul(o7- literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_back.png b/resources/skins/default/media/dialogs/menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..0747aa019a1033df1e6a7ffc65746320b717a520 GIT binary patch literal 15358 zcmeI3O^Do79Kc@{+Pby{zfh!yFs0yECLc4InWUMi+nv^3aO!qDvRf~0CV80+ok?Pn z?aXd3qNkn&K@cg31));#B7)F^TB&#u3MxqPAl@v3mtMSdeJ`1rWZup^MqBXm26mJG z`+vRn`@i>3F1fUP@|FE_&&~k=_Af2gR{+>`5ZxcyyBq!g^abTMx;)}9o(TbX^a=d8 z3w(I_8344aPIE0)8UJK^?T_0Hkpdan~hIt-FTnn}xx0?Iw`d2y5v8uUM zr6D%_1-R}kZUyl4*2$*1b>391+|k;c-q%ooE{qJW-|e`e*01K`xEi|0!$OXWO``MF z+&oUmtu>ap1uuY{k}vV5RH$%eH7_Y@MJ~R?$)Z#eL{*TAyeMg+s)-Ue_~mMI=uQtT zTU)807^Fi|HMbr`z9tC0UN7G(%mIIhw1Hlk`ShZBu{gLQTNQ6e`SutOAuzTpc}UKA!dS?0*c-v~PK)GSkg9oU6# z6e2%qqMyI+MP9h>O-VUYPfd(!(rAowPn>VJJ27<_9lwAO43JLr44a!i6jos9Z3HGf zegQ4z(4^Hzj-3`~EE3jCojdG1Q{1p7af`P?QXw6!Ra*$45qUw=^E$O;mn{zwatjN1 zSCzTvRvp*!dg1fBK)R&$PNTuP5y2Yvtw4va;ZCywQGnS)Wr7d_J zFF@0RwSq288lLD?_sk8vt>;}6hNu`}rbwt%@xeW2w8uw>ijGr5Maf%^rO9PzmqbJ4 zE3#?xt#Yx#S6X(FSBr9~rNFW(+O{s>N*pB{z%=w1B00 zJRb+mr>IsGyChY3rBW#H6{V=~YS~bDC{|2WR>X3v1moQ>?w-zU5PFz>5PJ5dCNs;p z?+E=`;GmXmbTT&oj%Jc#LYN(?79CvMO#(cw#%cKG_-I<&xVid95JKEKs<~nB7q}!vYYFM+lE$6 z7;fON$S0hP`IzN!0OkRYRv3?I^L5vWYVsf(F`V(j5C7!JbQ-;Ld$RwKTdbp=G!TAas%8qVplN3>N`}E;3wnK7^LxB7o3EhKtUJ&@x;E5W2{4 z(fJTshKm3~7a1-(A41D;5kTl7!$s#qXc;a72wh~j=zIt*!$kn0iwqZ?520na2q1Kk z;iB^)vIv+yIa1lW0 zA{EzM`uQnz(Kn|(^l|AYS8m-yAEt8V;z|R6t-}Dk^$q}kZ=>rU0Bnc=+&PE7Y5f3z zr@i;i-aLV9Z!Oj5oBi8={`B!((t$y)?`YyO9e)r3@&$m`Ty0i8DL%-hi!MD5i?fDvC#DnQ&-=0{)m)Kx%i4CTh o&uWKakYRt@a~3UTd;4K<#NK=Hz@Gs^=4k-?9j%NA@z zk(gFuLU(9Ep&a@zV2hh(N?_ScCgaGs9D2H+<^6s?%Q;!6(~cB&V^A}ttX(r^q9-+wPUMEB z$Sh32B-Bg;`S_~7R9rW8Bd(88ve-RlVKgU^NU?U+@g|d1OB?3=l?XurX;r5Y9ZW&C z8yfmRT88siqFb3!?e0xAR+^l$OjggBV~1IF44c)Hx8;^XeuhMJuR>cIN~WHU>UtuW zUu9h*2${AvYgKufyO*e%qGya*B3o%G&37pq3`r&oT24FXvvb}k@3GdS({m5!6gUn` zl2=ybi0FzM8!XGg`=UIuHVa%?4oq2@1MMG0GNmiZf@`rniY&zRbW$>dYEtTlY)b1F z*-|5>EUqe{O;6})G%Of&iEOoI%#~YdLRd3QNt0nX6hsXURaFF^%jI>&6tCUoQY5?I z?{2nBeLmUl_bR-bkMS`t=gvWlL~#44)FE9Suy#u!wQ`lIG5e_0_or2K(n$$SV~zf5 zOH@fSs#Lpbp!GS3ty{Eub7$FN_;7jaNJm1=ok=MvZ9r?s1#BZ*u6T$Q&nU~2+qr@y zTc=r2wl)M*M7FH8G_0btU{n9^1m^Q&_OWYZ;!wNXhRUbPsm4xX=rJ=RrD1D7S`$~Y z*YjD+ZI$Uo);j$YYWYOuTg&HXWW|o2PtiKm1hf{=xTM@1@FF{^tyDR$@3H1IF_6=H zogBJY-uf(9@$O1%@8zv{ccr!Dd_YspptDe|q8W2pCvW~(Sd1PLV6fTaad||xvboaH zrDkEGC#0hNYgnt&=|fvSXR7R}>g+8!c|@dS#5xMb{9Iwb=eeoOx1y&7^q|5PpH#|c zxwsRqaZtLWYn&KQM~mPzh%v=QM1%_{E}RB2rnrcRZ~?`I(;&ta7ZDLIptx`v#F*kD zBEkg}7fyp1Q(QzuxPaoqX%J(Ii--spP+T|-VoY%n5#a)g3#UPhDJ~)+TtIQ*G>9?9 zMMQ)PC@!1^F{ZePh;RYLh0`F$6c-T@E}*z@8pN35A|k>C6cS+ z7~lZ-br}G{1^}A$bxZdxK(8R)6b`jUv&YUJ-P`e5*9~*O{(Iwtt>eFc|F`%4>3ICh zwR4`jbGN$j%%?a1HTBu4?|e`{FMKR<;z#F}+Qh0kCu_3fwl05*Z69uVse9|{B_}6k z>vzBTr&c#RHEzN49mBQz;~i`Fzp(e<(diSP+dh7c5#RRv4BwPp^>v5C!?jPx4}DX2 z*N#mMM{7PjbB9{iw##bYs$4la>r!`*uD@Xm&{Z@*}UO{e(9>2Ejhy?xufDI12x_1hO6S=u!BVl4od aE?oy~P-4!)mf6+|mf`uGp`G)VJ@FsOJ0TVT literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_top.png b/resources/skins/default/media/dialogs/menu_top.png new file mode 100644 index 0000000000000000000000000000000000000000..26bc7d15bafd4e48db9f30f0ee2bbbb9612eb996 GIT binary patch literal 14768 zcmeI3Z)_7~9LFEP2~;pB$Uq@EOAt`oyKB3yceW*C1vazJu@%|ig}dE7-7d6u$K8!~ z3&tU8LeLkY5fEYwMgvCh1;IZd3^4k}pdtE7GyzPMh!|f;NX852bJwnY+I2i&c==qi z_Sfh6et*B`KF^=so7>&dzJ9LfQ4auMZfi@T6M$(;Q0$p?Kl)71x$*=0nr*c7SO7dQ z-}##c-aWJkfXH$+)opht+eBH<`Xxp0gZ_NhKxzP@tMi5=Z-q9~2m4hm=KcHB884$M zG4G~^B$qUr;egsQY{IVL_LMxlRTdQQ>bNJG7g2&Nv?V5=&1jaGk9pm^B8r`6*2}mO zduzDOlO0U6ZbGKPANI+7Fv2tne!fA71VT?U0gex|oWSxSAIFQFAaXoYyu5J_ zilU~H7CRH`isjI+n0LUo4UuJYxtu>2^y_9n%L{_Qasf6F@F9iI8q#bj@6)Vhg(MSs z63~)O)v#4vW1PHFpFU{Eyk4hJ=_=k=)+iOytRg!^kN8_RU@-Ej|kt}(?oIO{RUKg9(^j>D1^ zlvO#Bx}v6sDsu3V6pyT7F;I~MQd`$v&%>5j7CS}czui)r1=N_Je$O8t;EwEiet zZp4(ORV6m-8Qny~g7ILKt<{XV+?6J_YL+c&GHgx6QG;Jq6|`}sa9^;I_l2d9;FJ17 zoUbn(lzmbN%1{vmE)+u9C)p>ZPU!NWvs;R(mAg!h*(arb&Q#G!CuJ~=Gy1zNQ6tTy zQZ1^5*5?qmZqe#>&$2GKt+I8ZBcr-!k|CKEbaq_KJF(?Thgj*1ay;(N6(!j@&Em4N zA)pdvD_YCLDmjZb_5V&_AwOoHx<+mQYPZ`^g;cI;>Liw)wsVpRoBGk3xRbqJ$XaQu zP9J5R(?6qDPDG)#a(*UO?Bw~3I)|Ew)&d%riq)!`F_&}l7LJ83^pF7K;f97_LzJyar$LMGwSae2z`{y`1B@Rvhx_8k=7d>FkI$jB|7NdShI12D1+fU$A({RaSp901pP z0TA~CP_OTO;hS~n6~wyML{ln%@%qIk?JI8Ht5+_4{mS0duYuF!vxg7(H#eQzR{!Gh znW;yPDr0dqw0Fj1?0c`x-mR~?(Vb3@Z_qvO|31%u@WRZEJJRW!`2r`$Fa+2`4i9V>{{SoarwwE zk!xd%TZA^Y=kb5$EE!q6f5Dm$Ml!SlPu>q5PJcT&>rnX0`OznXTh4vbHuh89=cn5avwK!tdh5Wk-os}`p1n|iF8i&z zM-HA?|Hh4d?VEPae`&K|oX;FP`{k`I-_;{pXrVnr8l3SiGG6Cwe5-HYkFV) E58UV(?EnA( literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/white.jpg b/resources/skins/default/media/dialogs/white.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1206155018137636cf6c1c658768acce3283c22 GIT binary patch literal 8060 zcmeGhX>b$Q`RSH?VfhGzb}~Ykvz2w(mZio4OO|b=2pm~lnI5y+T}cb8U9r2ejr%7M za{emhAmm3U5OPjCZ4z?P$+YB7eubQnPA4TN$uud15R$Yr>i6DiSCTOqPdoipKl$x` z_kQo&?|tvBU-?A&l<7!(ipwIDNUTP7gisSg1`{#>)BxR30~-N00qr-?trfZv;I;~k zbvi3BmRoGF5Ictf{y_!)Ex;>lbo~Y+as%xM^Z>veaDN**caxdu{vF^hFl;Bk>;Bb% zT2~;X-3J*l!@9=uq9V$9vFM7$TmhdyqelMRWe~y~Sn0^^cB4har)Iu=CT!%%=e?fatlihB2*qa=VCTC+qqqB2C zQ`3UZMeXgK?d^-4O{7(BDl+qGXl`t5ZfS09X=&|fX=&-emzEAy#W@!P2str@`bjl>Z8`G?t$?tN|}A2GYP`+#s?>v&Cw&I~p1dGnoctG-=FMWH1{{Mzhgk zv)V1DrXVn#Ci9Xuzh&?ms@=U~SHRk__uwPZrCpt=W0_!&wEK9>wrt<{nNw$Jx%aX| zk6yog=(aQyfA(IbZ{h13-)FykY|nFVls-5&yyEu5HypX+`8PlO>b}Qcc32z9zH4~p~R!c8&VTr%Z3?A%ow_5_c_I6+o zj-`&D33g>pNxS#O#ye@b=j<{ocEGm0_u1FMiF+3^@r`{7`@ZhNr9D_tb)oz&YBG^$ zI?)<*KD{roY*p93#D?-~=hORNeW>e!&kmDY`DFR?#?fll2fKZ!KblCIaY7?x`yu2P zPBB7TMrf_4&;IP3_t>I?hklj&-3!cHM+q-NcxU0-3LU2+LXAa<6O;`lrC3q`wP4zq ztc+(wktY%fL19^;RKN@j<9m#kFus83j&oC3BFZUP2eVb1thAmgu<7`gG?3JXsS0C~ zC}zi5rBvLI*+v7i4UHiQiSWxJ4~ioW2}ni?Vi6C&5@0!O7Bv)gO~8oBD6a?=@0v0t zj#oH=m}f2v02C)kA11DERP+iFI?p{Q`8AOn9PmW&k@K^qVZI3wvuDkmp( zBqj;5Dk3OVWL<`rbV_zMYJe?v0PMQ(i;V3cxj5q?^$nAo^j1aPszMO=wnm-+LSKdFu~G_C=F2x zg-1Y~9LtT#T83a43&S0|jn=VQ)y#~?rlESz_SkWnSLtICy>=_MKw2v;iN(!%7G5r7 zD<|Z%@U>t@3Ve!+iXs+xQBYF_)-{cTU8tcqYxESC%hxg*H3s;m%j0=t`dsy)Qn-!U zoy@7_gx8|FiLYy}tIWh^Lc??P3L<%VJ#IQja0j`<#Xz!j1+zdP5?{Aqkl@!-l5(zoQL$3IlBRNXQY|#g z^J#WUNyww=2yr6k}`|Aw1H@0d1M^MICRX#{YuFBKLC9-S_FI5`MvD? zUUp7p2&MK}9 z3$=5={f|HX$?f8p0r%$Kq%T>Fv3YKET4Kki*Qe>}?R1!Nuer)~kE7RJszJw;)BmvzpK*R@HC0ZMp8q=m1hAq;I0g6a&oeJGT1FjIj=t)4tsq8Zy?|S z36H!}P^c-7ATQM{46!mTab(x)!WLmiS5gMt;Hf%$*i)YDkzE+RO560mNj; z0Ke{MQTs20u2Sc#&L(Mt{VPUd5=+76I1L+P?JZZo+AjKEMU4&%`)PK-jsJP!@%4KG z{G8qek9>iM&!>teMTX1noFN)aYyan{6NQvxV2?f*;bbzB5M+fCXf`o4;D&*8 za~u=t2?l&L+Y|6)!j#WLWqbUdOn-0I!$Pk126{7neRLo33-pIXx`ZRB=`)}o4MyX! zP%s?ojYj?ccqrQM3rFLDa3~xP`g=klO;_EIQ2c*pgP|i?Q7TZ76kLJIvEFS(Hs^Nf zZzn>QScn{iH&{zg#A5KFnB{n2;0Xc4#sgs`Zh~coC9&Wli5lT%2~A~aeU~1Zs>@tD zETK#4dUa<;IT^BPXJx*RvpXsf&w+0+4R!u5i zoL{P#EM}ESN@53d@ZRT??W!J~r-U2}<<#rOBlY6KqYllhzJltg39qI|Y2r}%FsPT@ ztF+QMf1i)Qd<5nrFdu>W2+T*||0n|0k2Y3-t;HmKlGPlht;Ju1k?bL8`=FGgMp=;j%%WqQC-1>sU5o(W%e$M z9em`>@*b(-@?*z)SDYH}+kJN5p`o-K9}bPsD>rW1y!g>)Uw{8g=6dC}J?y=YJ@>{3 ztNOF&N~^<>(ZtnTwqBddbK52+r*=-?aN|uk-*W5icig%Et^;=;zVH4A9(?HGBac7v zm&V+&9nJM7V2OZRrB4rVS3Iv(jdc04AXS$Jx951axnqKD2NdUSd3 z<>|gSv!X%1{OtV>mi*Ms zcRl#j%O_5M6HaX9cHVN}p{HLt`RTW-BUfL$ZTi-`A3pl(yPthme_r?A2k#xd@QVvy VJoC;o|NMQa{Mros_ze8yzX3F67cu|< literal 0 HcmV?d00001 From 16423e18ec271d5492f56b282d39a6381ecf3b6d Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 12:09:53 +0200 Subject: [PATCH 04/74] Ignore suspends for webservice --- resources/lib/webservice.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 382fb6ae2..7ba7d1115 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -49,6 +49,20 @@ def abort(self): except Exception: utils.ERROR() + 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. ''' From 4a3b38f5b676b6fba994e2e0f462fbab7f0b08e0 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 12:31:37 +0200 Subject: [PATCH 05/74] Increase logging --- resources/lib/webservice.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 7ba7d1115..dd8544389 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -46,8 +46,8 @@ def abort(self): conn = httplib.HTTPConnection('127.0.0.1:%d' % v.WEBSERVICE_PORT) conn.request('QUIT', '/') conn.getresponse() - except Exception: - utils.ERROR() + except Exception as error: + xbmc.log('Plex.WebService abort error: %s' % error, xbmc.LOGWARNING) def suspend(self): """ @@ -182,6 +182,7 @@ def handle_request(self, headers_only=False): def strm(self): ''' Return a dummy video and and queue real items. ''' + xbmc.log('PLEX.webserver: starting strm', xbmc.LOGWARNING) self.send_response(200) self.send_header(b'Content-type', b'text/html') self.end_headers() @@ -209,7 +210,8 @@ def strm(self): return path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id'] - self.wfile.write(bytes(path)) + xbmc.log('PLEX.webserver: sending %s' % path, xbmc.LOGWARNING) + self.wfile.write(bytes(path.encode('utf-8'))) if params['plex_id'] not in self.server.pending: xbmc.log('PLEX.webserver: %s: path: %s params: %s' % (str(id(self)), str(self.path), str(params)), From c0035c84a630f9d6489f031880a1006a98232e6c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 12:34:27 +0200 Subject: [PATCH 06/74] Fix monitor's playlist.onadd --- resources/lib/kodimonitor.py | 50 ++++++++++-------------------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index f35db30b7..5e33666e0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -178,26 +178,19 @@ def _hack_addon_paths_replay_video(): 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']: - 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'] + ''' + Detect widget playback. Widget for some reason, use audio playlists. + ''' + if data['position'] == 0: + if data['playlistid'] == 0: + utils.window('plex.playlist.audio', value='true') + else: + utils.window('plex.playlist.audio', clear=True) + self.playlistid = data['playlistid'] + if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: + LOG.info('Playlist ready') + utils.window('plex.playlist.ready', value='true') + utils.window('plex.playlist.start', clear=True) def _playlist_onremove(self, data): """ @@ -449,23 +442,6 @@ def _playback_cleanup(ended=False): app.PLAYSTATE.active_players = set() LOG.info('Finished PKC playback cleanup') - def Playlist_OnAdd(self, server, data, *args, **kwargs): - ''' - Detect widget playback. Widget for some reason, use audio playlists. - ''' - LOG.debug('Playlist_OnAdd: %s, %s', server, data) - if data['position'] == 0: - if data['playlistid'] == 0: - utils.window('plex.playlist.audio', value='true') - else: - utils.window('plex.playlist.audio', clear=True) - self.playlistid = data['playlistid'] - if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: - - LOG.info("--[ playlist ready ]") - utils.window('plex.playlist.ready', value='true') - utils.window('plex.playlist.start', clear=True) - def _record_playstate(status, ended): if not status['plex_id']: From 875d704e5a78c818070abca8e2e09a4d5e3913fd Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 13:52:03 +0200 Subject: [PATCH 07/74] Don't store filename in Kodi db --- resources/lib/itemtypes/movies.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 203f4ef8f..6a83a2d46 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -73,12 +73,11 @@ def add_update(self, xml, section, children=None): if do_indirect: # Set plugin path and media flags using real 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}&name={4}' + 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, - api.file_name(force_first_media=True)) + v.PLEX_TYPE_MOVIE) playurl = filename kodi_pathid = self.kodidb.get_path(path) From 20c1c6e502ea37cd8207f80cfdf6de4dda6b21a1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 13:55:44 +0200 Subject: [PATCH 08/74] Add missing default.py option --- default.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/default.py b/default.py index 131c923a5..f35ba57ce 100644 --- a/default.py +++ b/default.py @@ -39,7 +39,22 @@ def __init__(self): mode = params.get('mode', '') itemid = params.get('id', '') - if mode == 'play': + if mode == 'playstrm': + while not utils.window('plex.playlist.play'): + xbmc.sleep(50) + 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.ready', clear=True) + utils.window('plex.playlist.aborted', clear=True) + + elif mode == 'play': self.play() elif mode == 'plex_node': From dfcfa0edab82cf1b8845aea5a09ccc52280e14cd Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 14:18:23 +0200 Subject: [PATCH 09/74] Better detect videos playing for playback cleanup --- resources/lib/kodimonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 5e33666e0..d38fa658b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -431,7 +431,7 @@ def _playback_cleanup(ended=False): DU().downloadUrl( '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) - if playerid == 1: + if status['plex_type'] in v.PLEX_VIDEOTYPES: # Bookmarks might not be pickup up correctly, so let's do them # manually. Applies to addon paths, but direct paths might have # started playback via PMS From d7541b7f744d791f267d5d1c3503d4769db63f1c Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 11:37:31 +0200 Subject: [PATCH 10/74] TO BE CHECKED: better method to delete obsolete fileIds --- resources/lib/kodi_db/video.py | 12 +++++++----- resources/lib/kodimonitor.py | 12 ++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 1e959938c..aceb54473 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -178,11 +178,13 @@ def obsolete_file_ids(self): 'plugin://plugin.video.plexkodiconnect' These entries should be deleted as they're created falsely by Kodi. """ - 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.strPath LIKE \'%plex.direct%\' + """)) def show_id_from_path(self, path): """ diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d38fa658b..23243da74 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -517,13 +517,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: From 20bffc1b41fdfe7897b931b5cf0a3226ab1cab7d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 12:02:39 +0200 Subject: [PATCH 11/74] Enable webservice playback for shows --- resources/lib/itemtypes/tvshows.py | 34 +++++++++++++++++------------- resources/lib/kodi_db/video.py | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index fc616cdcb..6e713d34e 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -180,7 +180,8 @@ def add_update(self, xml, section, children=None): 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 @@ def add_update(self, xml, section, children=None): 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: diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index aceb54473..3e794d128 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -10,7 +10,7 @@ LOG = getLogger('PLEX.kodi_db.video') MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT -SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID +SHOW_PATH = 'http://127.0.0.1:%s/plex/kodi/shows/' % v.WEBSERVICE_PORT class KodiVideoDB(common.KodiDBBase): From 12befecc4aec3fdab0fd06ba0ac62cf37aacd245 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 13:18:15 +0200 Subject: [PATCH 12/74] Rewire video resume info --- resources/lib/app/playstate.py | 7 ++++--- resources/lib/kodimonitor.py | 6 +++--- resources/lib/playstrm.py | 5 ++--- resources/lib/windows/resume.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index c99aeafa5..3b9be12f9 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -53,9 +53,10 @@ def __init__(self): } 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 # 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? diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 23243da74..8fde54b46 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -21,8 +21,8 @@ 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 KodiMonitor(xbmc.Monitor): @@ -559,5 +559,5 @@ def _run(self): 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) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 08fa690d2..e44093b6d 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -206,9 +206,8 @@ def _resume(self): Resume item if available. Returns bool or raise an PlayStrmException if resume was cancelled by user. ''' - seektime = utils.window('plex.resume') - utils.window('plex.resume', clear=True) - seektime = seektime == 'true' if seektime else None + seektime = app.PLAYSTATE.resume_playback + app.PLAYSTATE.resume_playback = None auto_play = utils.window('plex.autoplay.bool') if auto_play: seektime = False diff --git a/resources/lib/windows/resume.py b/resources/lib/windows/resume.py index 3f242646a..a0d10db69 100644 --- a/resources/lib/windows/resume.py +++ b/resources/lib/windows/resume.py @@ -10,7 +10,7 @@ from logging import getLogger -LOG = getLogger('PLEX.resume') +LOG = getLogger('PLEX.windows.resume') XML_PATH = (xbmcaddon.Addon('plugin.video.plexkodiconnect').getAddonInfo('path'), "default", From 439857a9cebb44d682ca30c48b9222a9509f8803 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 13:27:26 +0200 Subject: [PATCH 13/74] Rewire autoplay flag --- resources/lib/app/playstate.py | 2 ++ resources/lib/kodimonitor.py | 7 +++++-- resources/lib/playstrm.py | 8 +++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 3b9be12f9..e44b21a19 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -57,6 +57,8 @@ def __init__(self): # 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? diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 8fde54b46..827adb718 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -32,6 +32,7 @@ class KodiMonitor(xbmc.Monitor): def __init__(self): self._already_slept = False self.hack_replay = None + self.playlistid = None xbmc.Monitor.__init__(self) for playerid in app.PLAYSTATE.player_states: app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template) @@ -202,14 +203,16 @@ def _playlist_onremove(self, data): """ pass - @staticmethod - def _playlist_onclear(data): + def _playlist_onclear(self, data): """ Called if a Kodi playlist is cleared. Example data dict: { u'playlistid': 1, } """ + 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 diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index e44093b6d..7fa81f4ef 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -208,19 +208,17 @@ def _resume(self): ''' seektime = app.PLAYSTATE.resume_playback app.PLAYSTATE.resume_playback = None - auto_play = utils.window('plex.autoplay.bool') - if auto_play: + if app.PLAYSTATE.autoplay: seektime = False LOG.info('Skip resume for autoplay') elif seektime is None: resume = self.api.resume_point() if resume: seektime = resume_dialog(resume) - LOG.info('Resume: %s', seektime) + LOG.info('User chose resume: %s', seektime) if seektime is None: raise PlayStrmException('User backed out of resume dialog.') - # Todo: Probably need to have a look here - utils.window('plex.autoplay.bool', value='true') + app.PLAYSTATE.autoplay = True return seektime def _set_intros(self, xml): From 797a58a3d583ef6db2b38fd1d4b701ac3465c735 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 13:33:00 +0200 Subject: [PATCH 14/74] Rewire plex.playlist.audio --- resources/lib/app/playstate.py | 3 +++ resources/lib/kodimonitor.py | 4 ++-- resources/lib/playstrm.py | 2 +- resources/lib/webservice.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index e44b21a19..1e63bece9 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -59,6 +59,9 @@ def __init__(self): self.resume_playback = None # Don't ask user whether to resume but immediatly resume self.autoplay = False + # Are we using the Kodi audio playlist (=True, e.g. for videos when + # starting from a widget!) or video playlist (=False)? + self.audioplaylist = None # 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? diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 827adb718..67b6afcad 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -184,9 +184,9 @@ def _playlist_onadd(self, data): ''' if data['position'] == 0: if data['playlistid'] == 0: - utils.window('plex.playlist.audio', value='true') + app.PLAYSTATE.audioplaylist = True else: - utils.window('plex.playlist.audio', clear=True) + app.PLAYSTATE.audioplaylist = False self.playlistid = data['playlistid'] if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: LOG.info('Playlist ready') diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 7fa81f4ef..8c86caa91 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -52,7 +52,7 @@ def __init__(self, params, server_id=None): self.transcode = params.get('transcode') if self.transcode is None: self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None - if utils.window('plex.playlist.audio'): + if app.PLAYSTATE.audioplaylist: LOG.debug('Audio playlist detected') self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) else: diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index dd8544389..c345cbf02 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -330,7 +330,7 @@ def run(self): utils.window('plex.playlist.ready', clear=True) utils.window('plex.playlist.start', clear=True) - utils.window('plex.playlist.audio', clear=True) + app.PLAYSTATE.audioplaylist = None self.server.threads.remove(self) self.server.pending = [] LOG.info('##===---- QueuePlay Stopped ----===##') From 484b03482e9f3f1386f7c5408b54dcca3c1f49c6 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 14:01:07 +0200 Subject: [PATCH 15/74] Fix resume flags for ListItems --- resources/lib/playstrm.py | 15 +++++++++------ resources/lib/widgets.py | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 8c86caa91..6a156809f 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -115,8 +115,7 @@ def play(self, start_position=None, delayed=True): else: self.start_index = max(self.kodi_playlist.getposition(), 0) self.index = self.start_index - listitem = widgets.get_listitem(self.xml[0]) - self._set_playlist(listitem) + self._set_playlist() LOG.info('Initiating play for %s', self) if not delayed: self.start_playback(self.start_index) @@ -134,7 +133,7 @@ def play_folder(self, position=None): if self.kodi_id and self.kodi_type: self.add_to_playlist(self.kodi_id, self.kodi_type, self.index) else: - listitem = widgets.get_listitem(self.xml[0]) + listitem = widgets.get_listitem(self.xml[0], resume=True) url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT args = { 'mode': 'play', @@ -156,7 +155,7 @@ def play_folder(self, position=None): index=self.index) return self.index - def _set_playlist(self, listitem): + def _set_playlist(self): ''' Verify seektime, set intros, set main item and set additional parts. Detect the seektime for video type content. Verify the default video @@ -192,6 +191,10 @@ def _set_playlist(self, listitem): PL.get_playlist_details_from_xml(self.playqueue, xml) # See that we add trailers, if they exist in the xml return self._set_intros(xml) + if seektime: + listitem = widgets.get_listitem(self.xml[0], resume=True) + else: + listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) play = PlayUtils(self.api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') @@ -233,7 +236,7 @@ def _set_intros(self, xml): # The main item we're looking at - skip! continue api = API(intro) - listitem = widgets.get_listitem(intro) + listitem = widgets.get_listitem(intro, resume=False) listitem.setSubtitles(api.cache_external_subs()) playqueue_item = PL.playlist_item_from_xml(intro) play = PlayUtils(api, playqueue_item) @@ -251,7 +254,7 @@ def _set_additional_parts(self): # The first part that we've already added continue self.api.set_part_number(part) - listitem = widgets.get_listitem(self.xml[0]) + listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) playqueue_item = PL.playlist_item_from_xml(self.xml[0]) play = PlayUtils(self.api, playqueue_item) diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 8cfadf6dd..b9d67e27d 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -33,11 +33,14 @@ KEY = None -def get_listitem(xml_element): +def get_listitem(xml_element, resume=True): """ - Returns a valid xbmcgui.ListItem() for xml_element + 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) From 4ed17f1a5b57d7b114a9a6f3c2a086c03885c80f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 15:00:14 +0200 Subject: [PATCH 16/74] Fix widgets not updating --- resources/lib/kodi_db/video.py | 8 ++++---- resources/lib/kodimonitor.py | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 3e794d128..2a52c34b2 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -174,16 +174,16 @@ def modify_file(self, filename, path_id, date_added): 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 files.idFile FROM files LEFT JOIN path ON path.idPath = files.idPath WHERE files.dateAdded IS NULL - AND path.strPath LIKE \'%plex.direct%\' + AND path.noUpdate IS NULL + AND path.dateAdded IS NULL """)) def show_id_from_path(self, path): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 67b6afcad..2202e1d36 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -435,7 +435,7 @@ def _playback_cleanup(ended=False): '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) if status['plex_type'] in v.PLEX_VIDEOTYPES: - # Bookmarks might not be pickup up correctly, so let's do them + # 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) @@ -503,13 +503,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(): @@ -520,7 +530,7 @@ 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(1) + # app.APP.monitor.waitForAbort(1) try: with kodi_db.KodiVideoDB() as kodidb: file_ids = list(kodidb.obsolete_file_ids()) From 5428dafe5926262b9e04f0056a442674064d491f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 7 Apr 2019 15:18:16 +0200 Subject: [PATCH 17/74] Simplify code --- resources/lib/playstrm.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 6a156809f..bdd09a545 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -74,16 +74,17 @@ def __repr__(self): "}}").format(self=self).encode('utf-8') __str__ = __repr__ - def add_to_playlist(self, kodi_id, kodi_type, index=None, playlistid=None): - playlistid = playlistid or self.kodi_playlist.getPlayListId() + def playlist_add_json(self): + playlistid = self.kodi_playlist.getPlayListId() LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s', - kodi_id, kodi_type, playlistid, index) - if index is None: - json_rpc.playlist_add(playlistid, {'%sid' % kodi_type: kodi_id}) + self.kodi_id, self.kodi_type, playlistid, self.index) + if self.index is None: + json_rpc.playlist_add(playlistid, + {'%sid' % self.kodi_type: self.kodi_id}) else: json_rpc.playlist_insert({'playlistid': playlistid, - 'position': index, - 'item': {'%sid' % kodi_type: kodi_id}}) + 'position': self.index, + 'item': {'%sid' % self.kodi_type: self.kodi_id}}) def remove_from_playlist(self, index): LOG.debug('Removing playlist item number %s from %s', index, self) @@ -131,7 +132,7 @@ def play_folder(self, position=None): self.index = self.start_index + 1 LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index) if self.kodi_id and self.kodi_type: - self.add_to_playlist(self.kodi_id, self.kodi_type, self.index) + self.playlist_add_json() else: listitem = widgets.get_listitem(self.xml[0], resume=True) url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT From f4c3674bc2aff1d5f27f6827726e4bf32eeaa17b Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 11 Apr 2019 16:49:39 +0200 Subject: [PATCH 18/74] Increase logging --- resources/lib/service_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index eda2dfed7..5734b3c78 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -531,6 +531,7 @@ def ServiceEntryPoint(self): continue elif not self.startup_completed: self.startup_completed = True + LOG.debug('Starting service threads') self.webservice.start() self.ws.start() self.sync.start() @@ -538,6 +539,7 @@ def ServiceEntryPoint(self): self.playqueue.start() if utils.settings('enable_alexa') == 'true': self.alexa.start() + LOG.debug('Service threads started') xbmc.sleep(100) From 0d36a2a3b9d6baf24f843dc089f9052f4db4684d Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 11 Apr 2019 17:43:38 +0200 Subject: [PATCH 19/74] Simplify code --- resources/lib/playstrm.py | 41 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index bdd09a545..e195f2926 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -31,6 +31,7 @@ def __init__(self, params, server_id=None): LOG.debug('Starting PlayStrm with server_id %s, params: %s', server_id, params) self.xml = None + self.playqueue_item = None self.api = None self.start_index = None self.index = None @@ -41,10 +42,10 @@ def __init__(self, params, server_id=None): self.synched = False else: self.synched = True - self._get_xml() - self.name = self.api.title() self.kodi_id = utils.cast(int, params.get('kodi_id')) self.kodi_type = params.get('kodi_type') + self._get_xml() + self.name = self.api.title() if ((self.kodi_id is None or self.kodi_type is None) and self.xml[0].get('pkc_db_item')): self.kodi_id = self.xml[0].get('pkc_db_item')['kodi_id'] @@ -86,6 +87,12 @@ def playlist_add_json(self): 'position': self.index, 'item': {'%sid' % self.kodi_type: self.kodi_id}}) + def playlist_add(self, url, listitem): + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) + self.playqueue_item.file = url.decode('utf-8') + self.playqueue.items.insert(self.index, self.playqueue_item) + self.index += 1 + def remove_from_playlist(self, index): LOG.debug('Removing playlist item number %s from %s', index, self) json_rpc.playlist_remove(self.kodi_playlist.getPlayListId(), @@ -101,7 +108,9 @@ def _get_xml(self): else: self.xml[0].set('pkc_db_item', None) self.api = API(self.xml[0]) - self.playqueue_item = PL.playlist_item_from_xml(self.xml[0]) + self.playqueue_item = PL.playlist_item_from_xml(self.xml[0], + kodi_id=self.kodi_id, + kodi_type=self.kodi_type) def start_playback(self, index=0): LOG.debug('Starting playback at %s', index) @@ -133,6 +142,7 @@ def play_folder(self, position=None): LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index) if self.kodi_id and self.kodi_type: self.playlist_add_json() + self.index += 1 else: listitem = widgets.get_listitem(self.xml[0], resume=True) url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT @@ -151,10 +161,8 @@ def play_folder(self, position=None): args['transcode'] = True url = utils.extend_url(url, args).encode('utf-8') listitem.setPath(url) - self.kodi_playlist.add(url=url, - listitem=listitem, - index=self.index) - return self.index + self.playlist_add(url, listitem) + return self.index - 1 def _set_playlist(self): ''' @@ -200,8 +208,7 @@ def _set_playlist(self): play = PlayUtils(self.api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) - self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) - self.index += 1 + self.playlist_add(url, listitem) if self.xml.get('PartCount'): self._set_additional_parts() @@ -238,14 +245,11 @@ def _set_intros(self, xml): continue api = API(intro) listitem = widgets.get_listitem(intro, resume=False) - listitem.setSubtitles(api.cache_external_subs()) - playqueue_item = PL.playlist_item_from_xml(intro) - play = PlayUtils(api, playqueue_item) + self.playqueue_item = PL.playlist_item_from_xml(intro) + play = PlayUtils(api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) - self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) - self.index += 1 - utils.window('plex.skip.%s' % api.plex_id(), value='true') + self.playlist_add(url, listitem) def _set_additional_parts(self): ''' Create listitems and add them to the stack of playlist. @@ -255,11 +259,14 @@ def _set_additional_parts(self): # The first part that we've already added continue self.api.set_part_number(part) + self.playqueue_item = PL.playlist_item_from_xml(self.xml[0], + kodi_id=self.kodi_id, + kodi_type=self.kodi_type) + self.playqueue_item.part = part listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) playqueue_item = PL.playlist_item_from_xml(self.xml[0]) play = PlayUtils(self.api, playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) - self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) - self.index += 1 + self.playlist_add(url, listitem) From 8c614f3e475b9e08a4ae88e81f04057b4c9ef915 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 11 Apr 2019 17:44:21 +0200 Subject: [PATCH 20/74] Don't automatically look up kodi_id from Plex DB --- resources/lib/playlist_func.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a0ba8645f..1825a1af6 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -423,15 +423,9 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None): # 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: + if kodi_id is not None and kodi_type 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() From fe52efd88eac3438dcb8725b5d675d417e154695 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 11 Apr 2019 18:21:14 +0200 Subject: [PATCH 21/74] Less playlist logging --- resources/lib/playlist_func.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 1825a1af6..b462cdc9f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -76,23 +76,23 @@ def __init__(self): self.kodi_playlist_playback = False def __repr__(self): - answ = ("{{" + 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(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__() + "'pkc_edit': {self.pkc_edit}, " + "}}").format(**{ + 'items': [x.plex_id for x in self.items or []], + 'self': self + }).encode('utf-8') + __str__ = __repr__ def is_pkc_clear(self): """ From b11ca482945a2c5cf5d2f7d46c07626de0efc378 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 13:05:52 +0200 Subject: [PATCH 22/74] Enable playqueue elements comparison --- resources/lib/playlist_func.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index b462cdc9f..cf583aaaa 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -149,6 +149,11 @@ class Playlist_Item(object): 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 + + Playlist_items compare as equal, if they + - have the same plex_id + - OR: have the same kodi_id AND kodi_type + - OR: have the same file """ def __init__(self): self._id = None @@ -168,6 +173,21 @@ def __init__(self): self._part = 0 self.force_transcode = False + def __eq__(self, item): + if self.plex_id is not None and item.plex_id is not None: + return self.plex_id == item.plex_id + elif (self.kodi_id is not None and item.kodi_id is not None and + self.kodi_type and item.kodi_type): + return (self.kodi_id == item.kodi_id and + self.kodi_type == item.kodi_type) + elif self.file and item.file: + return self.file == item.file + raise RuntimeError('playlist items not fully defined: %s, %s' % + (self, item)) + + def __ne__(self, item): + return not self == item + @property def plex_id(self): return self._plex_id From ad6c1605248abc425d0ab20e98c72644c8d0bf03 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 13:28:09 +0200 Subject: [PATCH 23/74] Don't sleep --- resources/lib/webservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index c345cbf02..9d192a214 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -299,7 +299,7 @@ def run(self): play.start_playback() else: utils.window('plex.playlist.play', value='true') - xbmc.sleep(1000) + # xbmc.sleep(1000) play.remove_from_playlist(start_position) break play = PlayStrm(params, params.get('ServerId')) From 643e6171c482efc6c4c2e0adb4323823d2f69566 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 13:29:35 +0200 Subject: [PATCH 24/74] Revamp monitor --- resources/lib/kodimonitor.py | 212 ++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 2202e1d36..597c2d1a6 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -25,6 +25,13 @@ utils.lang(12023).encode('utf-8')) +class MonitorError(Exception): + """ + Exception we raise for all errors associated with xbmc.Monitor + """ + pass + + class KodiMonitor(xbmc.Monitor): """ PKC implementation of the Kodi Monitor class. Invoke only once. @@ -32,11 +39,14 @@ class KodiMonitor(xbmc.Monitor): def __init__(self): self._already_slept = False self.hack_replay = None + # Info to the currently playing item + self.playerid = None self.playlistid = None - xbmc.Monitor.__init__(self) + 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): @@ -74,7 +84,7 @@ def onNotification(self, sender, method, data): 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()') @@ -279,7 +289,88 @@ def _json_item(self, playerid): 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.Playlist_Item() 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) + kodi_item = PL.playlist_item_from_kodi(kodi_playlist[position]) + if (position == 1 and + len(kodi_playlist) == len(self.playqueue.items) + 1 and + kodi_playlist[0].get('type') == 'unknown' and + kodi_playlist[0].get('file') and + kodi_playlist[0].get('file').startswith('http://127.0.0.1')): + if kodi_item == self.playqueue.items[0]: + # Delete the very first item that we used to start playback: + # { + # u'title': u'', + # u'type': u'unknown', + # u'file': u'http://127.0.0.1:57578/plex/kodi/....', + # u'label': u'' + # } + LOG.debug('Deleting the very first playqueue item') + js.playlist_remove(self.playqueue.playlistid, 0) + position = 0 + else: + LOG.debug('Different item in PKC playlist: %s vs. %s', + self.playqueue.items[0], kodi_item) + raise MonitorError() + elif kodi_item != self.playqueue.items[position]: + LOG.debug('Different playqueue items: %s vs. %s ', + kodi_item, self.playqueue.items[position]) + raise MonitorError() + # Return the PKC playqueue item - contains more info + return self.playqueue.items[position] + + def _load_playerstate(self, item): + """ + Pass in a PF.Playlist_Item(). 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 the player state: %s', status) + + def on_play(self, data): """ Called whenever playback is started. Example data: { @@ -288,87 +379,26 @@ def PlayBackStart(self, data): } Unfortunately when using Widgets, Kodi doesn't tell us shit """ + # Some init self._already_slept = False + self.playlistid = None + 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) @@ -388,29 +418,7 @@ def PlayBackStart(self, data): 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): From 9a9bc9f0eb3197d82f6566d41f3617b7b7c2680c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 14:18:13 +0200 Subject: [PATCH 25/74] Optimize code --- resources/lib/webservice.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 9d192a214..2966ca7c6 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -158,7 +158,8 @@ def do_GET(self): def handle_request(self, headers_only=False): '''Send headers and reponse ''' - xbmc.log('Plex.WebService handle_request called. path: %s ]' % self.path, xbmc.LOGWARNING) + xbmc.log('Plex.WebService handle_request called. headers %s, path: %s' + % (headers_only, self.path), xbmc.LOGWARNING) try: if b'extrafanart' in self.path or b'extrathumbs' in self.path: raise Exception('unsupported artwork request') @@ -170,10 +171,8 @@ def handle_request(self, headers_only=False): elif b'file.strm' not in self.path: self.images() - elif b'file.strm' in self.path: - self.strm() else: - xbmc.log(str(self.path), xbmc.LOGWARNING) + self.strm() except Exception as error: self.send_error(500, From 0acf47034341c7729981366cce286c2bd683f0c7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 14:22:38 +0200 Subject: [PATCH 26/74] Optimize logging --- resources/lib/webservice.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 2966ca7c6..702dadc3a 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -159,7 +159,7 @@ 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.LOGWARNING) + % (headers_only, self.path), xbmc.LOGDEBUG) try: if b'extrafanart' in self.path or b'extrathumbs' in self.path: raise Exception('unsupported artwork request') @@ -181,7 +181,7 @@ def handle_request(self, headers_only=False): def strm(self): ''' Return a dummy video and and queue real items. ''' - xbmc.log('PLEX.webserver: starting strm', xbmc.LOGWARNING) + xbmc.log('PLEX.webservice: starting strm', xbmc.LOGDEBUG) self.send_response(200) self.send_header(b'Content-type', b'text/html') self.end_headers() @@ -209,12 +209,11 @@ def strm(self): return path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id'] - xbmc.log('PLEX.webserver: sending %s' % path, xbmc.LOGWARNING) + xbmc.log('PLEX.webservice: sending %s' % path, xbmc.LOGDEBUG) self.wfile.write(bytes(path.encode('utf-8'))) if params['plex_id'] not in self.server.pending: - xbmc.log('PLEX.webserver: %s: path: %s params: %s' - % (str(id(self)), str(self.path), str(params)), - xbmc.LOGWARNING) + xbmc.log('PLEX.webservice: path %s params %s' % (self.path, params), + xbmc.LOGDEBUG) self.server.pending.append(params['plex_id']) self.server.queue.put(params) From 61ff2b72f332697cc09928289a90693e4fe75546 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 14:39:11 +0200 Subject: [PATCH 27/74] Increase logging --- resources/lib/playstrm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index e195f2926..b47ad95f0 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -120,6 +120,8 @@ def play(self, start_position=None, delayed=True): ''' Create and add listitems to the Kodi playlist. ''' + LOG.debug('play called with start_position %s, delayed %s', + start_position, delayed) if start_position is not None: self.start_index = start_position else: @@ -244,6 +246,7 @@ def _set_intros(self, xml): # The main item we're looking at - skip! continue api = API(intro) + LOG.debug('Adding trailer: %s', api.title()) listitem = widgets.get_listitem(intro, resume=False) self.playqueue_item = PL.playlist_item_from_xml(intro) play = PlayUtils(api, self.playqueue_item) @@ -259,6 +262,7 @@ def _set_additional_parts(self): # The first part that we've already added continue self.api.set_part_number(part) + LOG.debug('Adding addional part %s', part) self.playqueue_item = PL.playlist_item_from_xml(self.xml[0], kodi_id=self.kodi_id, kodi_type=self.kodi_type) From ac285467c48db75d51c5f9d1d94c2742cff698eb Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 14:45:41 +0200 Subject: [PATCH 28/74] Fix main movie being added as trailer --- resources/lib/playstrm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index b47ad95f0..1c34d3b80 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -201,7 +201,7 @@ def _set_playlist(self): return PL.get_playlist_details_from_xml(self.playqueue, xml) # See that we add trailers, if they exist in the xml return - self._set_intros(xml) + self._add_intros(xml) if seektime: listitem = widgets.get_listitem(self.xml[0], resume=True) else: @@ -212,7 +212,7 @@ def _set_playlist(self): listitem.setPath(url) self.playlist_add(url, listitem) if self.xml.get('PartCount'): - self._set_additional_parts() + self._add_additional_parts() def _resume(self): ''' @@ -234,7 +234,7 @@ def _resume(self): app.PLAYSTATE.autoplay = True return seektime - def _set_intros(self, xml): + def _add_intros(self, xml): ''' if we have any play them when the movie/show is not being resumed. ''' @@ -242,10 +242,10 @@ def _set_intros(self, xml): LOG.debug('No trailers returned from the PMS') return for intro in xml: - if utils.cast(int, xml.get('ratingKey')) == self.plex_id: - # The main item we're looking at - skip! - continue api = API(intro) + if not api.plex_type() == v.PLEX_TYPE_CLIP: + # E.g. the main item we're looking at - skip! + continue LOG.debug('Adding trailer: %s', api.title()) listitem = widgets.get_listitem(intro, resume=False) self.playqueue_item = PL.playlist_item_from_xml(intro) @@ -254,7 +254,7 @@ def _set_intros(self, xml): listitem.setPath(url) self.playlist_add(url, listitem) - def _set_additional_parts(self): + def _add_additional_parts(self): ''' Create listitems and add them to the stack of playlist. ''' for part, _ in enumerate(self.xml[0][0]): From 885e8dd58116858277d5eceb90c9ce01ecef8adc Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 15:08:05 +0200 Subject: [PATCH 29/74] Fix PKC wanting to initiate playback when it should not --- resources/lib/playstrm.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 1c34d3b80..97938b8dc 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -202,17 +202,19 @@ def _set_playlist(self): PL.get_playlist_details_from_xml(self.playqueue, xml) # See that we add trailers, if they exist in the xml return self._add_intros(xml) + # Add the main item if seektime: listitem = widgets.get_listitem(self.xml[0], resume=True) else: listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) + self.playqueue_item = PL.playlist_item_from_xml(self.xml[0]) play = PlayUtils(self.api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) self.playlist_add(url, listitem) - if self.xml.get('PartCount'): - self._add_additional_parts() + # Add additional file parts, if any exist + self._add_additional_parts() def _resume(self): ''' From d380aa8ac31bc7a07e69ca31b49c9292007bda1c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 15:08:25 +0200 Subject: [PATCH 30/74] Drop filename for url arg, but add kodi_type --- resources/lib/webservice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 702dadc3a..d4919b0bf 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -203,8 +203,8 @@ def strm(self): path += '&transcode=true' if params.get('kodi_id'): path += '&kodi_id=%s' % params['kodi_id'] - if params.get('Name'): - path += '&filename=%s' % params['Name'] + if params.get('kodi_type'): + path += '&kodi_type=%s' % params['kodi_type'] self.wfile.write(bytes(path)) return From 95b37b51f552b1ab44c15cd62f454301e608d02b Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 13 Apr 2019 15:17:15 +0200 Subject: [PATCH 31/74] Fix resume --- resources/lib/kodimonitor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 597c2d1a6..06959476e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -381,7 +381,6 @@ def on_play(self, data): """ # Some init self._already_slept = False - self.playlistid = None self.playerid = None # Get the type of media we're playing try: From c63d9ad4d6891ed77746489f13d6554ef8dfadae Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 14 Apr 2019 16:24:08 +0200 Subject: [PATCH 32/74] Some more switches to webservice, away from plugin playback --- resources/lib/playlists/db.py | 3 ++- resources/lib/plex_api.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/lib/playlists/db.py b/resources/lib/playlists/db.py index 51cde6902..255042af7 100644 --- a/resources/lib/playlists/db.py +++ b/resources/lib/playlists/db.py @@ -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:/plex/kodi/movies/file.strm?plex_id=... """ lines = iter(text.split('\n')) for line in lines: diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index f8152abf1..d70872d80 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -133,12 +133,14 @@ def path(self, force_first_media=True, force_addon=False, # 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 @@ def trailer(self): 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): From 3aa5c87ca01746a40c58ffed093ed4a39c3a5ffd Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 14 Apr 2019 16:46:44 +0200 Subject: [PATCH 33/74] Skip force close connection error messages --- resources/lib/webservice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index d4919b0bf..1fb234419 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -118,6 +118,10 @@ def handle(self): 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): From 2dac26ffc41d696ac2e580dc91d2635135994df1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 14 Apr 2019 17:21:57 +0200 Subject: [PATCH 34/74] Fix force transcoding --- resources/lib/app/playstate.py | 2 -- resources/lib/context_entry.py | 25 ++++++++++++++----------- resources/lib/playstrm.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 1e63bece9..67430e877 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -64,7 +64,5 @@ def __init__(self): self.audioplaylist = None # 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) self.active_players = set() diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 211ca7098..9c6a0236d 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -7,7 +7,7 @@ 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 @@ def _action_menu(self): """ 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 @@ def _delete_item(self): 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): """ diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 97938b8dc..3fd26f83e 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -108,9 +108,12 @@ def _get_xml(self): else: self.xml[0].set('pkc_db_item', None) self.api = API(self.xml[0]) - self.playqueue_item = PL.playlist_item_from_xml(self.xml[0], - kodi_id=self.kodi_id, - kodi_type=self.kodi_type) + + def set_playqueue_item(self, xml, kodi_id, kodi_type): + self.playqueue_item = PL.playlist_item_from_xml(xml, + kodi_id=kodi_id, + kodi_type=kodi_type) + self.playqueue_item.force_transcode = self.transcode def start_playback(self, index=0): LOG.debug('Starting playback at %s', index) @@ -196,7 +199,6 @@ def _set_playlist(self): utils.lang(30128), icon='{error}') app.PLAYSTATE.context_menu_play = False - app.PLAYSTATE.force_transcode = False app.PLAYSTATE.resume_playback = False return PL.get_playlist_details_from_xml(self.playqueue, xml) @@ -208,7 +210,7 @@ def _set_playlist(self): else: listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) - self.playqueue_item = PL.playlist_item_from_xml(self.xml[0]) + self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type) play = PlayUtils(self.api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) @@ -250,7 +252,7 @@ def _add_intros(self, xml): continue LOG.debug('Adding trailer: %s', api.title()) listitem = widgets.get_listitem(intro, resume=False) - self.playqueue_item = PL.playlist_item_from_xml(intro) + self.set_playqueue_item(intro, None, None) play = PlayUtils(api, self.playqueue_item) url = play.getPlayUrl().encode('utf-8') listitem.setPath(url) @@ -265,9 +267,7 @@ def _add_additional_parts(self): continue self.api.set_part_number(part) LOG.debug('Adding addional part %s', part) - self.playqueue_item = PL.playlist_item_from_xml(self.xml[0], - kodi_id=self.kodi_id, - kodi_type=self.kodi_type) + self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type) self.playqueue_item.part = part listitem = widgets.get_listitem(self.xml[0], resume=False) listitem.setSubtitles(self.api.cache_external_subs()) From 130ec674e57354bdb806466b8474e223fada84a8 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 14 Apr 2019 17:54:47 +0200 Subject: [PATCH 35/74] Fix companion playback crashing --- resources/lib/kodimonitor.py | 2 +- resources/lib/plexbmchelper/subscribers.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 06959476e..0f15bd386 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -368,7 +368,7 @@ def _load_playerstate(self, item): status['plex_type'] = item.plex_type status['playmethod'] = item.playmethod status['playcount'] = item.playcount - LOG.debug('Set the player state: %s', status) + LOG.debug('Set player state for player %s: %s', self.playerid, status) def on_play(self, data): """ diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index afa47fe51..2d6c291f8 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -159,18 +159,17 @@ def msg(self, players): 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 @@ def _plex_stream_index(self, playerid, stream_type): 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'] From 4fa1f48b4366677419d83ac033943ec05cfa6025 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 17 Apr 2019 16:32:11 +0200 Subject: [PATCH 36/74] Improve logging --- resources/lib/webservice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 1fb234419..8aa561616 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -72,6 +72,7 @@ def run(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) From 7753903c050b98be1492bfe9a516475f7b8bc1cf Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 19 Apr 2019 17:54:18 +0200 Subject: [PATCH 37/74] Be more resiliant when manipulating Plex playqueues --- resources/lib/playlist_func.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index cf583aaaa..e571e5a17 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -703,8 +703,13 @@ def move_playlist_item(playlist, before_pos, after_pos): 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")) + xml = DU().downloadUrl(url, action_type="PUT") + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not move playlist item') + return + _get_playListVersion_from_xml(playlist, xml) # Move our item's position in our internal playlist playlist.items.insert(after_pos, playlist.items.pop(before_pos)) LOG.debug('Done moving for %s', playlist) From bbd8e180023523f1e7fa666df748274ae02ffe6e Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 19 Apr 2019 17:55:13 +0200 Subject: [PATCH 38/74] Increase timeout --- resources/lib/webservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 8aa561616..2c336f7ea 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -286,7 +286,7 @@ def run(self): try: try: - params = self.server.queue.get(timeout=0.01) + params = self.server.queue.get(timeout=0.1) except Queue.Empty: count = 20 while not utils.window('plex.playlist.ready'): From 8660b12d1520355131c5d6e6454495ef6d7444c9 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 19 Apr 2019 17:55:31 +0200 Subject: [PATCH 39/74] Fix position --- resources/lib/playstrm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 3fd26f83e..deaf956f9 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -134,7 +134,7 @@ def play(self, start_position=None, delayed=True): LOG.info('Initiating play for %s', self) if not delayed: self.start_playback(self.start_index) - return self.start_index + return self.index def play_folder(self, position=None): ''' From 0d8b3b3ba74700332b509ccca50ecdb2a535c05a Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 19 Apr 2019 17:56:04 +0200 Subject: [PATCH 40/74] Remove arg --- resources/lib/playstrm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index deaf956f9..4e08f4710 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -152,7 +152,6 @@ def play_folder(self, position=None): listitem = widgets.get_listitem(self.xml[0], resume=True) url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT args = { - 'mode': 'play', 'plex_id': self.plex_id, 'plex_type': self.api.plex_type() } From 578ced789f364b89c39f7120b2da437d61163310 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 19 Apr 2019 17:56:14 +0200 Subject: [PATCH 41/74] Fix arg --- resources/lib/playstrm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 4e08f4710..adab3150b 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -162,7 +162,7 @@ def play_folder(self, position=None): if self.server_id: args['server_id'] = self.server_id if self.transcode: - args['transcode'] = True + args['transcode'] = 'true' url = utils.extend_url(url, args).encode('utf-8') listitem.setPath(url) self.playlist_add(url, listitem) From 1218cde0a2f41f991e1a8c571c8e8195d23bc9cf Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Apr 2019 18:03:20 +0200 Subject: [PATCH 42/74] Big update --- resources/lib/kodimonitor.py | 51 ++- resources/lib/playback.py | 2 +- resources/lib/playlist_func.py | 604 +++++++++++++++++++++++------ resources/lib/playqueue.py | 7 +- resources/lib/playstrm.py | 252 ++---------- resources/lib/playutils.py | 6 +- resources/lib/plex_db/playlists.py | 2 +- resources/lib/plex_functions.py | 6 +- resources/lib/utils.py | 10 + resources/lib/webservice.py | 103 +++-- 10 files changed, 637 insertions(+), 406 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 0f15bd386..d6c436e74 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -219,16 +219,20 @@ def _playlist_onclear(self, data): { u'playlistid': 1, } + Let's NOT use this as Kodi's responses when e.g. playing an entire + folder are NOT threadsafe: Playlist.OnAdd might be added first, then + Playlist.OnClear might be received LATER """ if self.playlistid == data['playlistid']: LOG.debug('Resetting autoplay') app.PLAYSTATE.autoplay = False - playqueue = PQ.PLAYQUEUES[data['playlistid']] - if not playqueue.is_pkc_clear(): - playqueue.pkc_edit = True - playqueue.clear(kodi=False) - else: - LOG.debug('Detected PKC clear - ignoring') + return + # playqueue = PQ.PLAYQUEUES[data['playlistid']] + # if not playqueue.is_pkc_clear(): + # playqueue.pkc_edit = True + # playqueue.clear(kodi=False) + # else: + # LOG.debug('Detected PKC clear - ignoring') @staticmethod def _get_ids(kodi_id, kodi_type, path): @@ -311,7 +315,7 @@ def _get_playerid(self, data): def _check_playing_item(self, data): """ - Returns a PF.Playlist_Item() for the currently playing item + Returns a PF.PlaylistItem() for the currently playing item Raises MonitorError or IndexError if we need to init the PKC playqueue """ info = js.get_player_props(self.playerid) @@ -320,26 +324,13 @@ def _check_playing_item(self, data): kodi_playlist = js.playlist_get_items(self.playerid) LOG.debug('Current Kodi playlist: %s', kodi_playlist) kodi_item = PL.playlist_item_from_kodi(kodi_playlist[position]) - if (position == 1 and - len(kodi_playlist) == len(self.playqueue.items) + 1 and - kodi_playlist[0].get('type') == 'unknown' and - kodi_playlist[0].get('file') and - kodi_playlist[0].get('file').startswith('http://127.0.0.1')): - if kodi_item == self.playqueue.items[0]: - # Delete the very first item that we used to start playback: - # { - # u'title': u'', - # u'type': u'unknown', - # u'file': u'http://127.0.0.1:57578/plex/kodi/....', - # u'label': u'' - # } - LOG.debug('Deleting the very first playqueue item') - js.playlist_remove(self.playqueue.playlistid, 0) - position = 0 - else: - LOG.debug('Different item in PKC playlist: %s vs. %s', - self.playqueue.items[0], kodi_item) - raise MonitorError() + if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy): + # Get rid of the very first element in the queue that Kodi marked + # as unplayed (the one to init the queue) + LOG.debug('Deleting the very first playqueue item') + js.playlist_remove(self.playqueue.playlistid, 0) + del self.playqueue.items[0] + position = 0 elif kodi_item != self.playqueue.items[position]: LOG.debug('Different playqueue items: %s vs. %s ', kodi_item, self.playqueue.items[position]) @@ -349,7 +340,7 @@ def _check_playing_item(self, data): def _load_playerstate(self, item): """ - Pass in a PF.Playlist_Item(). Will then set the currently playing + Pass in a PF.PlaylistItem(). Will then set the currently playing state with app.PLAYSTATE.player_states[self.playerid] """ if self.playqueue.id: @@ -431,6 +422,7 @@ def _playback_cleanup(ended=False): # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) app.CONN.plex_transient_token = None + LOG.debug('Playstate is: %s', app.PLAYSTATE.player_states) for playerid in app.PLAYSTATE.active_players: status = app.PLAYSTATE.player_states[playerid] # Remember the last played item later @@ -498,6 +490,9 @@ def _record_playstate(status, ended): playcount += 1 time = 0 with kodi_db.KodiVideoDB() as kodidb: + LOG.error('Setting file_id %s, time %s, totaltime %s, playcount %s, ' + 'last_played %s', + db_item['kodi_fileid'], time, totaltime, playcount, last_played) kodidb.set_resume(db_item['kodi_fileid'], time, totaltime, diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 0873b9c6f..82b42646b 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -464,7 +464,7 @@ def process_indirect(key, offset, resolve=True): playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() - item = PL.Playlist_Item() + item = PL.PlaylistItem() item.xml = xml[0] item.offset = offset item.plex_type = v.PLEX_TYPE_CLIP diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index e571e5a17..c0ad1f191 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -6,15 +6,16 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger +import xbmc + from .plex_api import API from .plex_db import PlexDB from . import plex_functions as PF -from .kodi_db import kodiid_from_filename +from .playutils import PlayUtils +from .kodi_db import kodiid_from_filename, KodiVideoDB from .downloadutils import DownloadUtils as DU -from . import utils -from . import json_rpc as js -from . import variables as v -from . import app +from . import utils, json_rpc as js, variables as v, app, widgets +from .windows.resume import resume_dialog ############################################################################### @@ -30,14 +31,14 @@ class PlaylistError(Exception): pass -class Playqueue_Object(object): +class PlayQueue(object): """ PKC object to represent PMS playQueues and Kodi playlist for queueing playlistid = None [int] Kodi playlist id (0, 1, 2) type = None [str] Kodi type: 'audio', 'video', 'picture' kodi_pl = None Kodi xbmc.PlayList object - items = [] [list] of Playlist_Items + items = [] [list] of PlaylistItem id = None [str] Plex playQueueID, unique Plex identifier version = None [int] Plex version of the playQueue selectedItemID = None @@ -74,8 +75,11 @@ def __init__(self): # To keep track if Kodi playback was initiated from a Kodi playlist # There are a couple of pitfalls, unfortunately... self.kodi_playlist_playback = False + # Playlist position/index used when initiating the playqueue + self.index = None + self.force_transcode = None - def __repr__(self): + def __unicode__(self): return ("{{" "'playlistid': {self.playlistid}, " "'id': {self.id}, " @@ -89,10 +93,14 @@ def __repr__(self): "'kodi_playlist_playback': {self.kodi_playlist_playback}, " "'pkc_edit': {self.pkc_edit}, " "}}").format(**{ - 'items': [x.plex_id for x in self.items or []], + 'items': ['%s/%s: %s' % (x.plex_id, x.id, x.name) + for x in self.items], 'self': self - }).encode('utf-8') - __str__ = __repr__ + }) + + def __str__(self): + return unicode(self).encode('utf-8') + __repr__ = __str__ def is_pkc_clear(self): """ @@ -127,10 +135,319 @@ def clear(self, kodi=True): self.plex_transient_token = None self.old_kodi_pl = [] self.kodi_playlist_playback = False + self.index = None + self.force_transcode = None LOG.debug('Playlist cleared: %s', self) + def init(self, plex_id, plex_type=None, position=None, synched=True, + force_transcode=None): + """ + Initializes the playQueue with e.g. trailers and additional file parts + Pass synched=False if you're sure that this item has not been synched + to Kodi + """ + LOG.error('Current Kodi playlist: %s', + js.playlist_get_items(self.playlistid)) + if position is not None: + self.index = position + else: + # Do NOT use kodi_pl.getposition() as that appears to be buggy + self.index = max(js.get_position(self.playlistid), 0) + LOG.debug('Initializing with plex_id %s, plex_type %s, position %s, ' + 'synched %s, force_transcode %s, index %s', plex_id, + plex_type, position, synched, force_transcode, self.index) + LOG.error('Actual start: %s', js.get_position(self.playlistid)) + if self.kodi_pl.size() != len(self.items): + # The original item that Kodi put into the playlist, e.g. + # { + # u'title': u'', + # u'type': u'unknown', + # u'file': u'http://127.0.0.1:57578/plex/kodi/....', + # u'label': u'' + # } + # We CANNOT delete that item right now - so let's add a dummy + # on the PKC side + LOG.debug('Detected Kodi playlist size %s to be off for PKC: %s', + self.kodi_pl.size(), len(self.items)) + while len(self.items) < self.kodi_pl.size(): + LOG.debug('Adding a dummy item to our playqueue') + playlistitem = PlaylistItemDummy() + self.items.insert(0, playlistitem) + self.force_transcode = force_transcode + if synched: + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_id(plex_id, plex_type) + else: + db_item = None + if db_item: + xml = None + section_uuid = db_item['section_uuid'] + plex_type = db_item['plex_type'] + else: + xml = PF.GetPlexMetadata(plex_id) + if xml in (None, 401): + raise PlaylistError('Could not get Plex metadata %s', plex_id) + section_uuid = xml.get('librarySectionUUID') + api = API(xml[0]) + plex_type = api.plex_type() + resume = self._resume_playback(db_item, xml) + trailers = False + if (not resume and plex_type == v.PLEX_TYPE_MOVIE and + utils.settings('enableCinema') == 'true'): + if utils.settings('askCinema') == "true": + # "Play trailers?" + trailers = utils.yesno_dialog(utils.lang(29999), + utils.lang(33016)) or False + else: + trailers = True + LOG.debug('Playing trailers: %s', trailers) + xml = PF.init_plex_playqueue(plex_id, + section_uuid, + plex_type=plex_type, + trailers=trailers) + if xml is None: + LOG.error('Could not get playqueue for plex_id %s UUID %s for %s', + plex_id, section_uuid, self) + raise PlaylistError('Could not get playqueue') + # See that we add trailers, if they exist in the xml return + self._add_intros(xml) + # Add the main item after the trailers + # Look at the LAST item + api = API(xml[-1]) + self._kodi_add_xml(xml[-1], api, resume) + # Add additional file parts, if any exist + self._add_additional_parts(xml) + self.update_details_from_xml(xml) + + @staticmethod + def _resume_playback(db_item=None, xml=None): + ''' + Pass in either db_item or xml + Resume item if available. Returns bool or raise an PlayStrmException if + resume was cancelled by user. + ''' + resume = app.PLAYSTATE.resume_playback + app.PLAYSTATE.resume_playback = None + if app.PLAYSTATE.autoplay: + resume = False + LOG.info('Skip resume for autoplay') + elif resume is None: + if db_item: + with KodiVideoDB(lock=False) as kodidb: + resume = kodidb.get_resume(db_item['kodi_fileid']) + else: + api = API(xml) + resume = api.resume_point() + if resume: + resume = resume_dialog(resume) + LOG.info('User chose resume: %s', resume) + if resume is None: + raise PlaylistError('User backed out of resume dialog') + app.PLAYSTATE.autoplay = True + return resume + + def _add_intros(self, xml): + ''' + if we have any play them when the movie/show is not being resumed. + ''' + if not len(xml) > 1: + LOG.debug('No trailers returned from the PMS') + return + for i, intro in enumerate(xml): + if i + 1 == len(xml): + # The main item we're looking at - skip! + break + api = API(intro) + LOG.debug('Adding trailer: %s', api.title()) + self._kodi_add_xml(intro, api) + + def _add_additional_parts(self, xml): + ''' Create listitems and add them to the stack of playlist. + ''' + api = API(xml[0]) + for part, _ in enumerate(xml[0][0]): + if part == 0: + # The first part that we've already added + continue + api.set_part_number(part) + LOG.debug('Adding addional part for %s: %s', api.title(), part) + self._kodi_add_xml(xml[0], api) + + def _kodi_add_xml(self, xml, api, resume=False): + playlistitem = PlaylistItem(xml_video_element=xml) + playlistitem.part = api.part + playlistitem.force_transcode = self.force_transcode + listitem = widgets.get_listitem(xml, resume=True) + listitem.setSubtitles(api.cache_external_subs()) + play = PlayUtils(api, playlistitem) + url = play.getPlayUrl() + listitem.setPath(url.encode('utf-8')) + self.kodi_add_item(playlistitem, self.index, listitem) + self.items.insert(self.index, playlistitem) + self.index += 1 + + def update_details_from_xml(self, xml): + """ + Updates the playlist details from the xml provided + """ + self.id = utils.cast(int, xml.get('%sID' % self.kind)) + self.version = utils.cast(int, xml.get('%sVersion' % self.kind)) + self.shuffled = utils.cast(int, xml.get('%sShuffled' % self.kind)) + self.selectedItemID = utils.cast(int, + xml.get('%sSelectedItemID' % self.kind)) + self.selectedItemOffset = utils.cast(int, + xml.get('%sSelectedItemOffset' + % self.kind)) + LOG.debug('Updated playlist from xml: %s', self) + + def add_item(self, item, pos, listitem=None): + """ + Adds a PlaylistItem to both Kodi and Plex at position pos [int] + Also changes self.items + Raises PlaylistError + """ + self.kodi_add_item(item, pos, listitem) + self.plex_add_item(item, pos) + + def kodi_add_item(self, item, pos, listitem=None): + """ + Adds a PlaylistItem to Kodi only. Will not change self.items + Raises PlaylistError + """ + if not isinstance(item, PlaylistItem): + raise PlaylistError('Wrong item %s of type %s received' + % (item, type(item))) + if pos > len(self.items): + raise PlaylistError('Position %s too large for playlist length %s' + % (pos, len(self.items))) + LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item) + if item.kodi_id is not None and item.kodi_type is not None: + # This method ensures we have full Kodi metadata, potentially + # with more artwork, for example, than Plex provides + if pos == len(self.items): + answ = js.playlist_add(self.playlistid, + {'%sid' % item.kodi_type: item.kodi_id}) + else: + answ = js.playlist_insert({'playlistid': self.playlistid, + 'position': pos, + 'item': {'%sid' % item.kodi_type: item.kodi_id}}) + if 'error' in answ: + raise PlaylistError('Kodi did not add item to playlist: %s', + answ) + else: + if not listitem: + if item.xml is None: + LOG.debug('Need to get metadata for item %s', item) + item.xml = PF.GetPlexMetadata(item.plex_id) + if item.xml in (None, 401): + raise PlaylistError('Could not get metadata for %s', item) + listitem = widgets.get_listitem(item.xml, resume=True) + url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT + args = { + 'plex_id': self.plex_id, + 'plex_type': self.plex_type + } + if item.force_transcode: + args['transcode'] = 'true' + url = utils.extend_url(url, args) + item.file = url + listitem.setPath(url.encode('utf-8')) + self.kodi_pl.add(url=listitem.getPath(), + listitem=listitem, + index=pos) + + def plex_add_item(self, item, pos): + """ + Adds a new PlaylistItem to the playlist at position pos [int] only on + the Plex side of things. Also changes self.items + Raises PlaylistError + """ + if not isinstance(item, PlaylistItem) or not item.uri: + raise PlaylistError('Wrong item %s of type %s received' + % (item, type(item))) + if pos > len(self.items): + raise PlaylistError('Position %s too large for playlist length %s' + % (pos, len(self.items))) + LOG.debug('Adding item to Plex playlist at position %s: %s', pos, item) + url = '{server}/%ss/%s?uri=%s' % (self.kind, self.id, item.uri) + # Will usually put the new item at the end of the Plex playlist + xml = DU().downloadUrl(url, action_type='PUT') + try: + xml[0].attrib + except (TypeError, AttributeError, KeyError, IndexError): + raise PlaylistError('Could not add item %s to playlist %s' + % (item, self)) + if len(xml) != len(self.items) + 1: + raise PlaylistError('Could not add item %s to playlist %s - wrong' + ' length received' % (item, self)) + for actual_pos, xml_video_element in enumerate(xml): + api = API(xml_video_element) + if api.plex_id() == item.plex_id: + break + else: + raise PlaylistError('Something went wrong - Plex id not found') + item.from_xml(xml[actual_pos]) + self.items.insert(actual_pos, item) + self.update_details_from_xml(xml) + if actual_pos != pos: + self.plex_move_item(actual_pos, pos) + LOG.debug('Added item %s on Plex side: %s', item, self) + + def kodi_remove_item(self, pos): + """ + Only manipulates the Kodi playlist. Won't change self.items + """ + LOG.debug('Removing position %s on the Kodi side for %s', pos, self) + answ = js.playlist_remove(self.playlistid, pos) + if 'error' in answ: + raise PlaylistError('Could not remove item: %s' % answ) + + def plex_move_item(self, before, after): + """ + Moves playlist item from before [int] to after [int] for Plex only. -class Playlist_Item(object): + Will also change self.items + """ + if before > len(self.items): + raise PlaylistError('Original position %s larger than current ' + 'playlist length %s', + before, len(self.items)) + elif after > len(self.items): + raise PlaylistError('Desired position %s larger than current ' + 'playlist length %s', + after, len(self.items)) + elif after == before: + raise PlaylistError('Desired position and original position are ' + 'identical: %s', after) + LOG.debug('Moving item from %s to %s on the Plex side for %s', + before, after, self) + if after == 0: + url = "{server}/%ss/%s/items/%s/move?after=0" % \ + (self.kind, + self.id, + self.items[before].id) + else: + url = "{server}/%ss/%s/items/%s/move?after=%s" % \ + (self.kind, + self.id, + self.items[before].id, + self.items[after - 1].id) + xml = DU().downloadUrl(url, action_type="PUT") + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + raise PlaylistError('Could not move playlist item from %s to %s ' + 'for %s' % (before, after, self)) + self.update_details_from_xml(xml) + self.items.insert(after, self.items.pop(before)) + LOG.debug('Done moving items for %s', self) + + def start_playback(self, pos=0): + LOG.info('Starting playback at %s for %s', pos, self) + xbmc.Player().play(self.kodi_pl, startpos=pos, windowed=False) + + +class PlaylistItem(object): """ Object to fill our playqueues and playlists with. @@ -150,110 +467,75 @@ class Playlist_Item(object): part = 0 [int] part number if Plex video consists of mult. parts force_transcode [bool] defaults to False - Playlist_items compare as equal, if they + PlaylistItem compare as equal, if they - have the same plex_id - OR: have the same kodi_id AND kodi_type - OR: have the same file """ - def __init__(self): - self._id = None - self._plex_id = None - self.plex_type = None + def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, + kodi_id=None, kodi_type=None, grab_xml=False, + lookup_kodi=True): + """ + Pass grab_xml=True in order to get Plex metadata from the PMS while + passing a plex_id. + Pass lookup_kodi=False to NOT check the plex.db for kodi_id and + kodi_type if they're missing (won't be done for clips anyway) + """ + self.name = None + self.id = None + self.plex_id = plex_id + self.plex_type = plex_type self.plex_uuid = None - self._kodi_id = None - self.kodi_type = None + self.kodi_id = kodi_id + self.kodi_type = kodi_type self.file = None self.uri = None self.guid = None self.xml = None self.playmethod = None - self._playcount = None - self._offset = None - # If Plex video consists of several parts; part number - self._part = 0 + self.playcount = None + self.offset = None + self.part = 0 self.force_transcode = False - - def __eq__(self, item): - if self.plex_id is not None and item.plex_id is not None: - return self.plex_id == item.plex_id - elif (self.kodi_id is not None and item.kodi_id is not None and - self.kodi_type and item.kodi_type): - return (self.kodi_id == item.kodi_id and - self.kodi_type == item.kodi_type) - elif self.file and item.file: - return self.file == item.file - raise RuntimeError('playlist items not fully defined: %s, %s' % - (self, item)) - - def __ne__(self, item): - return not self == item - - @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 = ("{{" + if grab_xml and plex_id is not None and xml_video_element is None: + xml_video_element = PF.GetPlexMetadata(plex_id) + try: + xml_video_element = xml_video_element[0] + except (TypeError, IndexError): + xml_video_element = None + if xml_video_element is not None: + self.from_xml(xml_video_element) + if (lookup_kodi and (kodi_id is None or kodi_type is None) and + self.plex_type != v.PLEX_TYPE_CLIP): + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_id(plex_id, plex_type) + if db_item is not None: + self.kodi_id = db_item['kodi_id'] + self.kodi_type = db_item['kodi_type'] + self.plex_uuid = db_item['section_uuid'] + self.set_uri() + + def __eq__(self, 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}', " - "'plex_uuid': '{self.plex_uuid}', " "'kodi_id': {self.kodi_id}, " "'kodi_type': '{self.kodi_type}', " "'file': '{self.file}', " @@ -263,13 +545,71 @@ def __repr__(self): "'playcount': {self.playcount}, " "'offset': {self.offset}, " "'force_transcode': {self.force_transcode}, " - "'part': {self.part}, ".format(self=self)) - answ = answ.encode('utf-8') - # etree xml.__repr__() could return string, not unicode - return answ + b"'xml': \"{self.xml}\"}}".format(self=self) + "'part': {self.part}" + "}}".format(self=self)) def __str__(self): - return self.__repr__() + return unicode(self).encode('utf-8') + __repr__ = __str__ + + def from_xml(self, xml_video_element): + """ + xml_video_element: etree xml piece 1 level underneath + item.id will only be set if you passed in an xml_video_element from + e.g. a playQueue + """ + api = API(xml_video_element) + self.name = api.title() + self.plex_id = api.plex_id() + self.plex_type = api.plex_type() + self.id = api.item_id() + self.guid = api.guid_html_escaped() + self.playcount = api.viewcount() + self.offset = api.resume_point() + self.xml = xml_video_element + + def from_kodi(self, playlist_item): + """ + playlist_item: dict contains keys 'id', 'type', 'file' (if applicable) + + Will thus set the attributes kodi_id, kodi_type, file, if applicable + If kodi_id & kodi_type are provided, plex_id and plex_type will be + looked up (if not already set) + """ + self.kodi_id = playlist_item.get('id') + self.kodi_type = playlist_item.get('type') + self.file = playlist_item.get('file') + if self.plex_id is None and self.kodi_id is not None and self.kodi_type: + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) + if db_item: + self.plex_id = db_item['plex_id'] + self.plex_type = db_item['plex_type'] + self.plex_uuid = db_item['section_uuid'] + if self.plex_id is None and self.file is not None: + try: + query = self.file.split('?', 1)[1] + except IndexError: + query = '' + query = dict(utils.parse_qsl(query)) + self.plex_id = utils.cast(int, query.get('plex_id')) + self.plex_type = query.get('itemType') + self.set_uri() + LOG.debug('Made playlist item from Kodi: %s', self) + + def set_uri(self): + if self.plex_id is None and self.file is not None: + self.uri = ('library://whatever/item/%s' + % utils.quote(self.file, safe='')) + elif self.plex_id is not None and self.plex_uuid is not None: + # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER + self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % + (self.plex_uuid, self.plex_id)) + elif self.plex_id is not None: + self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % + (self.plex_id, self.plex_id)) + else: + self.uri = None def plex_stream_index(self, kodi_stream_index, stream_type): """ @@ -327,6 +667,17 @@ def kodi_stream_index(self, plex_stream_index, stream_type): count += 1 +class PlaylistItemDummy(PlaylistItem): + """ + Let e.g. Kodimonitor detect that this is a dummy item + """ + def __init__(self, *args, **kwargs): + super(PlaylistItemDummy, self).__init__(*args, **kwargs) + self.name = 'dummy item' + self.id = 0 + self.plex_id = 0 + + def playlist_item_from_kodi(kodi_item): """ Turns the JSON answer from Kodi into a playlist element @@ -334,7 +685,7 @@ def playlist_item_from_kodi(kodi_item): Supply with data['item'] as returned from Kodi JSON-RPC interface. kodi_item dict contains keys 'id', 'type', 'file' (if applicable) """ - item = Playlist_Item() + item = PlaylistItem() item.kodi_id = kodi_item.get('id') item.kodi_type = kodi_item.get('type') if item.kodi_id: @@ -343,7 +694,7 @@ def playlist_item_from_kodi(kodi_item): if db_item: item.plex_id = db_item['plex_id'] item.plex_type = db_item['plex_type'] - item.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-) + item.plex_uuid = db_item['section_uuid'] item.file = kodi_item.get('file') if item.plex_id is None and item.file is not None: try: @@ -413,7 +764,7 @@ def playlist_item_from_plex(plex_id): Returns a Playlist_Item """ - item = Playlist_Item() + item = PlaylistItem() item.plex_id = plex_id with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_id(plex_id) @@ -436,7 +787,7 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None): xml_video_element: etree xml piece 1 level underneath """ - item = Playlist_Item() + item = PlaylistItem() api = API(xml_video_element) item.plex_id = api.plex_id() item.plex_type = api.plex_type() @@ -612,12 +963,14 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None): Returns the PKC PlayList item or raises PlaylistError """ + LOG.debug('Adding item to Plex playqueue with plex id %s, kodi_item %s at ' + 'position %s', plex_id, kodi_item, pos) verify_kodi_item(plex_id, kodi_item) if plex_id: item = playlist_item_from_plex(plex_id) else: item = playlist_item_from_kodi(kodi_item) - url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri) + url = '{server}/%ss/%s?uri=%s' % (playlist.kind, playlist.id, item.uri) # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: @@ -625,21 +978,27 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None): except (TypeError, AttributeError, KeyError, IndexError): raise PlaylistError('Could not add item %s to playlist %s' % (kodi_item, playlist)) - api = API(xml[-1]) - item.xml = xml[-1] + if len(xml) != len(playlist.items) + 1: + raise PlaylistError('Couldnt add item %s to playlist %s - wrong length' + % (kodi_item, playlist)) + for actual_pos, xml_video_element in enumerate(xml): + api = API(xml_video_element) + if api.plex_id() == item.plex_id: + break + else: + raise PlaylistError('Something went terribly wrong!') + utils.dump_xml(xml) + LOG.debug('Plex added the new item at position %s', actual_pos) + item.xml = xml[actual_pos] item.id = api.item_id() item.guid = api.guid_html_escaped() item.offset = api.resume_point() item.playcount = api.viewcount() - playlist.items.append(item) - if pos == len(playlist.items) - 1: - # Item was added at the end - _get_playListVersion_from_xml(playlist, xml) - else: + playlist.items.insert(actual_pos, item) + _get_playListVersion_from_xml(playlist, xml) + if actual_pos != pos: # Move the new item to the correct position - move_playlist_item(playlist, - len(playlist.items) - 1, - pos) + move_playlist_item(playlist, actual_pos, pos) LOG.debug('Successfully added item on the Plex side: %s', playlist) return item @@ -710,6 +1069,7 @@ def move_playlist_item(playlist, before_pos, after_pos): LOG.error('Could not move playlist item') return _get_playListVersion_from_xml(playlist, xml) + utils.dump_xml(xml) # Move our item's position in our internal playlist playlist.items.insert(after_pos, playlist.items.pop(before_pos)) LOG.debug('Done moving for %s', playlist) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 929ccc75d..dde6921b2 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -18,7 +18,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID -# Our PKC playqueues (3 instances of Playqueue_Object()) +# Our PKC playqueues (3 instances PlayQueue()) PLAYQUEUES = [] ############################################################################### @@ -38,7 +38,7 @@ def init_playqueues(): for queue in js.get_playlists(): if queue['playlistid'] != i: continue - playqueue = PL.Playqueue_Object() + playqueue = PL.PlayQueue() playqueue.playlistid = i playqueue.type = queue['type'] # Initialize each Kodi playlist @@ -206,6 +206,8 @@ def _run(self): with app.APP.lock_playqueues: for playqueue in PLAYQUEUES: kodi_pl = js.playlist_get_items(playqueue.playlistid) + playqueue.old_kodi_pl = list(kodi_pl) + continue if playqueue.old_kodi_pl != kodi_pl: if playqueue.id is None and (not app.SYNC.direct_paths or app.PLAYSTATE.context_menu_play): @@ -215,5 +217,4 @@ def _run(self): 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) diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index adab3150b..55b7f338c 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -2,13 +2,8 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger -import xbmc - -from .plex_api import API -from .playutils import PlayUtils -from .windows.resume import resume_dialog -from . import app, plex_functions as PF, utils, json_rpc, variables as v, \ - widgets, playlist_func as PL, playqueue as PQ +from . import app, utils, json_rpc, variables as v, playlist_func as PL, \ + playqueue as PQ LOG = getLogger('PLEX.playstrm') @@ -27,15 +22,8 @@ class PlayStrm(object): webserivce returns a dummy file to play. Meanwhile, PlayStrm adds the real listitems for items to play to the playlist. ''' - def __init__(self, params, server_id=None): - LOG.debug('Starting PlayStrm with server_id %s, params: %s', - server_id, params) - self.xml = None - self.playqueue_item = None - self.api = None - self.start_index = None - self.index = None - self.server_id = server_id + def __init__(self, params): + LOG.debug('Starting PlayStrm with params: %s', params) self.plex_id = utils.cast(int, params['plex_id']) self.plex_type = params.get('plex_type') if params.get('synched') and params['synched'].lower() == 'false': @@ -44,97 +32,46 @@ def __init__(self, params, server_id=None): self.synched = True self.kodi_id = utils.cast(int, params.get('kodi_id')) self.kodi_type = params.get('kodi_type') - self._get_xml() - self.name = self.api.title() - if ((self.kodi_id is None or self.kodi_type is None) and - self.xml[0].get('pkc_db_item')): - self.kodi_id = self.xml[0].get('pkc_db_item')['kodi_id'] - self.kodi_type = self.xml[0].get('pkc_db_item')['kodi_type'] - self.transcode = params.get('transcode') - if self.transcode is None: - self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None + self.force_transcode = params.get('transcode') == 'true' if app.PLAYSTATE.audioplaylist: LOG.debug('Audio playlist detected') self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) else: LOG.debug('Video playlist detected') self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO) - self.kodi_playlist = self.playqueue.kodi_pl - def __repr__(self): + def __unicode__(self): return ("{{" - "'name': '{self.name}', " "'plex_id': {self.plex_id}, " "'plex_type': '{self.plex_type}', " "'kodi_id': {self.kodi_id}, " "'kodi_type': '{self.kodi_type}', " - "'server_id': '{self.server_id}', " - "'transcode': {self.transcode}, " - "'start_index': {self.start_index}, " - "'index': {self.index}" - "}}").format(self=self).encode('utf-8') - __str__ = __repr__ - - def playlist_add_json(self): - playlistid = self.kodi_playlist.getPlayListId() - LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s', - self.kodi_id, self.kodi_type, playlistid, self.index) - if self.index is None: - json_rpc.playlist_add(playlistid, - {'%sid' % self.kodi_type: self.kodi_id}) - else: - json_rpc.playlist_insert({'playlistid': playlistid, - 'position': self.index, - 'item': {'%sid' % self.kodi_type: self.kodi_id}}) + "}}").format(self=self) - def playlist_add(self, url, listitem): - self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) - self.playqueue_item.file = url.decode('utf-8') - self.playqueue.items.insert(self.index, self.playqueue_item) - self.index += 1 - - def remove_from_playlist(self, index): - LOG.debug('Removing playlist item number %s from %s', index, self) - json_rpc.playlist_remove(self.kodi_playlist.getPlayListId(), - index) - - def _get_xml(self): - self.xml = PF.GetPlexMetadata(self.plex_id) - if self.xml in (None, 401): - raise PlayStrmException('No xml received from the PMS') - if self.synched: - # Adds a new key 'pkc_db_item' to self.xml[0].attrib - widgets.attach_kodi_ids(self.xml) - else: - self.xml[0].set('pkc_db_item', None) - self.api = API(self.xml[0]) - - def set_playqueue_item(self, xml, kodi_id, kodi_type): - self.playqueue_item = PL.playlist_item_from_xml(xml, - kodi_id=kodi_id, - kodi_type=kodi_type) - self.playqueue_item.force_transcode = self.transcode - - def start_playback(self, index=0): - LOG.debug('Starting playback at %s', index) - xbmc.Player().play(self.kodi_playlist, startpos=index, windowed=False) + def __str__(self): + return unicode(self).encode('utf-8') + __repr__ = __str__ def play(self, start_position=None, delayed=True): ''' - Create and add listitems to the Kodi playlist. + Create and add a single listitem to the Kodi playlist, potentially + with trailers and different file-parts ''' LOG.debug('play called with start_position %s, delayed %s', start_position, delayed) - if start_position is not None: - self.start_index = start_position - else: - self.start_index = max(self.kodi_playlist.getposition(), 0) - self.index = self.start_index - self._set_playlist() + LOG.debug('Kodi playlist BEFORE: %s', + json_rpc.playlist_get_items(self.playqueue.playlistid)) + self.playqueue.init(self.plex_id, + plex_type=self.plex_type, + position=start_position, + synched=self.synched, + force_transcode=self.force_transcode) LOG.info('Initiating play for %s', self) + LOG.debug('Kodi playlist AFTER: %s', + json_rpc.playlist_get_items(self.playqueue.playlistid)) if not delayed: - self.start_playback(self.start_index) - return self.index + self.playqueue.start_playback(start_position) + return self.playqueue.index def play_folder(self, position=None): ''' @@ -142,136 +79,13 @@ def play_folder(self, position=None): provided, add as Kodi would, otherwise queue playlist items using strm links to setup playback later. ''' - self.start_index = position or max(self.kodi_playlist.size(), 0) - self.index = self.start_index + 1 - LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index) - if self.kodi_id and self.kodi_type: - self.playlist_add_json() - self.index += 1 - else: - listitem = widgets.get_listitem(self.xml[0], resume=True) - url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT - args = { - 'plex_id': self.plex_id, - 'plex_type': self.api.plex_type() - } - if self.kodi_id: - args['kodi_id'] = self.kodi_id - if self.kodi_type: - args['kodi_type'] = self.kodi_type - if self.server_id: - args['server_id'] = self.server_id - if self.transcode: - args['transcode'] = 'true' - url = utils.extend_url(url, args).encode('utf-8') - listitem.setPath(url) - self.playlist_add(url, listitem) - return self.index - 1 - - def _set_playlist(self): - ''' - Verify seektime, set intros, set main item and set additional parts. - Detect the seektime for video type content. Verify the default video - action set in Kodi for accurate resume behavior. - ''' - seektime = self._resume() - trailers = False - if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and - utils.settings('enableCinema') == 'true'): - if utils.settings('askCinema') == "true": - # "Play trailers?" - trailers = utils.yesno_dialog(utils.lang(29999), - utils.lang(33016)) or False - else: - trailers = True - LOG.debug('Playing trailers: %s', trailers) - xml = PF.init_plex_playqueue(self.plex_id, - self.xml.get('librarySectionUUID'), - mediatype=self.plex_type, - trailers=trailers) - if xml is None: - LOG.error('Could not get playqueue for UUID %s for %s', - self.xml.get('librarySectionUUID'), self) - # "Play error" - utils.dialog('notification', - utils.lang(29999), - utils.lang(30128), - icon='{error}') - app.PLAYSTATE.context_menu_play = False - app.PLAYSTATE.resume_playback = False - return - PL.get_playlist_details_from_xml(self.playqueue, xml) - # See that we add trailers, if they exist in the xml return - self._add_intros(xml) - # Add the main item - if seektime: - listitem = widgets.get_listitem(self.xml[0], resume=True) - else: - listitem = widgets.get_listitem(self.xml[0], resume=False) - listitem.setSubtitles(self.api.cache_external_subs()) - self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type) - play = PlayUtils(self.api, self.playqueue_item) - url = play.getPlayUrl().encode('utf-8') - listitem.setPath(url) - self.playlist_add(url, listitem) - # Add additional file parts, if any exist - self._add_additional_parts() - - def _resume(self): - ''' - Resume item if available. Returns bool or raise an PlayStrmException if - resume was cancelled by user. - ''' - seektime = app.PLAYSTATE.resume_playback - app.PLAYSTATE.resume_playback = None - if app.PLAYSTATE.autoplay: - seektime = False - LOG.info('Skip resume for autoplay') - elif seektime is None: - resume = self.api.resume_point() - if resume: - seektime = resume_dialog(resume) - LOG.info('User chose resume: %s', seektime) - if seektime is None: - raise PlayStrmException('User backed out of resume dialog.') - app.PLAYSTATE.autoplay = True - return seektime - - def _add_intros(self, xml): - ''' - if we have any play them when the movie/show is not being resumed. - ''' - if not len(xml) > 1: - LOG.debug('No trailers returned from the PMS') - return - for intro in xml: - api = API(intro) - if not api.plex_type() == v.PLEX_TYPE_CLIP: - # E.g. the main item we're looking at - skip! - continue - LOG.debug('Adding trailer: %s', api.title()) - listitem = widgets.get_listitem(intro, resume=False) - self.set_playqueue_item(intro, None, None) - play = PlayUtils(api, self.playqueue_item) - url = play.getPlayUrl().encode('utf-8') - listitem.setPath(url) - self.playlist_add(url, listitem) - - def _add_additional_parts(self): - ''' Create listitems and add them to the stack of playlist. - ''' - for part, _ in enumerate(self.xml[0][0]): - if part == 0: - # The first part that we've already added - continue - self.api.set_part_number(part) - LOG.debug('Adding addional part %s', part) - self.set_playqueue_item(self.xml[0], self.kodi_id, self.kodi_type) - self.playqueue_item.part = part - listitem = widgets.get_listitem(self.xml[0], resume=False) - listitem.setSubtitles(self.api.cache_external_subs()) - playqueue_item = PL.playlist_item_from_xml(self.xml[0]) - play = PlayUtils(self.api, playqueue_item) - url = play.getPlayUrl().encode('utf-8') - listitem.setPath(url) - self.playlist_add(url, listitem) + start_position = position or max(self.playqueue.kodi_pl.size(), 0) + index = start_position + 1 + LOG.info('Play folder plex_id %s, index: %s', self.plex_id, index) + item = PL.PlaylistItem(plex_id=self.plex_id, + plex_type=self.plex_type, + kodi_id=self.kodi_id, + kodi_type=self.kodi_type) + self.playqueue.add_item(item, index) + index += 1 + return index - 1 diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 773f9fb83..96a22ea05 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -14,13 +14,13 @@ 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): """ diff --git a/resources/lib/plex_db/playlists.py b/resources/lib/plex_db/playlists.py index 3c14f2596..37d4f7c8e 100644 --- a/resources/lib/plex_db/playlists.py +++ b/resources/lib/plex_db/playlists.py @@ -20,7 +20,7 @@ def kodi_playlist_paths(self): 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 """ diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index b70efe8c7..a2e412cd8 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -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', diff --git a/resources/lib/utils.py b/resources/lib/utils.py index afcafdb15..ae5baa585 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -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' diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 2c336f7ea..aef3af38e 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -13,8 +13,8 @@ import xbmc import xbmcvfs -from . import backgroundthread, utils, variables as v, app -from .playstrm import PlayStrm +from . import backgroundthread, utils, variables as v, app, playqueue as PQ +from . import playlist_func as PL, json_rpc as js LOG = getLogger('PLEX.webservice') @@ -270,58 +270,110 @@ class QueuePlay(backgroundthread.KillableThread): def __init__(self, server): self.server = server + self.plex_id = None + self.plex_type = None + self.kodi_id = None + self.kodi_type = None + self.synched = None + self.force_transcode = None super(QueuePlay, self).__init__() + def load_params(self, params): + self.plex_id = utils.cast(int, params['plex_id']) + self.plex_type = params.get('plex_type') + self.kodi_id = utils.cast(int, params.get('kodi_id')) + self.kodi_type = params.get('kodi_type') + if params.get('synched') and params['synched'].lower() == 'false': + self.synched = False + else: + self.synched = True + if params.get('transcode') and params['transcode'].lower() == 'true': + self.force_transcode = True + else: + self.force_transcode = False + def run(self): - LOG.info('##===---- Starting QueuePlay ----===##') + LOG.debug('##===---- Starting QueuePlay ----===##') + if app.PLAYSTATE.audioplaylist: + LOG.debug('Audio playlist detected') + playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) + else: + LOG.debug('Video playlist detected') + playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO) + abort = False play_folder = False - play = None - start_position = None - position = None - - # Let Kodi catch up + # Position to start playback from (!!) + # Do NOT use kodi_pl.getposition() as that appears to be buggy + start_position = max(js.get_position(playqueue.playlistid), 0) + # Position to add next element to queue - we're doing this at the end + # of our playqueue + position = playqueue.kodi_pl.size() + LOG.debug('start %s, position %s for current playqueue: %s', + start_position, position, playqueue) + # Make sure we got at least 2 items in the queue - ugly + # TODO: find a better solution xbmc.sleep(200) - while True: - try: try: params = self.server.queue.get(timeout=0.1) except Queue.Empty: - count = 20 + count = 50 while not utils.window('plex.playlist.ready'): xbmc.sleep(50) if not count: LOG.info('Playback aborted') - raise Exception('PlaybackAborted') + raise Exception('Playback aborted') count -= 1 - LOG.info('Starting playback at position: %s', start_position) if play_folder: LOG.info('Start playing folder') xbmc.executebuiltin('Dialog.Close(busydialognocancel)') - play.start_playback() + playqueue.start_playback(start_position) else: + # TODO - do we need to do anything here? + # Originally, 1st failable item should have been removed utils.window('plex.playlist.play', value='true') - # xbmc.sleep(1000) - play.remove_from_playlist(start_position) + # playqueue.kodi_remove_item(start_position) break - play = PlayStrm(params, params.get('ServerId')) - - if start_position is None: - start_position = max(play.kodi_playlist.getposition(), 0) - position = start_position + 1 + self.load_params(params) if play_folder: - position = play.play_folder(position) + # position = play.play_folder(position) + item = PL.PlaylistItem(plex_id=self.plex_id, + plex_type=self.plex_type, + kodi_id=self.kodi_id, + kodi_type=self.kodi_type) + item.force_transcode = self.force_transcode + playqueue.add_item(item, position) + position += 1 else: if self.server.pending.count(params['plex_id']) != len(self.server.pending): + LOG.debug('Folder playback detected') play_folder = True utils.window('plex.playlist.start', str(start_position)) - position = play.play(position) + playqueue.init(self.plex_id, + plex_type=self.plex_type, + position=position, + synched=self.synched, + force_transcode=self.force_transcode) + # Do NOT start playback here - because Kodi already started + # it! + # playqueue.start_playback(position) + position = playqueue.index if play_folder: xbmc.executebuiltin('Activateutils.window(busydialognocancel)') + except PL.PlaylistError as error: + abort = True + LOG.warn('Not playing due to the following: %s', error) except Exception: + abort = True utils.ERROR() - play.kodi_playlist.clear() + try: + self.server.queue.task_done() + except ValueError: + # "task_done() called too many times" + pass + if abort: + playqueue.clear() xbmc.Player().stop() self.server.queue.queue.clear() if play_folder: @@ -329,11 +381,10 @@ def run(self): else: utils.window('plex.playlist.aborted', value='true') break - self.server.queue.task_done() utils.window('plex.playlist.ready', clear=True) utils.window('plex.playlist.start', clear=True) app.PLAYSTATE.audioplaylist = None self.server.threads.remove(self) self.server.pending = [] - LOG.info('##===---- QueuePlay Stopped ----===##') + LOG.debug('##===---- QueuePlay Stopped ----===##') From 6bd98fcefd83e909a5cf5cdcadd509102662e75e Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 4 May 2019 13:14:34 +0200 Subject: [PATCH 43/74] Cleanup --- default.py | 2 +- resources/lib/kodimonitor.py | 6 +- resources/lib/playlist_func.py | 7 +- resources/lib/service_entry.py | 2 + resources/lib/webservice.py | 117 ++++++++++++++++++++++++++------- 5 files changed, 101 insertions(+), 33 deletions(-) diff --git a/default.py b/default.py index f35ba57ce..f7be9f888 100644 --- a/default.py +++ b/default.py @@ -42,6 +42,7 @@ def __init__(self): if mode == 'playstrm': while not utils.window('plex.playlist.play'): xbmc.sleep(50) + LOG.error('waiting') if utils.window('plex.playlist.aborted'): LOG.info("playback aborted") break @@ -51,7 +52,6 @@ def __init__(self): False, xbmcgui.ListItem()) utils.window('plex.playlist.play', clear=True) - utils.window('plex.playlist.ready', clear=True) utils.window('plex.playlist.aborted', clear=True) elif mode == 'play': diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d6c436e74..146f6fea0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -195,10 +195,13 @@ def _playlist_onadd(self, data): if data['position'] == 0: if data['playlistid'] == 0: app.PLAYSTATE.audioplaylist = True + LOG.error('app.PLAYSTATE.audioplaylist set to True') else: app.PLAYSTATE.audioplaylist = False + LOG.error('app.PLAYSTATE.audioplaylist set to False') self.playlistid = data['playlistid'] - if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: + LOG.debug('plex.playlist.start: %s', utils.window('plex.playlist.start')) + if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')): LOG.info('Playlist ready') utils.window('plex.playlist.ready', value='true') utils.window('plex.playlist.start', clear=True) @@ -226,7 +229,6 @@ def _playlist_onclear(self, data): if self.playlistid == data['playlistid']: LOG.debug('Resetting autoplay') app.PLAYSTATE.autoplay = False - return # playqueue = PQ.PLAYQUEUES[data['playlistid']] # if not playqueue.is_pkc_clear(): # playqueue.pkc_edit = True diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index c0ad1f191..15a48e16d 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -148,15 +148,10 @@ def init(self, plex_id, plex_type=None, position=None, synched=True, """ LOG.error('Current Kodi playlist: %s', js.playlist_get_items(self.playlistid)) - if position is not None: - self.index = position - else: - # Do NOT use kodi_pl.getposition() as that appears to be buggy - self.index = max(js.get_position(self.playlistid), 0) LOG.debug('Initializing with plex_id %s, plex_type %s, position %s, ' 'synched %s, force_transcode %s, index %s', plex_id, plex_type, position, synched, force_transcode, self.index) - LOG.error('Actual start: %s', js.get_position(self.playlistid)) + self.index = position if self.kodi_pl.size() != len(self.items): # The original item that Kodi put into the playlist, e.g. # { diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 5734b3c78..b5f1c8a9a 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -497,6 +497,8 @@ def ServiceEntryPoint(self): 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() diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index aef3af38e..f5266f551 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -13,6 +13,7 @@ import xbmc import xbmcvfs +from .plex_db import PlexDB from . import backgroundthread, utils, variables as v, app, playqueue as PQ from . import playlist_func as PL, json_rpc as js @@ -138,15 +139,23 @@ def get_params(self): 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 params.get('transcode'): - params['transcode'] = params['transcode'].lower() == 'true' - if params.get('server') and params['server'].lower() == 'none': - params['server'] = None + 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() + params['plex_type'] = v.PLEX_TYPE_CLIP if 'video' in players \ + else v.PLEX_TYPE_SONG return params @@ -223,7 +232,7 @@ def strm(self): self.server.pending.append(params['plex_id']) self.server.queue.put(params) if not len(self.server.threads): - queue = QueuePlay(self.server) + queue = QueuePlay(self.server, params['plex_type']) queue.start() self.server.threads.append(queue) @@ -268,21 +277,40 @@ class QueuePlay(backgroundthread.KillableThread): players with this method. ''' - def __init__(self, server): + def __init__(self, server, plex_type): self.server = server + self.plex_type = plex_type self.plex_id = None - self.plex_type = None self.kodi_id = None self.kodi_type = None self.synched = None self.force_transcode = None super(QueuePlay, self).__init__() + def __unicode__(self): + return ("{{" + "'plex_id': {self.plex_id}, " + "'plex_type': '{self.plex_type}', " + "'kodi_id': {self.kodi_id}, " + "'kodi_type': '{self.kodi_type}', " + "'synched: '{self.synched}', " + "'force_transcode: '{self.force_transcode}', " + "}}").format(self=self) + + def __str__(self): + return unicode(self).encode('utf-8') + __repr__ = __str__ + 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') and params['synched'].lower() == 'false': self.synched = False else: @@ -293,31 +321,64 @@ def load_params(self, params): self.force_transcode = False def run(self): + """ + We cannot use js.get_players() to reliably get the active player + Use Kodimonitor's OnNotification and OnAdd + """ LOG.debug('##===---- Starting QueuePlay ----===##') - if app.PLAYSTATE.audioplaylist: - LOG.debug('Audio playlist detected') - playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) - else: - LOG.debug('Video playlist detected') - playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO) abort = False play_folder = False + i = 0 + while app.PLAYSTATE.audioplaylist is None: + # Needed particulary for widget-playback + # We need to wait until kodimonitor notification OnAdd is triggered + # in order to detect the playlist type (video vs. audio) and thus + # e.g. determine whether playback has been init. from widgets + xbmc.sleep(50) + i += 1 + if i > 100: + raise Exception('Kodi OnAdd not received - cancelling') + if app.PLAYSTATE.audioplaylist and self.plex_type in v.PLEX_VIDEOTYPES: + # 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; relaunching') + video_widget_playback = 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() + utils.window('plex.playlist.ready', value='true') + 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) + # Position to start playback from (!!) # Do NOT use kodi_pl.getposition() as that appears to be buggy - start_position = max(js.get_position(playqueue.playlistid), 0) + 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 playqueue + # of our current playqueue position = playqueue.kodi_pl.size() - LOG.debug('start %s, position %s for current playqueue: %s', + LOG.debug('start_position %s, position %s for current playqueue: %s', start_position, position, playqueue) - # Make sure we got at least 2 items in the queue - ugly - # TODO: find a better solution - xbmc.sleep(200) while True: try: try: - params = self.server.queue.get(timeout=0.1) + params = self.server.queue.get(block=False) except Queue.Empty: + LOG.debug('Wrapping up') + if xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + # avoid issues with ongoing Live TV playback + xbmc.Player().stop() count = 50 while not utils.window('plex.playlist.ready'): xbmc.sleep(50) @@ -329,11 +390,18 @@ def run(self): 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') + utils.window('plex.playlist.play', value='true') + xbmc.sleep(2000) + LOG.info('Current PKC queue: %s', playqueue) + LOG.info('current Kodi queue: %s', js.playlist_get_items(playqueue.playlistid)) + playqueue.start_playback() else: - # TODO - do we need to do anything here? - # Originally, 1st failable item should have been removed + LOG.info('Start normal playback') + # Release default.py utils.window('plex.playlist.play', value='true') - # playqueue.kodi_remove_item(start_position) + LOG.debug('Done wrapping up') break self.load_params(params) if play_folder: @@ -349,7 +417,8 @@ def run(self): if self.server.pending.count(params['plex_id']) != len(self.server.pending): LOG.debug('Folder playback detected') play_folder = True - utils.window('plex.playlist.start', str(start_position)) + # Set to start_position + 1 because first item will fail + utils.window('plex.playlist.start', str(start_position + 1)) playqueue.init(self.plex_id, plex_type=self.plex_type, position=position, From 48cda467c35daae7ee4b7cf339b586389bc381d9 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 4 May 2019 14:26:18 +0200 Subject: [PATCH 44/74] Move method --- resources/lib/kodimonitor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 146f6fea0..fd2c55911 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -101,12 +101,6 @@ def onNotification(self, sender, method, data): with app.APP.lock_playqueues: _playback_cleanup() elif method == 'Playlist.OnAdd': - if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW: - # Hitting the "browse" button on tv show info dialog - # Hence show the tv show directly - xbmc.executebuiltin("Dialog.Close(all, true)") - js.activate_window('videos', - 'videodb://tvshows/titles/%s/' % data['item']['id']) with app.APP.lock_playqueues: self._playlist_onadd(data) elif method == 'Playlist.OnRemove': @@ -192,6 +186,14 @@ def _playlist_onadd(self, data): ''' Detect widget playback. Widget for some reason, use audio playlists. ''' + 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 + if data['position'] == 0: if data['playlistid'] == 0: app.PLAYSTATE.audioplaylist = True From 353cb04532d2a7ee97e01d029805db9ed8bd5399 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 4 May 2019 14:29:18 +0200 Subject: [PATCH 45/74] New functions --- resources/lib/playqueue.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index dde6921b2..ccd0fa9f4 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -64,8 +64,31 @@ def get_playqueue_from_type(kodi_playlist_type): if playqueue.type == kodi_playlist_type: break else: - raise ValueError('Wrong playlist type passed in: %s', - kodi_playlist_type) + 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 From 45fc9fa8bed8ceb9750bb031e06c0ac58f26592f Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 4 May 2019 16:13:26 +0200 Subject: [PATCH 46/74] Better way to detect video widget playback --- resources/lib/app/playstate.py | 3 --- resources/lib/kodimonitor.py | 11 ++--------- resources/lib/webservice.py | 15 ++------------- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 67430e877..7736ed17d 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -59,9 +59,6 @@ def __init__(self): self.resume_playback = None # Don't ask user whether to resume but immediatly resume self.autoplay = False - # Are we using the Kodi audio playlist (=True, e.g. for videos when - # starting from a widget!) or video playlist (=False)? - self.audioplaylist = None # Was the playback initiated by the user using the Kodi context menu? self.context_menu_play = False # Which Kodi player is/has been active? (either int 1, 2 or 3) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index fd2c55911..44fbff92b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -184,7 +184,7 @@ def _hack_addon_paths_replay_video(): def _playlist_onadd(self, data): ''' - Detect widget playback. Widget for some reason, use audio playlists. + 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 @@ -195,16 +195,9 @@ def _playlist_onadd(self, data): return if data['position'] == 0: - if data['playlistid'] == 0: - app.PLAYSTATE.audioplaylist = True - LOG.error('app.PLAYSTATE.audioplaylist set to True') - else: - app.PLAYSTATE.audioplaylist = False - LOG.error('app.PLAYSTATE.audioplaylist set to False') self.playlistid = data['playlistid'] - LOG.debug('plex.playlist.start: %s', utils.window('plex.playlist.start')) if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')): - LOG.info('Playlist ready') + LOG.debug('Playlist ready') utils.window('plex.playlist.ready', value='true') utils.window('plex.playlist.start', clear=True) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index f5266f551..064d37c83 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -328,17 +328,8 @@ def run(self): LOG.debug('##===---- Starting QueuePlay ----===##') abort = False play_folder = False - i = 0 - while app.PLAYSTATE.audioplaylist is None: - # Needed particulary for widget-playback - # We need to wait until kodimonitor notification OnAdd is triggered - # in order to detect the playlist type (video vs. audio) and thus - # e.g. determine whether playback has been init. from widgets - xbmc.sleep(50) - i += 1 - if i > 100: - raise Exception('Kodi OnAdd not received - cancelling') - if app.PLAYSTATE.audioplaylist and self.plex_type in v.PLEX_VIDEOTYPES: + if (self.plex_type in v.PLEX_VIDEOTYPES 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; relaunching') @@ -393,7 +384,6 @@ def run(self): elif video_widget_playback: LOG.info('Start widget video playback') utils.window('plex.playlist.play', value='true') - xbmc.sleep(2000) LOG.info('Current PKC queue: %s', playqueue) LOG.info('current Kodi queue: %s', js.playlist_get_items(playqueue.playlistid)) playqueue.start_playback() @@ -453,7 +443,6 @@ def run(self): utils.window('plex.playlist.ready', clear=True) utils.window('plex.playlist.start', clear=True) - app.PLAYSTATE.audioplaylist = None self.server.threads.remove(self) self.server.pending = [] LOG.debug('##===---- QueuePlay Stopped ----===##') From 8ad6d1bccea70e0eaf19271eeee88ffcf2d0422d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 10:21:59 +0200 Subject: [PATCH 47/74] Clear playqueue on playback startup --- resources/lib/webservice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 064d37c83..ab1e76124 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -347,6 +347,7 @@ def run(self): else: LOG.debug('Audio playback detected') playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) + playqueue.clear(kodi=False) # Position to start playback from (!!) # Do NOT use kodi_pl.getposition() as that appears to be buggy From 1123a2ee3c4c973a5847d011b116906aaf68e7f5 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 10:29:04 +0200 Subject: [PATCH 48/74] Fix widget playback not starting up --- default.py | 2 +- resources/lib/webservice.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/default.py b/default.py index f7be9f888..ae7f7e8d3 100644 --- a/default.py +++ b/default.py @@ -41,7 +41,7 @@ def __init__(self): if mode == 'playstrm': while not utils.window('plex.playlist.play'): - xbmc.sleep(50) + xbmc.sleep(25) LOG.error('waiting') if utils.window('plex.playlist.aborted'): LOG.info("playback aborted") diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index ab1e76124..3343980e1 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -334,11 +334,16 @@ def run(self): # We will empty everything and start with a fresh VIDEO playlist LOG.debug('Widget video playback detected; relaunching') video_widget_playback = True + # Release default.py + utils.window('plex.playlist.ready', value='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() - utils.window('plex.playlist.ready', value='true') + # 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: From dc56c2a6a2099488ae96b470e165b21a1d816280 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 10:42:44 +0200 Subject: [PATCH 49/74] Improve code --- resources/lib/webservice.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 3343980e1..8eb7d21fa 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -283,8 +283,8 @@ def __init__(self, server, plex_type): self.plex_id = None self.kodi_id = None self.kodi_type = None - self.synched = None - self.force_transcode = None + self.synched = True + self.force_transcode = False super(QueuePlay, self).__init__() def __unicode__(self): @@ -311,23 +311,10 @@ def load_params(self, params): self.force_transcode = params['transcode'].lower() == 'true' if params.get('server') and params['server'].lower() == 'none': self.server = None - if params.get('synched') and params['synched'].lower() == 'false': - self.synched = False - else: - self.synched = True - if params.get('transcode') and params['transcode'].lower() == 'true': - self.force_transcode = True - else: - self.force_transcode = False + if params.get('synched'): + self.synched = not params['synched'].lower() == 'false' - def run(self): - """ - We cannot use js.get_players() to reliably get the active player - Use Kodimonitor's OnNotification and OnAdd - """ - LOG.debug('##===---- Starting QueuePlay ----===##') - abort = False - play_folder = False + def _get_playqueue(self): if (self.plex_type in v.PLEX_VIDEOTYPES and xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): # Video launched from a widget - which starts a Kodi AUDIO playlist @@ -353,7 +340,17 @@ def run(self): LOG.debug('Audio playback detected') playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) playqueue.clear(kodi=False) + return playqueue, video_widget_playback + def run(self): + """ + We cannot use js.get_players() to reliably get the active player + Use Kodimonitor's OnNotification and OnAdd + """ + LOG.debug('##===---- Starting QueuePlay ----===##') + 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: From b586ac09c4da2e74e7a8d3d846f6b8cfcb068f4e Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 11:00:41 +0200 Subject: [PATCH 50/74] Fix moving of items in Plex playqueue --- resources/lib/playlist_func.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 15a48e16d..93d784b10 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -403,17 +403,10 @@ def plex_move_item(self, before, after): Will also change self.items """ - if before > len(self.items): - raise PlaylistError('Original position %s larger than current ' - 'playlist length %s', - before, len(self.items)) - elif after > len(self.items): - raise PlaylistError('Desired position %s larger than current ' - 'playlist length %s', - after, len(self.items)) - elif after == before: - raise PlaylistError('Desired position and original position are ' - 'identical: %s', after) + if before > len(self.items) or after > len(self.items) or after == before: + raise PlaylistError('Illegal original position %s and/or desired ' + 'position %s for playlist length %s' % + (before, after, len(self.items))) LOG.debug('Moving item from %s to %s on the Plex side for %s', before, after, self) if after == 0: @@ -421,6 +414,12 @@ def plex_move_item(self, before, after): (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, From 7616d6dc26bd1909182525754459350cf016fc03 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 11:14:27 +0200 Subject: [PATCH 51/74] Fixup --- resources/lib/webservice.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 8eb7d21fa..ff3b2893d 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -223,12 +223,8 @@ def strm(self): return path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id'] - xbmc.log('PLEX.webservice: sending %s' % path, xbmc.LOGDEBUG) self.wfile.write(bytes(path.encode('utf-8'))) if params['plex_id'] not in self.server.pending: - xbmc.log('PLEX.webservice: path %s params %s' % (self.path, params), - xbmc.LOGDEBUG) - self.server.pending.append(params['plex_id']) self.server.queue.put(params) if not len(self.server.threads): @@ -367,7 +363,9 @@ def run(self): while True: try: try: - params = self.server.queue.get(block=False) + # 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)'): @@ -435,8 +433,8 @@ def run(self): # "task_done() called too many times" pass if abort: - playqueue.clear() xbmc.Player().stop() + playqueue.clear() self.server.queue.queue.clear() if play_folder: xbmc.executebuiltin('Dialog.Close(busydialognocancel)') From cef07c3598cc3a2e07b2cf327e0cbcc02ccb0031 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 11:29:43 +0200 Subject: [PATCH 52/74] Set kodi_type for PlaylistItem automatically from plex_type --- resources/lib/playlist_func.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 93d784b10..b57470ef7 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -556,6 +556,7 @@ def from_xml(self, xml_video_element): self.name = api.title() self.plex_id = api.plex_id() self.plex_type = api.plex_type() + self.kodi_type = v.KODITYPE_FROM_PLEXTYPE[self.plex_type] self.id = api.item_id() self.guid = api.guid_html_escaped() self.playcount = api.viewcount() From 1d01f4794e00ed68aa97098d2cc6ad39be6d6bf5 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 11:31:00 +0200 Subject: [PATCH 53/74] Fixup --- resources/lib/playlist_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index b57470ef7..2c76f8d52 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -502,7 +502,7 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, if (lookup_kodi and (kodi_id is None or kodi_type is None) and self.plex_type != v.PLEX_TYPE_CLIP): with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_id(plex_id, plex_type) + 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'] From a1f4960bca1c38d42c828ef98b7fd762a549ac1d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 5 May 2019 12:11:43 +0200 Subject: [PATCH 54/74] Revert "Set kodi_type for PlaylistItem automatically from plex_type" This reverts commit cef07c3598cc3a2e07b2cf327e0cbcc02ccb0031. --- resources/lib/playlist_func.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 2c76f8d52..9512d1c63 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -556,7 +556,6 @@ def from_xml(self, xml_video_element): self.name = api.title() self.plex_id = api.plex_id() self.plex_type = api.plex_type() - self.kodi_type = v.KODITYPE_FROM_PLEXTYPE[self.plex_type] self.id = api.item_id() self.guid = api.guid_html_escaped() self.playcount = api.viewcount() From ea4a062aac9e826d5d015da3e904cd7320f82b27 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 12 May 2019 14:38:31 +0200 Subject: [PATCH 55/74] Big update --- resources/lib/app/playstate.py | 3 + resources/lib/kodimonitor.py | 13 +-- resources/lib/playlist_func.py | 188 ++++++++++++++++++++++++-------- resources/lib/plex_companion.py | 135 ++++++++++++----------- resources/lib/webservice.py | 87 +++++++++------ 5 files changed, 272 insertions(+), 154 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 7736ed17d..e0b829603 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -63,3 +63,6 @@ def __init__(self): self.context_menu_play = False # Which Kodi player is/has been active? (either int 1, 2 or 3) 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 diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 44fbff92b..c76049851 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -322,12 +322,10 @@ def _check_playing_item(self, data): LOG.debug('Current Kodi playlist: %s', kodi_playlist) kodi_item = PL.playlist_item_from_kodi(kodi_playlist[position]) if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy): - # Get rid of the very first element in the queue that Kodi marked - # as unplayed (the one to init the queue) - LOG.debug('Deleting the very first playqueue item') - js.playlist_remove(self.playqueue.playlistid, 0) - del self.playqueue.items[0] - position = 0 + # This dummy item will be deleted by webservice soon - it won't + # play + LOG.debug('Dummy item detected') + position = 1 elif kodi_item != self.playqueue.items[position]: LOG.debug('Different playqueue items: %s vs. %s ', kodi_item, self.playqueue.items[position]) @@ -487,9 +485,6 @@ def _record_playstate(status, ended): playcount += 1 time = 0 with kodi_db.KodiVideoDB() as kodidb: - LOG.error('Setting file_id %s, time %s, totaltime %s, playcount %s, ' - 'last_played %s', - db_item['kodi_fileid'], time, totaltime, playcount, last_played) kodidb.set_resume(db_item['kodi_fileid'], time, totaltime, diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 9512d1c63..48442e322 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -5,8 +5,7 @@ """ from __future__ import absolute_import, division, unicode_literals from logging import getLogger - -import xbmc +import threading from .plex_api import API from .plex_db import PlexDB @@ -139,20 +138,59 @@ def clear(self, kodi=True): self.force_transcode = None LOG.debug('Playlist cleared: %s', self) - def init(self, plex_id, plex_type=None, position=None, synched=True, - force_transcode=None): + 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 + """ + 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 + """ + self.index = startpos + 1 + xml = PF.GetPlexMetadata(plex_id) + if xml in (None, 401): + raise PlaylistError('Could not get Plex metadata %s for %s', + plex_id, self.items[startpos]) + api = API(xml[0]) + resume = self._resume_playback(None, xml[0]) + self._kodi_add_xml(xml[0], api, resume) + # Add additional file parts, if any exist + self._add_additional_parts(xml) + + 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 """ - LOG.error('Current Kodi playlist: %s', - js.playlist_get_items(self.playlistid)) - LOG.debug('Initializing with plex_id %s, plex_type %s, position %s, ' - 'synched %s, force_transcode %s, index %s', plex_id, - plex_type, position, synched, force_transcode, self.index) self.index = position - if self.kodi_pl.size() != len(self.items): + while len(self.items) < self.kodi_pl.size(): # The original item that Kodi put into the playlist, e.g. # { # u'title': u'', @@ -161,13 +199,10 @@ def init(self, plex_id, plex_type=None, position=None, synched=True, # u'label': u'' # } # We CANNOT delete that item right now - so let's add a dummy - # on the PKC side - LOG.debug('Detected Kodi playlist size %s to be off for PKC: %s', - self.kodi_pl.size(), len(self.items)) - while len(self.items) < self.kodi_pl.size(): - LOG.debug('Adding a dummy item to our playqueue') - playlistitem = PlaylistItemDummy() - self.items.insert(0, playlistitem) + # 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: @@ -316,7 +351,11 @@ def kodi_add_item(self, item, pos, listitem=None): raise PlaylistError('Position %s too large for playlist length %s' % (pos, len(self.items))) LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item) - if item.kodi_id is not None and item.kodi_type is not None: + 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): @@ -330,24 +369,24 @@ def kodi_add_item(self, item, pos, listitem=None): raise PlaylistError('Kodi did not add item to playlist: %s', answ) else: - if not listitem: - if item.xml is None: - LOG.debug('Need to get metadata for item %s', item) - item.xml = PF.GetPlexMetadata(item.plex_id) - if item.xml in (None, 401): - raise PlaylistError('Could not get metadata for %s', item) - listitem = widgets.get_listitem(item.xml, resume=True) - url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT - args = { - 'plex_id': self.plex_id, - 'plex_type': self.plex_type - } - if item.force_transcode: - args['transcode'] = 'true' - url = utils.extend_url(url, args) - item.file = url - listitem.setPath(url.encode('utf-8')) - self.kodi_pl.add(url=listitem.getPath(), + if item.xml is None: + LOG.debug('Need to get metadata for item %s', item) + item.xml = PF.GetPlexMetadata(item.plex_id) + if item.xml in (None, 401): + raise PlaylistError('Could not get metadata for %s', item) + api = API(item.xml[0]) + listitem = widgets.get_listitem(item.xml, resume=True) + url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT + args = { + 'plex_id': item.plex_id, + 'plex_type': api.plex_type() + } + if item.force_transcode: + args['transcode'] = 'true' + url = utils.extend_url(url, args) + item.file = url + listitem.setPath(url.encode('utf-8')) + self.kodi_pl.add(url=url.encode('utf-8'), listitem=listitem, index=pos) @@ -372,9 +411,6 @@ def plex_add_item(self, item, pos): except (TypeError, AttributeError, KeyError, IndexError): raise PlaylistError('Could not add item %s to playlist %s' % (item, self)) - if len(xml) != len(self.items) + 1: - raise PlaylistError('Could not add item %s to playlist %s - wrong' - ' length received' % (item, self)) for actual_pos, xml_video_element in enumerate(xml): api = API(xml_video_element) if api.plex_id() == item.plex_id: @@ -395,7 +431,7 @@ def kodi_remove_item(self, pos): LOG.debug('Removing position %s on the Kodi side for %s', pos, self) answ = js.playlist_remove(self.playlistid, pos) if 'error' in answ: - raise PlaylistError('Could not remove item: %s' % answ) + raise PlaylistError('Could not remove item: %s' % answ['error']) def plex_move_item(self, before, after): """ @@ -436,9 +472,71 @@ def plex_move_item(self, before, after): self.items.insert(after, self.items.pop(before)) LOG.debug('Done moving items for %s', self) - def start_playback(self, pos=0): - LOG.info('Starting playback at %s for %s', pos, self) - xbmc.Player().play(self.kodi_pl, startpos=pos, windowed=False) + 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 + 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) class PlaylistItem(object): @@ -1069,7 +1167,7 @@ def move_playlist_item(playlist, before_pos, after_pos): LOG.debug('Done moving for %s', playlist) -def get_PMS_playlist(playlist, playlist_id=None): +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 @@ -1077,7 +1175,7 @@ def get_PMS_playlist(playlist, playlist_id=None): Returns None if something went wrong """ playlist_id = playlist_id if playlist_id else playlist.id - if playlist.kind == 'playList': + if playlist and playlist.kind == 'playList': xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id) else: xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 267e42e02..285e4773e 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -15,7 +15,6 @@ 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 +39,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 +48,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 = PL.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 PL.PlaylistError() + 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 +76,48 @@ def __init__(self): @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 PL.PlaylistError() 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 PL.PlaylistError() + 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 PL.PlaylistError() 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': + if data.get('offset') not in ('0', None): offset = float(data['offset']) / 1000.0 else: offset = None - playback.play_xml(playqueue, xml, offset) + 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 data.get('offset') not in (None, '0'): 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,7 +148,7 @@ def _process_playlist(data): xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata') - return + raise PL.PlaylistError() api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) @@ -167,20 +165,23 @@ def _process_streams(data): """ 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): @@ -220,23 +221,29 @@ def _process_tasks(self, task): """ 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 PL.PlaylistError: + 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): """ diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index ff3b2893d..400bd9b79 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -13,16 +13,16 @@ 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 playlist_func as PL, json_rpc as js +from . import playlist_func as PL, 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): @@ -48,7 +48,7 @@ def abort(self): conn.request('QUIT', '/') conn.getresponse() except Exception as error: - xbmc.log('Plex.WebService abort error: %s' % error, xbmc.LOGWARNING) + xbmc.log('PLEX.webservice abort error: %s' % error, xbmc.LOGWARNING) def suspend(self): """ @@ -124,7 +124,7 @@ def handle(self): # Silence "[Errno 10054] An existing connection was forcibly # closed by the remote host" return - xbmc.log('Plex.WebService handle error: %s' % error, xbmc.LOGWARNING) + xbmc.log('PLEX.webservice handle error: %s' % error, xbmc.LOGWARNING) def do_QUIT(self): ''' send 200 OK response, and set server.stop to True @@ -144,7 +144,12 @@ def get_params(self): 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: @@ -154,9 +159,20 @@ def get_params(self): else: LOG.debug('No plex_type found, using Kodi player id') players = js.get_players() - params['plex_type'] = v.PLEX_TYPE_CLIP if 'video' in players \ - else v.PLEX_TYPE_SONG - + 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): @@ -172,7 +188,7 @@ def do_GET(self): def handle_request(self, headers_only=False): '''Send headers and reponse ''' - xbmc.log('Plex.WebService handle_request called. headers %s, path: %s' + 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: @@ -311,11 +327,13 @@ def load_params(self, params): self.synched = not params['synched'].lower() == 'false' def _get_playqueue(self): - if (self.plex_type in v.PLEX_VIDEOTYPES and - xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): + 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; relaunching') + LOG.debug('Widget video playback detected') video_widget_playback = True # Release default.py utils.window('plex.playlist.ready', value='true') @@ -335,14 +353,9 @@ def _get_playqueue(self): else: LOG.debug('Audio playback detected') playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) - playqueue.clear(kodi=False) return playqueue, video_widget_playback def run(self): - """ - We cannot use js.get_players() to reliably get the active player - Use Kodimonitor's OnNotification and OnAdd - """ LOG.debug('##===---- Starting QueuePlay ----===##') abort = False play_folder = False @@ -358,6 +371,8 @@ def run(self): # 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 + utils.window('plex.playlist.start', str(start_position + 1)) LOG.debug('start_position %s, position %s for current playqueue: %s', start_position, position, playqueue) while True: @@ -370,7 +385,7 @@ def run(self): LOG.debug('Wrapping up') if xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): # avoid issues with ongoing Live TV playback - xbmc.Player().stop() + app.APP.player.stop() count = 50 while not utils.window('plex.playlist.ready'): xbmc.sleep(50) @@ -392,48 +407,47 @@ def run(self): LOG.info('Start normal playback') # Release default.py utils.window('plex.playlist.play', value='true') + # 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: - # position = play.play_folder(position) - item = PL.PlaylistItem(plex_id=self.plex_id, - plex_type=self.plex_type, - kodi_id=self.kodi_id, - kodi_type=self.kodi_type) - item.force_transcode = self.force_transcode - playqueue.add_item(item, position) + playlistitem = PL.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 - # Set to start_position + 1 because first item will fail - utils.window('plex.playlist.start', str(start_position + 1)) - playqueue.init(self.plex_id, + 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! - # playqueue.start_playback(position) position = playqueue.index - if play_folder: - xbmc.executebuiltin('Activateutils.window(busydialognocancel)') - except PL.PlaylistError as error: - abort = True - LOG.warn('Not playing due to the following: %s', error) except Exception: abort = True - utils.ERROR() + utils.ERROR(notify=True) try: self.server.queue.task_done() except ValueError: - # "task_done() called too many times" + # "task_done() called too many times" when aborting pass if abort: - xbmc.Player().stop() + app.APP.player.stop() playqueue.clear() self.server.queue.queue.clear() if play_folder: @@ -444,6 +458,7 @@ def run(self): utils.window('plex.playlist.ready', clear=True) utils.window('plex.playlist.start', clear=True) + app.PLAYSTATE.initiated_by_plex = False self.server.threads.remove(self) self.server.pending = [] LOG.debug('##===---- QueuePlay Stopped ----===##') From 0ce29dc0ce22a348992e0bf5f3ed3c0130b65642 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 19 May 2019 18:47:09 +0200 Subject: [PATCH 56/74] Fix Plex Companion telling the wrong item is playing --- resources/lib/playlist_func.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 48442e322..761c151ee 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -180,7 +180,10 @@ def _resolve(self, plex_id, startpos): plex_id, self.items[startpos]) api = API(xml[0]) resume = self._resume_playback(None, xml[0]) - self._kodi_add_xml(xml[0], api, resume) + self._kodi_add_xml(xml[0], + api, + resume, + playlistitem=self.items[startpos]) # Add additional file parts, if any exist self._add_additional_parts(xml) @@ -303,8 +306,9 @@ def _add_additional_parts(self, xml): LOG.debug('Adding addional part for %s: %s', api.title(), part) self._kodi_add_xml(xml[0], api) - def _kodi_add_xml(self, xml, api, resume=False): - playlistitem = PlaylistItem(xml_video_element=xml) + def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): + if not playlistitem: + playlistitem = PlaylistItem(xml_video_element=xml) playlistitem.part = api.part playlistitem.force_transcode = self.force_transcode listitem = widgets.get_listitem(xml, resume=True) From 7725af5a6f18c09dca69635bbf9a173485646190 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 21 May 2019 17:56:48 +0200 Subject: [PATCH 57/74] Fix resume from Plex Companion --- resources/lib/playlist_func.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 761c151ee..3ec90b85f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -173,19 +173,29 @@ def _resolve(self, plex_id, startpos): The Plex playqueue has already been initialized. We resolve the path from original webservice http://127.0.0.1 to the "correct" Plex one """ + playlistitem = self.items[startpos] + # Add an additional item with the resolved path after the current one self.index = startpos + 1 xml = PF.GetPlexMetadata(plex_id) if xml in (None, 401): raise PlaylistError('Could not get Plex metadata %s for %s', plex_id, self.items[startpos]) api = API(xml[0]) - resume = self._resume_playback(None, 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=self.items[startpos]) + 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): @@ -517,6 +527,9 @@ def init_from_xml(self, xml, offset=None, start_plex_id=None, repeat=None, 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): @@ -593,6 +606,11 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=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 grab_xml and plex_id is not None and xml_video_element is None: xml_video_element = PF.GetPlexMetadata(plex_id) try: From 9d79f7819069e804446631ba80ffdb4fcf7fafd3 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 21 May 2019 18:08:09 +0200 Subject: [PATCH 58/74] Fix TypeError --- resources/lib/plex_companion.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 285e4773e..a55d442be 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -103,16 +103,15 @@ def _process_alexa(data): raise PL.PlaylistError() playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) - if data.get('offset') not in ('0', None): - offset = float(data['offset']) / 1000.0 - else: - offset = None + 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') not in (None, '0'): + if utils.cast(float, data.get('offset')): app.PLAYSTATE.resume_playback = True path = ('http://127.0.0.1:%s/plex/play/file.strm?plex_id=%s' % (v.WEBSERVICE_PORT, api.plex_id())) @@ -155,7 +154,7 @@ def _process_playlist(data): 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 From f7237d70338aeca92879b3499ce3193bad6d414c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 25 May 2019 13:12:29 +0200 Subject: [PATCH 59/74] Cleanup --- resources/lib/kodimonitor.py | 24 +++--------------------- resources/lib/playlist_func.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c76049851..6fe8b4645 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -250,24 +250,6 @@ def _get_ids(kodi_id, kodi_type, path): 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 @@ -320,15 +302,15 @@ def _check_playing_item(self, data): 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) - kodi_item = PL.playlist_item_from_kodi(kodi_playlist[position]) + playlistitem = PL.PlaylistItem(kodi_item=kodi_playlist[position]) if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy): # This dummy item will be deleted by webservice soon - it won't # play LOG.debug('Dummy item detected') position = 1 - elif kodi_item != self.playqueue.items[position]: + elif playlistitem != self.playqueue.items[position]: LOG.debug('Different playqueue items: %s vs. %s ', - kodi_item, self.playqueue.items[position]) + playlistitem, self.playqueue.items[position]) raise MonitorError() # Return the PKC playqueue item - contains more info return self.playqueue.items[position] diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 3ec90b85f..981c086fd 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -582,7 +582,7 @@ class PlaylistItem(object): - OR: have the same file """ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, - kodi_id=None, kodi_type=None, grab_xml=False, + 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 @@ -595,9 +595,14 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, self.plex_id = plex_id self.plex_type = plex_type self.plex_uuid = None - self.kodi_id = kodi_id - self.kodi_type = kodi_type - self.file = None + if kodi_item: + self.kodi_id = kodi_item['id'] + self.kodi_type = kodi_item['type'] + self.file = kodi_item.get('file') + else: + self.kodi_id = kodi_id + self.kodi_type = kodi_type + self.file = None self.uri = None self.guid = None self.xml = None @@ -619,7 +624,7 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, xml_video_element = None if xml_video_element is not None: self.from_xml(xml_video_element) - if (lookup_kodi and (kodi_id is None or kodi_type is None) and + 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) From d397fb5b20fe0033aae2db21d134fdf1a15af053 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 25 May 2019 13:35:14 +0200 Subject: [PATCH 60/74] Cleanup --- resources/lib/kodimonitor.py | 50 +-- resources/lib/playback.py | 551 ------------------------------ resources/lib/playback_starter.py | 108 +++++- 3 files changed, 100 insertions(+), 609 deletions(-) delete mode 100644 resources/lib/playback.py diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6fe8b4645..21e58c0fb 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,7 +14,7 @@ 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 utils, timing, plex_functions as PF from . import json_rpc as js, playqueue as PQ, playlist_func as PL from . import backgroundthread, app, variables as v @@ -38,7 +38,6 @@ class KodiMonitor(xbmc.Monitor): """ def __init__(self): self._already_slept = False - self.hack_replay = None # Info to the currently playing item self.playerid = None self.playlistid = None @@ -78,23 +77,11 @@ def onNotification(self, sender, method, data): 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.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: @@ -149,39 +136,6 @@ def onNotification(self, sender, method, data): 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 when a new item is added to a Kodi playqueue diff --git a/resources/lib/playback.py b/resources/lib/playback.py deleted file mode 100644 index 82b42646b..000000000 --- a/resources/lib/playback.py +++ /dev/null @@ -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 " - 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.PlaylistItem() - 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)) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 5800f0eb9..00a088351 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -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, playlist_func as PL ############################################################################### @@ -34,16 +36,102 @@ def run(self): 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 = PL.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 From 9d517c2c3d971868c5422a606033ea72f19d2b6c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 25 May 2019 20:49:29 +0200 Subject: [PATCH 61/74] Refactoring --- resources/lib/kodimonitor.py | 15 +- resources/lib/playback_starter.py | 4 +- resources/lib/playlist_func.py | 1337 ----------------- resources/lib/playqueue/__init__.py | 14 + resources/lib/playqueue/common.py | 301 ++++ resources/lib/playqueue/functions.py | 164 ++ .../{playqueue.py => playqueue/monitor.py} | 130 +- resources/lib/playqueue/playqueue.py | 601 ++++++++ resources/lib/playqueue/queue.py | 0 resources/lib/playstrm.py | 5 +- resources/lib/plex_companion.py | 19 +- resources/lib/webservice.py | 4 +- 12 files changed, 1116 insertions(+), 1478 deletions(-) delete mode 100644 resources/lib/playlist_func.py create mode 100644 resources/lib/playqueue/__init__.py create mode 100644 resources/lib/playqueue/common.py create mode 100644 resources/lib/playqueue/functions.py rename resources/lib/{playqueue.py => playqueue/monitor.py} (56%) create mode 100644 resources/lib/playqueue/playqueue.py create mode 100644 resources/lib/playqueue/queue.py diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 21e58c0fb..78846b7fc 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,9 +14,8 @@ from .plex_db import PlexDB from . import kodi_db from .downloadutils import DownloadUtils as DU -from . import utils, timing, plex_functions as PF -from . import json_rpc as js, playqueue as PQ, playlist_func as PL -from . import backgroundthread, app, variables as v +from . import utils, timing, plex_functions as PF, json_rpc as js +from . import playqueue as PQ, backgroundthread, app, variables as v LOG = getLogger('PLEX.kodimonitor') @@ -256,8 +255,8 @@ def _check_playing_item(self, data): position = info['position'] if info['position'] != -1 else 0 kodi_playlist = js.playlist_get_items(self.playerid) LOG.debug('Current Kodi playlist: %s', kodi_playlist) - playlistitem = PL.PlaylistItem(kodi_item=kodi_playlist[position]) - if isinstance(self.playqueue.items[0], PL.PlaylistItemDummy): + playlistitem = PQ.PlaylistItem(kodi_item=kodi_playlist[position]) + if isinstance(self.playqueue.items[0], PQ.PlaylistItemDummy): # This dummy item will be deleted by webservice soon - it won't # play LOG.debug('Dummy item detected') @@ -328,8 +327,10 @@ def on_play(self, data): 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: diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 00a088351..974c2ba74 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -5,7 +5,7 @@ from .plex_api import API from . import utils, context_entry, transfer, backgroundthread, variables as v -from . import app, plex_functions as PF, playqueue as PQ, playlist_func as PL +from . import app, plex_functions as PF, playqueue as PQ ############################################################################### @@ -81,7 +81,7 @@ def process_indirect(key, offset, resolve=True): playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() - item = PL.PlaylistItem(xml_video_element=xml[0]) + item = PQ.PlaylistItem(xml_video_element=xml[0]) item.offset = offset item.playmethod = 'DirectStream' diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py deleted file mode 100644 index 981c086fd..000000000 --- a/resources/lib/playlist_func.py +++ /dev/null @@ -1,1337 +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 -import threading - -from .plex_api import API -from .plex_db import PlexDB -from . import plex_functions as PF -from .playutils import PlayUtils -from .kodi_db import kodiid_from_filename, KodiVideoDB -from .downloadutils import DownloadUtils as DU -from . import utils, json_rpc as js, variables as v, app, widgets -from .windows.resume import resume_dialog - -############################################################################### - -LOG = getLogger('PLEX.playlist_func') - -############################################################################### - - -class PlaylistError(Exception): - """ - Exception for our playlist constructs - """ - pass - - -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 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 - """ - LOG.debug('Play called with plex_id %s, plex_type %s, position %s, ' - 'synched %s, force_transcode %s, startpos %s', plex_id, - plex_type, position, synched, force_transcode, startpos) - resolve = False - try: - if plex_id == self.items[startpos].plex_id: - resolve = True - except IndexError: - pass - if resolve: - LOG.info('Resolving playback') - self._resolve(plex_id, startpos) - else: - LOG.info('Initializing playback') - self.init(plex_id, - plex_type, - startpos, - position, - synched, - force_transcode) - - def _resolve(self, plex_id, startpos): - """ - The Plex playqueue has already been initialized. We resolve the path - from original webservice http://127.0.0.1 to the "correct" Plex one - """ - playlistitem = self.items[startpos] - # Add an additional item with the resolved path after the current one - self.index = startpos + 1 - xml = PF.GetPlexMetadata(plex_id) - if xml in (None, 401): - raise PlaylistError('Could not get Plex metadata %s for %s', - plex_id, self.items[startpos]) - api = API(xml[0]) - if playlistitem.resume is None: - # Potentially ask user to resume - resume = self._resume_playback(None, xml[0]) - else: - # Do NOT ask user - resume = playlistitem.resume - # Use the original playlistitem to retain all info! - self._kodi_add_xml(xml[0], - api, - resume, - playlistitem=playlistitem) - # Add additional file parts, if any exist - self._add_additional_parts(xml) - # Note: the CURRENT playlistitem will be deleted through webservice.py - # once the path resolution has completed - - def init(self, plex_id, plex_type=None, startpos=None, position=None, - synched=True, force_transcode=None): - """ - Initializes the Plex and PKC playqueue for playback - """ - self.index = position - while len(self.items) < self.kodi_pl.size(): - # The original item that Kodi put into the playlist, e.g. - # { - # u'title': u'', - # u'type': u'unknown', - # u'file': u'http://127.0.0.1:57578/plex/kodi/....', - # u'label': u'' - # } - # We CANNOT delete that item right now - so let's add a dummy - # on the PKC side to keep all indicees lined up. - # The failing item will be deleted in webservice.py - LOG.debug('Adding a dummy item to our playqueue') - self.items.insert(0, PlaylistItemDummy()) - self.force_transcode = force_transcode - if synched: - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_id(plex_id, plex_type) - else: - db_item = None - if db_item: - xml = None - section_uuid = db_item['section_uuid'] - plex_type = db_item['plex_type'] - else: - xml = PF.GetPlexMetadata(plex_id) - if xml in (None, 401): - raise PlaylistError('Could not get Plex metadata %s', plex_id) - section_uuid = xml.get('librarySectionUUID') - api = API(xml[0]) - plex_type = api.plex_type() - resume = self._resume_playback(db_item, xml) - trailers = False - if (not resume and plex_type == v.PLEX_TYPE_MOVIE and - utils.settings('enableCinema') == 'true'): - if utils.settings('askCinema') == "true": - # "Play trailers?" - trailers = utils.yesno_dialog(utils.lang(29999), - utils.lang(33016)) or False - else: - trailers = True - LOG.debug('Playing trailers: %s', trailers) - xml = PF.init_plex_playqueue(plex_id, - section_uuid, - plex_type=plex_type, - trailers=trailers) - if xml is None: - LOG.error('Could not get playqueue for plex_id %s UUID %s for %s', - plex_id, section_uuid, self) - raise PlaylistError('Could not get playqueue') - # See that we add trailers, if they exist in the xml return - self._add_intros(xml) - # Add the main item after the trailers - # Look at the LAST item - api = API(xml[-1]) - self._kodi_add_xml(xml[-1], api, resume) - # Add additional file parts, if any exist - self._add_additional_parts(xml) - self.update_details_from_xml(xml) - - @staticmethod - def _resume_playback(db_item=None, xml=None): - ''' - Pass in either db_item or xml - Resume item if available. Returns bool or raise an PlayStrmException if - resume was cancelled by user. - ''' - resume = app.PLAYSTATE.resume_playback - app.PLAYSTATE.resume_playback = None - if app.PLAYSTATE.autoplay: - resume = False - LOG.info('Skip resume for autoplay') - elif resume is None: - if db_item: - with KodiVideoDB(lock=False) as kodidb: - resume = kodidb.get_resume(db_item['kodi_fileid']) - else: - api = API(xml) - resume = api.resume_point() - if resume: - resume = resume_dialog(resume) - LOG.info('User chose resume: %s', resume) - if resume is None: - raise PlaylistError('User backed out of resume dialog') - app.PLAYSTATE.autoplay = True - return resume - - def _add_intros(self, xml): - ''' - if we have any play them when the movie/show is not being resumed. - ''' - if not len(xml) > 1: - LOG.debug('No trailers returned from the PMS') - return - for i, intro in enumerate(xml): - if i + 1 == len(xml): - # The main item we're looking at - skip! - break - api = API(intro) - LOG.debug('Adding trailer: %s', api.title()) - self._kodi_add_xml(intro, api) - - def _add_additional_parts(self, xml): - ''' Create listitems and add them to the stack of playlist. - ''' - api = API(xml[0]) - for part, _ in enumerate(xml[0][0]): - if part == 0: - # The first part that we've already added - continue - api.set_part_number(part) - LOG.debug('Adding addional part for %s: %s', api.title(), part) - self._kodi_add_xml(xml[0], api) - - def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): - if not playlistitem: - playlistitem = PlaylistItem(xml_video_element=xml) - playlistitem.part = api.part - playlistitem.force_transcode = self.force_transcode - listitem = widgets.get_listitem(xml, resume=True) - listitem.setSubtitles(api.cache_external_subs()) - play = PlayUtils(api, playlistitem) - url = play.getPlayUrl() - listitem.setPath(url.encode('utf-8')) - self.kodi_add_item(playlistitem, self.index, listitem) - self.items.insert(self.index, playlistitem) - self.index += 1 - - def update_details_from_xml(self, xml): - """ - Updates the playlist details from the xml provided - """ - self.id = utils.cast(int, xml.get('%sID' % self.kind)) - self.version = utils.cast(int, xml.get('%sVersion' % self.kind)) - self.shuffled = utils.cast(int, xml.get('%sShuffled' % self.kind)) - self.selectedItemID = utils.cast(int, - xml.get('%sSelectedItemID' % self.kind)) - self.selectedItemOffset = utils.cast(int, - xml.get('%sSelectedItemOffset' - % self.kind)) - LOG.debug('Updated playlist from xml: %s', self) - - def add_item(self, item, pos, listitem=None): - """ - Adds a PlaylistItem to both Kodi and Plex at position pos [int] - Also changes self.items - Raises PlaylistError - """ - self.kodi_add_item(item, pos, listitem) - self.plex_add_item(item, pos) - - def kodi_add_item(self, item, pos, listitem=None): - """ - Adds a PlaylistItem to Kodi only. Will not change self.items - Raises PlaylistError - """ - if not isinstance(item, PlaylistItem): - raise PlaylistError('Wrong item %s of type %s received' - % (item, type(item))) - if pos > len(self.items): - raise PlaylistError('Position %s too large for playlist length %s' - % (pos, len(self.items))) - LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item) - if listitem: - self.kodi_pl.add(url=listitem.getPath(), - listitem=listitem, - index=pos) - elif item.kodi_id is not None and item.kodi_type is not None: - # This method ensures we have full Kodi metadata, potentially - # with more artwork, for example, than Plex provides - if pos == len(self.items): - answ = js.playlist_add(self.playlistid, - {'%sid' % item.kodi_type: item.kodi_id}) - else: - answ = js.playlist_insert({'playlistid': self.playlistid, - 'position': pos, - 'item': {'%sid' % item.kodi_type: item.kodi_id}}) - if 'error' in answ: - raise PlaylistError('Kodi did not add item to playlist: %s', - answ) - else: - if item.xml is None: - LOG.debug('Need to get metadata for item %s', item) - item.xml = PF.GetPlexMetadata(item.plex_id) - if item.xml in (None, 401): - raise PlaylistError('Could not get metadata for %s', item) - api = API(item.xml[0]) - listitem = widgets.get_listitem(item.xml, resume=True) - url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT - args = { - 'plex_id': item.plex_id, - 'plex_type': api.plex_type() - } - if item.force_transcode: - args['transcode'] = 'true' - url = utils.extend_url(url, args) - item.file = url - listitem.setPath(url.encode('utf-8')) - self.kodi_pl.add(url=url.encode('utf-8'), - listitem=listitem, - index=pos) - - def plex_add_item(self, item, pos): - """ - Adds a new PlaylistItem to the playlist at position pos [int] only on - the Plex side of things. Also changes self.items - Raises PlaylistError - """ - if not isinstance(item, PlaylistItem) or not item.uri: - raise PlaylistError('Wrong item %s of type %s received' - % (item, type(item))) - if pos > len(self.items): - raise PlaylistError('Position %s too large for playlist length %s' - % (pos, len(self.items))) - LOG.debug('Adding item to Plex playlist at position %s: %s', pos, item) - url = '{server}/%ss/%s?uri=%s' % (self.kind, self.id, item.uri) - # Will usually put the new item at the end of the Plex playlist - xml = DU().downloadUrl(url, action_type='PUT') - try: - xml[0].attrib - except (TypeError, AttributeError, KeyError, IndexError): - raise PlaylistError('Could not add item %s to playlist %s' - % (item, self)) - for actual_pos, xml_video_element in enumerate(xml): - api = API(xml_video_element) - if api.plex_id() == item.plex_id: - break - else: - raise PlaylistError('Something went wrong - Plex id not found') - item.from_xml(xml[actual_pos]) - self.items.insert(actual_pos, item) - self.update_details_from_xml(xml) - if actual_pos != pos: - self.plex_move_item(actual_pos, pos) - LOG.debug('Added item %s on Plex side: %s', item, self) - - def kodi_remove_item(self, pos): - """ - Only manipulates the Kodi playlist. Won't change self.items - """ - LOG.debug('Removing position %s on the Kodi side for %s', pos, self) - answ = js.playlist_remove(self.playlistid, pos) - if 'error' in answ: - raise PlaylistError('Could not remove item: %s' % answ['error']) - - def plex_move_item(self, before, after): - """ - Moves playlist item from before [int] to after [int] for Plex only. - - Will also change self.items - """ - if before > len(self.items) or after > len(self.items) or after == before: - raise PlaylistError('Illegal original position %s and/or desired ' - 'position %s for playlist length %s' % - (before, after, len(self.items))) - LOG.debug('Moving item from %s to %s on the Plex side for %s', - before, after, self) - if after == 0: - url = "{server}/%ss/%s/items/%s/move?after=0" % \ - (self.kind, - self.id, - self.items[before].id) - elif after > before: - url = "{server}/%ss/%s/items/%s/move?after=%s" % \ - (self.kind, - self.id, - self.items[before].id, - self.items[after].id) - else: - url = "{server}/%ss/%s/items/%s/move?after=%s" % \ - (self.kind, - self.id, - self.items[before].id, - self.items[after - 1].id) - xml = DU().downloadUrl(url, action_type="PUT") - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - raise PlaylistError('Could not move playlist item from %s to %s ' - 'for %s' % (before, after, self)) - self.update_details_from_xml(xml) - self.items.insert(after, self.items.pop(before)) - LOG.debug('Done moving items for %s', self) - - def init_from_xml(self, xml, offset=None, start_plex_id=None, repeat=None, - transient_token=None): - """ - Play all items contained in the xml passed in. Called by Plex Companion. - Either supply the ratingKey of the starting Plex element. Or set - playqueue.selectedItemID - - offset [float]: will seek to position offset after playback start - start_plex_id [int]: the plex_id of the element that should be - played - repeat [int]: 0: don't repear - 1: repeat item - 2: repeat everything - transient_token [unicode]: temporary token received from the PMS - - Will stop current playback and start playback at the end - """ - LOG.debug("init_from_xml called with offset %s, start_plex_id %s", - offset, start_plex_id) - app.APP.player.stop() - self.clear() - self.update_details_from_xml(xml) - self.repeat = 0 if not repeat else repeat - self.plex_transient_token = transient_token - for pos, xml_video_element in enumerate(xml): - playlistitem = PlaylistItem(xml_video_element=xml_video_element) - self.kodi_add_item(playlistitem, pos) - self.items.append(playlistitem) - # Where do we start playback? - if start_plex_id is not None: - for startpos, item in enumerate(self.items): - if item.plex_id == start_plex_id: - break - else: - startpos = 0 - else: - for startpos, item in enumerate(self.items): - if item.id == self.selectedItemID: - break - else: - startpos = 0 - # Set resume for the item we should play - do NOT ask user since we - # initiated from the other Companion client - self.items[startpos].resume = True if offset else False - self.start_playback(pos=startpos, offset=offset) - - def start_playback(self, pos=0, offset=0): - """ - Seek immediately after kicking off playback is not reliable. - Threaded, since we need to return BEFORE seeking - """ - LOG.info('Starting playback at %s offset %s for %s', pos, offset, self) - thread = threading.Thread(target=self._threaded_playback, - args=(self.kodi_pl, pos, offset)) - thread.start() - - @staticmethod - def _threaded_playback(kodi_playlist, pos, offset): - app.APP.player.play(kodi_playlist, startpos=pos, windowed=False) - if offset: - i = 0 - while not app.APP.is_playing: - app.APP.monitor.waitForAbort(0.1) - i += 1 - if i > 50: - LOG.warn('Could not seek to %s', offset) - return - js.seek_to(offset) - - -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 - 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 - if kodi_item: - self.kodi_id = kodi_item['id'] - self.kodi_type = kodi_item['type'] - self.file = kodi_item.get('file') - else: - self.kodi_id = kodi_id - self.kodi_type = kodi_type - self.file = None - self.uri = None - self.guid = None - self.xml = None - self.playmethod = None - self.playcount = None - self.offset = None - 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 grab_xml and plex_id is not None and xml_video_element is None: - xml_video_element = PF.GetPlexMetadata(plex_id) - try: - xml_video_element = xml_video_element[0] - except (TypeError, IndexError): - xml_video_element = None - if xml_video_element is not None: - self.from_xml(xml_video_element) - if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and - self.plex_type != v.PLEX_TYPE_CLIP): - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_id(self.plex_id, self.plex_type) - if db_item is not None: - self.kodi_id = db_item['kodi_id'] - self.kodi_type = db_item['kodi_type'] - self.plex_uuid = db_item['section_uuid'] - self.set_uri() - - def __eq__(self, other): - if self.plex_id is not None and other.plex_id is not None: - return self.plex_id == other.plex_id - elif (self.kodi_id is not None and other.kodi_id is not None and - self.kodi_type and other.kodi_type): - return (self.kodi_id == other.kodi_id and - self.kodi_type == other.kodi_type) - elif self.file and other.file: - return self.file == other.file - raise RuntimeError('PlaylistItems not fully defined: %s, %s' % - (self, other)) - - def __ne__(self, other): - return not self == other - - def __unicode__(self): - return ("{{" - "'name': '{self.name}', " - "'id': {self.id}, " - "'plex_id': {self.plex_id}, " - "'plex_type': '{self.plex_type}', " - "'kodi_id': {self.kodi_id}, " - "'kodi_type': '{self.kodi_type}', " - "'file': '{self.file}', " - "'uri': '{self.uri}', " - "'guid': '{self.guid}', " - "'playmethod': '{self.playmethod}', " - "'playcount': {self.playcount}, " - "'offset': {self.offset}, " - "'force_transcode': {self.force_transcode}, " - "'part': {self.part}" - "}}".format(self=self)) - - def __str__(self): - return unicode(self).encode('utf-8') - __repr__ = __str__ - - def from_xml(self, xml_video_element): - """ - xml_video_element: etree xml piece 1 level underneath - item.id will only be set if you passed in an xml_video_element from - e.g. a playQueue - """ - api = API(xml_video_element) - self.name = api.title() - self.plex_id = api.plex_id() - self.plex_type = api.plex_type() - self.id = api.item_id() - self.guid = api.guid_html_escaped() - self.playcount = api.viewcount() - self.offset = api.resume_point() - self.xml = xml_video_element - - def from_kodi(self, playlist_item): - """ - playlist_item: dict contains keys 'id', 'type', 'file' (if applicable) - - Will thus set the attributes kodi_id, kodi_type, file, if applicable - If kodi_id & kodi_type are provided, plex_id and plex_type will be - looked up (if not already set) - """ - self.kodi_id = playlist_item.get('id') - self.kodi_type = playlist_item.get('type') - self.file = playlist_item.get('file') - if self.plex_id is None and self.kodi_id is not None and self.kodi_type: - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) - if db_item: - self.plex_id = db_item['plex_id'] - self.plex_type = db_item['plex_type'] - self.plex_uuid = db_item['section_uuid'] - if self.plex_id is None and self.file is not None: - try: - query = self.file.split('?', 1)[1] - except IndexError: - query = '' - query = dict(utils.parse_qsl(query)) - self.plex_id = utils.cast(int, query.get('plex_id')) - self.plex_type = query.get('itemType') - self.set_uri() - LOG.debug('Made playlist item from Kodi: %s', self) - - def set_uri(self): - if self.plex_id is None and self.file is not None: - self.uri = ('library://whatever/item/%s' - % utils.quote(self.file, safe='')) - elif self.plex_id is not None and self.plex_uuid is not None: - # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER - self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (self.plex_uuid, self.plex_id)) - elif self.plex_id is not None: - self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (self.plex_id, self.plex_id)) - else: - self.uri = None - - def plex_stream_index(self, kodi_stream_index, stream_type): - """ - Pass in the kodi_stream_index [int] in order to receive the Plex stream - index. - - stream_type: 'video', 'audio', 'subtitle' - - Returns None if unsuccessful - """ - stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] - count = 0 - if kodi_stream_index == -1: - # Kodi telling us "it's the last one" - iterator = list(reversed(self.xml[0][self.part])) - kodi_stream_index = 0 - else: - iterator = self.xml[0][self.part] - # Kodi indexes differently than Plex - for stream in iterator: - if (stream.attrib['streamType'] == stream_type and - 'key' in stream.attrib): - if count == kodi_stream_index: - return stream.attrib['id'] - count += 1 - for stream in iterator: - if (stream.attrib['streamType'] == stream_type and - 'key' not in stream.attrib): - if count == kodi_stream_index: - return stream.attrib['id'] - count += 1 - - def kodi_stream_index(self, plex_stream_index, stream_type): - """ - Pass in the kodi_stream_index [int] in order to receive the Plex stream - index. - - stream_type: 'video', 'audio', 'subtitle' - - Returns None if unsuccessful - """ - stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] - count = 0 - for stream in self.xml[0][self.part]: - if (stream.attrib['streamType'] == stream_type and - 'key' in stream.attrib): - if stream.attrib['id'] == plex_stream_index: - return count - count += 1 - for stream in self.xml[0][self.part]: - if (stream.attrib['streamType'] == stream_type and - 'key' not in stream.attrib): - if stream.attrib['id'] == plex_stream_index: - return count - count += 1 - - -class PlaylistItemDummy(PlaylistItem): - """ - Let e.g. Kodimonitor detect that this is a dummy item - """ - def __init__(self, *args, **kwargs): - super(PlaylistItemDummy, self).__init__(*args, **kwargs) - self.name = 'dummy item' - self.id = 0 - self.plex_id = 0 - - -def playlist_item_from_kodi(kodi_item): - """ - Turns the JSON answer from Kodi into a playlist element - - Supply with data['item'] as returned from Kodi JSON-RPC interface. - kodi_item dict contains keys 'id', 'type', 'file' (if applicable) - """ - item = PlaylistItem() - item.kodi_id = kodi_item.get('id') - item.kodi_type = kodi_item.get('type') - if item.kodi_id: - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_kodi_id(kodi_item['id'], kodi_item['type']) - if db_item: - item.plex_id = db_item['plex_id'] - item.plex_type = db_item['plex_type'] - item.plex_uuid = db_item['section_uuid'] - item.file = kodi_item.get('file') - if item.plex_id is None and item.file is not None: - try: - 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 = PlaylistItem() - item.plex_id = plex_id - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_id(plex_id) - if db_item: - item.plex_type = db_item['plex_type'] - item.kodi_id = db_item['kodi_id'] - item.kodi_type = db_item['kodi_type'] - else: - raise KeyError('Could not find plex_id %s in database' % plex_id) - 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 - """ - item = PlaylistItem() - api = API(xml_video_element) - item.plex_id = api.plex_id() - item.plex_type = api.plex_type() - # item.id will only be set if you passed in an xml_video_element from e.g. - # a playQueue - item.id = api.item_id() - if kodi_id is not None and kodi_type is not None: - item.kodi_id = kodi_id - item.kodi_type = 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 - """ - LOG.debug('Adding item to Plex playqueue with plex id %s, kodi_item %s at ' - 'position %s', plex_id, kodi_item, pos) - verify_kodi_item(plex_id, kodi_item) - if plex_id: - item = playlist_item_from_plex(plex_id) - else: - item = playlist_item_from_kodi(kodi_item) - url = '{server}/%ss/%s?uri=%s' % (playlist.kind, playlist.id, item.uri) - # 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)) - if len(xml) != len(playlist.items) + 1: - raise PlaylistError('Couldnt add item %s to playlist %s - wrong length' - % (kodi_item, playlist)) - for actual_pos, xml_video_element in enumerate(xml): - api = API(xml_video_element) - if api.plex_id() == item.plex_id: - break - else: - raise PlaylistError('Something went terribly wrong!') - utils.dump_xml(xml) - LOG.debug('Plex added the new item at position %s', actual_pos) - item.xml = xml[actual_pos] - item.id = api.item_id() - item.guid = api.guid_html_escaped() - item.offset = api.resume_point() - item.playcount = api.viewcount() - playlist.items.insert(actual_pos, item) - _get_playListVersion_from_xml(playlist, xml) - if actual_pos != pos: - # Move the new item to the correct position - move_playlist_item(playlist, actual_pos, 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 - xml = DU().downloadUrl(url, action_type="PUT") - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not move playlist item') - return - _get_playListVersion_from_xml(playlist, xml) - utils.dump_xml(xml) - # Move our item's position in our internal playlist - playlist.items.insert(after_pos, playlist.items.pop(before_pos)) - LOG.debug('Done moving for %s', playlist) - - -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 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') diff --git a/resources/lib/playqueue/__init__.py b/resources/lib/playqueue/__init__.py new file mode 100644 index 000000000..88cdade04 --- /dev/null +++ b/resources/lib/playqueue/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly +""" +from __future__ import absolute_import, division, unicode_literals + +from .common import PlaylistItem, PlaylistItemDummy, PlaylistError, PLAYQUEUES +from .playqueue import PlayQueue +from .monitor import PlayqueueMonitor +from .functions import init_playqueues, get_playqueue_from_type, \ + playqueue_from_plextype, playqueue_from_id, get_PMS_playlist, \ + init_playqueue_from_plex_children, get_pms_playqueue, \ + get_plextype_from_xml diff --git a/resources/lib/playqueue/common.py b/resources/lib/playqueue/common.py new file mode 100644 index 000000000..37c5d1b82 --- /dev/null +++ b/resources/lib/playqueue/common.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +from ..plex_db import PlexDB +from ..plex_api import API +from .. import plex_functions as PF, utils, kodi_db, variables as v, app + +LOG = getLogger('PLEX.playqueue') + +# Our PKC playqueues (3 instances PlayQueue()) +PLAYQUEUES = [] + + +class PlaylistError(Exception): + """ + Exception for our playlist constructs + """ + pass + + +class PlaylistItem(object): + """ + Object to fill our playqueues and playlists with. + + id = None [int] Plex playlist/playqueue id, e.g. playQueueItemID + plex_id = None [int] Plex unique item id, "ratingKey" + plex_type = None [str] Plex type, e.g. 'movie', 'clip' + plex_uuid = None [str] Plex librarySectionUUID + kodi_id = None [int] Kodi unique kodi id (unique only within type!) + kodi_type = None [str] Kodi type: 'movie' + file = None [str] Path to the item's file. STRING!! + uri = None [str] Weird Plex uri path involving plex_uuid. STRING! + guid = None [str] Weird Plex guid + xml = None [etree] XML from PMS, 1 lvl below + playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' + playcount = None [int] how many times the item has already been played + offset = None [int] the item's view offset UPON START in Plex time + part = 0 [int] part number if Plex video consists of mult. parts + force_transcode [bool] defaults to False + + PlaylistItem compare as equal, if they + - have the same plex_id + - OR: have the same kodi_id AND kodi_type + - OR: have the same file + """ + def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, + kodi_id=None, kodi_type=None, kodi_item=None, grab_xml=False, + lookup_kodi=True): + """ + Pass grab_xml=True in order to get Plex metadata from the PMS while + passing a plex_id. + Pass lookup_kodi=False to NOT check the plex.db for kodi_id and + kodi_type if they're missing (won't be done for clips anyway) + """ + self.name = None + self.id = None + self.plex_id = plex_id + self.plex_type = plex_type + self.plex_uuid = None + self.kodi_id = kodi_id + self.kodi_type = kodi_type + self.file = None + if kodi_item: + self.kodi_id = kodi_item.get('id') + self.kodi_type = kodi_item.get('type') + self.file = kodi_item.get('file') + self.uri = None + self.guid = None + self.xml = None + self.playmethod = None + self.playcount = None + self.offset = None + self.part = 0 + self.force_transcode = False + # Shall we ask user to resume this item? + # None: ask user to resume + # False: do NOT resume, don't ask user + # True: do resume, don't ask user + self.resume = None + if (self.plex_id is None and + (self.kodi_id is not None and self.kodi_type is not None)): + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) + if db_item: + self.plex_id = db_item['plex_id'] + self.plex_type = db_item['plex_type'] + self.plex_uuid = db_item['section_uuid'] + if grab_xml and plex_id is not None and xml_video_element is None: + xml_video_element = PF.GetPlexMetadata(plex_id) + try: + xml_video_element = xml_video_element[0] + except (TypeError, IndexError): + xml_video_element = None + if xml_video_element is not None: + self.from_xml(xml_video_element) + if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and + self.plex_type != v.PLEX_TYPE_CLIP): + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_id(self.plex_id, self.plex_type) + if db_item is not None: + self.kodi_id = db_item['kodi_id'] + self.kodi_type = db_item['kodi_type'] + self.plex_uuid = db_item['section_uuid'] + if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and + self.plex_type != v.PLEX_TYPE_CLIP): + self._guess_id_from_file() + self.set_uri() + + def __eq__(self, other): + if self.plex_id is not None and other.plex_id is not None: + return self.plex_id == other.plex_id + elif (self.kodi_id is not None and other.kodi_id is not None and + self.kodi_type and other.kodi_type): + return (self.kodi_id == other.kodi_id and + self.kodi_type == other.kodi_type) + elif self.file and other.file: + return self.file == other.file + raise RuntimeError('PlaylistItems not fully defined: %s, %s' % + (self, other)) + + def __ne__(self, other): + return not self == other + + def __unicode__(self): + return ("{{" + "'name': '{self.name}', " + "'id': {self.id}, " + "'plex_id': {self.plex_id}, " + "'plex_type': '{self.plex_type}', " + "'kodi_id': {self.kodi_id}, " + "'kodi_type': '{self.kodi_type}', " + "'file': '{self.file}', " + "'uri': '{self.uri}', " + "'guid': '{self.guid}', " + "'playmethod': '{self.playmethod}', " + "'playcount': {self.playcount}, " + "'offset': {self.offset}, " + "'force_transcode': {self.force_transcode}, " + "'part': {self.part}" + "}}".format(self=self)) + + def __str__(self): + return unicode(self).encode('utf-8') + __repr__ = __str__ + + def from_xml(self, xml_video_element): + """ + xml_video_element: etree xml piece 1 level underneath + item.id will only be set if you passed in an xml_video_element from + e.g. a playQueue + """ + api = API(xml_video_element) + self.name = api.title() + self.plex_id = api.plex_id() + self.plex_type = api.plex_type() + self.id = api.item_id() + self.guid = api.guid_html_escaped() + self.playcount = api.viewcount() + self.offset = api.resume_point() + self.xml = xml_video_element + self.set_uri() + + def from_kodi(self, playlist_item): + """ + playlist_item: dict contains keys 'id', 'type', 'file' (if applicable) + + Will thus set the attributes kodi_id, kodi_type, file, if applicable + If kodi_id & kodi_type are provided, plex_id and plex_type will be + looked up (if not already set) + """ + self.kodi_id = playlist_item.get('id') + self.kodi_type = playlist_item.get('type') + self.file = playlist_item.get('file') + if self.plex_id is None and self.kodi_id is not None and self.kodi_type: + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) + if db_item: + self.plex_id = db_item['plex_id'] + self.plex_type = db_item['plex_type'] + self.plex_uuid = db_item['section_uuid'] + if self.plex_id is None and self.file is not None: + try: + query = self.file.split('?', 1)[1] + except IndexError: + query = '' + query = dict(utils.parse_qsl(query)) + self.plex_id = utils.cast(int, query.get('plex_id')) + self.plex_type = query.get('itemType') + self.set_uri() + LOG.debug('Made playlist item from Kodi: %s', self) + + def set_uri(self): + if self.plex_id is None and self.file is not None: + self.uri = ('library://whatever/item/%s' + % utils.quote(self.file, safe='')) + elif self.plex_id is not None and self.plex_uuid is not None: + # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER + self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % + (self.plex_uuid, self.plex_id)) + elif self.plex_id is not None: + self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % + (self.plex_id, self.plex_id)) + else: + self.uri = None + + def _guess_id_from_file(self): + """ + """ + if not self.file: + return + # Special case playlist startup - got type but no id + if (not app.SYNC.direct_paths and app.SYNC.enable_music and + self.kodi_type == v.KODI_TYPE_SONG and + self.file.startswith('http')): + self.kodi_id, _ = kodi_db.kodiid_from_filename(self.file, + v.KODI_TYPE_SONG) + LOG.debug('Detected song. Research results: %s', self) + return + # Need more info since we don't have kodi_id nor type. Use file path. + if (self.file.startswith('plugin') or + (self.file.startswith('http') and not + self.file.startswith('http://127.0.0.1:%s' % v.WEBSERVICE_PORT))): + return + LOG.debug('Starting research for Kodi id since we didnt get one') + # Try the VIDEO DB first - will find both movies and episodes + self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file, + db_type='video') + if self.kodi_id is None: + # No movie or episode found - try MUSIC DB now for songs + self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file, + db_type='music') + self.kodi_type = None if self.kodi_id is None else self.kodi_type + LOG.debug('Research results for guessing Kodi id: %s', self) + + def plex_stream_index(self, kodi_stream_index, stream_type): + """ + Pass in the kodi_stream_index [int] in order to receive the Plex stream + index. + + stream_type: 'video', 'audio', 'subtitle' + + Returns None if unsuccessful + """ + stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] + count = 0 + if kodi_stream_index == -1: + # Kodi telling us "it's the last one" + iterator = list(reversed(self.xml[0][self.part])) + kodi_stream_index = 0 + else: + iterator = self.xml[0][self.part] + # Kodi indexes differently than Plex + for stream in iterator: + if (stream.attrib['streamType'] == stream_type and + 'key' in stream.attrib): + if count == kodi_stream_index: + return stream.attrib['id'] + count += 1 + for stream in iterator: + if (stream.attrib['streamType'] == stream_type and + 'key' not in stream.attrib): + if count == kodi_stream_index: + return stream.attrib['id'] + count += 1 + + def kodi_stream_index(self, plex_stream_index, stream_type): + """ + Pass in the kodi_stream_index [int] in order to receive the Plex stream + index. + + stream_type: 'video', 'audio', 'subtitle' + + Returns None if unsuccessful + """ + stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] + count = 0 + for stream in self.xml[0][self.part]: + if (stream.attrib['streamType'] == stream_type and + 'key' in stream.attrib): + if stream.attrib['id'] == plex_stream_index: + return count + count += 1 + for stream in self.xml[0][self.part]: + if (stream.attrib['streamType'] == stream_type and + 'key' not in stream.attrib): + if stream.attrib['id'] == plex_stream_index: + return count + count += 1 + + +class PlaylistItemDummy(PlaylistItem): + """ + Let e.g. Kodimonitor detect that this is a dummy item + """ + def __init__(self, *args, **kwargs): + super(PlaylistItemDummy, self).__init__(*args, **kwargs) + self.name = 'dummy item' + self.id = 0 + self.plex_id = 0 diff --git a/resources/lib/playqueue/functions.py b/resources/lib/playqueue/functions.py new file mode 100644 index 000000000..5fc262045 --- /dev/null +++ b/resources/lib/playqueue/functions.py @@ -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') diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue/monitor.py similarity index 56% rename from resources/lib/playqueue.py rename to resources/lib/playqueue/monitor.py index ccd0fa9f4..957cc9baf 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue/monitor.py @@ -7,113 +7,11 @@ from logging import getLogger import copy -import xbmc +from .common import PlaylistError, PlaylistItem, PLAYQUEUES +from .. import backgroundthread, json_rpc as js, utils, app -from .plex_api import API -from . import playlist_func as PL, plex_functions as PF -from . import backgroundthread, utils, json_rpc as js, app, variables as v -############################################################################### -LOG = getLogger('PLEX.playqueue') - -PLUGIN = 'plugin://%s' % v.ADDON_ID - -# Our PKC playqueues (3 instances PlayQueue()) -PLAYQUEUES = [] -############################################################################### - - -def init_playqueues(): - """ - Call this once on startup to initialize the PKC playqueue objects in - the list PLAYQUEUES - """ - if PLAYQUEUES: - LOG.debug('Playqueues have already been initialized') - return - # Initialize Kodi playqueues - with app.APP.lock_playqueues: - for i in (0, 1, 2): - # Just in case the Kodi response is not sorted correctly - for queue in js.get_playlists(): - if queue['playlistid'] != i: - continue - playqueue = PL.PlayQueue() - playqueue.playlistid = i - playqueue.type = queue['type'] - # Initialize each Kodi playlist - if playqueue.type == v.KODI_TYPE_AUDIO: - playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - elif playqueue.type == v.KODI_TYPE_VIDEO: - playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - else: - # Currently, only video or audio playqueues available - playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - # Overwrite 'picture' with 'photo' - playqueue.type = v.KODI_TYPE_PHOTO - PLAYQUEUES.append(playqueue) - LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES) - - -def get_playqueue_from_type(kodi_playlist_type): - """ - Returns the playqueue according to the kodi_playlist_type ('video', - 'audio', 'picture') passed in - """ - for playqueue in PLAYQUEUES: - if playqueue.type == kodi_playlist_type: - break - else: - raise ValueError('Wrong playlist type passed in: %s' - % kodi_playlist_type) - return playqueue - - -def playqueue_from_plextype(plex_type): - if plex_type in v.PLEX_VIDEOTYPES: - plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST - elif plex_type in v.PLEX_AUDIOTYPES: - plex_type = v.PLEX_TYPE_AUDIO_PLAYLIST - else: - plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST - for playqueue in PLAYQUEUES: - if playqueue.type == plex_type: - break - return playqueue - - -def playqueue_from_id(kodi_playlist_id): - for playqueue in PLAYQUEUES: - if playqueue.playlistid == kodi_playlist_id: - break - else: - raise ValueError('Wrong playlist id passed in: %s of type %s' - % (kodi_playlist_id, type(kodi_playlist_id))) - return playqueue - - -def init_playqueue_from_plex_children(plex_id, transient_token=None): - """ - Init a new playqueue e.g. from an album. Alexa does this - - Returns the playqueue - """ - xml = PF.GetAllPlexChildren(plex_id) - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not download the PMS xml for %s', plex_id) - return - playqueue = get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - playqueue.clear() - for i, child in enumerate(xml): - api = API(child) - PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id()) - playqueue.plex_transient_token = transient_token - LOG.debug('Firing up Kodi player') - app.APP.player.play(playqueue.kodi_pl, None, False, 0) - return playqueue +LOG = getLogger('PLEX.playqueue_monitor') class PlayqueueMonitor(backgroundthread.KillableThread): @@ -169,26 +67,24 @@ def _compare_playqueues(self, playqueue, new_kodi_playqueue): LOG.debug('Playqueue item %s moved to position %s', i + j, i) try: - PL.move_playlist_item(playqueue, i + j, i) - except PL.PlaylistError: + playqueue.plex_move_item(i + j, i) + except PlaylistError: LOG.error('Could not modify playqueue positions') LOG.error('This is likely caused by mixing audio and ' 'video tracks in the Kodi playqueue') del old[j], index[j] break else: + playlistitem = PlaylistItem(kodi_item=new_item) LOG.debug('Detected new Kodi element at position %s: %s ', - i, new_item) + i, playlistitem) try: if playqueue.id is None: - PL.init_plex_playqueue(playqueue, kodi_item=new_item) + playqueue.init(playlistitem) else: - PL.add_item_to_plex_playqueue(playqueue, - i, - kodi_item=new_item) - except PL.PlaylistError: - # Could not add the element - pass + playqueue.plex_add_item(playlistitem, i) + except PlaylistError: + LOG.warn('Couldnt add new item to Plex: %s', playlistitem) except IndexError: # This is really a hack - happens when using Addon Paths # and repeatedly starting the same element. Kodi will then @@ -206,8 +102,8 @@ def _compare_playqueues(self, playqueue, new_kodi_playqueue): return LOG.debug('Detected deletion of playqueue element at pos %s', i) try: - PL.delete_playlist_item_from_PMS(playqueue, i) - except PL.PlaylistError: + playqueue.plex_remove_item(i) + except PlaylistError: LOG.error('Could not delete PMS element from position %s', i) LOG.error('This is likely caused by mixing audio and ' 'video tracks in the Kodi playqueue') diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py new file mode 100644 index 000000000..7114c75bd --- /dev/null +++ b/resources/lib/playqueue/playqueue.py @@ -0,0 +1,601 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly +""" +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger +import threading + +from .common import PlaylistItem, PlaylistItemDummy, PlaylistError + +from ..downloadutils import DownloadUtils as DU +from ..plex_api import API +from ..plex_db import PlexDB +from ..kodi_db import KodiVideoDB +from ..playutils import PlayUtils +from ..windows.resume import resume_dialog +from .. import plex_functions as PF, utils, widgets, variables as v, app +from .. import json_rpc as js + + +LOG = getLogger('PLEX.playqueue') + + +class PlayQueue(object): + """ + PKC object to represent PMS playQueues and Kodi playlist for queueing + + playlistid = None [int] Kodi playlist id (0, 1, 2) + type = None [str] Kodi type: 'audio', 'video', 'picture' + kodi_pl = None Kodi xbmc.PlayList object + items = [] [list] of PlaylistItem + id = None [str] Plex playQueueID, unique Plex identifier + version = None [int] Plex version of the playQueue + selectedItemID = None + [str] Plex selectedItemID, playing element in queue + selectedItemOffset = None + [str] Offset of the playing element in queue + shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ??? + repeat = 0 [int] 0: not repeated, 1: ??? 2: ??? + + If Companion playback is initiated by another user: + plex_transient_token = None + """ + kind = 'playQueue' + + def __init__(self): + self.id = None + self.type = None + self.playlistid = None + self.kodi_pl = None + self.items = [] + self.version = None + self.selectedItemID = None + self.selectedItemOffset = None + self.shuffled = 0 + self.repeat = 0 + self.plex_transient_token = None + # Need a hack for detecting swaps of elements + self.old_kodi_pl = [] + # Did PKC itself just change the playqueue so the PKC playqueue monitor + # should not pick up any changes? + self.pkc_edit = False + # Workaround to avoid endless loops of detecting PL clears + self._clear_list = [] + # To keep track if Kodi playback was initiated from a Kodi playlist + # There are a couple of pitfalls, unfortunately... + self.kodi_playlist_playback = False + # Playlist position/index used when initiating the playqueue + self.index = None + self.force_transcode = None + + def __unicode__(self): + return ("{{" + "'playlistid': {self.playlistid}, " + "'id': {self.id}, " + "'version': {self.version}, " + "'type': '{self.type}', " + "'items': {items}, " + "'selectedItemID': {self.selectedItemID}, " + "'selectedItemOffset': {self.selectedItemOffset}, " + "'shuffled': {self.shuffled}, " + "'repeat': {self.repeat}, " + "'kodi_playlist_playback': {self.kodi_playlist_playback}, " + "'pkc_edit': {self.pkc_edit}, " + "}}").format(**{ + 'items': ['%s/%s: %s' % (x.plex_id, x.id, x.name) + for x in self.items], + 'self': self + }) + + def __str__(self): + return unicode(self).encode('utf-8') + __repr__ = __str__ + + def is_pkc_clear(self): + """ + Returns True if PKC has cleared the Kodi playqueue just recently. + Then this clear will be ignored from now on + """ + try: + self._clear_list.pop() + except IndexError: + return False + else: + return True + + def clear(self, kodi=True): + """ + Resets the playlist object to an empty playlist. + + Pass kodi=False in order to NOT clear the Kodi playqueue + """ + # kodi monitor's on_clear method will only be called if there were some + # items to begin with + if kodi and self.kodi_pl.size() != 0: + self._clear_list.append(None) + self.kodi_pl.clear() # Clear Kodi playlist object + self.items = [] + self.id = None + self.version = None + self.selectedItemID = None + self.selectedItemOffset = None + self.shuffled = 0 + self.repeat = 0 + self.plex_transient_token = None + self.old_kodi_pl = [] + self.kodi_playlist_playback = False + self.index = None + self.force_transcode = None + LOG.debug('Playlist cleared: %s', self) + + def init(self, playlistitem): + """ + Hit if Kodi initialized playback and we need to catch up on the PKC + and Plex side; e.g. for direct paths. + + Kodi side will NOT be changed, e.g. no trailers will be added, but Kodi + playqueue taken as-is + """ + LOG.debug('Playqueue init called') + self.clear(kodi=False) + if not isinstance(playlistitem, PlaylistItem) or playlistitem.uri is None: + raise RuntimeError('Didnt receive a valid PlaylistItem but %s: %s' + % (type(playlistitem), playlistitem)) + try: + params = { + 'next': 0, + 'type': self.type, + 'uri': playlistitem.uri + } + xml = DU().downloadUrl(url="{server}/%ss" % self.kind, + action_type="POST", + parameters=params) + self.update_details_from_xml(xml) + # Need to update the details for the playlist item + playlistitem.from_xml(xml[0]) + except (KeyError, IndexError, TypeError): + LOG.error('Could not init Plex playlist with %s', playlistitem) + raise PlaylistError() + self.items.append(playlistitem) + LOG.debug('Initialized the playqueue on the Plex side: %s', self) + + def play(self, plex_id, plex_type=None, startpos=None, position=None, + synched=True, force_transcode=None): + """ + Initializes the playQueue with e.g. trailers and additional file parts + Pass synched=False if you're sure that this item has not been synched + to Kodi + + Or resolves webservice paths to actual paths + + Hit by webservice.py + """ + LOG.debug('Play called with plex_id %s, plex_type %s, position %s, ' + 'synched %s, force_transcode %s, startpos %s', plex_id, + plex_type, position, synched, force_transcode, startpos) + resolve = False + try: + if plex_id == self.items[startpos].plex_id: + resolve = True + except IndexError: + pass + if resolve: + LOG.info('Resolving playback') + self._resolve(plex_id, startpos) + else: + LOG.info('Initializing playback') + self._init(plex_id, + plex_type, + startpos, + position, + synched, + force_transcode) + + def _resolve(self, plex_id, startpos): + """ + The Plex playqueue has already been initialized. We resolve the path + from original webservice http://127.0.0.1 to the "correct" Plex one + """ + playlistitem = self.items[startpos] + # Add an additional item with the resolved path after the current one + self.index = startpos + 1 + xml = PF.GetPlexMetadata(plex_id) + if xml in (None, 401): + raise PlaylistError('Could not get Plex metadata %s for %s', + plex_id, self.items[startpos]) + api = API(xml[0]) + if playlistitem.resume is None: + # Potentially ask user to resume + resume = self._resume_playback(None, xml[0]) + else: + # Do NOT ask user + resume = playlistitem.resume + # Use the original playlistitem to retain all info! + self._kodi_add_xml(xml[0], + api, + resume, + playlistitem=playlistitem) + # Add additional file parts, if any exist + self._add_additional_parts(xml) + # Note: the CURRENT playlistitem will be deleted through webservice.py + # once the path resolution has completed + + def _init(self, plex_id, plex_type=None, startpos=None, position=None, + synched=True, force_transcode=None): + """ + Initializes the Plex and PKC playqueue for playback. Possibly adds + additionals trailers + """ + self.index = position + while len(self.items) < self.kodi_pl.size(): + # The original item that Kodi put into the playlist, e.g. + # { + # u'title': u'', + # u'type': u'unknown', + # u'file': u'http://127.0.0.1:57578/plex/kodi/....', + # u'label': u'' + # } + # We CANNOT delete that item right now - so let's add a dummy + # on the PKC side to keep all indicees lined up. + # The failing item will be deleted in webservice.py + LOG.debug('Adding a dummy item to our playqueue') + self.items.insert(0, PlaylistItemDummy()) + self.force_transcode = force_transcode + if synched: + with PlexDB(lock=False) as plexdb: + db_item = plexdb.item_by_id(plex_id, plex_type) + else: + db_item = None + if db_item: + xml = None + section_uuid = db_item['section_uuid'] + plex_type = db_item['plex_type'] + else: + xml = PF.GetPlexMetadata(plex_id) + if xml in (None, 401): + raise PlaylistError('Could not get Plex metadata %s', plex_id) + section_uuid = xml.get('librarySectionUUID') + api = API(xml[0]) + plex_type = api.plex_type() + resume = self._resume_playback(db_item, xml) + trailers = False + if (not resume and plex_type == v.PLEX_TYPE_MOVIE and + utils.settings('enableCinema') == 'true'): + if utils.settings('askCinema') == "true": + # "Play trailers?" + trailers = utils.yesno_dialog(utils.lang(29999), + utils.lang(33016)) or False + else: + trailers = True + LOG.debug('Playing trailers: %s', trailers) + xml = PF.init_plex_playqueue(plex_id, + section_uuid, + plex_type=plex_type, + trailers=trailers) + if xml is None: + LOG.error('Could not get playqueue for plex_id %s UUID %s for %s', + plex_id, section_uuid, self) + raise PlaylistError('Could not get playqueue') + # See that we add trailers, if they exist in the xml return + self._add_intros(xml) + # Add the main item after the trailers + # Look at the LAST item + api = API(xml[-1]) + self._kodi_add_xml(xml[-1], api, resume) + # Add additional file parts, if any exist + self._add_additional_parts(xml) + self.update_details_from_xml(xml) + + @staticmethod + def _resume_playback(db_item=None, xml=None): + ''' + Pass in either db_item or xml + Resume item if available. Returns bool or raise an PlayStrmException if + resume was cancelled by user. + ''' + resume = app.PLAYSTATE.resume_playback + app.PLAYSTATE.resume_playback = None + if app.PLAYSTATE.autoplay: + resume = False + LOG.info('Skip resume for autoplay') + elif resume is None: + if db_item: + with KodiVideoDB(lock=False) as kodidb: + resume = kodidb.get_resume(db_item['kodi_fileid']) + else: + api = API(xml) + resume = api.resume_point() + if resume: + resume = resume_dialog(resume) + LOG.info('User chose resume: %s', resume) + if resume is None: + raise PlaylistError('User backed out of resume dialog') + app.PLAYSTATE.autoplay = True + return resume + + def _add_intros(self, xml): + ''' + if we have any play them when the movie/show is not being resumed. + ''' + if not len(xml) > 1: + LOG.debug('No trailers returned from the PMS') + return + for i, intro in enumerate(xml): + if i + 1 == len(xml): + # The main item we're looking at - skip! + break + api = API(intro) + LOG.debug('Adding trailer: %s', api.title()) + self._kodi_add_xml(intro, api) + + def _add_additional_parts(self, xml): + ''' Create listitems and add them to the stack of playlist. + ''' + api = API(xml[0]) + for part, _ in enumerate(xml[0][0]): + if part == 0: + # The first part that we've already added + continue + api.set_part_number(part) + LOG.debug('Adding addional part for %s: %s', api.title(), part) + self._kodi_add_xml(xml[0], api) + + def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): + if not playlistitem: + playlistitem = PlaylistItem(xml_video_element=xml) + playlistitem.part = api.part + playlistitem.force_transcode = self.force_transcode + listitem = widgets.get_listitem(xml, resume=True) + listitem.setSubtitles(api.cache_external_subs()) + play = PlayUtils(api, playlistitem) + url = play.getPlayUrl() + listitem.setPath(url.encode('utf-8')) + self.kodi_add_item(playlistitem, self.index, listitem) + self.items.insert(self.index, playlistitem) + self.index += 1 + + def update_details_from_xml(self, xml): + """ + Updates the playlist details from the xml provided + """ + self.id = utils.cast(int, xml.get('%sID' % self.kind)) + self.version = utils.cast(int, xml.get('%sVersion' % self.kind)) + self.shuffled = utils.cast(int, xml.get('%sShuffled' % self.kind)) + self.selectedItemID = utils.cast(int, + xml.get('%sSelectedItemID' % self.kind)) + self.selectedItemOffset = utils.cast(int, + xml.get('%sSelectedItemOffset' + % self.kind)) + LOG.debug('Updated playlist from xml: %s', self) + + def add_item(self, item, pos, listitem=None): + """ + Adds a PlaylistItem to both Kodi and Plex at position pos [int] + Also changes self.items + Raises PlaylistError + """ + self.kodi_add_item(item, pos, listitem) + self.plex_add_item(item, pos) + + def kodi_add_item(self, item, pos, listitem=None): + """ + Adds a PlaylistItem to Kodi only. Will not change self.items + Raises PlaylistError + """ + if not isinstance(item, PlaylistItem): + raise PlaylistError('Wrong item %s of type %s received' + % (item, type(item))) + if pos > len(self.items): + raise PlaylistError('Position %s too large for playlist length %s' + % (pos, len(self.items))) + LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item) + if listitem: + self.kodi_pl.add(url=listitem.getPath(), + listitem=listitem, + index=pos) + elif item.kodi_id is not None and item.kodi_type is not None: + # This method ensures we have full Kodi metadata, potentially + # with more artwork, for example, than Plex provides + if pos == len(self.items): + answ = js.playlist_add(self.playlistid, + {'%sid' % item.kodi_type: item.kodi_id}) + else: + answ = js.playlist_insert({'playlistid': self.playlistid, + 'position': pos, + 'item': {'%sid' % item.kodi_type: item.kodi_id}}) + if 'error' in answ: + raise PlaylistError('Kodi did not add item to playlist: %s', + answ) + else: + if item.xml is None: + LOG.debug('Need to get metadata for item %s', item) + item.xml = PF.GetPlexMetadata(item.plex_id) + if item.xml in (None, 401): + raise PlaylistError('Could not get metadata for %s', item) + api = API(item.xml[0]) + listitem = widgets.get_listitem(item.xml, resume=True) + url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT + args = { + 'plex_id': item.plex_id, + 'plex_type': api.plex_type() + } + if item.force_transcode: + args['transcode'] = 'true' + url = utils.extend_url(url, args) + item.file = url + listitem.setPath(url.encode('utf-8')) + self.kodi_pl.add(url=url.encode('utf-8'), + listitem=listitem, + index=pos) + + def plex_add_item(self, item, pos): + """ + Adds a new PlaylistItem to the playlist at position pos [int] only on + the Plex side of things. Also changes self.items + Raises PlaylistError + """ + if not isinstance(item, PlaylistItem) or not item.uri: + raise PlaylistError('Wrong item %s of type %s received' + % (item, type(item))) + if pos > len(self.items): + raise PlaylistError('Position %s too large for playlist length %s' + % (pos, len(self.items))) + LOG.debug('Adding item to Plex playlist at position %s: %s', pos, item) + url = '{server}/%ss/%s?uri=%s' % (self.kind, self.id, item.uri) + # Will usually put the new item at the end of the Plex playlist + xml = DU().downloadUrl(url, action_type='PUT') + try: + xml[0].attrib + except (TypeError, AttributeError, KeyError, IndexError): + raise PlaylistError('Could not add item %s to playlist %s' + % (item, self)) + for actual_pos, xml_video_element in enumerate(xml): + api = API(xml_video_element) + if api.plex_id() == item.plex_id: + break + else: + raise PlaylistError('Something went wrong - Plex id not found') + item.from_xml(xml[actual_pos]) + self.items.insert(actual_pos, item) + self.update_details_from_xml(xml) + if actual_pos != pos: + self.plex_move_item(actual_pos, pos) + LOG.debug('Added item %s on Plex side: %s', item, self) + + def kodi_remove_item(self, pos): + """ + Only manipulates the Kodi playlist. Won't change self.items + """ + LOG.debug('Removing position %s on the Kodi side for %s', pos, self) + answ = js.playlist_remove(self.playlistid, pos) + if 'error' in answ: + raise PlaylistError('Could not remove item: %s' % answ['error']) + + def plex_remove_item(self, pos): + """ + Removes an item from Plex as well as our self.items item list + """ + LOG.debug('Deleting position %s on the Plex side for: %s', pos, self) + try: + xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" % + (self.kind, + self.id, + self.items[pos].id, + self.repeat), + action_type="DELETE") + self.update_details_from_xml(xml) + del self.items[pos] + except IndexError: + LOG.error('Could not delete item at position %s on the Plex side', + pos) + raise PlaylistError() + + def plex_move_item(self, before, after): + """ + Moves playlist item from before [int] to after [int] for Plex only. + + Will also change self.items + """ + if before > len(self.items) or after > len(self.items) or after == before: + raise PlaylistError('Illegal original position %s and/or desired ' + 'position %s for playlist length %s' % + (before, after, len(self.items))) + LOG.debug('Moving item from %s to %s on the Plex side for %s', + before, after, self) + if after == 0: + url = "{server}/%ss/%s/items/%s/move?after=0" % \ + (self.kind, + self.id, + self.items[before].id) + elif after > before: + url = "{server}/%ss/%s/items/%s/move?after=%s" % \ + (self.kind, + self.id, + self.items[before].id, + self.items[after].id) + else: + url = "{server}/%ss/%s/items/%s/move?after=%s" % \ + (self.kind, + self.id, + self.items[before].id, + self.items[after - 1].id) + xml = DU().downloadUrl(url, action_type="PUT") + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + raise PlaylistError('Could not move playlist item from %s to %s ' + 'for %s' % (before, after, self)) + self.update_details_from_xml(xml) + self.items.insert(after, self.items.pop(before)) + LOG.debug('Done moving items for %s', self) + + def init_from_xml(self, xml, offset=None, start_plex_id=None, repeat=None, + transient_token=None): + """ + Play all items contained in the xml passed in. Called by Plex Companion. + Either supply the ratingKey of the starting Plex element. Or set + playqueue.selectedItemID + + offset [float]: will seek to position offset after playback start + start_plex_id [int]: the plex_id of the element that should be + played + repeat [int]: 0: don't repear + 1: repeat item + 2: repeat everything + transient_token [unicode]: temporary token received from the PMS + + Will stop current playback and start playback at the end + """ + LOG.debug("init_from_xml called with offset %s, start_plex_id %s", + offset, start_plex_id) + app.APP.player.stop() + self.clear() + self.update_details_from_xml(xml) + self.repeat = 0 if not repeat else repeat + self.plex_transient_token = transient_token + for pos, xml_video_element in enumerate(xml): + playlistitem = PlaylistItem(xml_video_element=xml_video_element) + self.kodi_add_item(playlistitem, pos) + self.items.append(playlistitem) + # Where do we start playback? + if start_plex_id is not None: + for startpos, item in enumerate(self.items): + if item.plex_id == start_plex_id: + break + else: + startpos = 0 + else: + for startpos, item in enumerate(self.items): + if item.id == self.selectedItemID: + break + else: + startpos = 0 + # Set resume for the item we should play - do NOT ask user since we + # initiated from the other Companion client + self.items[startpos].resume = True if offset else False + self.start_playback(pos=startpos, offset=offset) + + def start_playback(self, pos=0, offset=0): + """ + Seek immediately after kicking off playback is not reliable. + Threaded, since we need to return BEFORE seeking + """ + LOG.info('Starting playback at %s offset %s for %s', pos, offset, self) + thread = threading.Thread(target=self._threaded_playback, + args=(self.kodi_pl, pos, offset)) + thread.start() + + @staticmethod + def _threaded_playback(kodi_playlist, pos, offset): + app.APP.player.play(kodi_playlist, startpos=pos, windowed=False) + if offset: + i = 0 + while not app.APP.is_playing: + app.APP.monitor.waitForAbort(0.1) + i += 1 + if i > 50: + LOG.warn('Could not seek to %s', offset) + return + js.seek_to(offset) diff --git a/resources/lib/playqueue/queue.py b/resources/lib/playqueue/queue.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index 55b7f338c..37e58f4c8 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -2,8 +2,7 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from . import app, utils, json_rpc, variables as v, playlist_func as PL, \ - playqueue as PQ +from . import app, utils, json_rpc, variables as v, playqueue as PQ LOG = getLogger('PLEX.playstrm') @@ -82,7 +81,7 @@ def play_folder(self, position=None): start_position = position or max(self.playqueue.kodi_pl.size(), 0) index = start_position + 1 LOG.info('Play folder plex_id %s, index: %s', self.plex_id, index) - item = PL.PlaylistItem(plex_id=self.plex_id, + item = PQ.PlaylistItem(plex_id=self.plex_id, plex_type=self.plex_type, kodi_id=self.kodi_id, kodi_type=self.kodi_type) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index a55d442be..8d2728f9b 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -14,7 +14,6 @@ from .plex_api import API from . import utils from . import plex_functions as PF -from . import playlist_func as PL from . import json_rpc as js from . import playqueue as PQ from . import variables as v @@ -48,10 +47,10 @@ def update_playqueue_from_PMS(playqueue, if transient_token is None: transient_token = playqueue.plex_transient_token with app.APP.lock_playqueues: - xml = PL.get_PMS_playlist(playlist_id=playqueue_id) + xml = PQ.get_PMS_playlist(playlist_id=playqueue_id) if xml is None: LOG.error('Could now download playqueue %s', playqueue_id) - raise PL.PlaylistError() + raise PQ.PlaylistError() app.PLAYSTATE.initiated_by_plex = True playqueue.init_from_xml(xml, offset=offset, @@ -82,7 +81,7 @@ def _process_alexa(data): xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata for: %s', data) - raise PL.PlaylistError() + raise PQ.PlaylistError() api = API(xml[0]) if api.plex_type() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') @@ -91,7 +90,7 @@ def _process_alexa(data): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download the album xml for %s', data) - raise PL.PlaylistError() + raise PQ.PlaylistError() playqueue = PQ.get_playqueue_from_type('audio') playqueue.init_from_xml(xml, transient_token=data.get('token')) @@ -100,7 +99,7 @@ def _process_alexa(data): xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key) if xml is None: LOG.error('Could not get playqueue for %s', data) - raise PL.PlaylistError() + raise PQ.PlaylistError() playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) offset = utils.cast(float, data.get('offset')) or None @@ -147,7 +146,7 @@ def _process_playlist(data): xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata') - raise PL.PlaylistError() + raise PQ.PlaylistError() api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) @@ -187,12 +186,12 @@ 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( @@ -235,7 +234,7 @@ def _process_tasks(self, task): self._process_refresh(data) elif task['action'] == 'setStreams': self._process_streams(data) - except PL.PlaylistError: + except PQ.PlaylistError: LOG.error('Could not process companion data: %s', data) # "Play Error" utils.dialog('notification', diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 400bd9b79..937a67030 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -16,7 +16,7 @@ from .plex_api import API from .plex_db import PlexDB from . import backgroundthread, utils, variables as v, app, playqueue as PQ -from . import playlist_func as PL, json_rpc as js, plex_functions as PF +from . import json_rpc as js, plex_functions as PF LOG = getLogger('PLEX.webservice') @@ -416,7 +416,7 @@ def run(self): break self.load_params(params) if play_folder: - playlistitem = PL.PlaylistItem(plex_id=self.plex_id, + playlistitem = PQ.PlaylistItem(plex_id=self.plex_id, plex_type=self.plex_type, kodi_id=self.kodi_id, kodi_type=self.kodi_type) From 3d4bde878e44de9f2af3bcc54e7b5220ddf044fb Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 25 May 2019 20:52:41 +0200 Subject: [PATCH 62/74] Cleanup --- resources/lib/playqueue/common.py | 9 +-------- resources/lib/playqueue/queue.py | 0 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 resources/lib/playqueue/queue.py diff --git a/resources/lib/playqueue/common.py b/resources/lib/playqueue/common.py index 37c5d1b82..4e6327c20 100644 --- a/resources/lib/playqueue/common.py +++ b/resources/lib/playqueue/common.py @@ -1,15 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals -from logging import getLogger from ..plex_db import PlexDB from ..plex_api import API from .. import plex_functions as PF, utils, kodi_db, variables as v, app -LOG = getLogger('PLEX.playqueue') - -# Our PKC playqueues (3 instances PlayQueue()) +# Our PKC playqueues (3 instances of PlayQueue()) PLAYQUEUES = [] @@ -189,7 +186,6 @@ def from_kodi(self, playlist_item): self.plex_id = utils.cast(int, query.get('plex_id')) self.plex_type = query.get('itemType') self.set_uri() - LOG.debug('Made playlist item from Kodi: %s', self) def set_uri(self): if self.plex_id is None and self.file is not None: @@ -216,14 +212,12 @@ def _guess_id_from_file(self): self.file.startswith('http')): self.kodi_id, _ = kodi_db.kodiid_from_filename(self.file, v.KODI_TYPE_SONG) - LOG.debug('Detected song. Research results: %s', self) return # Need more info since we don't have kodi_id nor type. Use file path. if (self.file.startswith('plugin') or (self.file.startswith('http') and not self.file.startswith('http://127.0.0.1:%s' % v.WEBSERVICE_PORT))): return - LOG.debug('Starting research for Kodi id since we didnt get one') # Try the VIDEO DB first - will find both movies and episodes self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file, db_type='video') @@ -232,7 +226,6 @@ def _guess_id_from_file(self): self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(self.file, db_type='music') self.kodi_type = None if self.kodi_id is None else self.kodi_type - LOG.debug('Research results for guessing Kodi id: %s', self) def plex_stream_index(self, kodi_stream_index, stream_type): """ diff --git a/resources/lib/playqueue/queue.py b/resources/lib/playqueue/queue.py deleted file mode 100644 index e69de29bb..000000000 From d3752e1958e912a7217696b45bb6c25a89dfe354 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 11:26:14 +0200 Subject: [PATCH 63/74] Rename to PlayqueueError --- resources/lib/playqueue/__init__.py | 2 +- resources/lib/playqueue/common.py | 2 +- resources/lib/playqueue/monitor.py | 8 ++--- resources/lib/playqueue/playqueue.py | 45 +++++++++++++--------------- resources/lib/plex_companion.py | 12 ++++---- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/resources/lib/playqueue/__init__.py b/resources/lib/playqueue/__init__.py index 88cdade04..3b6220e04 100644 --- a/resources/lib/playqueue/__init__.py +++ b/resources/lib/playqueue/__init__.py @@ -5,7 +5,7 @@ """ from __future__ import absolute_import, division, unicode_literals -from .common import PlaylistItem, PlaylistItemDummy, PlaylistError, PLAYQUEUES +from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError, PLAYQUEUES from .playqueue import PlayQueue from .monitor import PlayqueueMonitor from .functions import init_playqueues, get_playqueue_from_type, \ diff --git a/resources/lib/playqueue/common.py b/resources/lib/playqueue/common.py index 4e6327c20..4245771ca 100644 --- a/resources/lib/playqueue/common.py +++ b/resources/lib/playqueue/common.py @@ -10,7 +10,7 @@ PLAYQUEUES = [] -class PlaylistError(Exception): +class PlayqueueError(Exception): """ Exception for our playlist constructs """ diff --git a/resources/lib/playqueue/monitor.py b/resources/lib/playqueue/monitor.py index 957cc9baf..911e9be7e 100644 --- a/resources/lib/playqueue/monitor.py +++ b/resources/lib/playqueue/monitor.py @@ -7,7 +7,7 @@ from logging import getLogger import copy -from .common import PlaylistError, PlaylistItem, PLAYQUEUES +from .common import PlayqueueError, PlaylistItem, PLAYQUEUES from .. import backgroundthread, json_rpc as js, utils, app @@ -68,7 +68,7 @@ def _compare_playqueues(self, playqueue, new_kodi_playqueue): i + j, i) try: playqueue.plex_move_item(i + j, i) - except PlaylistError: + 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') @@ -83,7 +83,7 @@ def _compare_playqueues(self, playqueue, new_kodi_playqueue): playqueue.init(playlistitem) else: playqueue.plex_add_item(playlistitem, i) - except PlaylistError: + except PlayqueueError: LOG.warn('Couldnt add new item to Plex: %s', playlistitem) except IndexError: # This is really a hack - happens when using Addon Paths @@ -103,7 +103,7 @@ def _compare_playqueues(self, playqueue, new_kodi_playqueue): LOG.debug('Detected deletion of playqueue element at pos %s', i) try: playqueue.plex_remove_item(i) - except PlaylistError: + 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') diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index 7114c75bd..7abfd5b0e 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -1,13 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" -Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly -""" from __future__ import absolute_import, division, unicode_literals from logging import getLogger import threading -from .common import PlaylistItem, PlaylistItemDummy, PlaylistError +from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError from ..downloadutils import DownloadUtils as DU from ..plex_api import API @@ -157,7 +154,7 @@ def init(self, playlistitem): playlistitem.from_xml(xml[0]) except (KeyError, IndexError, TypeError): LOG.error('Could not init Plex playlist with %s', playlistitem) - raise PlaylistError() + raise PlayqueueError() self.items.append(playlistitem) LOG.debug('Initialized the playqueue on the Plex side: %s', self) @@ -203,7 +200,7 @@ def _resolve(self, plex_id, startpos): self.index = startpos + 1 xml = PF.GetPlexMetadata(plex_id) if xml in (None, 401): - raise PlaylistError('Could not get Plex metadata %s for %s', + raise PlayqueueError('Could not get Plex metadata %s for %s', plex_id, self.items[startpos]) api = API(xml[0]) if playlistitem.resume is None: @@ -255,7 +252,7 @@ def _init(self, plex_id, plex_type=None, startpos=None, position=None, else: xml = PF.GetPlexMetadata(plex_id) if xml in (None, 401): - raise PlaylistError('Could not get Plex metadata %s', plex_id) + raise PlayqueueError('Could not get Plex metadata %s', plex_id) section_uuid = xml.get('librarySectionUUID') api = API(xml[0]) plex_type = api.plex_type() @@ -277,7 +274,7 @@ def _init(self, plex_id, plex_type=None, startpos=None, position=None, if xml is None: LOG.error('Could not get playqueue for plex_id %s UUID %s for %s', plex_id, section_uuid, self) - raise PlaylistError('Could not get playqueue') + 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 @@ -311,7 +308,7 @@ def _resume_playback(db_item=None, xml=None): resume = resume_dialog(resume) LOG.info('User chose resume: %s', resume) if resume is None: - raise PlaylistError('User backed out of resume dialog') + raise PlayqueueError('User backed out of resume dialog') app.PLAYSTATE.autoplay = True return resume @@ -374,7 +371,7 @@ def add_item(self, item, pos, listitem=None): """ Adds a PlaylistItem to both Kodi and Plex at position pos [int] Also changes self.items - Raises PlaylistError + Raises PlayqueueError """ self.kodi_add_item(item, pos, listitem) self.plex_add_item(item, pos) @@ -382,13 +379,13 @@ def add_item(self, item, pos, listitem=None): def kodi_add_item(self, item, pos, listitem=None): """ Adds a PlaylistItem to Kodi only. Will not change self.items - Raises PlaylistError + Raises PlayqueueError """ if not isinstance(item, PlaylistItem): - raise PlaylistError('Wrong item %s of type %s received' + raise PlayqueueError('Wrong item %s of type %s received' % (item, type(item))) if pos > len(self.items): - raise PlaylistError('Position %s too large for playlist length %s' + 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: @@ -406,14 +403,14 @@ def kodi_add_item(self, item, pos, listitem=None): 'position': pos, 'item': {'%sid' % item.kodi_type: item.kodi_id}}) if 'error' in answ: - raise PlaylistError('Kodi did not add item to playlist: %s', + 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 PlaylistError('Could not get metadata for %s', item) + 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 @@ -434,13 +431,13 @@ def plex_add_item(self, item, pos): """ Adds a new PlaylistItem to the playlist at position pos [int] only on the Plex side of things. Also changes self.items - Raises PlaylistError + Raises PlayqueueError """ if not isinstance(item, PlaylistItem) or not item.uri: - raise PlaylistError('Wrong item %s of type %s received' + raise PlayqueueError('Wrong item %s of type %s received' % (item, type(item))) if pos > len(self.items): - raise PlaylistError('Position %s too large for playlist length %s' + 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) @@ -449,14 +446,14 @@ def plex_add_item(self, item, pos): try: xml[0].attrib except (TypeError, AttributeError, KeyError, IndexError): - raise PlaylistError('Could not add item %s to playlist %s' + 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 PlaylistError('Something went wrong - Plex id not found') + 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) @@ -471,7 +468,7 @@ def kodi_remove_item(self, pos): LOG.debug('Removing position %s on the Kodi side for %s', pos, self) answ = js.playlist_remove(self.playlistid, pos) if 'error' in answ: - raise PlaylistError('Could not remove item: %s' % answ['error']) + raise PlayqueueError('Could not remove item: %s' % answ['error']) def plex_remove_item(self, pos): """ @@ -490,7 +487,7 @@ def plex_remove_item(self, pos): except IndexError: LOG.error('Could not delete item at position %s on the Plex side', pos) - raise PlaylistError() + raise PlayqueueError() def plex_move_item(self, before, after): """ @@ -499,7 +496,7 @@ def plex_move_item(self, before, after): Will also change self.items """ if before > len(self.items) or after > len(self.items) or after == before: - raise PlaylistError('Illegal original position %s and/or desired ' + 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', @@ -525,7 +522,7 @@ def plex_move_item(self, before, after): try: xml[0].attrib except (TypeError, IndexError, AttributeError): - raise PlaylistError('Could not move playlist item from %s to %s ' + 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)) diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 8d2728f9b..ade780eba 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -50,7 +50,7 @@ def update_playqueue_from_PMS(playqueue, xml = PQ.get_PMS_playlist(playlist_id=playqueue_id) if xml is None: LOG.error('Could now download playqueue %s', playqueue_id) - raise PQ.PlaylistError() + raise PQ.PlayqueueError() app.PLAYSTATE.initiated_by_plex = True playqueue.init_from_xml(xml, offset=offset, @@ -81,7 +81,7 @@ def _process_alexa(data): xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata for: %s', data) - raise PQ.PlaylistError() + raise PQ.PlayqueueError() api = API(xml[0]) if api.plex_type() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') @@ -90,7 +90,7 @@ def _process_alexa(data): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download the album xml for %s', data) - raise PQ.PlaylistError() + raise PQ.PlayqueueError() playqueue = PQ.get_playqueue_from_type('audio') playqueue.init_from_xml(xml, transient_token=data.get('token')) @@ -99,7 +99,7 @@ def _process_alexa(data): xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key) if xml is None: LOG.error('Could not get playqueue for %s', data) - raise PQ.PlaylistError() + raise PQ.PlayqueueError() playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) offset = utils.cast(float, data.get('offset')) or None @@ -146,7 +146,7 @@ def _process_playlist(data): xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata') - raise PQ.PlaylistError() + raise PQ.PlayqueueError() api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) @@ -234,7 +234,7 @@ def _process_tasks(self, task): self._process_refresh(data) elif task['action'] == 'setStreams': self._process_streams(data) - except PQ.PlaylistError: + except PQ.PlayqueueError: LOG.error('Could not process companion data: %s', data) # "Play Error" utils.dialog('notification', From fb21bc7d718d6b41936f8570848fd4184f8cb434 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 11:53:59 +0200 Subject: [PATCH 64/74] Cleanup --- resources/lib/playqueue/common.py | 66 ++++++++++++++++------------ resources/lib/playqueue/playqueue.py | 20 ++++----- 2 files changed, 47 insertions(+), 39 deletions(-) diff --git a/resources/lib/playqueue/common.py b/resources/lib/playqueue/common.py index 4245771ca..ca991f741 100644 --- a/resources/lib/playqueue/common.py +++ b/resources/lib/playqueue/common.py @@ -12,7 +12,7 @@ class PlayqueueError(Exception): """ - Exception for our playlist constructs + Exception for our playqueue constructs """ pass @@ -60,7 +60,7 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, self.kodi_type = kodi_type self.file = None if kodi_item: - self.kodi_id = kodi_item.get('id') + 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 @@ -76,15 +76,9 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, # False: do NOT resume, don't ask user # True: do resume, don't ask user self.resume = None - if (self.plex_id is None and - (self.kodi_id is not None and self.kodi_type is not None)): - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) - if db_item: - self.plex_id = db_item['plex_id'] - self.plex_type = db_item['plex_type'] - self.plex_uuid = db_item['section_uuid'] - if grab_xml and plex_id is not None and xml_video_element is None: + 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] @@ -99,11 +93,13 @@ def __init__(self, plex_id=None, plex_type=None, xml_video_element=None, 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.set_uri() + 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: @@ -142,6 +138,20 @@ 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 @@ -157,7 +167,9 @@ def from_xml(self, xml_video_element): self.playcount = api.viewcount() self.offset = api.resume_point() self.xml = xml_video_element - self.set_uri() + if self.kodi_id is None or not self.kodi_type: + self._from_plex_db() + self._set_uri() def from_kodi(self, playlist_item): """ @@ -167,17 +179,12 @@ def from_kodi(self, playlist_item): If kodi_id & kodi_type are provided, plex_id and plex_type will be looked up (if not already set) """ - self.kodi_id = playlist_item.get('id') + self.kodi_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: - with PlexDB(lock=False) as plexdb: - db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type) - if db_item: - self.plex_id = db_item['plex_id'] - self.plex_type = db_item['plex_type'] - self.plex_uuid = db_item['section_uuid'] - if self.plex_id is None and self.file is not None: + self._from_plex_db() + if self.plex_id is None and self.file: try: query = self.file.split('?', 1)[1] except IndexError: @@ -185,14 +192,13 @@ def from_kodi(self, playlist_item): 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() + self._set_uri() - def set_uri(self): + def _set_uri(self): if self.plex_id is None and self.file is not None: self.uri = ('library://whatever/item/%s' % utils.quote(self.file, safe='')) elif self.plex_id is not None and self.plex_uuid is not None: - # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % (self.plex_uuid, self.plex_id)) elif self.plex_id is not None: @@ -203,6 +209,8 @@ def set_uri(self): 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 @@ -219,12 +227,12 @@ def _guess_id_from_file(self): 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') + 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_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): @@ -289,6 +297,6 @@ class PlaylistItemDummy(PlaylistItem): """ def __init__(self, *args, **kwargs): super(PlaylistItemDummy, self).__init__(*args, **kwargs) - self.name = 'dummy item' + self.name = 'PKC Dummy playqueue item' self.id = 0 self.plex_id = 0 diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index 7abfd5b0e..52a99cbc5 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -201,7 +201,7 @@ def _resolve(self, plex_id, startpos): 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]) + plex_id, self.items[startpos]) api = API(xml[0]) if playlistitem.resume is None: # Potentially ask user to resume @@ -383,10 +383,10 @@ def kodi_add_item(self, item, pos, listitem=None): """ if not isinstance(item, PlaylistItem): raise PlayqueueError('Wrong item %s of type %s received' - % (item, type(item))) + % (item, type(item))) if pos > len(self.items): raise PlayqueueError('Position %s too large for playlist length %s' - % (pos, len(self.items))) + % (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(), @@ -404,7 +404,7 @@ def kodi_add_item(self, item, pos, listitem=None): 'item': {'%sid' % item.kodi_type: item.kodi_id}}) if 'error' in answ: raise PlayqueueError('Kodi did not add item to playlist: %s', - answ) + answ) else: if item.xml is None: LOG.debug('Need to get metadata for item %s', item) @@ -435,10 +435,10 @@ def plex_add_item(self, item, pos): """ if not isinstance(item, PlaylistItem) or not item.uri: raise PlayqueueError('Wrong item %s of type %s received' - % (item, type(item))) + % (item, type(item))) if pos > len(self.items): raise PlayqueueError('Position %s too large for playlist length %s' - % (pos, len(self.items))) + % (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 @@ -447,7 +447,7 @@ def plex_add_item(self, item, pos): xml[0].attrib except (TypeError, AttributeError, KeyError, IndexError): raise PlayqueueError('Could not add item %s to playlist %s' - % (item, self)) + % (item, self)) for actual_pos, xml_video_element in enumerate(xml): api = API(xml_video_element) if api.plex_id() == item.plex_id: @@ -497,8 +497,8 @@ def plex_move_item(self, before, after): """ 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))) + '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: @@ -523,7 +523,7 @@ def plex_move_item(self, before, after): xml[0].attrib except (TypeError, IndexError, AttributeError): raise PlayqueueError('Could not move playlist item from %s to %s ' - 'for %s' % (before, after, self)) + '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) From 6e692d22c2e888ce2c5045437cd85ebcf1587f47 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 12:06:27 +0200 Subject: [PATCH 65/74] Fix resume when user chose to not resume --- resources/lib/playqueue/playqueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index 52a99cbc5..f6852d161 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -344,7 +344,8 @@ def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): playlistitem = PlaylistItem(xml_video_element=xml) playlistitem.part = api.part playlistitem.force_transcode = self.force_transcode - listitem = widgets.get_listitem(xml, resume=True) + playlistitem.resume = resume + listitem = widgets.get_listitem(xml, resume=resume) listitem.setSubtitles(api.cache_external_subs()) play = PlayUtils(api, playlistitem) url = play.getPlayUrl() From 725416751fbb731bccdcdc2df57e5f2d218a6dab Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 12:30:49 +0200 Subject: [PATCH 66/74] Revert "Fix resume when user chose to not resume" This reverts commit 6e692d22c2e888ce2c5045437cd85ebcf1587f47. --- resources/lib/playqueue/playqueue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index f6852d161..52a99cbc5 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -344,8 +344,7 @@ def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): 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 = widgets.get_listitem(xml, resume=True) listitem.setSubtitles(api.cache_external_subs()) play = PlayUtils(api, playlistitem) url = play.getPlayUrl() From bb2fff5909330f17da01de940153756e538e45ae Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 12:51:04 +0200 Subject: [PATCH 67/74] Fix resume when user chose to not resume --- resources/lib/playqueue/playqueue.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index 52a99cbc5..525743a7f 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -289,7 +289,7 @@ def _init(self, plex_id, plex_type=None, startpos=None, position=None, def _resume_playback(db_item=None, xml=None): ''' Pass in either db_item or xml - Resume item if available. Returns bool or raise an PlayStrmException if + Resume item if available. Returns bool or raise a PlayqueueError if resume was cancelled by user. ''' resume = app.PLAYSTATE.resume_playback @@ -325,7 +325,7 @@ def _add_intros(self, xml): break api = API(intro) LOG.debug('Adding trailer: %s', api.title()) - self._kodi_add_xml(intro, api) + self._kodi_add_xml(intro, api, resume=False) def _add_additional_parts(self, xml): ''' Create listitems and add them to the stack of playlist. @@ -337,14 +337,20 @@ def _add_additional_parts(self, xml): continue api.set_part_number(part) LOG.debug('Adding addional part for %s: %s', api.title(), part) - self._kodi_add_xml(xml[0], api) + self._kodi_add_xml(xml[0], api, resume=False) - def _kodi_add_xml(self, xml, api, resume=False, playlistitem=None): + 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 - listitem = widgets.get_listitem(xml, resume=True) + playlistitem.resume = resume + listitem = widgets.get_listitem(xml, resume=resume) listitem.setSubtitles(api.cache_external_subs()) play = PlayUtils(api, playlistitem) url = play.getPlayUrl() @@ -466,6 +472,7 @@ 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) + LOG.error('Current Kodi playlist: %s', js.playlist_get_items(self.playlistid)) answ = js.playlist_remove(self.playlistid, pos) if 'error' in answ: raise PlayqueueError('Could not remove item: %s' % answ['error']) From 0f9e754815643ebe85f22c792077653f3097ba46 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 12:52:32 +0200 Subject: [PATCH 68/74] Cleanup --- resources/lib/webservice.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 937a67030..76155672c 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -299,20 +299,6 @@ def __init__(self, server, plex_type): self.force_transcode = False super(QueuePlay, self).__init__() - def __unicode__(self): - return ("{{" - "'plex_id': {self.plex_id}, " - "'plex_type': '{self.plex_type}', " - "'kodi_id': {self.kodi_id}, " - "'kodi_type': '{self.kodi_type}', " - "'synched: '{self.synched}', " - "'force_transcode: '{self.force_transcode}', " - "}}").format(self=self) - - def __str__(self): - return unicode(self).encode('utf-8') - __repr__ = __str__ - def load_params(self, params): self.plex_id = utils.cast(int, params['plex_id']) self.plex_type = params.get('plex_type') @@ -400,8 +386,6 @@ def run(self): elif video_widget_playback: LOG.info('Start widget video playback') utils.window('plex.playlist.play', value='true') - LOG.info('Current PKC queue: %s', playqueue) - LOG.info('current Kodi queue: %s', js.playlist_get_items(playqueue.playlistid)) playqueue.start_playback() else: LOG.info('Start normal playback') From a650c42cfdc4adf664fcb8379db12b75cd9303cc Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 13:13:13 +0200 Subject: [PATCH 69/74] Lock playqueue activities --- resources/lib/webservice.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 76155672c..f66b2d051 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -342,7 +342,19 @@ def _get_playqueue(self): return playqueue, video_widget_playback def run(self): - LOG.debug('##===---- Starting QueuePlay ----===##') + with app.APP.lock_playqueues: + LOG.debug('##===---- Starting QueuePlay ----===##') + try: + self._run() + finally: + utils.window('plex.playlist.ready', clear=True) + utils.window('plex.playlist.start', clear=True) + 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() @@ -439,10 +451,3 @@ def run(self): else: utils.window('plex.playlist.aborted', value='true') break - - utils.window('plex.playlist.ready', clear=True) - utils.window('plex.playlist.start', clear=True) - app.PLAYSTATE.initiated_by_plex = False - self.server.threads.remove(self) - self.server.pending = [] - LOG.debug('##===---- QueuePlay Stopped ----===##') From 7e676eb0434ac9466098768f18a6966ebccd6bff Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 17:28:05 +0200 Subject: [PATCH 70/74] Fix playback startup --- resources/lib/kodimonitor.py | 6 ++---- resources/lib/webservice.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 78846b7fc..829e113c8 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -87,13 +87,11 @@ def onNotification(self, sender, method, data): with app.APP.lock_playqueues: _playback_cleanup() elif method == 'Playlist.OnAdd': - 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') diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index f66b2d051..01f115417 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -322,6 +322,8 @@ def _get_playqueue(self): 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 utils.window('plex.playlist.ready', value='true') playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) playqueue.clear() @@ -397,7 +399,6 @@ def _run(self): playqueue.start_playback(start_position) elif video_widget_playback: LOG.info('Start widget video playback') - utils.window('plex.playlist.play', value='true') playqueue.start_playback() else: LOG.info('Start normal playback') From e204ef9849109552f91ee0af839fe563833d1734 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 17:35:37 +0200 Subject: [PATCH 71/74] Replace window var plex.playlist.ready with app.PLAYSTATE var --- resources/lib/app/playstate.py | 8 +++++++- resources/lib/kodimonitor.py | 2 +- resources/lib/webservice.py | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index e0b829603..c90d6e306 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -61,8 +61,14 @@ def __init__(self): self.autoplay = False # Was the playback initiated by the user using the Kodi context menu? self.context_menu_play = 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 diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 829e113c8..4112fef1d 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -149,7 +149,7 @@ def _playlist_onadd(self, data): self.playlistid = data['playlistid'] if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')): LOG.debug('Playlist ready') - utils.window('plex.playlist.ready', value='true') + app.PLAYSTATE.playlist_ready = True utils.window('plex.playlist.start', clear=True) def _playlist_onremove(self, data): diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 01f115417..73957b5bd 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -324,7 +324,7 @@ def _get_playqueue(self): # Release default.py utils.window('plex.playlist.play', value='true') # The playlist will be ready anyway - utils.window('plex.playlist.ready', value='true') + 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) @@ -349,7 +349,7 @@ def run(self): try: self._run() finally: - utils.window('plex.playlist.ready', clear=True) + app.PLAYSTATE.playlist_ready = False utils.window('plex.playlist.start', clear=True) app.PLAYSTATE.initiated_by_plex = False self.server.threads.remove(self) @@ -387,7 +387,7 @@ def _run(self): # avoid issues with ongoing Live TV playback app.APP.player.stop() count = 50 - while not utils.window('plex.playlist.ready'): + while not app.PLAYSTATE.playlist_ready: xbmc.sleep(50) if not count: LOG.info('Playback aborted') From 27c4c6ac385db40c1b23646aea328b0bec219e0e Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 17:41:17 +0200 Subject: [PATCH 72/74] Replace window var plex.playlist.start with app.PLAYSTATE var --- resources/lib/app/playstate.py | 3 +++ resources/lib/kodimonitor.py | 4 ++-- resources/lib/webservice.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index c90d6e306..052b43327 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -72,3 +72,6 @@ def __init__(self): # 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 diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4112fef1d..75727da76 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -147,10 +147,10 @@ def _playlist_onadd(self, data): if data['position'] == 0: self.playlistid = data['playlistid'] - if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')): + if app.PLAYSTATE.playlist_start_pos == data['position']: LOG.debug('Playlist ready') app.PLAYSTATE.playlist_ready = True - utils.window('plex.playlist.start', clear=True) + app.PLAYSTATE.playlist_start_pos = None def _playlist_onremove(self, data): """ diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 73957b5bd..a771554fe 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -350,7 +350,7 @@ def run(self): self._run() finally: app.PLAYSTATE.playlist_ready = False - utils.window('plex.playlist.start', clear=True) + app.PLAYSTATE.playlist_start_pos = None app.PLAYSTATE.initiated_by_plex = False self.server.threads.remove(self) self.server.pending = [] @@ -372,7 +372,7 @@ def _run(self): # of our current playqueue position = playqueue.kodi_pl.size() # Set to start_position + 1 because first item will fail - utils.window('plex.playlist.start', str(start_position + 1)) + 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: From 48d288ac5302deffb943a7ef9a548168824e1b07 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 17:43:22 +0200 Subject: [PATCH 73/74] Remove some logging --- default.py | 1 - resources/lib/playqueue/playqueue.py | 1 - 2 files changed, 2 deletions(-) diff --git a/default.py b/default.py index ae7f7e8d3..ad06a5786 100644 --- a/default.py +++ b/default.py @@ -42,7 +42,6 @@ def __init__(self): if mode == 'playstrm': while not utils.window('plex.playlist.play'): xbmc.sleep(25) - LOG.error('waiting') if utils.window('plex.playlist.aborted'): LOG.info("playback aborted") break diff --git a/resources/lib/playqueue/playqueue.py b/resources/lib/playqueue/playqueue.py index 525743a7f..2e26dc989 100644 --- a/resources/lib/playqueue/playqueue.py +++ b/resources/lib/playqueue/playqueue.py @@ -472,7 +472,6 @@ 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) - LOG.error('Current Kodi playlist: %s', js.playlist_get_items(self.playlistid)) answ = js.playlist_remove(self.playlistid, pos) if 'error' in answ: raise PlayqueueError('Could not remove item: %s' % answ['error']) From 6cba4a1d012659e626911534a243466a35cc76d0 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 26 May 2019 17:56:16 +0200 Subject: [PATCH 74/74] Fix playback startup for Plex Companion --- resources/lib/webservice.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index a771554fe..793b8d1d2 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -404,11 +404,12 @@ def _run(self): LOG.info('Start normal playback') # Release default.py utils.window('plex.playlist.play', value='true') - # 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] + 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)