presence.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. const presence = new Presence({
  2. clientId: "938732156346314795",
  3. }),
  4. user = document.cookie
  5. .split(";")
  6. .find(val => val.trim().startsWith("letterboxd"))
  7. .split("=")[1],
  8. browsingTimestamp = Math.floor(Date.now() / 1000);
  9. function generateButtonText(text: string): [ButtonData] {
  10. return [
  11. {
  12. label: text.replace("Viewing", "View"),
  13. url: window.location.href,
  14. },
  15. ];
  16. }
  17. function clarifyString(str: string) {
  18. return str
  19. .replaceAll(String.fromCharCode(160), " ")
  20. .replaceAll(String.fromCharCode(8217), "'");
  21. }
  22. function getImageURLByAlt(alt: string) {
  23. return Array.from(document.querySelectorAll("img")).find(
  24. img => img.alt === alt
  25. )?.src;
  26. }
  27. function filterIterable<T extends Element>(
  28. itr: NodeListOf<T>,
  29. fnc: (val: T, ind?: number) => boolean
  30. ) {
  31. return Array.from(itr).find((element, ind) => fnc(element, ind));
  32. }
  33. const enum Assets {
  34. Logo = "https://cdn.rcd.gg/PreMiD/websites/L/Letterboxd/assets/logo.png",
  35. }
  36. presence.on("UpdateData", async () => {
  37. const path = document.location.pathname.slice(1).split("/");
  38. path.pop();
  39. const presenceData: PresenceData = {
  40. largeImageKey: Assets.Logo,
  41. startTimestamp: browsingTimestamp,
  42. };
  43. if (path[0]) {
  44. switch (path[0]) {
  45. case "lists":
  46. presenceData.details = "Viewing all lists";
  47. presenceData.buttons = generateButtonText(presenceData.details);
  48. break;
  49. case "members":
  50. presenceData.details = "Viewing all members";
  51. presenceData.buttons = generateButtonText(presenceData.details);
  52. break;
  53. case "journal":
  54. presenceData.details = "Viewing the journal";
  55. presenceData.buttons = generateButtonText(presenceData.details);
  56. break;
  57. case "search":
  58. presenceData.details = `Searching for ${path[1].replaceAll("+", " ")}`;
  59. break;
  60. case "settings": {
  61. presenceData.details = "Changing their settings";
  62. presenceData.smallImageKey = getImageURLByAlt(user);
  63. break;
  64. }
  65. case "list": {
  66. presenceData.details = "Creating a list";
  67. presenceData.smallImageKey = getImageURLByAlt(user);
  68. break;
  69. }
  70. case "invitations": {
  71. presenceData.details = "Viewing their invitations";
  72. presenceData.smallImageKey = getImageURLByAlt(user);
  73. break;
  74. }
  75. case "actor":
  76. case "director": {
  77. const name = (
  78. document.querySelectorAll(
  79. ".title-1.prettify"
  80. )[0] as HTMLHeadingElement
  81. ).textContent.replace(
  82. path[0] === "director" ? "FILMS DIRECTED BY\n" : "FILMS STARRING\n",
  83. ""
  84. ),
  85. pfp = (
  86. (
  87. document.querySelectorAll(
  88. ".avatar.person-image.image-loaded"
  89. )[0] as HTMLDivElement
  90. ).firstElementChild as HTMLImageElement
  91. ).src;
  92. presenceData.details = `Viewing ${
  93. path[0] === "director" ? "director" : "actor"
  94. }: ${name}`;
  95. presenceData.largeImageKey = pfp;
  96. presenceData.smallImageKey = Assets.Logo;
  97. presenceData.buttons = generateButtonText(presenceData.details);
  98. break;
  99. }
  100. case "activity": {
  101. const name = (
  102. document.querySelectorAll(".title-3")[0]
  103. .firstElementChild as HTMLAnchorElement
  104. ).textContent;
  105. presenceData.smallImageKey = getImageURLByAlt(name);
  106. presenceData.smallImageText = name;
  107. if (path[1]) {
  108. switch (path[1]) {
  109. case "you":
  110. presenceData.details = "Viewing personal activity";
  111. break;
  112. case "incoming":
  113. presenceData.details = "Viewing incoming activity";
  114. break;
  115. }
  116. } else presenceData.details = "Viewing all activity";
  117. break;
  118. }
  119. case "films": {
  120. if (path[1]) {
  121. switch (path[1]) {
  122. case "upcoming":
  123. presenceData.details = "Viewing upcoming films";
  124. break;
  125. case "popular":
  126. presenceData.details = "Viewing popular films";
  127. break;
  128. case "genre":
  129. presenceData.details = `Viewing ${path[2] ?? "unknown"} films`;
  130. break;
  131. case "decade":
  132. presenceData.details = `Viewing films from the ${
  133. path[2] ?? "unknown"
  134. }`;
  135. }
  136. } else presenceData.details = "Viewing films";
  137. if (!presenceData.details) presenceData.details = "Viewing films";
  138. presenceData.buttons = generateButtonText(
  139. presenceData.details as string
  140. );
  141. break;
  142. }
  143. case "film": {
  144. if (path[1]) {
  145. switch (path[1]) {
  146. default: {
  147. if (path[2]) {
  148. if (path[2] === "trailer") {
  149. const header = document.querySelector(
  150. "#featured-film-header"
  151. ),
  152. title = clarifyString(
  153. (header.firstElementChild as HTMLElement).textContent
  154. );
  155. presenceData.details = "Viewing the trailer of...";
  156. presenceData.state = `${title}, ${
  157. (
  158. header.lastElementChild.firstElementChild
  159. .firstElementChild as HTMLAnchorElement
  160. ).textContent
  161. }`;
  162. presenceData.largeImageKey = getImageURLByAlt(title);
  163. presenceData.smallImageKey = Assets.Logo;
  164. delete presenceData.startTimestamp;
  165. presenceData.buttons = [
  166. { label: "Watch trailer", url: window.location.href },
  167. ];
  168. } else {
  169. const title = document.querySelectorAll(
  170. ".contextual-title"
  171. )[0].firstElementChild.firstElementChild
  172. .nextElementSibling as HTMLAnchorElement,
  173. year = (
  174. title.nextElementSibling
  175. .firstElementChild as HTMLAnchorElement
  176. ).textContent;
  177. switch (path[2]) {
  178. case "members":
  179. presenceData.details = `Viewing people who have seen ${title.textContent}, ${year}`;
  180. break;
  181. case "fans":
  182. presenceData.details = `Viewing fans of ${title.textContent}, ${year}`;
  183. break;
  184. case "likes":
  185. presenceData.details = `Viewing people who have liked ${title.textContent}, ${year}`;
  186. break;
  187. case "ratings":
  188. presenceData.details = `Viewing ratings of ${title.textContent}, ${year}`;
  189. break;
  190. case "reviews":
  191. presenceData.details = `Viewing reviews of ${title.textContent}, ${year}`;
  192. break;
  193. case "lists":
  194. presenceData.details = `Viewing lists that include ${title.textContent}, ${year}`;
  195. break;
  196. }
  197. presenceData.buttons = generateButtonText(
  198. presenceData.details as string
  199. );
  200. presenceData.largeImageKey = getImageURLByAlt(
  201. clarifyString(title.textContent)
  202. );
  203. presenceData.smallImageKey = Assets.Logo;
  204. }
  205. } else {
  206. const header = document.querySelector("#featured-film-header"),
  207. title = clarifyString(
  208. (header.firstElementChild as HTMLElement).textContent
  209. );
  210. presenceData.details = `${title}, ${
  211. (
  212. header.lastElementChild.firstElementChild
  213. .firstElementChild as HTMLAnchorElement
  214. ).textContent
  215. }`;
  216. presenceData.state = `By ${
  217. (
  218. header.lastElementChild.lastElementChild
  219. .firstElementChild as HTMLSpanElement
  220. ).textContent
  221. }`;
  222. presenceData.buttons = [
  223. { label: `View ${title}`, url: window.location.href },
  224. ];
  225. presenceData.largeImageKey = getImageURLByAlt(title);
  226. presenceData.smallImageKey = Assets.Logo;
  227. break;
  228. }
  229. }
  230. }
  231. }
  232. break;
  233. }
  234. default:
  235. if (path[1]) {
  236. switch (path[1]) {
  237. case "watchlist":
  238. presenceData.details = "Viewing their watchlist";
  239. presenceData.buttons = generateButtonText(presenceData.details);
  240. break;
  241. case "lists":
  242. presenceData.details = "Viewing their lists";
  243. presenceData.buttons = generateButtonText(presenceData.details);
  244. break;
  245. case "likes":
  246. presenceData.details = "Viewing their liked films";
  247. presenceData.buttons = generateButtonText(presenceData.details);
  248. break;
  249. case "following":
  250. presenceData.details = "Viewing who they've followed";
  251. presenceData.buttons = generateButtonText(presenceData.details);
  252. break;
  253. case "followers":
  254. presenceData.details = "Viewing their followers";
  255. break;
  256. case "blocked":
  257. presenceData.details = "Viewing who they've blocked";
  258. break;
  259. case "activity": {
  260. if (path[2]) {
  261. presenceData.details =
  262. "Viewing who they've followed's activity";
  263. } else presenceData.details = "Viewing their activity";
  264. break;
  265. }
  266. case "films":
  267. {
  268. if (path[2]) {
  269. switch (path[2]) {
  270. case "reviews":
  271. presenceData.details = "Viewing their reviews";
  272. presenceData.buttons = generateButtonText(
  273. presenceData.details
  274. );
  275. break;
  276. case "ratings":
  277. presenceData.details = "Viewing their ratings";
  278. presenceData.buttons = generateButtonText(
  279. presenceData.details
  280. );
  281. break;
  282. case "diary":
  283. presenceData.details = "Viewing their diary";
  284. presenceData.buttons = generateButtonText(
  285. presenceData.details
  286. );
  287. break;
  288. }
  289. } else {
  290. presenceData.details = "Viewing their films";
  291. presenceData.buttons = generateButtonText(
  292. presenceData.details
  293. );
  294. }
  295. }
  296. break;
  297. case "tags": {
  298. if (path[2]) {
  299. switch (path[2]) {
  300. case "diary":
  301. presenceData.details = "Viewing their diary tags";
  302. presenceData.buttons = generateButtonText(
  303. presenceData.details
  304. );
  305. break;
  306. case "reviews":
  307. presenceData.details = "Viewing their tagged reviews";
  308. presenceData.buttons = generateButtonText(
  309. presenceData.details
  310. );
  311. break;
  312. case "lists":
  313. presenceData.details = "Viewing their tagged lists";
  314. presenceData.buttons = generateButtonText(
  315. presenceData.details
  316. );
  317. break;
  318. }
  319. } else {
  320. presenceData.details = "Viewing their tagged films";
  321. presenceData.buttons = generateButtonText(presenceData.details);
  322. }
  323. break;
  324. }
  325. case "stats": {
  326. const name = document.querySelectorAll(".yir-member-subtitle")[0]
  327. .lastElementChild as HTMLAnchorElement;
  328. presenceData.details = `Viewing ${name.textContent}'s statistics`;
  329. presenceData.largeImageKey = (
  330. name.previousElementSibling
  331. .firstElementChild as HTMLImageElement
  332. ).src;
  333. presenceData.smallImageKey = Assets.Logo;
  334. presenceData.buttons = [
  335. {
  336. label: `View ${name.textContent}'s stats`,
  337. url: window.location.href,
  338. },
  339. ];
  340. break;
  341. }
  342. case "list": {
  343. const title = (
  344. document.querySelectorAll(
  345. ".title-1.prettify"
  346. )[0] as HTMLHeadingElement
  347. ).textContent,
  348. name = (
  349. document.querySelectorAll(".name")[0]
  350. .firstElementChild as HTMLSpanElement
  351. ).textContent;
  352. presenceData.details = `Viewing the list ${title}`;
  353. presenceData.buttons = generateButtonText(presenceData.details);
  354. presenceData.state = `By ${name}`;
  355. presenceData.smallImageKey = getImageURLByAlt(name);
  356. presenceData.smallImageText = name;
  357. break;
  358. }
  359. case "friends": {
  360. const title = document.querySelectorAll(".contextual-title")[0]
  361. .firstElementChild.firstElementChild
  362. .nextElementSibling as HTMLAnchorElement,
  363. year = (
  364. title.nextElementSibling
  365. .firstElementChild as HTMLAnchorElement
  366. ).textContent;
  367. if (path[4]) {
  368. switch (path[4]) {
  369. case "ratings":
  370. presenceData.details = "Viewing friends' ratings of...";
  371. break;
  372. case "reviews":
  373. presenceData.details = "Viewing friends' reviews of...";
  374. break;
  375. case "lists":
  376. presenceData.details =
  377. "Viewing friends' lists that include...";
  378. break;
  379. case "fans":
  380. presenceData.details = "Viewing friends who are fans of...";
  381. break;
  382. case "likes":
  383. presenceData.details = "Viewing friends who have liked...";
  384. break;
  385. }
  386. } else presenceData.details = "Viewing friends who have seen...";
  387. presenceData.state = `${title.textContent}, ${year}`;
  388. presenceData.largeImageKey = getImageURLByAlt(
  389. clarifyString(title.textContent)
  390. );
  391. presenceData.smallImageKey = Assets.Logo;
  392. break;
  393. }
  394. case "film": {
  395. const title = clarifyString(
  396. (
  397. document.querySelectorAll(".film-title-wrapper")[0]
  398. .firstElementChild as HTMLAnchorElement
  399. ).textContent
  400. ),
  401. rater = filterIterable(
  402. document.querySelectorAll("span"),
  403. val => {
  404. if (val.getAttribute("itemprop") === "name") return true;
  405. }
  406. ).textContent,
  407. rating = filterIterable(
  408. document.querySelectorAll("span"),
  409. val => {
  410. if (val.className.startsWith("rating rating-large"))
  411. return true;
  412. }
  413. ).textContent;
  414. presenceData.details = `Review of ${title}`;
  415. presenceData.state = `By ${rater} (${rating})`;
  416. presenceData.buttons = [
  417. { label: "View review", url: window.location.href },
  418. ];
  419. presenceData.largeImageKey = getImageURLByAlt(title);
  420. presenceData.smallImageKey = getImageURLByAlt(rater);
  421. presenceData.smallImageText = rater;
  422. break;
  423. }
  424. }
  425. if (
  426. [
  427. "watchlist",
  428. "films",
  429. "activity",
  430. "blocked",
  431. "followers",
  432. "following",
  433. "tags",
  434. "likes",
  435. "lists",
  436. ].includes(path[1])
  437. ) {
  438. const name = (
  439. document.querySelectorAll(".title-3")[0]
  440. .firstElementChild as HTMLAnchorElement
  441. ).textContent;
  442. if (path[0] !== user && path[0] !== user.toLowerCase()) {
  443. presenceData.details = (presenceData.details as string)
  444. .replace("their", `${name}'s`)
  445. .replace("they've", `${name} has`);
  446. }
  447. presenceData.smallImageKey = getImageURLByAlt(name);
  448. presenceData.smallImageText = name;
  449. }
  450. } else {
  451. const name = (
  452. document.querySelectorAll(".title-1")[0] as HTMLHeadingElement
  453. ).textContent;
  454. // I could use the get image func but ID is available so its fine (smaller loop (I think))
  455. presenceData.details = `Viewing ${
  456. path[0] === user ? "their own" : `${name}'s`
  457. } profile`;
  458. presenceData.state = `(${
  459. path[0] === user ? `${name}/${path[0]}` : path[0]
  460. })`;
  461. presenceData.largeImageKey = (
  462. document.querySelector("#avatar-zoom")
  463. .previousElementSibling as HTMLImageElement
  464. ).src;
  465. presenceData.smallImageKey = Assets.Logo;
  466. presenceData.buttons = [
  467. { label: `View ${name}`, url: window.location.href },
  468. ];
  469. break;
  470. }
  471. }
  472. } else presenceData.details = "At home";
  473. if (!(await presence.getSetting("show_buttons"))) delete presenceData.buttons;
  474. presence.setActivity(presenceData);
  475. });