Support for specifying Discord IPC pipe number

This commit is contained in:
Phin 2023-11-05 15:54:36 +05:30
parent d6ac5e5e90
commit 453855b1c3
6 changed files with 76 additions and 43 deletions

View file

@ -51,6 +51,7 @@ The config file is stored in a directory named `data`.
* `listenForUser` (string, optional) - The script reacts to alerts originating only from this username. Defaults to the parent user's username if not set.
* `blacklistedLibraries` (list, optional) - Alerts originating from libraries in this list are ignored.
* `whitelistedLibraries` (list, optional) - If set, alerts originating from libraries that are not in this list are ignored.
* `ipcPipeNumber` (int, optional) - A number in the range of `0-9` to specify the Discord IPC pipe to connect to. Defaults to `-1`, which specifies that the first existing pipe in the range should be used. When a Discord client is launched, it binds to the first unbound pipe number, which is typically `0`.
### Obtaining an Imgur client ID

View file

@ -2,7 +2,7 @@ import os
import sys
name = "Discord Rich Presence for Plex"
version = "2.4.1"
version = "2.4.2"
plexClientID = "discord-rich-presence-plex"
discordClientID = "413407336082833418"

View file

@ -10,27 +10,41 @@ import time
class DiscordIpcService:
def __init__(self):
self.ipcPipe = (("/run/app" if os.path.isdir("/run/app") else os.environ.get("XDG_RUNTIME_DIR", os.environ.get("TMPDIR", os.environ.get("TMP", os.environ.get("TEMP", "/tmp"))))) + "/discord-ipc-0") if isUnix else r"\\?\pipe\discord-ipc-0"
self.loop: asyncio.AbstractEventLoop = None # pyright: ignore[reportGeneralTypeIssues]
self.pipeReader: asyncio.StreamReader = None # pyright: ignore[reportGeneralTypeIssues]
self.pipeWriter: asyncio.StreamWriter = None # pyright: ignore[reportGeneralTypeIssues]
def __init__(self, ipcPipeNumber: Optional[int]):
ipcPipeNumber = ipcPipeNumber or -1
ipcPipeNumbers = range(10) if ipcPipeNumber == -1 else [ipcPipeNumber]
ipcPipeBase = ("/run/app" if os.path.isdir("/run/app") else os.environ.get("XDG_RUNTIME_DIR", os.environ.get("TMPDIR", os.environ.get("TMP", os.environ.get("TEMP", "/tmp"))))) if isUnix else r"\\?\pipe"
self.ipcPipes = [os.path.join(ipcPipeBase, f"discord-ipc-{ipcPipeNumber}") for ipcPipeNumber in ipcPipeNumbers]
self.loop: Optional[asyncio.AbstractEventLoop] = None
self.pipeReader: Optional[asyncio.StreamReader] = None
self.pipeWriter: Optional[asyncio.StreamWriter] = None
self.connected = False
async def handshake(self) -> None:
try:
if isUnix:
self.pipeReader, self.pipeWriter = await asyncio.open_unix_connection(self.ipcPipe) # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
else:
self.pipeReader = asyncio.StreamReader()
self.pipeWriter = (await self.loop.create_pipe_connection(lambda: asyncio.StreamReaderProtocol(self.pipeReader), self.ipcPipe))[0] # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
self.write(0, { "v": 1, "client_id": discordClientID })
if await self.read():
self.connected = True
except:
logger.exception("An unexpected error occured during an IPC handshake operation")
if not self.loop:
return
for ipcPipe in self.ipcPipes:
try:
if isUnix:
self.pipeReader, self.pipeWriter = await asyncio.open_unix_connection(ipcPipe) # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
else:
self.pipeReader = asyncio.StreamReader()
self.pipeWriter = (await self.loop.create_pipe_connection(lambda: asyncio.StreamReaderProtocol(self.pipeReader), ipcPipe))[0] # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
self.write(0, { "v": 1, "client_id": discordClientID })
if await self.read():
self.connected = True
logger.info(f"Connected to Discord IPC pipe {ipcPipe}")
break
except FileNotFoundError:
pass
except:
logger.exception(f"An unexpected error occured while connecting to Discord IPC pipe {ipcPipe}")
if not self.connected:
logger.error(f"Discord IPC pipe not found (attempted pipes: {', '.join(self.ipcPipes)})")
async def read(self) -> Optional[Any]:
if not self.pipeReader:
return
try:
dataBytes = await self.pipeReader.read(1024)
data = json.loads(dataBytes[8:].decode("utf-8"))
@ -41,6 +55,8 @@ class DiscordIpcService:
self.connected = False
def write(self, op: int, payload: Any) -> None:
if not self.pipeWriter:
return
try:
logger.debug("[WRITE] %s", payload)
payload = json.dumps(payload)
@ -51,17 +67,19 @@ class DiscordIpcService:
def connect(self) -> None:
if self.connected:
logger.debug("Attempt to connect Discord IPC pipe while already connected")
logger.warning("Attempt to connect to Discord IPC pipe while already connected")
return
logger.info("Connecting Discord IPC pipe")
logger.info("Connecting to Discord IPC pipe")
self.loop = asyncio.new_event_loop()
self.loop.run_until_complete(self.handshake())
def disconnect(self) -> None:
if not self.connected:
logger.debug("Attempt to disconnect Discord IPC pipe while not connected")
logger.warning("Attempt to disconnect from Discord IPC pipe while not connected")
return
logger.info("Disconnecting Discord IPC pipe")
if not self.loop or not self.pipeWriter or not self.pipeReader:
return
logger.info("Disconnecting from Discord IPC pipe")
try:
self.pipeWriter.close()
except:
@ -77,6 +95,11 @@ class DiscordIpcService:
self.connected = False
def setActivity(self, activity: models.discord.Activity) -> None:
if not self.connected:
logger.warning("Attempt to set activity while not connected to Discord IPC pipe")
return
if not self.loop:
return
logger.info("Activity update: %s", activity)
payload = {
"cmd": "SET_ACTIVITY",

View file

@ -47,7 +47,7 @@ class PlexAlertListener(threading.Thread):
self.token = token
self.serverConfig = serverConfig
self.logger = LoggerWithPrefix(f"[{self.serverConfig['name']}] ") # pyright: ignore[reportTypedDictNotRequiredAccess]
self.discordIpcService = DiscordIpcService()
self.discordIpcService = DiscordIpcService(self.serverConfig.get("ipcPipeNumber"))
self.updateTimeoutTimer: Optional[threading.Timer] = None
self.connectionTimeoutTimer: Optional[threading.Timer] = None
self.account: Optional[MyPlexAccount] = None
@ -63,22 +63,22 @@ class PlexAlertListener(threading.Thread):
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.logger.info("Signed in as Plex user '%s'", self.account.username)
self.listenForUser = self.serverConfig.get("listenForUser", "") or 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.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.logger.info("Connected to %s '%s'", self.productName, resource.name)
self.alertListener = AlertListener(self.server, self.handleAlert, self.reconnect)
self.alertListener.start()
self.logger.info("Listening for alerts from user \"%s\"", self.listenForUser)
self.logger.info("Listening for alerts from user '%s'", self.listenForUser)
self.connectionTimeoutTimer = threading.Timer(self.connectionTimeoutTimerInterval, self.connectionTimeout)
self.connectionTimeoutTimer.start()
connected = True
@ -86,7 +86,7 @@ class PlexAlertListener(threading.Thread):
if not self.server:
raise Exception("Server not found")
except Exception as e:
self.logger.error("Failed to connect to %s \"%s\": %s", self.productName, self.serverConfig["name"], e) # pyright: ignore[reportTypedDictNotRequiredAccess]
self.logger.error("Failed to connect to %s '%s': %s", self.productName, self.serverConfig["name"], e) # pyright: ignore[reportTypedDictNotRequiredAccess]
self.logger.error("Reconnecting in 10 seconds")
time.sleep(10)
@ -108,7 +108,8 @@ class PlexAlertListener(threading.Thread):
def disconnectRpc(self) -> None:
self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0
self.discordIpcService.disconnect()
if self.discordIpcService.connected:
self.discordIpcService.disconnect()
self.cancelTimers()
def cancelTimers(self) -> None:
@ -145,10 +146,10 @@ class PlexAlertListener(threading.Thread):
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)
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)
self.logger.debug("Library '%s' is not whitelisted, ignoring", libraryName)
return
if self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey:
if self.updateTimeoutTimer:
@ -166,7 +167,7 @@ class PlexAlertListener(threading.Thread):
self.disconnectRpc()
return
elif state == "stopped":
self.logger.debug("Received \"stopped\" state alert from unknown session, ignoring")
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)
@ -180,9 +181,9 @@ class PlexAlertListener(threading.Thread):
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)
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)
self.logger.debug("Username '%s' doesn't match '%s', ignoring", sessionUsername, self.listenForUser)
return
else:
self.logger.debug("No matching session found, ignoring")
@ -221,7 +222,7 @@ class PlexAlertListener(threading.Thread):
largeText = "Listening to music"
thumb = item.thumb
else:
self.logger.debug("Unsupported media type \"%s\", ignoring", mediaType)
self.logger.debug("Unsupported media type '%s', ignoring", mediaType)
return
thumbUrl = ""
if thumb and config["display"]["posters"]["enabled"]:

25
main.py
View file

@ -38,7 +38,7 @@ import logging
import models.config
import time
def main() -> None:
def init() -> None:
if not os.path.exists(dataDirectoryPath):
os.mkdir(dataDirectoryPath)
for oldFilePath in ["config.json", "cache.json", "console.log"]:
@ -53,6 +53,9 @@ def main() -> None:
logger.addHandler(fileHandler)
logger.info("%s - v%s", name, version)
loadCache()
def main() -> None:
init()
if not config["users"]:
logger.info("No users found in the config file")
user = authNewUser()
@ -92,9 +95,10 @@ def authNewUser() -> Optional[models.config.User]:
else:
logger.info(f"Authentication timed out ({formatSeconds(180)})")
def testIpc() -> None:
def testIpc(ipcPipeNumber: int) -> None:
init()
logger.info("Testing Discord IPC connection")
discordIpcService = DiscordIpcService()
discordIpcService = DiscordIpcService(ipcPipeNumber)
discordIpcService.connect()
discordIpcService.setActivity({
"details": "details",
@ -111,9 +115,12 @@ def testIpc() -> None:
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else ""
if not mode:
main()
elif mode == "test-ipc":
testIpc()
else:
print(f"Invalid mode: {mode}")
try:
if not mode:
main()
elif mode == "test-ipc":
testIpc(int(sys.argv[2]) if len(sys.argv) > 2 else -1)
else:
print(f"Invalid mode: {mode}")
except KeyboardInterrupt:
pass

View file

@ -23,6 +23,7 @@ class Server(TypedDict, total = False):
listenForUser: str
blacklistedLibraries: list[str]
whitelistedLibraries: list[str]
ipcPipeNumber: int
class User(TypedDict):
token: str