import asyncio
import datetime
import hashlib
import json
import os
import plexapi.myplex
import struct
import subprocess
import sys
import tempfile
import threading
import time
import webbrowser

### SEULEMENT CE QUE VOUS DEVEZ EDITER 

MyDomain = "https://your.plex.dns:32400" #Soit via votre nom de domaine, soit via le domaine de Plex, c'est à dire, https://app.plex.tv
Serveur = "MyPlex" #Le nom de votre Serveur Plex, visible dans Paramètres > Général > Nom d'usage
NomUser = "username" #Le nom d'utilisateur Plex pour récupérer les métadonnées et les informations de lecture en temps réel
MotDePasse = "password" #Votre mot de passe de votre compte Plex
MonToken = "your_token" #Votre Token, accessible a la fin de l'url lorsque vous consultez le fichier XML de l'un de vos médias
BlackList = ["Musique", "BackingTracks"] #séparée par des virgules avec espace, entourées par des "", entrent 2 balises [], ex: ["bib1", "bib2"]
UserOnly = "username" #Si vous avez crée des utilisateurs gérés, pour éviter de partager l'état de lecture d'un autre utilisateur que vous-même, précisez le psuedo du bon utilisateur a priori le même que le 3ème paramètre que vous...
InfosSupp = "true" #Ceci partage les métadonnées de votre épisode si c'est sur True, sinon mettez False pour n'afficher que l'état de lecture
TempsRestant = "true" #True = Afficher le temps restant de lecture / False = Afficher le nombre de minutes que vous avez déjà consulté depuis le démarrage de l'épisode

### END DE CE QUE VOUS DEVEZ EDITER

webbrowser.open(MyDomain)

class plexConfig:

	extraLogging = (InfosSupp)
	timeRemaining = (TempsRestant)

	def __init__(self, serverName = "", username = "", password = "", token = "", listenForUser = "", blacklistedLibraries = None, whitelistedLibraries = None, clientID = "413407336082833418"):
		self.serverName = serverName
		self.username = username
		self.password = password
		self.token = token
		self.listenForUser = (username if listenForUser == "" else listenForUser).lower()
		self.blacklistedLibraries = blacklistedLibraries
		self.whitelistedLibraries = whitelistedLibraries
		self.clientID = clientID

plexConfigs = [
        plexConfig(serverName = (Serveur), username = (NomUser), password = (MotDePasse), token = (MonToken), blacklistedLibraries = (BlackList), listenForUser = (UserOnly))
]

class discordRichPresence:

	def __init__(self, clientID, child):
		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
		self.child = child

	async def read(self):
		try:
			data = await self.pipeReader.read(1024)
			self.child.log("[READ] " + str(json.loads(data[8:].decode("utf-8"))))
		except Exception as e:
			self.child.log("[READ] " + str(e))
			self.stop()

	def write(self, op, payload):
		payload = json.dumps(payload)
		self.child.log("[WRITE] " + str(payload))
		data = self.pipeWriter.write(struct.pack("<ii", op, len(payload)) + payload.encode("utf-8"))

	async def handshake(self):
		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))

	def start(self):
		self.child.log("Opening Discord IPC Pipe")
		emptyProcessFilePath = tempfile.gettempdir() + ("/" if isLinux else "\\") + "discordRichPresencePlex-emptyProcess.py"
		if (not os.path.exists(emptyProcessFilePath)):
			with open(emptyProcessFilePath, "w") as emptyProcessFile:
				emptyProcessFile.write("import time\n\ntry:\n\twhile (True):\n\t\ttime.sleep(3600)\nexcept:\n\tpass")
		self.process = subprocess.Popen(["python3" if isLinux else "pythonw", emptyProcessFilePath])
		self.loop = asyncio.new_event_loop() if isLinux else asyncio.ProactorEventLoop()
		self.loop.run_until_complete(self.handshake())

	def stop(self):
		self.child.log("Closing Discord IPC Pipe")
		self.child.lastState, self.child.lastSessionKey, self.child.lastRatingKey = None, None, None
		self.process.kill()
		if (self.child.stopTimer):
			self.child.stopTimer.cancel()
			self.child.stopTimer = None
		if (self.child.stopTimer2):
			self.child.stopTimer2.cancel()
			self.child.stopTimer2 = None
		if (self.pipeWriter):
			try:
				self.pipeWriter.close()
			except:
				pass
			self.pipeWriter = None
		if (self.pipeReader):
			try:
				self.loop.run_until_complete(self.pipeReader.read(1024))
			except:
				pass
			self.pipeReader = None
		try:
			self.loop.close()
		except:
			pass
		self.running = False

	def send(self, activity):
		payload = {
			"cmd": "SET_ACTIVITY",
			"args": {
				"activity": activity,
				"pid": self.process.pid
			},
			"nonce": "{0:.20f}".format(time.time())
		}
		self.write(1, payload)
		self.loop.run_until_complete(self.read())

class discordRichPresencePlex(discordRichPresence):

	productName = "Plex Media Server"
	stopTimerInterval = 5
	stopTimer2Interval = 35
	checkConnectionTimerInterval = 60
	maximumIgnores = 3

	def __init__(self, plexConfig):
		self.plexConfig = plexConfig
		self.instanceID = hashlib.md5(str(id(self)).encode("UTF-8")).hexdigest()[:5]
		super().__init__(plexConfig.clientID, self)
		self.plexAccount = None
		self.plexServer = None
		self.isServerOwner = False
		self.plexAlertListener = None
		self.lastState = None
		self.lastSessionKey = None
		self.lastRatingKey = None
		self.stopTimer = None
		self.stopTimer2 = None
		self.checkConnectionTimer = None
		self.ignoreCount = 0

	def run(self):
		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()
						try:
							self.plexServer.account()
							self.isServerOwner = True
						except:
							pass
						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")
					break
			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

	def checkConnection(self):
		try:
			self.log("Request for clients list to check connection: " + str(self.plexServer.clients()), extra = True)
			self.checkConnectionTimer = threading.Timer(self.checkConnectionTimerInterval, self.checkConnection)
			self.checkConnectionTimer.start()
		except Exception as e:
			self.log("Connection to Plex lost: " + str(e))
			self.log("Reconnecting")
			self.run()

	def log(self, text, colour = "", extra = False):
		timestamp = datetime.datetime.now().strftime("%I:%M:%S %p")
		prefix = "[" + timestamp + "] [" + self.plexConfig.serverName + "/" + self.instanceID + "] "
		lock.acquire()
		if (extra):
			if (self.plexConfig.extraLogging):
				print(prefix + colourText(str(text), colour))
		else:
			print(prefix + colourText(str(text), colour))
		lock.release()

	def onPlexServerAlert(self, data):
		if (not self.plexServer):
			return
		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"])
				self.log("Received Update: " + colourText(sessionData, "yellow").replace("'", "\""), extra = True)
				metadata = self.plexServer.fetchItem(ratingKey)
				libraryName = metadata.section().title
				if (isinstance(self.plexConfig.blacklistedLibraries, list)):
					if (libraryName in self.plexConfig.blacklistedLibraries):
						self.log("Library \"" + libraryName + "\" is blacklisted, ignoring", "yellow", True)
						return
				if (isinstance(self.plexConfig.whitelistedLibraries, list)):
					if (libraryName not in self.plexConfig.whitelistedLibraries):
						self.log("Library \"" + libraryName + "\" is not whitelisted, ignoring", "yellow", True)
						return
				if (self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey):
					if (self.stopTimer2):
						self.stopTimer2.cancel()
						self.stopTimer2 = None
					if (self.lastState == state):
						if (self.ignoreCount == self.maximumIgnores):
							self.ignoreCount = 0
						else:
							self.log("Nothing changed, ignoring", "yellow", True)
							self.ignoreCount += 1
							self.stopTimer2 = threading.Timer(self.stopTimer2Interval, self.stopOnNoUpdate)
							self.stopTimer2.start()
							return
					elif (state == "stopped"):
						self.lastState, self.lastSessionKey, self.lastRatingKey = None, None, None
						self.stopTimer = threading.Timer(self.stopTimerInterval, self.stop)
						self.stopTimer.start()
						self.log("Started stopTimer", "yellow", True)
						return
				elif (state == "stopped"):
					self.log("\"stopped\" state update from unknown session key, ignoring", "yellow", True)
					return
				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
				if (self.stopTimer):
					self.stopTimer.cancel()
					self.stopTimer = None
				if (self.stopTimer2):
					self.stopTimer2.cancel()
				self.stopTimer2 = threading.Timer(self.stopTimer2Interval, self.stopOnNoUpdate)
				self.stopTimer2.start()
				self.lastState, self.lastSessionKey, self.lastRatingKey = state, sessionKey, ratingKey
				mediaType = metadata.type
				if (state != "playing"):
					extra = secondsToText(viewOffset / 1000, ":") + "/" + secondsToText(metadata.duration / 1000, ":")
				else:
					extra = secondsToText(metadata.duration / 1000)
				if (mediaType == "movie"):
					title = metadata.title + " (" + str(metadata.year) + ")"
					extra = extra + " · " + ", ".join([genre.tag for genre in metadata.genres[:3]])
					largeText = "Watching a Movie"
				elif (mediaType == "episode"):
					title = metadata.grandparentTitle
					extra = extra + " · S" + str(metadata.parentIndex) + " · E" + str(metadata.index) + " - " + metadata.title
					largeText = "Watching a TV Show"
				elif (mediaType == "track"):
					title = metadata.title
					artist = metadata.originalTitle
					if (not artist):
						artist = metadata.grandparentTitle
					extra = artist + " · " + metadata.parentTitle
					largeText = "Listening to Music"
				else:
					self.log("Unsupported media type \"" + mediaType + "\", ignoring", "red", True)
					return
				activity = {
					"details": title,
					"state": extra,
					"assets": {
						"large_text": largeText,
						"large_image": "logo",
						"small_text": state.capitalize(),
						"small_image": state
					},
				}
				if (state == "playing"):
					currentTimestamp = int(time.time())
					if (self.plexConfig.timeRemaining):
						activity["timestamps"] = {"end": round(currentTimestamp + ((metadata.duration - viewOffset) / 1000))}
					else:
						activity["timestamps"] = {"start": round(currentTimestamp - (viewOffset / 1000))}
				if (not self.running):
					self.start()
				if (self.running):
					self.send(activity)
				else:
					self.stop()
		except Exception as e:
			self.log("onPlexServerAlert Error: " + str(e))

	def stopOnNoUpdate(self):
		self.log("No updates from session key " + str(self.lastSessionKey) + ", stopping", "red", True)
		self.stop()

isLinux = sys.platform in ["linux", "darwin"]
lock = threading.Semaphore(value = 1)

os.system("clear" if isLinux else "cls")

if (len(plexConfigs) == 0):
	print("Error: plexConfigs list is empty")
	sys.exit()

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

def secondsToText(seconds, joiner = ""):
	seconds = round(seconds)
	text = {"h": seconds // 3600, "m": seconds // 60 % 60, "s": seconds % 60}
	if (joiner == ""):
		text = [str(v) + k for k, v in text.items() if v > 0]
	else:
		if (text["h"] == 0):
			del text["h"]
		text = [str(v).rjust(2, "0") for k, v in text.items()]
	return joiner.join(text)

discordRichPresencePlexInstances = []
for config in plexConfigs:
	discordRichPresencePlexInstances.append(discordRichPresencePlex(config))
try:
	for discordRichPresencePlexInstance in discordRichPresencePlexInstances:
		discordRichPresencePlexInstance.run()
	while (True):
		time.sleep(3600)
except KeyboardInterrupt:
	for discordRichPresencePlexInstance in discordRichPresencePlexInstances:
		discordRichPresencePlexInstance.reset()
except Exception as e:
	print("Error: " + str(e))
