def startAlertListener(self, callback=None): """ Creates a websocket connection to the Plex Server to optionally recieve notifications. These often include messages from Plex about media scans as well as updates to currently running Transcode Sessions. NOTE: You need websocket-client installed in order to use this feature. >> pip install websocket-client Parameters: callback (func): Callback function to call on recieved messages. raises: :class:`plexapi.exception.Unsupported`: Websocket-client not installed. """ notifier = AlertListener(self, callback) notifier.start() return notifier
def startAlertListener(self, callback=None): """ Creates a websocket connection to the Plex Server to optionally recieve notifications. These often include messages from Plex about media scans as well as updates to currently running Transcode Sessions. NOTE: You need websocket-client installed in order to use this feature. >> pip install websocket-client Parameters: callback (func): Callback function to call on recieved messages. raises: :class:`~plexapi.exception.Unsupported`: Websocket-client not installed. """ notifier = AlertListener(self, callback) notifier.start() return notifier
def launch_alert_listener(interval=0): """ Launch a `plexapi.AlertListener` thread to receive updates directly from the Plex Media Server. :param float interval: the interval in seconds after which the system should check that the `AlertListener` is still alive. Set to 0 to disable rechecking. Default: 0 """ try: plex = get_plex() listener = AlertListener(server=plex, callback=library_scan_callback) listener.setName('AlertListener') listener.start() app.logger.info('Started listener: %s', listener) except Exception as ex: app.logger.warn('Exception while trying to start listener: %s', ex) if interval > 0: event = threading.Event() thread = threading.Thread(target=check_alert_listener, args=(event, interval)) thread.setName('AlertListenerWatcher') thread.setDaemon(True) thread.start()
class PlexAlertListener(threading.Thread): productName = "Plex Media Server" updateTimeoutTimerInterval = 30 connectionTimeoutTimerInterval = 60 maximumIgnores = 2 def __init__(self, token: str, serverConfig: models.config.Server): super().__init__() self.daemon = True self.token = token self.serverConfig = serverConfig self.logger = LoggerWithPrefix( f"[{self.serverConfig['name']}/{hashlib.md5(str(id(self)).encode('UTF-8')).hexdigest()[:5].upper()}] " ) self.discordRpcService = DiscordRpcService() self.updateTimeoutTimer: Optional[threading.Timer] = None self.connectionTimeoutTimer: Optional[threading.Timer] = None self.account: Optional[MyPlexAccount] = None self.server: Optional[PlexServer] = None self.alertListener: Optional[AlertListener] = None self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0 self.listenForUser, self.isServerOwner, self.ignoreCount = "", False, 0 self.start() def run(self) -> None: connected = False while not connected: try: self.logger.info("Signing into Plex") self.account = MyPlexAccount(token=self.token) self.logger.info("Signed in as Plex user \"%s\"", self.account.username) self.listenForUser = self.serverConfig.get( "listenForUser", self.account.username) self.server = None for resource in self.account.resources(): if resource.product == self.productName and resource.name.lower( ) == self.serverConfig["name"].lower(): self.logger.info("Connecting to %s \"%s\"", self.productName, self.serverConfig["name"]) self.server = resource.connect() try: self.server.account() self.isServerOwner = True except: pass self.logger.info("Connected to %s \"%s\"", self.productName, resource.name) self.alertListener = AlertListener( self.server, self.handlePlexAlert, self.reconnect) self.alertListener.start() self.logger.info( "Listening for alerts from user \"%s\"", self.listenForUser) self.connectionTimeoutTimer = threading.Timer( self.connectionTimeoutTimerInterval, self.connectionTimeout) self.connectionTimeoutTimer.start() connected = True break if not self.server: self.logger.error("%s \"%s\" not found", self.productName, self.serverConfig["name"]) break except Exception as e: self.logger.error("Failed to connect to %s \"%s\": %s", self.productName, self.serverConfig["name"], e) self.logger.error("Reconnecting in 10 seconds") time.sleep(10) def disconnect(self) -> None: if self.alertListener: try: self.alertListener.stop() except: pass self.disconnectRpc() self.account, self.server, self.alertListener, self.listenForUser, self.isServerOwner, self.ignoreCount = None, None, None, "", False, 0 self.logger.info("Stopped listening for alerts") def reconnect(self, exception: Exception) -> None: self.logger.error("Connection to Plex lost: %s", exception) self.disconnect() self.logger.error("Reconnecting") self.run() def disconnectRpc(self) -> None: self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0 self.discordRpcService.disconnect() self.cancelTimers() def cancelTimers(self) -> None: if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() if self.connectionTimeoutTimer: self.connectionTimeoutTimer.cancel() self.updateTimeoutTimer, self.connectionTimeoutTimer = None, None def updateTimeout(self) -> None: self.logger.debug("No recent updates from session key %s", self.lastSessionKey) self.disconnectRpc() def connectionTimeout(self) -> None: try: assert self.server self.logger.debug( "Request for list of clients to check connection: %s", self.server.clients()) except Exception as e: self.reconnect(e) else: self.connectionTimeoutTimer = threading.Timer( self.connectionTimeoutTimerInterval, self.connectionTimeout) self.connectionTimeoutTimer.start() def handlePlexAlert(self, alert: models.plex.Alert) -> None: try: if alert[ "type"] == "playing" and "PlaySessionStateNotification" in alert: stateNotification = alert["PlaySessionStateNotification"][0] state = stateNotification["state"] sessionKey = int(stateNotification["sessionKey"]) ratingKey = int(stateNotification["ratingKey"]) viewOffset = int(stateNotification["viewOffset"]) self.logger.debug("Received alert: %s", stateNotification) assert self.server item: PlexPartialObject = self.server.fetchItem(ratingKey) libraryName: str = item.section().title if "blacklistedLibraries" in self.serverConfig and libraryName in self.serverConfig[ "blacklistedLibraries"]: self.logger.debug( "Library \"%s\" is blacklisted, ignoring", libraryName) return if "whitelistedLibraries" in self.serverConfig and libraryName not in self.serverConfig[ "whitelistedLibraries"]: self.logger.debug( "Library \"%s\" is not whitelisted, ignoring", libraryName) return if self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey: if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() self.updateTimeoutTimer = None if self.lastState == state and self.ignoreCount < self.maximumIgnores: self.logger.debug("Nothing changed, ignoring") self.ignoreCount += 1 self.updateTimeoutTimer = threading.Timer( self.updateTimeoutTimerInterval, self.updateTimeout) self.updateTimeoutTimer.start() return else: self.ignoreCount = 0 if state == "stopped": self.disconnectRpc() return elif state == "stopped": self.logger.debug( "Received \"stopped\" state alert from unknown session, ignoring" ) return if self.isServerOwner: self.logger.debug("Searching sessions for session key %s", sessionKey) sessions: list[Playable] = self.server.sessions() if len(sessions) < 1: self.logger.debug("Empty session list, ignoring") return for session in sessions: self.logger.debug("%s, Session Key: %s, Usernames: %s", session, session.sessionKey, session.usernames) if session.sessionKey == sessionKey: self.logger.debug("Session found") sessionUsername: str = session.usernames[0] if sessionUsername.lower( ) == self.listenForUser.lower(): self.logger.debug( "Username \"%s\" matches \"%s\", continuing", sessionUsername, self.listenForUser) break self.logger.debug( "Username \"%s\" doesn't match \"%s\", ignoring", sessionUsername, self.listenForUser) return else: self.logger.debug( "No matching session found, ignoring") return if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() self.updateTimeoutTimer = threading.Timer( self.updateTimeoutTimerInterval, self.updateTimeout) self.updateTimeoutTimer.start() self.lastState, self.lastSessionKey, self.lastRatingKey = state, sessionKey, ratingKey mediaType: str = item.type title: str thumb: str if mediaType in ["movie", "episode"]: stateStrings: list[str] = [ formatSeconds(item.duration / 1000) ] if mediaType == "movie": title = f"{item.title} ({item.year})" stateStrings.append( f"{', '.join(genre.tag for genre in item.genres[:3])}" ) largeText = "Watching a movie" thumb = item.thumb else: title = item.grandparentTitle stateStrings.append( f"S{item.parentIndex:02}E{item.index:02}") stateStrings.append(item.title) largeText = "Watching a TV show" thumb = item.grandparentThumb if state != "playing": stateStrings.append( f"{formatSeconds(viewOffset / 1000, ':')} elapsed") stateText = " ยท ".join(stateString for stateString in stateStrings if stateString) elif mediaType == "track": title = item.title stateText = f"{item.originalTitle or item.grandparentTitle} - {item.parentTitle} ({self.server.fetchItem(item.parentRatingKey).year})" largeText = "Listening to music" thumb = item.thumb else: self.logger.debug( "Unsupported media type \"%s\", ignoring", mediaType) return thumbUrl = "" if config["display"]["posters"]["enabled"]: thumbUrl = getKey(thumb) if not thumbUrl: self.logger.debug("Uploading image") thumbUrl = uploadImage(self.server.url(thumb, True)) setKey(thumb, thumbUrl) activity: models.discord.Activity = { "details": title[:128], "state": stateText[:128], "assets": { "large_text": largeText, "large_image": thumbUrl or "logo", "small_text": state.capitalize(), "small_image": state, }, } if state == "playing": currentTimestamp = int(time.time()) if config["display"]["useRemainingTime"]: activity["timestamps"] = { "end": round(currentTimestamp + ((item.duration - viewOffset) / 1000)) } else: activity["timestamps"] = { "start": round(currentTimestamp - (viewOffset / 1000)) } if not self.discordRpcService.connected: self.discordRpcService.connect() if self.discordRpcService.connected: self.discordRpcService.setActivity(activity) except: self.logger.exception( "An unexpected error occured in the alert handler")