mirror of
https://github.com/phin05/discord-rich-presence-plex
synced 2024-11-24 18:43:02 +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.
|
||||
* `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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
27
core/plex.py
27
core/plex.py
|
@ -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
25
main.py
|
@ -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
|
||||
|
|
|
@ -23,6 +23,7 @@ class Server(TypedDict, total = False):
|
|||
listenForUser: str
|
||||
blacklistedLibraries: list[str]
|
||||
whitelistedLibraries: list[str]
|
||||
ipcPipeNumber: int
|
||||
|
||||
class User(TypedDict):
|
||||
token: str
|
||||
|
|
Loading…
Reference in a new issue