discord-rich-presence-plex/discordRichPresencePlex.py

388 lines
13 KiB
Python
Raw Normal View History

2018-02-15 00:24:15 +05:30
import asyncio
2018-04-26 00:33:31 +05:30
import datetime
2018-04-25 00:42:36 +05:30
import hashlib
2018-02-15 00:24:15 +05:30
import json
import os
import plexapi.myplex
import struct
import subprocess
import sys
import tempfile
import threading
import time
2018-03-11 21:31:58 +05:30
class plexConfig:
2018-04-26 00:33:31 +05:30
extraLogging = True
2018-11-16 02:58:55 +05:30
timeRemaining = False
2018-04-25 00:42:36 +05:30
2018-09-13 23:17:39 +05:30
def __init__(self, serverName = "", username = "", password = "", token = "", listenForUser = "", clientID = "413407336082833418"):
2018-04-25 00:42:36 +05:30
self.serverName = serverName
self.username = username
self.password = password
self.token = token
self.listenForUser = (username if listenForUser == "" else listenForUser).lower()
2018-09-13 23:17:39 +05:30
self.clientID = clientID
2018-04-25 00:42:36 +05:30
plexConfigs = [
# plexConfig(serverName = "", username = "", password = "", token = "", listenForUser = "")
]
2018-02-15 00:24:15 +05:30
class discordRichPresence:
2018-04-25 00:42:36 +05:30
def __init__(self, clientID, child):
2018-02-15 00:24:15 +05:30
self.IPCPipe = ((os.environ.get("XDG_RUNTIME_DIR", None) or os.environ.get("TMPDIR", None) or os.environ.get("TMP", None) or os.environ.get("TEMP", None) or "/tmp") + "/discord-ipc-0") if isLinux else "\\\\?\\pipe\\discord-ipc-0"
self.clientID = clientID
self.pipeReader = None
self.pipeWriter = None
self.process = None
self.running = False
2018-04-25 00:42:36 +05:30
self.child = child
2018-02-15 00:24:15 +05:30
async def read(self):
2018-02-15 01:47:33 +05:30
try:
data = await self.pipeReader.read(1024)
2018-04-25 00:42:36 +05:30
self.child.log("[READ] " + str(json.loads(data[8:].decode("utf-8"))))
2018-02-15 01:47:33 +05:30
except Exception as e:
2018-04-27 11:33:25 +05:30
self.child.log("[READ] " + str(e))
self.stop()
2018-02-15 00:24:15 +05:30
def write(self, op, payload):
payload = json.dumps(payload)
2018-04-25 00:42:36 +05:30
self.child.log("[WRITE] " + str(payload))
2018-02-15 00:24:15 +05:30
data = self.pipeWriter.write(struct.pack("<ii", op, len(payload)) + payload.encode("utf-8"))
async def handshake(self):
2018-04-27 11:33:25 +05:30
try:
if (isLinux):
self.pipeReader, self.pipeWriter = await asyncio.open_unix_connection(self.IPCPipe, loop = self.loop)
else:
self.pipeReader = asyncio.StreamReader(loop = self.loop)
self.pipeWriter, _ = await self.loop.create_pipe_connection(lambda: asyncio.StreamReaderProtocol(self.pipeReader, loop = self.loop), self.IPCPipe)
self.write(0, {"v": 1, "client_id": self.clientID})
await self.read()
self.running = True
except Exception as e:
self.child.log("[HANDSHAKE] " + str(e))
2018-02-15 00:24:15 +05:30
def start(self):
2018-04-25 00:42:36 +05:30
self.child.log("Opening Discord IPC Pipe")
2018-02-15 00:24:15 +05:30
emptyProcessFilePath = tempfile.gettempdir() + "\\discordRichPresencePlex-emptyProcess.py"
if (not os.path.exists(emptyProcessFilePath)):
with open(emptyProcessFilePath, "w") as emptyProcessFile:
2018-04-26 00:33:31 +05:30
emptyProcessFile.write("import time\n\ntry:\n\twhile (True):\n\t\ttime.sleep(3600)\nexcept:\n\tpass")
2018-03-15 01:09:40 +05:30
self.process = subprocess.Popen(["python3" if isLinux else "pythonw", emptyProcessFilePath])
2018-08-09 13:23:18 +05:30
self.loop = asyncio.new_event_loop() if isLinux else asyncio.ProactorEventLoop()
2018-02-15 00:24:15 +05:30
self.loop.run_until_complete(self.handshake())
def stop(self):
2018-04-25 00:42:36 +05:30
self.child.log("Closing Discord IPC Pipe")
2018-04-27 11:33:25 +05:30
self.child.lastState, self.child.lastSessionKey, self.child.lastRatingKey = None, None, None
2018-02-15 00:24:15 +05:30
self.process.kill()
2018-09-12 00:21:26 +05:30
if (self.child.stopTimer):
self.child.stopTimer.cancel()
self.child.stopTimer = None
if (self.child.stopTimer2):
self.child.stopTimer2.cancel()
self.child.stopTimer2 = None
2018-08-24 11:03:49 +05:30
if (self.pipeWriter):
try:
self.pipeWriter.close()
except:
pass
2018-09-12 00:21:26 +05:30
self.pipeWriter = None
if (self.pipeReader):
2018-08-24 11:03:49 +05:30
try:
self.loop.run_until_complete(self.pipeReader.read(1024))
except:
pass
2018-09-12 00:21:26 +05:30
self.pipeReader = None
2018-06-11 04:44:50 +05:30
try:
2018-04-27 11:33:25 +05:30
self.loop.close()
except:
pass
2018-02-15 00:24:15 +05:30
self.running = False
def send(self, activity):
payload = {
"cmd": "SET_ACTIVITY",
"args": {
"activity": activity,
"pid": self.process.pid
},
"nonce": "{0:.20f}".format(time.time())
}
2018-08-09 13:23:18 +05:30
self.write(1, payload)
2018-02-15 00:24:15 +05:30
self.loop.run_until_complete(self.read())
class discordRichPresencePlex(discordRichPresence):
productName = "Plex Media Server"
2018-03-15 01:09:40 +05:30
stopTimerInterval = 5
stopTimer2Interval = 35
2018-04-25 00:42:36 +05:30
checkConnectionTimerInterval = 60
2018-08-24 11:03:49 +05:30
maximumIgnores = 3
2018-02-15 00:24:15 +05:30
2018-04-25 00:42:36 +05:30
def __init__(self, plexConfig):
self.plexConfig = plexConfig
self.instanceID = hashlib.md5(str(id(self)).encode("UTF-8")).hexdigest()[:5]
2018-09-13 23:17:39 +05:30
super().__init__(plexConfig.clientID, self)
2018-08-24 11:03:49 +05:30
self.plexAccount = None
self.plexServer = None
2018-10-22 20:26:22 +05:30
self.isServerOwner = False
2018-08-24 11:03:49 +05:30
self.plexAlertListener = None
self.lastState = None
self.lastSessionKey = None
self.lastRatingKey = None
self.stopTimer = None
self.stopTimer2 = None
self.checkConnectionTimer = None
self.ignoreCount = 0
2018-02-15 00:24:15 +05:30
def run(self):
2018-04-27 11:33:25 +05:30
self.reset()
connected = False
while (not connected):
try:
if (self.plexConfig.token):
self.plexAccount = plexapi.myplex.MyPlexAccount(self.plexConfig.username, token = self.plexConfig.token)
else:
self.plexAccount = plexapi.myplex.MyPlexAccount(self.plexConfig.username, self.plexConfig.password)
self.log("Logged in as Plex User \"" + self.plexAccount.username + "\"")
self.plexServer = None
for resource in self.plexAccount.resources():
if (resource.product == self.productName and resource.name == self.plexConfig.serverName):
self.plexServer = resource.connect()
2018-10-22 20:03:12 +05:30
try:
self.plexServer.account()
self.isServerOwner = True
except:
2018-10-22 20:26:22 +05:30
pass
2018-04-27 11:33:25 +05:30
self.log("Connected to " + self.productName + " \"" + self.plexConfig.serverName + "\"")
self.plexAlertListener = self.plexServer.startAlertListener(self.onPlexServerAlert)
self.log("Listening for PlaySessionStateNotification alerts from user \"" + self.plexConfig.listenForUser + "\"")
if (self.checkConnectionTimer):
self.checkConnectionTimer.cancel()
self.checkConnectionTimer = None
self.checkConnectionTimer = threading.Timer(self.checkConnectionTimerInterval, self.checkConnection)
self.checkConnectionTimer.start()
connected = True
break
if (not self.plexServer):
self.log(self.productName + " \"" + self.plexConfig.serverName + "\" not found")
2018-10-22 20:26:22 +05:30
break
2018-04-27 11:33:25 +05:30
except Exception as e:
self.log("Failed to connect to Plex: " + str(e))
self.log("Reconnecting in 10 seconds")
time.sleep(10)
def reset(self):
if (self.running):
self.stop()
self.plexAccount, self.plexServer = None, None
if (self.plexAlertListener):
try:
self.plexAlertListener.stop()
except:
pass
self.plexAlertListener = None
if (self.stopTimer):
self.stopTimer.cancel()
self.stopTimer = None
if (self.stopTimer2):
self.stopTimer2.cancel()
self.stopTimer2 = None
if (self.checkConnectionTimer):
self.checkConnectionTimer.cancel()
self.checkConnectionTimer = None
2018-04-25 00:42:36 +05:30
def checkConnection(self):
try:
2018-10-22 20:03:12 +05:30
self.log("Request for clients list to check connection: " + str(self.plexServer.clients()), extra = True)
2018-04-25 00:42:36 +05:30
self.checkConnectionTimer = threading.Timer(self.checkConnectionTimerInterval, self.checkConnection)
self.checkConnectionTimer.start()
2018-04-26 00:33:31 +05:30
except Exception as e:
self.log("Connection to Plex lost: " + str(e))
self.log("Reconnecting")
2018-04-25 00:42:36 +05:30
self.run()
def log(self, text, colour = "", extra = False):
2018-04-26 00:33:31 +05:30
timestamp = datetime.datetime.now().strftime("%I:%M:%S %p")
prefix = "[" + timestamp + "] [" + self.plexConfig.serverName + "/" + self.instanceID + "] "
2018-04-25 00:42:36 +05:30
lock.acquire()
if (extra):
2018-04-25 01:05:38 +05:30
if (self.plexConfig.extraLogging):
2018-04-25 00:42:36 +05:30
print(prefix + colourText(str(text), colour))
2018-02-15 00:24:15 +05:30
else:
2018-04-25 00:42:36 +05:30
print(prefix + colourText(str(text), colour))
lock.release()
2018-02-15 00:24:15 +05:30
def onPlexServerAlert(self, data):
2018-04-27 11:33:25 +05:30
if (not self.plexServer):
return
2018-02-15 00:24:15 +05:30
try:
if (data["type"] == "playing" and "PlaySessionStateNotification" in data):
sessionData = data["PlaySessionStateNotification"][0]
state = sessionData["state"]
sessionKey = int(sessionData["sessionKey"])
ratingKey = int(sessionData["ratingKey"])
viewOffset = int(sessionData["viewOffset"])
2018-04-25 00:42:36 +05:30
self.log("Received Update: " + colourText(sessionData, "yellow").replace("'", "\""), extra = True)
2018-02-15 00:24:15 +05:30
if (self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey):
2018-03-15 01:09:40 +05:30
if (self.stopTimer2):
self.stopTimer2.cancel()
self.stopTimer2 = None
2018-02-15 00:24:15 +05:30
if (self.lastState == state):
2018-08-24 11:03:49 +05:30
if (self.ignoreCount == self.maximumIgnores):
self.ignoreCount = 0
else:
self.log("Nothing changed, ignoring", "yellow", extra = True)
self.ignoreCount += 1
self.stopTimer2 = threading.Timer(self.stopTimer2Interval, self.stopOnNoUpdate)
self.stopTimer2.start()
return
2018-02-15 00:24:15 +05:30
elif (state == "stopped"):
self.lastState, self.lastSessionKey, self.lastRatingKey = None, None, None
2018-03-15 01:09:40 +05:30
self.stopTimer = threading.Timer(self.stopTimerInterval, self.stop)
2018-02-15 00:24:15 +05:30
self.stopTimer.start()
2018-04-25 00:42:36 +05:30
self.log("Started stopTimer", "yellow", True)
2018-02-15 00:24:15 +05:30
return
elif (state == "stopped"):
2018-04-25 00:42:36 +05:30
self.log("\"stopped\" state update from unknown session key, ignoring", "yellow", True)
2018-02-15 00:24:15 +05:30
return
2018-10-22 20:03:12 +05:30
if (self.isServerOwner):
self.log("Checking Sessions for Session Key " + colourText(sessionKey, "yellow"), extra = True)
plexServerSessions = self.plexServer.sessions()
if (len(plexServerSessions) < 1):
self.log("Empty session list, ignoring", "red", True)
return
for session in plexServerSessions:
self.log(str(session) + ", Session Key: " + colourText(session.sessionKey, "yellow") + ", Users: " + colourText(session.usernames, "yellow").replace("'", "\""), extra = True)
sessionFound = False
if (session.sessionKey == sessionKey):
sessionFound = True
self.log("Session found", "green", True)
if (session.usernames[0].lower() == self.plexConfig.listenForUser):
self.log("Username \"" + session.usernames[0].lower() + "\" matches \"" + self.plexConfig.listenForUser + "\", continuing", "green", True)
break
else:
self.log("Username \"" + session.usernames[0].lower() + "\" doesn't match \"" + self.plexConfig.listenForUser + "\", ignoring", "red", True)
return
if (not sessionFound):
self.log("No matching session found", "red", True)
return
2018-04-25 00:42:36 +05:30
if (self.stopTimer):
self.stopTimer.cancel()
self.stopTimer = None
2018-03-15 01:09:40 +05:30
if (self.stopTimer2):
self.stopTimer2.cancel()
self.stopTimer2 = threading.Timer(self.stopTimer2Interval, self.stopOnNoUpdate)
self.stopTimer2.start()
2018-02-15 00:24:15 +05:30
self.lastState, self.lastSessionKey, self.lastRatingKey = state, sessionKey, ratingKey
metadata = self.plexServer.fetchItem(ratingKey)
mediaType = metadata.type
if (mediaType == "movie"):
title = metadata.title + " (" + str(metadata.year) + ")"
if (state != "playing"):
2018-11-16 02:58:55 +05:30
extra = secondsToText(viewOffset / 1000, 2) + "/" + secondsToText(metadata.duration / 1000, 2)
2018-02-15 00:24:15 +05:30
else:
2018-11-16 02:58:55 +05:30
extra = secondsToText(metadata.duration / 1000)
2018-02-15 00:24:15 +05:30
extra = extra + " · " + ", ".join([genre.tag for genre in metadata.genres[:3]])
2018-02-16 00:13:45 +05:30
largeText = "Watching a Movie"
2018-02-15 00:24:15 +05:30
elif (mediaType == "episode"):
title = metadata.grandparentTitle
2018-11-16 02:58:55 +05:30
if (state != "playing"):
extra = secondsToText(viewOffset / 1000, 2) + "/" + secondsToText(metadata.duration / 1000, 2)
else:
extra = secondsToText(metadata.duration / 1000)
extra = extra + " · S" + str(metadata.parentIndex) + " · E" + str(metadata.index) + " - " + metadata.title
2018-02-16 00:13:45 +05:30
largeText = "Watching a TV Show"
2018-02-15 00:24:15 +05:30
elif (mediaType == "track"):
title = metadata.title
2018-03-11 21:31:58 +05:30
artist = metadata.originalTitle
if (not artist):
artist = metadata.grandparentTitle
extra = artist + " · " + metadata.parentTitle
2018-02-16 00:13:45 +05:30
largeText = "Listening to Music"
2018-02-15 00:24:15 +05:30
else:
2018-04-25 00:42:36 +05:30
self.log("Unsupported media type \"" + mediaType + "\", ignoring", "red", True)
2018-02-15 00:24:15 +05:30
return
activity = {
"details": title,
"state": extra,
"assets": {
2018-02-16 00:13:45 +05:30
"large_text": largeText,
2018-02-15 00:53:03 +05:30
"large_image": "logo",
2018-02-15 00:24:15 +05:30
"small_text": state.capitalize(),
"small_image": state
},
}
if (state == "playing"):
2018-03-11 21:31:58 +05:30
currentTimestamp = int(time.time())
2018-11-16 02:58:55 +05:30
if (self.plexConfig.timeRemaining):
activity["timestamps"] = {"end": currentTimestamp + ((metadata.duration - viewOffset) / 1000)}
else:
activity["timestamps"] = {"start": currentTimestamp - (viewOffset / 1000)}
2018-02-15 00:24:15 +05:30
if (not self.running):
self.start()
2018-04-27 11:33:25 +05:30
if (self.running):
self.send(activity)
else:
self.stop()
2018-02-15 00:24:15 +05:30
except Exception as e:
2018-04-26 00:33:31 +05:30
self.log("onPlexServerAlert Error: " + str(e))
2018-02-15 00:24:15 +05:30
2018-03-15 01:09:40 +05:30
def stopOnNoUpdate(self):
2018-04-25 00:42:36 +05:30
self.log("No updates from session key " + str(self.lastSessionKey) + ", stopping", "red", True)
2018-03-15 01:09:40 +05:30
self.stop()
2018-03-12 01:27:52 +05:30
isLinux = sys.platform in ["linux", "darwin"]
2018-04-25 00:42:36 +05:30
lock = threading.Semaphore(value = 1)
os.system("clear" if isLinux else "cls")
if (len(plexConfigs) == 0):
print("Error: plexConfigs list is empty")
sys.exit()
2018-03-12 01:27:52 +05:30
colours = {
"red": "91",
"green": "92",
"yellow": "93",
"blue": "94",
"magenta": "96",
"cyan": "97"
}
def colourText(text, colour = ""):
prefix = ""
suffix = ""
colour = colour.lower()
if (colour in colours):
prefix = "\033[" + colours[colour] + "m"
suffix = "\033[0m"
return prefix + str(text) + suffix
2018-11-16 02:58:55 +05:30
def secondsToText(seconds, formatting = 1):
seconds = round(seconds)
text = {"h": seconds // 3600, "m": seconds // 60 % 60, "s": seconds % 60}
if (formatting == 1):
text = [str(v) + k for k, v in text.items() if v > 0]
return "".join(text)
else:
return ":".join(list(str(v).rjust(2, "0") for k, v in text.items()))
2018-04-25 00:42:36 +05:30
discordRichPresencePlexInstances = []
for config in plexConfigs:
discordRichPresencePlexInstances.append(discordRichPresencePlex(config))
2018-02-15 00:24:15 +05:30
try:
2018-04-25 00:42:36 +05:30
for discordRichPresencePlexInstance in discordRichPresencePlexInstances:
discordRichPresencePlexInstance.run()
2018-02-15 00:24:15 +05:30
while True:
2018-04-25 00:42:36 +05:30
time.sleep(3600)
2018-02-15 00:24:15 +05:30
except KeyboardInterrupt:
2018-04-25 00:42:36 +05:30
for discordRichPresencePlexInstance in discordRichPresencePlexInstances:
2018-04-27 11:33:25 +05:30
discordRichPresencePlexInstance.reset()
2018-02-15 00:24:15 +05:30
except Exception as e:
print("Error: " + str(e))