presence.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. import youtubeOldResolver from "./video_sources/old";
  2. import youtubeShortsResolver from "./video_sources/shorts";
  3. import youtubeEmbedResolver from "./video_sources/embed";
  4. import youtubeMoviesResolver from "./video_sources/movies";
  5. import youtubeTVResolver from "./video_sources/tv";
  6. import youtubeResolver from "./video_sources/default";
  7. import youtubeMiniplayerResolver from "./video_sources/miniplayer";
  8. import youtubeApiResolver from "./video_sources/api";
  9. import {
  10. Resolver,
  11. presence,
  12. strings,
  13. getSetting,
  14. checkStringLanguage,
  15. getThumbnail,
  16. } from "./util";
  17. import { pvPrivacyUI } from "./util/pvPrivacyUI";
  18. const browsingTimestamp = Math.floor(Date.now() / 1000);
  19. enum YouTubeAssets {
  20. Logo = "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube/assets/logo.png",
  21. Studio = "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube/assets/0.png",
  22. Shorts = "https://cdn.rcd.gg/PreMiD/websites/Y/YouTube/assets/1.png",
  23. }
  24. enum LogoMode {
  25. YouTubeLogo = 0,
  26. Thumbnail = 1,
  27. Channel = 2,
  28. }
  29. const nullResolver: Resolver = {
  30. isActive: () => true,
  31. getTitle: () => document.title,
  32. getUploader: () => "",
  33. getChannelURL: () => "",
  34. getVideoID: () => "",
  35. };
  36. presence.on("UpdateData", async () => {
  37. const [
  38. newLang,
  39. privacy,
  40. privacyTtl,
  41. privacyButtonShown,
  42. time,
  43. vidDetail,
  44. vidState,
  45. channelPic,
  46. logo,
  47. buttons,
  48. hideHome,
  49. hidePaused,
  50. ] = [
  51. getSetting<string>("lang", "en"),
  52. getSetting<boolean>("privacy", true),
  53. getSetting<number>("privacy-ttl", 1),
  54. getSetting<boolean>("privacy-shown", true),
  55. getSetting<boolean>("time", true),
  56. getSetting<string>("vidDetail", "%title%"),
  57. getSetting<string>("vidState", "%uploader%"),
  58. getSetting<boolean>("channelPic", false),
  59. getSetting<number>("largeImage", 0),
  60. getSetting<boolean>("buttons", true),
  61. getSetting<boolean>("hideHome", false),
  62. getSetting<boolean>("hidePaused", true),
  63. ],
  64. { pathname, hostname, search, href } = document.location;
  65. // Update strings if user selected another language.
  66. if (!checkStringLanguage(newLang)) return;
  67. // If there is a vid playing
  68. const video = Array.from(
  69. document.querySelectorAll<HTMLVideoElement>(".video-stream")
  70. ).find(video => video.duration);
  71. if (video) {
  72. const { mediaSession } = navigator;
  73. if (mediaSession.playbackState !== "playing" && hidePaused)
  74. return presence.clearActivity();
  75. const resolver = [
  76. youtubeEmbedResolver,
  77. youtubeShortsResolver,
  78. youtubeOldResolver,
  79. youtubeTVResolver,
  80. youtubeResolver,
  81. youtubeMiniplayerResolver,
  82. youtubeMoviesResolver,
  83. youtubeApiResolver,
  84. nullResolver,
  85. ].find(resolver => resolver.isActive()),
  86. title = resolver.getTitle(),
  87. uploaderName = resolver.getUploader();
  88. let pfp: string;
  89. const live = !!document.querySelector(".ytp-live"),
  90. isPlaylistLoop =
  91. document
  92. .querySelector("#playlist-actions .yt-icon-button#button")
  93. ?.getAttribute("aria-pressed") === "true",
  94. playlistTitle =
  95. document
  96. .querySelector(
  97. "#content #header-description > h3:nth-child(1) > yt-formatted-string > a"
  98. )
  99. ?.textContent.trim() ?? "",
  100. playlistQueueElements = document.querySelectorAll<HTMLSpanElement>(
  101. "#content #publisher-container > div > yt-formatted-string > span"
  102. );
  103. let playlistQueue = "";
  104. if (playlistTitle) {
  105. if (playlistQueueElements.length > 1)
  106. playlistQueue = `${playlistQueueElements[0].textContent} / ${playlistQueueElements[2].textContent}`;
  107. else {
  108. playlistQueue = document.querySelector<HTMLSpanElement>(
  109. "#content #publisher-container > div > span"
  110. ).textContent;
  111. }
  112. }
  113. if (logo === LogoMode.Channel) {
  114. pfp =
  115. resolver === youtubeMiniplayerResolver
  116. ? ""
  117. : document
  118. .querySelector<HTMLImageElement>(
  119. "#avatar.ytd-video-owner-renderer > img"
  120. )
  121. ?.src.replace(/=s\d+/, "=s512");
  122. }
  123. const unlistedPathElement = document.querySelector<SVGPathElement>(
  124. "g#privacy_unlisted > path"
  125. ),
  126. unlistedBadgeElement = document.querySelector<SVGPathElement>(
  127. "h1.title+ytd-badge-supported-renderer path"
  128. ),
  129. unlistedVideo =
  130. unlistedPathElement &&
  131. unlistedBadgeElement &&
  132. unlistedPathElement?.getAttribute("d") ===
  133. unlistedBadgeElement?.getAttribute("d"),
  134. videoId = resolver.getVideoID(),
  135. [startTimestamp, endTimestamp] = presence.getTimestampsfromMedia(video),
  136. presenceData: PresenceData = {
  137. type: ActivityType.Watching,
  138. details: vidDetail
  139. .replace("%title%", title.trim())
  140. .replace("%uploader%", uploaderName.trim())
  141. .replace("%playlistTitle%", playlistTitle.trim())
  142. .replace("%playlistQueue%", playlistQueue.trim()),
  143. state: vidState
  144. .replace("%title%", title.trim())
  145. .replace("%uploader%", uploaderName.trim())
  146. .replace("%playlistTitle%", playlistTitle.trim())
  147. .replace("%playlistQueue%", playlistQueue.trim()),
  148. largeImageKey:
  149. unlistedVideo || logo === LogoMode.YouTubeLogo || pfp === ""
  150. ? YouTubeAssets.Logo
  151. : logo === LogoMode.Thumbnail
  152. ? await getThumbnail(videoId)
  153. : pfp,
  154. smallImageKey: video.paused
  155. ? Assets.Pause
  156. : video.loop
  157. ? Assets.RepeatOne
  158. : isPlaylistLoop
  159. ? Assets.Repeat
  160. : Assets.Play,
  161. smallImageText: video.paused
  162. ? strings.pause
  163. : video.loop
  164. ? "On loop"
  165. : isPlaylistLoop
  166. ? "Playlist on loop"
  167. : strings.play,
  168. startTimestamp,
  169. endTimestamp,
  170. };
  171. if (vidState.includes("{0}")) delete presenceData.state;
  172. // Remove timestamps if paused or live
  173. if (video.paused || live) {
  174. delete presenceData.startTimestamp;
  175. delete presenceData.endTimestamp;
  176. if (live) {
  177. presenceData.smallImageKey = Assets.Live;
  178. presenceData.smallImageText = strings.live;
  179. }
  180. }
  181. let perVideoPrivacy = privacy;
  182. if (resolver === youtubeResolver) {
  183. if (privacyButtonShown) {
  184. perVideoPrivacy = pvPrivacyUI(
  185. privacy,
  186. new URLSearchParams(search).get("v"),
  187. privacyTtl
  188. );
  189. } else {
  190. const enablePrivacyElement =
  191. document.querySelector("#pmdEnablePrivacy");
  192. if (enablePrivacyElement) {
  193. enablePrivacyElement.remove();
  194. document.querySelector("#pmdEnablePrivacyTooltip").remove();
  195. }
  196. }
  197. }
  198. // Update title to indicate when an ad is being played
  199. if (document.querySelector(".ytp-ad-player-overlay")) {
  200. presenceData.details = strings.ad;
  201. delete presenceData.state;
  202. } else if (perVideoPrivacy) {
  203. //defaults to privacy setting, but allows it to be overwritten
  204. if (live) presenceData.details = strings.watchLive;
  205. else presenceData.details = strings.watchVid;
  206. delete presenceData.state;
  207. presenceData.largeImageKey = YouTubeAssets.Logo;
  208. presenceData.startTimestamp = browsingTimestamp;
  209. delete presenceData.endTimestamp;
  210. } else if (buttons && !unlistedVideo) {
  211. presenceData.buttons = [
  212. {
  213. label: live ? strings.watchStreamButton : strings.watchVideoButton,
  214. url: href.includes("/watch?v=")
  215. ? href.split("&")[0]
  216. : `https://www.youtube.com/watch?v=${videoId}`,
  217. },
  218. {
  219. label: strings.viewChannelButton,
  220. url: resolver.getChannelURL(),
  221. },
  222. ];
  223. }
  224. if (!time) {
  225. delete presenceData.startTimestamp;
  226. delete presenceData.endTimestamp;
  227. }
  228. if (resolver === youtubeShortsResolver) {
  229. presenceData.largeImageKey = YouTubeAssets.Shorts;
  230. presenceData.smallImageKey = video.paused ? Assets.Pause : Assets.Play;
  231. presenceData.smallImageText = video.paused ? strings.pause : strings.play;
  232. }
  233. if (!presenceData.details) presence.setActivity();
  234. else presence.setActivity(presenceData);
  235. } else if (hostname === "www.youtube.com" || hostname === "youtube.com") {
  236. const presenceData: PresenceData = {
  237. largeImageKey: YouTubeAssets.Logo,
  238. startTimestamp: browsingTimestamp,
  239. type: ActivityType.Watching,
  240. };
  241. let searching = false;
  242. switch (true) {
  243. case pathname === "/": {
  244. const child =
  245. document.querySelector(
  246. '[class="style-scope ytd-feed-filter-chip-bar-renderer iron-selected"]'
  247. ) ?? null; // Select selected child
  248. if (
  249. (child &&
  250. Array.prototype.indexOf.call(child.parentElement.children, child)) >
  251. 0
  252. ) {
  253. // Get index of child element from parent
  254. // if the current child index is bigger than 0 continue
  255. presenceData.details = strings.browsingTypeVideos.replace(
  256. "{0}",
  257. child?.textContent.trim().toLowerCase()
  258. );
  259. } else if (hideHome) return presence.clearActivity();
  260. else presenceData.details = strings.viewHome;
  261. break;
  262. }
  263. case pathname.includes("/results"): {
  264. searching = true;
  265. const search =
  266. document.querySelector<HTMLInputElement>(
  267. "#search-input > div > div:nth-child(2) > input"
  268. ) ??
  269. document.querySelector<HTMLInputElement>("#search-input > input");
  270. presenceData.details = strings.search;
  271. presenceData.state = search.value;
  272. presenceData.smallImageKey = Assets.Search;
  273. break;
  274. }
  275. case pathname.includes("/@"):
  276. case pathname.includes("/channel"):
  277. case pathname.includes("/c"):
  278. case pathname.includes("/user"): {
  279. const tabSelected = document
  280. .querySelector(
  281. '[class="style-scope ytd-feed-filter-chip-bar-renderer iron-selected"]'
  282. )
  283. ?.textContent.trim(),
  284. documentTitle = document.title.substring(
  285. 0,
  286. document.title.lastIndexOf(" - YouTube")
  287. );
  288. let user: string;
  289. // Get channel name when viewing a community post
  290. if (
  291. documentTitle.includes(
  292. document
  293. .querySelector("#author-text.ytd-backstage-post-renderer")
  294. ?.textContent.trim()
  295. )
  296. ) {
  297. user = document.querySelector(
  298. "#author-text.ytd-backstage-post-renderer"
  299. ).textContent;
  300. // Get channel name when viewing a channel
  301. } else if (
  302. documentTitle.includes(
  303. document.querySelector("#text.ytd-channel-name")?.textContent
  304. ) &&
  305. document.querySelector("#text.ytd-channel-name")?.textContent
  306. )
  307. user = document.querySelector("#text.ytd-channel-name").textContent;
  308. // Get channel name from website's title
  309. else if (/\(([^)]+)\)/.test(documentTitle))
  310. user = documentTitle.replace(/\(([^)]+)\)/, "");
  311. else user = documentTitle;
  312. if (
  313. user.replace(/\s+/g, "") === "" ||
  314. user.replace(/\s+/g, "") === "\u200c"
  315. )
  316. user = "null";
  317. if (pathname.includes("/videos")) {
  318. presenceData.details = `${
  319. strings.browsingThrough
  320. } ${tabSelected} ${document
  321. .querySelector(
  322. '[class="style-scope ytd-tabbed-page-header"] [aria-selected="true"]'
  323. )
  324. ?.textContent.trim()
  325. .toLowerCase()}`;
  326. presenceData.state = `${strings.ofChannel} ${user}`;
  327. } else if (pathname.includes("/shorts")) {
  328. presenceData.details = strings.browseShorts;
  329. presenceData.state = `${strings.ofChannel} ${user}`;
  330. } else if (pathname.includes("/playlists")) {
  331. presenceData.details = strings.browsingPlayl;
  332. presenceData.state = `${strings.ofChannel} ${user}`;
  333. } else if (pathname.includes("/community")) {
  334. presenceData.details = strings.viewCPost;
  335. presenceData.state = `${strings.ofChannel} ${user}`;
  336. presenceData.largeImageKey =
  337. logo === LogoMode.Thumbnail
  338. ? document
  339. .querySelector('[id="post"]')
  340. ?.querySelectorAll("img")[1]?.src
  341. : logo === LogoMode.Channel
  342. ? document.querySelector('[id="post"]')?.querySelector("img")?.src
  343. : YouTubeAssets.Logo;
  344. } else if (pathname.includes("/about")) {
  345. presenceData.details = strings.readChannel;
  346. presenceData.state = user;
  347. presenceData.smallImageKey = Assets.Reading;
  348. } else if (pathname.includes("/search")) {
  349. searching = true;
  350. presenceData.details = strings.searchChannel.replace("{0}", user);
  351. presenceData.state = new URLSearchParams(search).get("query");
  352. presenceData.smallImageKey = Assets.Search;
  353. } else {
  354. presenceData.details = strings.viewChannel;
  355. presenceData.state = user;
  356. }
  357. if (channelPic) {
  358. const channelImg =
  359. // Self channel
  360. (
  361. document.querySelector<HTMLImageElement>(
  362. "yt-img-shadow.ytd-channel-avatar-editor > img"
  363. ) ??
  364. document.querySelector<HTMLImageElement>(
  365. "#avatar.ytd-c4-tabbed-header-renderer > img"
  366. ) ??
  367. // When viewing a community post
  368. document.querySelector<HTMLImageElement>(
  369. "#author-thumbnail > a > yt-img-shadow > img"
  370. ) ??
  371. // When viewing a channel on the normal channel page
  372. document
  373. .querySelector(".yt-spec-avatar-shape")
  374. ?.querySelector("img")
  375. )?.src.replace(/=s[0-9]+/, "=s512") ?? YouTubeAssets.Logo;
  376. if (channelImg) presenceData.largeImageKey = channelImg;
  377. }
  378. break;
  379. }
  380. case pathname.includes("/post"): {
  381. presenceData.details = strings.viewCPost;
  382. const selector = document.querySelector("#author-text");
  383. if (selector)
  384. presenceData.state = `${strings.ofChannel} ${selector.textContent}`;
  385. break;
  386. }
  387. case pathname.includes("/feed/trending"): {
  388. presenceData.details = strings.trending;
  389. break;
  390. }
  391. case pathname.includes("/feed/subscriptions"): {
  392. presenceData.details = strings.browsingThrough;
  393. presenceData.state = strings.subscriptions;
  394. break;
  395. }
  396. case pathname.includes("/feed/library"): {
  397. presenceData.details = strings.browsingThrough;
  398. presenceData.state = strings.library;
  399. break;
  400. }
  401. case pathname.includes("/feed/history"): {
  402. presenceData.details = strings.browsingThrough;
  403. presenceData.state = strings.history;
  404. break;
  405. }
  406. case pathname.includes("/feed/purchases"): {
  407. presenceData.details = strings.browsingThrough;
  408. presenceData.state = strings.purchases;
  409. break;
  410. }
  411. case pathname.includes("/playlist"): {
  412. presenceData.details = strings.viewPlaylist;
  413. const title =
  414. document.querySelector("#text-displayed") ??
  415. document.querySelector(
  416. "ytd-playlist-header-renderer yt-dynamic-sizing-formatted-string.ytd-playlist-header-renderer"
  417. ) ??
  418. document.querySelector("#title > yt-formatted-string > a");
  419. presenceData.state = title.textContent.trim();
  420. break;
  421. }
  422. case pathname.includes("/premium"): {
  423. presenceData.details = strings.readAbout;
  424. presenceData.state = "Youtube Premium";
  425. presenceData.smallImageKey = Assets.Reading;
  426. break;
  427. }
  428. case pathname.includes("/gaming"): {
  429. presenceData.details = strings.browsingThrough;
  430. presenceData.state = "Youtube Gaming";
  431. presenceData.smallImageKey = Assets.Reading;
  432. break;
  433. }
  434. case pathname.includes("/account"): {
  435. presenceData.details = strings.viewAccount;
  436. break;
  437. }
  438. case pathname.includes("/reporthistory"): {
  439. presenceData.details = strings.reports;
  440. break;
  441. }
  442. case pathname.includes("/intl"): {
  443. presenceData.details = strings.readAbout;
  444. presenceData.state = document.title.substring(
  445. 0,
  446. document.title.lastIndexOf(" - YouTube")
  447. );
  448. presenceData.smallImageKey = Assets.Reading;
  449. break;
  450. }
  451. case pathname.includes("/upload"): {
  452. presenceData.details = strings.upload;
  453. presenceData.smallImageKey = Assets.Writing;
  454. break;
  455. }
  456. case pathname.includes("/view_all_playlists"): {
  457. presenceData.details = strings.viewAllPlayL;
  458. break;
  459. }
  460. case pathname.includes("/my_live_events"): {
  461. presenceData.details = strings.viewEvent;
  462. break;
  463. }
  464. case pathname.includes("/live_dashboard"): {
  465. presenceData.details = strings.viewLiveDash;
  466. break;
  467. }
  468. case pathname.includes("/audiolibrary"): {
  469. presenceData.details = strings.viewAudio;
  470. break;
  471. }
  472. }
  473. if (privacy) {
  474. if (searching) {
  475. presenceData.details = strings.searchSomething;
  476. delete presenceData.state;
  477. } else {
  478. presenceData.details = strings.browsing;
  479. delete presenceData.state;
  480. delete presenceData.smallImageKey;
  481. }
  482. }
  483. if (!time) {
  484. delete presenceData.startTimestamp;
  485. delete presenceData.endTimestamp;
  486. }
  487. if (!presenceData.details) presence.setActivity();
  488. else presence.setActivity(presenceData, true);
  489. } else if (hostname === "studio.youtube.com") {
  490. const presenceData: PresenceData = {
  491. largeImageKey: YouTubeAssets.Logo,
  492. smallImageKey: YouTubeAssets.Studio,
  493. smallImageText: "Youtube Studio",
  494. startTimestamp: browsingTimestamp,
  495. };
  496. switch (true) {
  497. case pathname.includes("/videos"): {
  498. presenceData.details = strings.studioVid;
  499. break;
  500. }
  501. case pathname.includes("/video"): {
  502. const title = document.querySelector("#entity-name");
  503. if (pathname.includes("/edit")) {
  504. presenceData.details = strings.studioEdit;
  505. presenceData.state = title.textContent;
  506. } else if (pathname.includes("/analytics")) {
  507. presenceData.details = strings.studioAnaly;
  508. presenceData.state = title.textContent;
  509. } else if (pathname.includes("/comments")) {
  510. presenceData.details = strings.studioComments;
  511. presenceData.state = title.textContent;
  512. } else if (pathname.includes("/translations")) {
  513. presenceData.details = strings.studioTranslate;
  514. presenceData.state = title.textContent;
  515. }
  516. break;
  517. }
  518. case pathname.includes("/analytics"): {
  519. presenceData.details = strings.studioTheir;
  520. presenceData.state = strings.studioCAnaly;
  521. break;
  522. }
  523. case pathname.includes("/comments"): {
  524. presenceData.details = strings.studioTheir;
  525. presenceData.state = strings.studioCComments;
  526. break;
  527. }
  528. case pathname.includes("/translations"): {
  529. presenceData.details = strings.studioTheir;
  530. presenceData.state = strings.studioCTranslate;
  531. break;
  532. }
  533. case pathname.includes("/channel"): {
  534. presenceData.details = strings.studioDash;
  535. break;
  536. }
  537. case pathname.includes("/artist"): {
  538. presenceData.details = strings.studioTheir;
  539. presenceData.state = strings.studioArtist;
  540. break;
  541. }
  542. }
  543. if (privacy) {
  544. presenceData.details = strings.browsing;
  545. delete presenceData.state;
  546. delete presenceData.smallImageKey;
  547. }
  548. if (!time) {
  549. delete presenceData.startTimestamp;
  550. delete presenceData.endTimestamp;
  551. }
  552. if (!presenceData.details) presence.setActivity();
  553. else presence.setActivity(presenceData);
  554. }
  555. });