Example #1
0
    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
Example #2
0
    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
Example #3
0
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")