A Python script to show your currently playing Yandex music in your Discord status!
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

207 lignes
5.5 KiB

  1. # References:
  2. # * https://github.com/devsnek/discord-rpc/tree/master/src/transports/IPC.js
  3. # * https://github.com/devsnek/discord-rpc/tree/master/example/main.js
  4. # * https://github.com/discordapp/discord-rpc/tree/master/documentation/hard-mode.md
  5. # * https://github.com/discordapp/discord-rpc/tree/master/src
  6. # * https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields
  7. from abc import ABCMeta, abstractmethod
  8. import json
  9. import logging
  10. import os
  11. import socket
  12. import sys
  13. import struct
  14. import uuid
  15. OP_HANDSHAKE = 0
  16. OP_FRAME = 1
  17. OP_CLOSE = 2
  18. OP_PING = 3
  19. OP_PONG = 4
  20. logger = logging.getLogger(__name__)
  21. class DiscordIpcError(Exception):
  22. pass
  23. class DiscordIpcClient(metaclass=ABCMeta):
  24. """Work with an open Discord instance via its JSON IPC for its rich presence API.
  25. In a blocking way.
  26. Classmethod `for_platform`
  27. will resolve to one of WinDiscordIpcClient or UnixDiscordIpcClient,
  28. depending on the current platform.
  29. Supports context handler protocol.
  30. """
  31. def __init__(self, client_id):
  32. self.client_id = client_id
  33. self._connect()
  34. self._do_handshake()
  35. logger.info("connected via ID %s", client_id)
  36. @classmethod
  37. def for_platform(cls, client_id, platform=sys.platform):
  38. if platform == 'win32':
  39. return WinDiscordIpcClient(client_id)
  40. else:
  41. return UnixDiscordIpcClient(client_id)
  42. @abstractmethod
  43. def _connect(self):
  44. pass
  45. def _do_handshake(self):
  46. ret_op, ret_data = self.send_recv({'v': 1, 'client_id': self.client_id}, op=OP_HANDSHAKE)
  47. # {'cmd': 'DISPATCH', 'data': {'v': 1, 'config': {...}}, 'evt': 'READY', 'nonce': None}
  48. if ret_op == OP_FRAME and ret_data['cmd'] == 'DISPATCH' and ret_data['evt'] == 'READY':
  49. return
  50. else:
  51. if ret_op == OP_CLOSE:
  52. self.close()
  53. raise RuntimeError(ret_data)
  54. @abstractmethod
  55. def _write(self, date: bytes):
  56. pass
  57. @abstractmethod
  58. def _recv(self, size: int) -> bytes:
  59. pass
  60. def _recv_header(self) -> (int, int):
  61. header = self._recv_exactly(8)
  62. return struct.unpack("<II", header)
  63. def _recv_exactly(self, size) -> bytes:
  64. buf = b""
  65. size_remaining = size
  66. while size_remaining:
  67. chunk = self._recv(size_remaining)
  68. buf += chunk
  69. size_remaining -= len(chunk)
  70. return buf
  71. def close(self):
  72. logger.warning("closing connection")
  73. try:
  74. self.send({}, op=OP_CLOSE)
  75. finally:
  76. self._close()
  77. @abstractmethod
  78. def _close(self):
  79. pass
  80. def __enter__(self):
  81. return self
  82. def __exit__(self, *_):
  83. self.close()
  84. def send_recv(self, data, op=OP_FRAME):
  85. self.send(data, op)
  86. return self.recv()
  87. def send(self, data, op=OP_FRAME):
  88. logger.debug("sending %s", data)
  89. data_str = json.dumps(data, separators=(',', ':'))
  90. data_bytes = data_str.encode('utf-8')
  91. header = struct.pack("<II", op, len(data_bytes))
  92. self._write(header)
  93. self._write(data_bytes)
  94. def recv(self) -> (int, "JSON"):
  95. """Receives a packet from discord.
  96. Returns op code and payload.
  97. """
  98. op, length = self._recv_header()
  99. payload = self._recv_exactly(length)
  100. data = json.loads(payload.decode('utf-8'))
  101. logger.debug("received %s", data)
  102. return op, data
  103. def set_activity(self, act):
  104. # act
  105. data = {
  106. 'cmd': 'SET_ACTIVITY',
  107. 'args': {'pid': os.getpid(),
  108. 'activity': act},
  109. 'nonce': str(uuid.uuid4())
  110. }
  111. self.send(data)
  112. class WinDiscordIpcClient(DiscordIpcClient):
  113. _pipe_pattern = R'\\?\pipe\discord-ipc-{}'
  114. def _connect(self):
  115. for i in range(10):
  116. path = self._pipe_pattern.format(i)
  117. try:
  118. self._f = open(path, "w+b")
  119. except OSError as e:
  120. logger.error("failed to open {!r}: {}".format(path, e))
  121. else:
  122. break
  123. else:
  124. return DiscordIpcError("Failed to connect to Discord pipe")
  125. self.path = path
  126. def _write(self, data: bytes):
  127. self._f.write(data)
  128. self._f.flush()
  129. def _recv(self, size: int) -> bytes:
  130. return self._f.read(size)
  131. def _close(self):
  132. self._f.close()
  133. class UnixDiscordIpcClient(DiscordIpcClient):
  134. def _connect(self):
  135. self._sock = socket.socket(socket.AF_UNIX)
  136. pipe_pattern = self._get_pipe_pattern()
  137. for i in range(10):
  138. path = pipe_pattern.format(i)
  139. if not os.path.exists(path):
  140. continue
  141. try:
  142. self._sock.connect(path)
  143. except OSError as e:
  144. logger.error("failed to open {!r}: {}".format(path, e))
  145. else:
  146. break
  147. else:
  148. return DiscordIpcError("Failed to connect to Discord pipe")
  149. @staticmethod
  150. def _get_pipe_pattern():
  151. env_keys = ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP')
  152. for env_key in env_keys:
  153. dir_path = os.environ.get(env_key)
  154. if dir_path:
  155. break
  156. else:
  157. dir_path = '/tmp'
  158. return os.path.join(dir_path, 'discord-ipc-{}')
  159. def _write(self, data: bytes):
  160. self._sock.sendall(data)
  161. def _recv(self, size: int) -> bytes:
  162. return self._sock.recv(size)
  163. def _close(self):
  164. self._sock.close()