class KodiMonitor(Monitor): def __init__(self, callback): self.mgr = callback self.doUtils = DownloadUtils().downloadUrl self.xbmcplayer = Player() self.playqueue = self.mgr.playqueue Monitor.__init__(self) log.info("Kodi monitor started.") def onScanStarted(self, library): log.debug("Kodi library scan %s running." % library) if library == "video": window('plex_kodiScan', value="true") def onScanFinished(self, library): log.debug("Kodi library scan %s finished." % library) if library == "video": window('plex_kodiScan', clear=True) def onSettingsChanged(self): """ Monitor the PKC settings for changes made by the user """ log.debug('PKC settings change detected') changed = False # Reset the window variables from the settings variables for settings_value, window_value in WINDOW_SETTINGS.iteritems(): if window(window_value) != settings(settings_value): changed = True log.debug('PKC window settings changed: %s is now %s' % (settings_value, settings(settings_value))) window(window_value, value=settings(settings_value)) if settings_value == 'fetch_pms_item_number': log.info('Requesting playlist/nodes refresh') plex_command('RUN_LIB_SCAN', 'views') # Reset the state variables in state.py for settings_value, state_name in STATE_SETTINGS.iteritems(): new = settings(settings_value) if new == 'true': new = True elif new == 'false': new = False if getattr(state, state_name) != new: changed = True log.debug('PKC state settings %s changed from %s to %s' % (settings_value, getattr(state, state_name), new)) setattr(state, state_name, new) # Special cases, overwrite all internal settings state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) # Never set through the user # state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) if changed is True: # Assume that the user changed the settings so that we can now find # the path to all media files state.STOP_SYNC = False state.PATH_VERIFIED = False @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): if data: data = loads(data, 'utf-8') log.debug("Method: %s Data: %s" % (method, data)) if method == "Player.OnPlay": self.PlayBackStart(data) elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') pass elif method == "VideoLibrary.OnUpdate": # Manually marking as watched/unwatched playcount = data.get('playcount') item = data.get('item') try: kodiid = item['id'] item_type = item['type'] except (KeyError, TypeError): log.info("Item is invalid for playstate update.") else: # Send notification to the server. with plexdb.Get_Plex_DB() as plexcur: plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type) try: itemid = plex_dbitem[0] except TypeError: log.error("Could not find itemid in plex database for a " "video library update") else: # Stop from manually marking as watched unwatched, with # actual playback. if window('plex_skipWatched%s' % itemid) == "true": # property is set in player.py window('plex_skipWatched%s' % itemid, clear=True) else: # notify the server if playcount > 0: scrobble(itemid, 'watched') else: scrobble(itemid, 'unwatched') elif method == "VideoLibrary.OnRemove": pass elif method == "System.OnSleep": # Connection is going to sleep log.info("Marking the server as offline. SystemOnSleep activated.") window('plex_online', value="sleep") elif method == "System.OnWake": # Allow network to wake up sleep(10000) window('plex_onWake', value="true") window('plex_online', value="false") elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": sleep(5000) plex_command('RUN_LIB_SCAN', 'full') elif method == "System.OnQuit": log.info('Kodi OnQuit detected - shutting down') state.STOP_PKC = True def PlayBackStart(self, data): """ Called whenever a playback is started """ # Get currently playing file - can take a while. Will be utf-8! try: currentFile = self.xbmcplayer.getPlayingFile() except: currentFile = None count = 0 while currentFile is None: sleep(100) try: currentFile = self.xbmcplayer.getPlayingFile() except: pass if count == 50: log.info("No current File, cancel OnPlayBackStart...") return else: count += 1 # Just to be on the safe side currentFile = tryDecode(currentFile) log.debug("Currently playing file is: %s" % currentFile) # Get the type of media we're playing try: typus = data['item']['type'] except (TypeError, KeyError): log.info("Item is invalid for PMS playstate update.") return log.debug("Playing itemtype is (or appears to be): %s" % typus) # Try to get a Kodi ID # If PKC was used - native paths, not direct paths plex_id = window('plex_%s.itemid' % tryEncode(currentFile)) # Get rid of the '' if the window property was not set plex_id = None if not plex_id else plex_id kodiid = None if plex_id is None: log.debug('Did not get Plex id from window properties') try: kodiid = data['item']['id'] except (TypeError, KeyError): log.debug('Did not get a Kodi id from Kodi, darn') # For direct paths, if we're not streaming something # When using Widgets, Kodi doesn't tell us shit so we need this hack if (kodiid is None and plex_id is None and typus != 'song' and not currentFile.startswith('http')): (kodiid, typus) = get_kodiid_from_filename(currentFile) if kodiid is None: return if plex_id is None: # Get Plex' item id with plexdb.Get_Plex_DB() as plexcursor: plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus) try: plex_id = plex_dbitem[0] except TypeError: log.info("No Plex id returned for kodiid %s. Aborting playback" " report" % kodiid) return log.debug("Found Plex id %s for Kodi id %s for type %s" % (plex_id, kodiid, typus)) # Switch subtitle tracks if applicable subtitle = window('plex_%s.subtitle' % tryEncode(currentFile)) if window(tryEncode('plex_%s.playmethod' % currentFile)) \ == 'Transcode' and subtitle: if window('plex_%s.subtitle' % currentFile) == 'None': self.xbmcplayer.showSubtitles(False) else: self.xbmcplayer.setSubtitleStream(int(subtitle)) # Set some stuff if Kodi initiated playback if ((settings('useDirectPaths') == "1" and not typus == "song") or (typus == "song" and settings('enableMusic') == "true")): if self.StartDirectPath(plex_id, typus, tryEncode(currentFile)) is False: log.error('Could not initiate monitoring; aborting') return # Save currentFile for cleanup later and to be able to access refs window('plex_lastPlayedFiled', value=currentFile) window('plex_currently_playing_itemid', value=plex_id) window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id) log.info('Finish playback startup') def StartDirectPath(self, plex_id, type, currentFile): """ Set some additional stuff if playback was initiated by Kodi, not PKC """ xml = self.doUtils('{server}/library/metadata/%s' % plex_id) try: xml[0].attrib except: log.error('Did not receive a valid XML for plex_id %s.' % plex_id) return False # Setup stuff, because playback was started by Kodi, not PKC api = API(xml[0]) listitem = api.CreateListItemFromPlexItem() api.set_playback_win_props(currentFile, listitem) if type == "song" and settings('streamMusic') == "true": window('plex_%s.playmethod' % currentFile, value="DirectStream") else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") log.debug('Window properties set for direct paths!')
class PlexCompanion(Thread): """ Plex Companion monitoring class. Invoke only once """ def __init__(self): LOG.info("----===## Starting PlexCompanion ##===----") # Init Plex Companion queue # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails() LOG.debug("Registration string is:\n%s", self.client.getClientDetails()) # kodi player instance self.player = Player() self.httpd = False self.subscription_manager = None Thread.__init__(self) def _process_alexa(self, data): xml = PF.GetPlexMetadata(data['key']) try: xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata for: %s', data) return 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')) 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 playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() PL.get_playlist_details_from_xml(playqueue, xml) playqueue.plex_transient_token = data.get('token') if data.get('offset') != '0': offset = float(data['offset']) / 1000.0 else: offset = None playback.play_xml(playqueue, xml, offset) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') if data.get('offset') != '0': state.RESUMABLE = True state.RESUME_PLAYBACK = True playback.playback_triage(api.plex_id(), api.plex_type(), resolve=False) @staticmethod def _process_node(data): """ E.g. watch later initiated by Companion. Basically navigating Plex """ state.PLEX_TRANSIENT_TOKEN = data.get('key') params = { 'mode': 'plex_node', 'key': '{server}%s' % data.get('key'), 'offset': data.get('offset') } executebuiltin('RunPlugin(plugin://%s?%s)' % (v.ADDON_ID, urlencode(params))) def _process_playlist(self, data): # Get the playqueue ID _, container_key, query = PF.ParseContainerKey(data['containerKey']) try: playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) except KeyError: # E.g. Plex web does not supply the media type # Still need to figure out the type (video vs. music vs. pix) xml = PF.GetPlexMetadata(data['key']) try: xml[0].attrib except (AttributeError, IndexError, TypeError): LOG.error('Could not download Plex metadata') return api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) update_playqueue_from_PMS(playqueue, playqueue_id=container_key, repeat=query.get('repeat'), offset=data.get('offset'), transient_token=data.get('token')) def _process_streams(self, data): """ Plex Companion client adjusted audio or subtitle stream """ 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') self.player.setAudioStream(index) elif 'subtitleStreamID' in data: if data['subtitleStreamID'] == '0': self.player.showSubtitles(False) else: index = playqueue.items[pos].kodi_stream_index( data['subtitleStreamID'], 'subtitle') self.player.setSubtitleStream(index) else: LOG.error('Unknown setStreams command: %s', data) def _process_refresh(self, data): """ example data: {'playQueueID': '8475', 'commandID': '11'} """ xml = PL.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) if plex_type is None: return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) playqueue.clear() return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) update_playqueue_from_PMS(playqueue, data['playQueueID']) def _process_tasks(self, task): """ Processes tasks picked up e.g. by Companion listener, e.g. {'action': 'playlist', 'data': {'address': 'xyz.plex.direct', 'commandID': '7', 'containerKey': '/playQueues/6669?own=1&repeat=0&window=200', 'key': '/library/metadata/220493', 'machineIdentifier': 'xyz', 'offset': '0', 'port': '32400', 'protocol': 'https', 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd', 'type': 'video'}} """ LOG.debug('Processing: %s', task) data = task['data'] if task['action'] == 'alexa': with state.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 state.LOCK_PLAYQUEUES: self._process_playlist(data) elif task['action'] == 'refreshPlayQueue': with state.LOCK_PLAYQUEUES: self._process_refresh(data) elif task['action'] == 'setStreams': try: self._process_streams(data) except KeyError: pass def run(self): """ Ensure that sockets will be closed no matter what """ try: self._run() finally: try: self.httpd.socket.shutdown(SHUT_RDWR) except AttributeError: pass finally: try: self.httpd.socket.close() except AttributeError: pass LOG.info("----===## Plex Companion stopped ##===----") def _run(self): httpd = self.httpd # Cache for quicker while loops client = self.client stopped = self.stopped suspended = self.suspended # Start up instances request_mgr = httppersist.RequestMgr() subscription_manager = subscribers.SubscriptionMgr( request_mgr, self.player) self.subscription_manager = subscription_manager if utils.settings('plexCompanion') == 'true': # Start up httpd start_count = 0 while True: try: httpd = listener.ThreadedHTTPServer( client, subscription_manager, ('', v.COMPANION_PORT), listener.MyHandler) httpd.timeout = 0.95 break except: LOG.error("Unable to start PlexCompanion. Traceback:") import traceback LOG.error(traceback.print_exc()) sleep(3000) if start_count == 3: LOG.error("Error: Unable to start web helper.") httpd = False break start_count += 1 else: LOG.info('User deactivated Plex Companion') client.start_all() message_count = 0 if httpd: thread = Thread(target=httpd.handle_request) while not stopped(): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations while suspended(): if stopped(): break sleep(1000) try: message_count += 1 if httpd: if not thread.isAlive(): # Use threads cause the method will stall thread = Thread(target=httpd.handle_request) thread.start() if message_count == 3000: message_count = 0 if client.check_client_registration(): LOG.debug('Client is still registered') else: LOG.debug( 'Client is no longer registered. Plex ' 'Companion still running on port %s', v.COMPANION_PORT) client.register_as_client() # Get and set servers if message_count % 30 == 0: subscription_manager.serverlist = client.getServerList() subscription_manager.notify() if not httpd: message_count = 0 except: LOG.warn("Error in loop, continuing anyway. Traceback:") import traceback LOG.warn(traceback.format_exc()) # See if there's anything we need to process try: task = state.COMPANION_QUEUE.get(block=False) except Empty: pass else: # Got instructions, process them self._process_tasks(task) state.COMPANION_QUEUE.task_done() # Don't sleep continue sleep(50) subscription_manager.signal_stop() client.stop_all()
class KodiMonitor(Monitor): def __init__(self, callback): self.mgr = callback self.doUtils = downloadutils.DownloadUtils().downloadUrl self.xbmcplayer = Player() self.playqueue = self.mgr.playqueue Monitor.__init__(self) log.info("Kodi monitor started.") def onScanStarted(self, library): log.debug("Kodi library scan %s running." % library) if library == "video": window('plex_kodiScan', value="true") def onScanFinished(self, library): log.debug("Kodi library scan %s finished." % library) if library == "video": window('plex_kodiScan', clear=True) def onSettingsChanged(self): """ Monitor the PKC settings for changes made by the user """ # settings: window-variable items = { 'logLevel': 'plex_logLevel', 'enableContext': 'plex_context', 'plex_restricteduser': '******', 'dbSyncIndicator': 'dbSyncIndicator', 'remapSMB': 'remapSMB', 'replaceSMB': 'replaceSMB', 'force_transcode_pix': 'plex_force_transcode_pix', 'fetch_pms_item_number': 'fetch_pms_item_number' } # Path replacement for typus in REMAP_TYPE_FROM_PLEXTYPE.values(): for arg in ('Org', 'New'): key = 'remapSMB%s%s' % (typus, arg) items[key] = key # Reset the window variables from the settings variables for settings_value, window_value in items.iteritems(): if window(window_value) != settings(settings_value): log.debug('PKC settings changed: %s is now %s' % (settings_value, settings(settings_value))) window(window_value, value=settings(settings_value)) if settings_value == 'fetch_pms_item_number': log.info('Requesting playlist/nodes refresh') window('plex_runLibScan', value="views") @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): if data: data = loads(data, 'utf-8') log.debug("Method: %s Data: %s" % (method, data)) if method == "Player.OnPlay": self.PlayBackStart(data) elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') pass elif method == "VideoLibrary.OnUpdate": # Manually marking as watched/unwatched playcount = data.get('playcount') item = data.get('item') try: kodiid = item['id'] item_type = item['type'] except (KeyError, TypeError): log.info("Item is invalid for playstate update.") else: # Send notification to the server. with plexdb.Get_Plex_DB() as plexcur: plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type) try: itemid = plex_dbitem[0] except TypeError: log.error("Could not find itemid in plex database for a " "video library update") else: # Stop from manually marking as watched unwatched, with # actual playback. if window('plex_skipWatched%s' % itemid) == "true": # property is set in player.py window('plex_skipWatched%s' % itemid, clear=True) else: # notify the server if playcount != 0: scrobble(itemid, 'watched') else: scrobble(itemid, 'unwatched') elif method == "VideoLibrary.OnRemove": pass elif method == "System.OnSleep": # Connection is going to sleep log.info("Marking the server as offline. SystemOnSleep activated.") window('plex_online', value="sleep") elif method == "System.OnWake": # Allow network to wake up sleep(10000) window('plex_onWake', value="true") window('plex_online', value="false") elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": sleep(5000) window('plex_runLibScan', value="full") def PlayBackStart(self, data): """ Called whenever a playback is started """ # Get currently playing file - can take a while. Will be utf-8! try: currentFile = self.xbmcplayer.getPlayingFile() except: currentFile = None count = 0 while currentFile is None: sleep(100) try: currentFile = self.xbmcplayer.getPlayingFile() except: pass if count == 50: log.info("No current File, cancel OnPlayBackStart...") return else: count += 1 # Just to be on the safe side currentFile = tryDecode(currentFile) log.debug("Currently playing file is: %s" % currentFile) # Get the type of media we're playing try: typus = data['item']['type'] except (TypeError, KeyError): log.info("Item is invalid for PMS playstate update.") return log.debug("Playing itemtype is (or appears to be): %s" % typus) # Try to get a Kodi ID # If PKC was used - native paths, not direct paths plex_id = window('plex_%s.itemid' % tryEncode(currentFile)) # Get rid of the '' if the window property was not set plex_id = None if not plex_id else plex_id kodiid = None if plex_id is None: log.debug('Did not get Plex id from window properties') try: kodiid = data['item']['id'] except (TypeError, KeyError): log.debug('Did not get a Kodi id from Kodi, darn') # For direct paths, if we're not streaming something # When using Widgets, Kodi doesn't tell us shit so we need this hack if (kodiid is None and plex_id is None and typus != 'song' and not currentFile.startswith('http')): (kodiid, typus) = get_kodiid_from_filename(currentFile) if kodiid is None: return if plex_id is None: # Get Plex' item id with plexdb.Get_Plex_DB() as plexcursor: plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus) try: plex_id = plex_dbitem[0] except TypeError: log.info("No Plex id returned for kodiid %s. Aborting playback" " report" % kodiid) return log.debug("Found Plex id %s for Kodi id %s for type %s" % (plex_id, kodiid, typus)) # Switch subtitle tracks if applicable subtitle = window('plex_%s.subtitle' % tryEncode(currentFile)) if window(tryEncode('plex_%s.playmethod' % currentFile)) \ == 'Transcode' and subtitle: if window('plex_%s.subtitle' % currentFile) == 'None': self.xbmcplayer.showSubtitles(False) else: self.xbmcplayer.setSubtitleStream(int(subtitle)) # Set some stuff if Kodi initiated playback if ((settings('useDirectPaths') == "1" and not typus == "song") or (typus == "song" and settings('enableMusic') == "true")): if self.StartDirectPath(plex_id, typus, tryEncode(currentFile)) is False: log.error('Could not initiate monitoring; aborting') return # Save currentFile for cleanup later and to be able to access refs window('plex_lastPlayedFiled', value=currentFile) window('plex_currently_playing_itemid', value=plex_id) window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id) log.info('Finish playback startup') def StartDirectPath(self, plex_id, type, currentFile): """ Set some additional stuff if playback was initiated by Kodi, not PKC """ xml = self.doUtils('{server}/library/metadata/%s' % plex_id) try: xml[0].attrib except: log.error('Did not receive a valid XML for plex_id %s.' % plex_id) return False # Setup stuff, because playback was started by Kodi, not PKC api = API(xml[0]) listitem = api.CreateListItemFromPlexItem() api.set_playback_win_props(currentFile, listitem) if type == "song" and settings('streamMusic') == "true": window('plex_%s.playmethod' % currentFile, value="DirectStream") else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") log.debug('Window properties set for direct paths!')