mirror of
https://github.com/phin05/discord-rich-presence-plex
synced 2025-02-16 13:48:26 +00:00
Support for specifying Discord IPC pipe number
This commit is contained in:
parent
d6ac5e5e90
commit
453855b1c3
6 changed files with 76 additions and 43 deletions
|
@ -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.
|
* `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.
|
* `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.
|
* `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
|
### Obtaining an Imgur client ID
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
name = "Discord Rich Presence for Plex"
|
name = "Discord Rich Presence for Plex"
|
||||||
version = "2.4.1"
|
version = "2.4.2"
|
||||||
|
|
||||||
plexClientID = "discord-rich-presence-plex"
|
plexClientID = "discord-rich-presence-plex"
|
||||||
discordClientID = "413407336082833418"
|
discordClientID = "413407336082833418"
|
||||||
|
|
|
@ -10,27 +10,41 @@ import time
|
||||||
|
|
||||||
class DiscordIpcService:
|
class DiscordIpcService:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, ipcPipeNumber: Optional[int]):
|
||||||
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"
|
ipcPipeNumber = ipcPipeNumber or -1
|
||||||
self.loop: asyncio.AbstractEventLoop = None # pyright: ignore[reportGeneralTypeIssues]
|
ipcPipeNumbers = range(10) if ipcPipeNumber == -1 else [ipcPipeNumber]
|
||||||
self.pipeReader: asyncio.StreamReader = None # pyright: ignore[reportGeneralTypeIssues]
|
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.pipeWriter: asyncio.StreamWriter = None # pyright: ignore[reportGeneralTypeIssues]
|
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
|
self.connected = False
|
||||||
|
|
||||||
async def handshake(self) -> None:
|
async def handshake(self) -> None:
|
||||||
try:
|
if not self.loop:
|
||||||
if isUnix:
|
return
|
||||||
self.pipeReader, self.pipeWriter = await asyncio.open_unix_connection(self.ipcPipe) # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
|
for ipcPipe in self.ipcPipes:
|
||||||
else:
|
try:
|
||||||
self.pipeReader = asyncio.StreamReader()
|
if isUnix:
|
||||||
self.pipeWriter = (await self.loop.create_pipe_connection(lambda: asyncio.StreamReaderProtocol(self.pipeReader), self.ipcPipe))[0] # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
|
self.pipeReader, self.pipeWriter = await asyncio.open_unix_connection(ipcPipe) # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
|
||||||
self.write(0, { "v": 1, "client_id": discordClientID })
|
else:
|
||||||
if await self.read():
|
self.pipeReader = asyncio.StreamReader()
|
||||||
self.connected = True
|
self.pipeWriter = (await self.loop.create_pipe_connection(lambda: asyncio.StreamReaderProtocol(self.pipeReader), ipcPipe))[0] # pyright: ignore[reportGeneralTypeIssues,reportUnknownMemberType]
|
||||||
except:
|
self.write(0, { "v": 1, "client_id": discordClientID })
|
||||||
logger.exception("An unexpected error occured during an IPC handshake operation")
|
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]:
|
async def read(self) -> Optional[Any]:
|
||||||
|
if not self.pipeReader:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
dataBytes = await self.pipeReader.read(1024)
|
dataBytes = await self.pipeReader.read(1024)
|
||||||
data = json.loads(dataBytes[8:].decode("utf-8"))
|
data = json.loads(dataBytes[8:].decode("utf-8"))
|
||||||
|
@ -41,6 +55,8 @@ class DiscordIpcService:
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
|
||||||
def write(self, op: int, payload: Any) -> None:
|
def write(self, op: int, payload: Any) -> None:
|
||||||
|
if not self.pipeWriter:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
logger.debug("[WRITE] %s", payload)
|
logger.debug("[WRITE] %s", payload)
|
||||||
payload = json.dumps(payload)
|
payload = json.dumps(payload)
|
||||||
|
@ -51,17 +67,19 @@ class DiscordIpcService:
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
if self.connected:
|
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
|
return
|
||||||
logger.info("Connecting Discord IPC pipe")
|
logger.info("Connecting to Discord IPC pipe")
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
self.loop.run_until_complete(self.handshake())
|
self.loop.run_until_complete(self.handshake())
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
def disconnect(self) -> None:
|
||||||
if not self.connected:
|
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
|
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:
|
try:
|
||||||
self.pipeWriter.close()
|
self.pipeWriter.close()
|
||||||
except:
|
except:
|
||||||
|
@ -77,6 +95,11 @@ class DiscordIpcService:
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
|
||||||
def setActivity(self, activity: models.discord.Activity) -> None:
|
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)
|
logger.info("Activity update: %s", activity)
|
||||||
payload = {
|
payload = {
|
||||||
"cmd": "SET_ACTIVITY",
|
"cmd": "SET_ACTIVITY",
|
||||||
|
|
27
core/plex.py
27
core/plex.py
|
@ -47,7 +47,7 @@ class PlexAlertListener(threading.Thread):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.serverConfig = serverConfig
|
self.serverConfig = serverConfig
|
||||||
self.logger = LoggerWithPrefix(f"[{self.serverConfig['name']}] ") # pyright: ignore[reportTypedDictNotRequiredAccess]
|
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.updateTimeoutTimer: Optional[threading.Timer] = None
|
||||||
self.connectionTimeoutTimer: Optional[threading.Timer] = None
|
self.connectionTimeoutTimer: Optional[threading.Timer] = None
|
||||||
self.account: Optional[MyPlexAccount] = None
|
self.account: Optional[MyPlexAccount] = None
|
||||||
|
@ -63,22 +63,22 @@ class PlexAlertListener(threading.Thread):
|
||||||
try:
|
try:
|
||||||
self.logger.info("Signing into Plex")
|
self.logger.info("Signing into Plex")
|
||||||
self.account = MyPlexAccount(token = self.token)
|
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.listenForUser = self.serverConfig.get("listenForUser", "") or self.account.username
|
||||||
self.server = None
|
self.server = None
|
||||||
for resource in self.account.resources():
|
for resource in self.account.resources():
|
||||||
if resource.product == self.productName and resource.name.lower() == self.serverConfig["name"].lower():
|
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()
|
self.server = resource.connect()
|
||||||
try:
|
try:
|
||||||
self.server.account()
|
self.server.account()
|
||||||
self.isServerOwner = True
|
self.isServerOwner = True
|
||||||
except:
|
except:
|
||||||
pass
|
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 = AlertListener(self.server, self.handleAlert, self.reconnect)
|
||||||
self.alertListener.start()
|
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 = threading.Timer(self.connectionTimeoutTimerInterval, self.connectionTimeout)
|
||||||
self.connectionTimeoutTimer.start()
|
self.connectionTimeoutTimer.start()
|
||||||
connected = True
|
connected = True
|
||||||
|
@ -86,7 +86,7 @@ class PlexAlertListener(threading.Thread):
|
||||||
if not self.server:
|
if not self.server:
|
||||||
raise Exception("Server not found")
|
raise Exception("Server not found")
|
||||||
except Exception as e:
|
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")
|
self.logger.error("Reconnecting in 10 seconds")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
@ -108,7 +108,8 @@ class PlexAlertListener(threading.Thread):
|
||||||
|
|
||||||
def disconnectRpc(self) -> None:
|
def disconnectRpc(self) -> None:
|
||||||
self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0
|
self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0
|
||||||
self.discordIpcService.disconnect()
|
if self.discordIpcService.connected:
|
||||||
|
self.discordIpcService.disconnect()
|
||||||
self.cancelTimers()
|
self.cancelTimers()
|
||||||
|
|
||||||
def cancelTimers(self) -> None:
|
def cancelTimers(self) -> None:
|
||||||
|
@ -145,10 +146,10 @@ class PlexAlertListener(threading.Thread):
|
||||||
item: PlexPartialObject = self.server.fetchItem(ratingKey)
|
item: PlexPartialObject = self.server.fetchItem(ratingKey)
|
||||||
libraryName: str = item.section().title
|
libraryName: str = item.section().title
|
||||||
if "blacklistedLibraries" in self.serverConfig and libraryName in self.serverConfig["blacklistedLibraries"]:
|
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
|
return
|
||||||
if "whitelistedLibraries" in self.serverConfig and libraryName not in self.serverConfig["whitelistedLibraries"]:
|
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
|
return
|
||||||
if self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey:
|
if self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey:
|
||||||
if self.updateTimeoutTimer:
|
if self.updateTimeoutTimer:
|
||||||
|
@ -166,7 +167,7 @@ class PlexAlertListener(threading.Thread):
|
||||||
self.disconnectRpc()
|
self.disconnectRpc()
|
||||||
return
|
return
|
||||||
elif state == "stopped":
|
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
|
return
|
||||||
if self.isServerOwner:
|
if self.isServerOwner:
|
||||||
self.logger.debug("Searching sessions for session key %s", sessionKey)
|
self.logger.debug("Searching sessions for session key %s", sessionKey)
|
||||||
|
@ -180,9 +181,9 @@ class PlexAlertListener(threading.Thread):
|
||||||
self.logger.debug("Session found")
|
self.logger.debug("Session found")
|
||||||
sessionUsername: str = session.usernames[0]
|
sessionUsername: str = session.usernames[0]
|
||||||
if sessionUsername.lower() == self.listenForUser.lower():
|
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
|
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
|
return
|
||||||
else:
|
else:
|
||||||
self.logger.debug("No matching session found, ignoring")
|
self.logger.debug("No matching session found, ignoring")
|
||||||
|
@ -221,7 +222,7 @@ class PlexAlertListener(threading.Thread):
|
||||||
largeText = "Listening to music"
|
largeText = "Listening to music"
|
||||||
thumb = item.thumb
|
thumb = item.thumb
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Unsupported media type \"%s\", ignoring", mediaType)
|
self.logger.debug("Unsupported media type '%s', ignoring", mediaType)
|
||||||
return
|
return
|
||||||
thumbUrl = ""
|
thumbUrl = ""
|
||||||
if thumb and config["display"]["posters"]["enabled"]:
|
if thumb and config["display"]["posters"]["enabled"]:
|
||||||
|
|
25
main.py
25
main.py
|
@ -38,7 +38,7 @@ import logging
|
||||||
import models.config
|
import models.config
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def main() -> None:
|
def init() -> None:
|
||||||
if not os.path.exists(dataDirectoryPath):
|
if not os.path.exists(dataDirectoryPath):
|
||||||
os.mkdir(dataDirectoryPath)
|
os.mkdir(dataDirectoryPath)
|
||||||
for oldFilePath in ["config.json", "cache.json", "console.log"]:
|
for oldFilePath in ["config.json", "cache.json", "console.log"]:
|
||||||
|
@ -53,6 +53,9 @@ def main() -> None:
|
||||||
logger.addHandler(fileHandler)
|
logger.addHandler(fileHandler)
|
||||||
logger.info("%s - v%s", name, version)
|
logger.info("%s - v%s", name, version)
|
||||||
loadCache()
|
loadCache()
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
init()
|
||||||
if not config["users"]:
|
if not config["users"]:
|
||||||
logger.info("No users found in the config file")
|
logger.info("No users found in the config file")
|
||||||
user = authNewUser()
|
user = authNewUser()
|
||||||
|
@ -92,9 +95,10 @@ def authNewUser() -> Optional[models.config.User]:
|
||||||
else:
|
else:
|
||||||
logger.info(f"Authentication timed out ({formatSeconds(180)})")
|
logger.info(f"Authentication timed out ({formatSeconds(180)})")
|
||||||
|
|
||||||
def testIpc() -> None:
|
def testIpc(ipcPipeNumber: int) -> None:
|
||||||
|
init()
|
||||||
logger.info("Testing Discord IPC connection")
|
logger.info("Testing Discord IPC connection")
|
||||||
discordIpcService = DiscordIpcService()
|
discordIpcService = DiscordIpcService(ipcPipeNumber)
|
||||||
discordIpcService.connect()
|
discordIpcService.connect()
|
||||||
discordIpcService.setActivity({
|
discordIpcService.setActivity({
|
||||||
"details": "details",
|
"details": "details",
|
||||||
|
@ -111,9 +115,12 @@ def testIpc() -> None:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||||
if not mode:
|
try:
|
||||||
main()
|
if not mode:
|
||||||
elif mode == "test-ipc":
|
main()
|
||||||
testIpc()
|
elif mode == "test-ipc":
|
||||||
else:
|
testIpc(int(sys.argv[2]) if len(sys.argv) > 2 else -1)
|
||||||
print(f"Invalid mode: {mode}")
|
else:
|
||||||
|
print(f"Invalid mode: {mode}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Server(TypedDict, total = False):
|
||||||
listenForUser: str
|
listenForUser: str
|
||||||
blacklistedLibraries: list[str]
|
blacklistedLibraries: list[str]
|
||||||
whitelistedLibraries: list[str]
|
whitelistedLibraries: list[str]
|
||||||
|
ipcPipeNumber: int
|
||||||
|
|
||||||
class User(TypedDict):
|
class User(TypedDict):
|
||||||
token: str
|
token: str
|
||||||
|
|
Loading…
Add table
Reference in a new issue