presence.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. const presence = new Presence({
  2. clientId: "463151177836658699",
  3. });
  4. let prevTitleAuthor = "",
  5. presenceData: PresenceData,
  6. mediaTimestamps: [number, number],
  7. oldPath: string,
  8. startTimestamp: number,
  9. videoListenerAttached = false,
  10. useTimeLeftChanged = false;
  11. presence.on("UpdateData", async () => {
  12. const { pathname, search, href } = document.location,
  13. [
  14. showButtons,
  15. showTimestamps,
  16. showCover,
  17. hidePaused,
  18. showBrowsing,
  19. privacyMode,
  20. useTimeLeft,
  21. showAsListening,
  22. ] = await Promise.all([
  23. presence.getSetting<boolean>("buttons"),
  24. presence.getSetting<boolean>("timestamps"),
  25. presence.getSetting<boolean>("cover"),
  26. presence.getSetting<boolean>("hidePaused"),
  27. presence.getSetting<boolean>("browsing"),
  28. presence.getSetting<boolean>("privacy"),
  29. presence.getSetting<boolean>("useTimeLeft"),
  30. presence.getSetting<boolean>("showAsListening"),
  31. ]),
  32. { mediaSession } = navigator,
  33. watchID =
  34. href.match(/v=([^&#]{5,})/)?.[1] ??
  35. document
  36. .querySelector<HTMLAnchorElement>("a.ytp-title-link.yt-uix-sessionlink")
  37. ?.href.match(/v=([^&#]{5,})/)?.[1],
  38. repeatMode = document
  39. .querySelector('ytmusic-player-bar[slot="player-bar"]')
  40. ?.getAttribute("repeat-mode"),
  41. videoElement =
  42. document.querySelector<HTMLVideoElement>("video.video-stream");
  43. if (useTimeLeftChanged !== useTimeLeft && !privacyMode) {
  44. useTimeLeftChanged = useTimeLeft;
  45. updateSongTimestamps(useTimeLeft);
  46. }
  47. if (videoElement && !privacyMode) {
  48. if (!videoListenerAttached) {
  49. //* If video scrobbled, update timestamps
  50. videoElement.addEventListener("seeked", () =>
  51. updateSongTimestamps(useTimeLeft)
  52. );
  53. //* If video resumes playing, update timestamps
  54. videoElement.addEventListener("play", () =>
  55. updateSongTimestamps(useTimeLeft)
  56. );
  57. videoListenerAttached = true;
  58. }
  59. //* Element got removed from the DOM (eg, song with song/video switch)
  60. } else {
  61. prevTitleAuthor = "";
  62. videoListenerAttached = false;
  63. }
  64. presenceData = {};
  65. if (hidePaused && mediaSession?.playbackState !== "playing")
  66. return presence.clearActivity();
  67. if (["playing", "paused"].includes(mediaSession?.playbackState)) {
  68. if (privacyMode) {
  69. return presence.setActivity({
  70. type: ActivityType.Listening,
  71. largeImageKey:
  72. "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/logo.png",
  73. });
  74. }
  75. if (!mediaSession?.metadata?.title || isNaN(videoElement?.duration ?? NaN))
  76. return;
  77. if (
  78. prevTitleAuthor !==
  79. mediaSession.metadata.title +
  80. mediaSession.metadata.artist +
  81. document
  82. .querySelector<HTMLSpanElement>("#left-controls > span")
  83. ?.textContent?.trim()
  84. ) {
  85. updateSongTimestamps(useTimeLeft);
  86. if (mediaTimestamps[0] === mediaTimestamps[1]) return;
  87. prevTitleAuthor =
  88. mediaSession.metadata.title +
  89. mediaSession.metadata.artist +
  90. document
  91. .querySelector<HTMLSpanElement>("#left-controls > span")
  92. ?.textContent?.trim();
  93. }
  94. const albumArtistBtnLink = mediaSession?.metadata?.album
  95. ? [...document.querySelectorAll<HTMLAnchorElement>(".byline a")]?.at(-1)
  96. ?.href
  97. : document.querySelector<HTMLAnchorElement>(".byline a")?.href,
  98. buttons: [ButtonData, ButtonData?] = [
  99. {
  100. label: "Listen Along",
  101. url: `https://music.youtube.com/watch?v=${watchID}`,
  102. },
  103. ];
  104. if (albumArtistBtnLink) {
  105. buttons.push({
  106. label: `View ${mediaSession.metadata.album ? "Album" : "Artist"}`,
  107. url: albumArtistBtnLink,
  108. });
  109. }
  110. presenceData = {
  111. type: ActivityType.Listening,
  112. name: showAsListening ? mediaSession.metadata.title : "YouTube Music",
  113. largeImageKey: showCover
  114. ? mediaSession?.metadata?.artwork?.at(-1)?.src ??
  115. "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/1.png"
  116. : "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/1.png",
  117. details: mediaSession.metadata.album,
  118. state: mediaSession.metadata.artist,
  119. ...(showButtons && {
  120. buttons,
  121. }),
  122. ...(mediaSession.playbackState === "paused" ||
  123. (repeatMode && repeatMode !== "NONE")
  124. ? {
  125. smallImageKey:
  126. mediaSession.playbackState === "paused"
  127. ? Assets.Pause
  128. : repeatMode === "ONE"
  129. ? Assets.RepeatOne
  130. : Assets.Repeat,
  131. smallImageText:
  132. mediaSession.playbackState === "paused"
  133. ? "Paused"
  134. : repeatMode === "ONE"
  135. ? "On loop"
  136. : "Playlist on loop",
  137. }
  138. : null),
  139. ...(showTimestamps &&
  140. mediaSession.playbackState === "playing" && {
  141. startTimestamp: mediaTimestamps[0],
  142. endTimestamp: mediaTimestamps[1],
  143. }),
  144. };
  145. } else if (showBrowsing) {
  146. if (privacyMode) {
  147. return presence.setActivity({
  148. largeImageKey:
  149. "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/logo.png",
  150. details: "Browsing YouTube Music",
  151. });
  152. }
  153. if (oldPath !== pathname) {
  154. oldPath = pathname;
  155. startTimestamp = Math.floor(Date.now() / 1000);
  156. }
  157. presenceData = {
  158. type: ActivityType.Playing,
  159. largeImageKey:
  160. "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/logo.png",
  161. details: "Browsing",
  162. startTimestamp,
  163. };
  164. if (pathname === "/") presenceData.details = "Browsing Home";
  165. if (pathname === "/explore") presenceData.details = "Browsing Explore";
  166. if (pathname.match(/\/library\//)) {
  167. presenceData.details = "Browsing Library";
  168. presenceData.state = document.querySelector(
  169. "#tabs .iron-selected .tab"
  170. )?.textContent;
  171. }
  172. if (pathname.match(/^\/playlist/)) {
  173. presenceData.details = "Browsing Playlist";
  174. if (search === "?list=LM") presenceData.state = "Liked Music";
  175. else {
  176. presenceData.state =
  177. document.querySelector(".metadata .title")?.textContent;
  178. presenceData.buttons = [
  179. {
  180. label: "Show Playlist",
  181. url: href,
  182. },
  183. ];
  184. }
  185. presenceData.largeImageKey =
  186. document.querySelector<HTMLImageElement>("#thumbnail img")?.src;
  187. presenceData.smallImageKey =
  188. "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube%20Music/assets/0.png";
  189. }
  190. if (pathname.match(/^\/search/)) {
  191. presenceData.details = "Searching";
  192. presenceData.state = document.querySelector<HTMLInputElement>(
  193. ".search-container input"
  194. )?.value;
  195. presenceData.buttons = [
  196. {
  197. label: "View Search",
  198. url: href,
  199. },
  200. ];
  201. }
  202. if (pathname.match(/^\/channel/)) {
  203. presenceData.details = "Browsing Channel";
  204. presenceData.state =
  205. document.querySelector("#header .title")?.textContent;
  206. presenceData.buttons = [
  207. {
  208. label: "Show Channel",
  209. url: href,
  210. },
  211. ];
  212. }
  213. if (pathname.match(/^\/new_releases/)) {
  214. presenceData.details = "Browsing New Releases";
  215. presenceData.buttons = [
  216. {
  217. label: "Show New Releases",
  218. url: href,
  219. },
  220. ];
  221. }
  222. if (pathname.match(/^\/charts/)) {
  223. presenceData.details = "Browsing Charts";
  224. presenceData.buttons = [
  225. {
  226. label: "Show Charts",
  227. url: href,
  228. },
  229. ];
  230. }
  231. if (pathname.match(/^\/moods_and_genres/)) {
  232. presenceData.details = "Browsing Moods & Genres";
  233. presenceData.buttons = [
  234. {
  235. label: "Show Moods & Genres",
  236. url: href,
  237. },
  238. ];
  239. }
  240. }
  241. presence.setActivity(presenceData);
  242. });
  243. function updateSongTimestamps(useTimeLeft: boolean) {
  244. const [currTimes, totalTimes] =
  245. document
  246. .querySelector<HTMLSpanElement>("#left-controls > span")
  247. ?.textContent?.trim()
  248. ?.split(" / ") ?? [];
  249. if (useTimeLeft && currTimes && totalTimes) {
  250. mediaTimestamps = presence.getTimestamps(
  251. presence.timestampFromFormat(currTimes),
  252. presence.timestampFromFormat(totalTimes)
  253. );
  254. } else if (currTimes) {
  255. mediaTimestamps = [
  256. Date.now() / 1000 - presence.timestampFromFormat(currTimes),
  257. 0,
  258. ];
  259. }
  260. }