diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 409534d0..203f4ef8 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -72,10 +72,13 @@ class Movie(ItemBase): scraper='metadata.local') if do_indirect: # Set plugin path and media flags using real filename - filename = api.file_name(force_first_media=True) - path = 'plugin://%s.movies/' % v.ADDON_ID - filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' - % (path, plex_id, v.PLEX_TYPE_MOVIE, filename)) + path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT + filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}&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 98fd92fb..1e959938 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -5,11 +5,11 @@ from logging import getLogger from sqlite3 import IntegrityError from . import common -from .. import path_ops, timing, variables as v, app +from .. import path_ops, timing, variables as v LOG = getLogger('PLEX.kodi_db.video') -MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID +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 f5ea2288..f35db30b 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 cfc76844..08fa690d 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -3,11 +3,12 @@ from __future__ import absolute_import, division, unicode_literals 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 @@ class PlayStrm(object): 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 @@ class PlayStrm(object): 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 @@ class PlayStrm(object): 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 @@ class PlayStrm(object): 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 @@ class PlayStrm(object): 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 7120a2cd..382fb6ae 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -33,7 +33,7 @@ class WebService(backgroundthread.KillableThread): 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 @@ class WebService(backgroundthread.KillableThread): 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 @@ class WebService(backgroundthread.KillableThread): 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 @@ class HttpServer(BaseHTTPServer.HTTPServer): 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 @@ class HttpServer(BaseHTTPServer.HTTPServer): 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 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ''' 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 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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 @@ class QueuePlay(backgroundthread.KillableThread): 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 @@ class QueuePlay(backgroundthread.KillableThread): 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 @@ class QueuePlay(backgroundthread.KillableThread): 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 de3e9168..8cfadf6d 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 00000000..3f242646 --- /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 00000000..43240c97 --- /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 00000000..6422ff5a Binary files /dev/null and b/resources/skins/default/media/dialogs/dialog_back.png differ 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 00000000..0747aa01 Binary files /dev/null and b/resources/skins/default/media/dialogs/menu_back.png differ diff --git a/resources/skins/default/media/dialogs/menu_bottom.png b/resources/skins/default/media/dialogs/menu_bottom.png new file mode 100644 index 00000000..6638c2c3 Binary files /dev/null and b/resources/skins/default/media/dialogs/menu_bottom.png differ 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 00000000..26bc7d15 Binary files /dev/null and b/resources/skins/default/media/dialogs/menu_top.png differ diff --git a/resources/skins/default/media/dialogs/white.jpg b/resources/skins/default/media/dialogs/white.jpg new file mode 100644 index 00000000..a1206155 Binary files /dev/null and b/resources/skins/default/media/dialogs/white.jpg differ