presence.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import type {
  2. AgeRestriction,
  3. AnimeData,
  4. CharacterData,
  5. CollectionData,
  6. PersonData,
  7. PublisherData,
  8. ReviewData,
  9. TeamData,
  10. UserData,
  11. } from './lib.js'
  12. import { ActivityType, Assets } from 'premid'
  13. import { AnimeLib } from './lib.js'
  14. const presence = new Presence({
  15. clientId: '1320289587943444552',
  16. })
  17. const browsingTimestamp = Math.floor(Date.now() / 1000)
  18. enum ActivityAssets {
  19. Logo = 'https://cdn.rcd.gg/PreMiD/websites/A/AnimeLib/assets/logo.png',
  20. Play = 'https://cdn.rcd.gg/PreMiD/resources/play.png',
  21. Pause = 'https://cdn.rcd.gg/PreMiD/resources/pause.png',
  22. }
  23. type RouteName =
  24. | ''
  25. | 'anime'
  26. | 'characters'
  27. | 'people'
  28. | 'catalog'
  29. | 'user'
  30. | 'top-views'
  31. | 'collections'
  32. | 'reviews'
  33. | 'team'
  34. | 'franchise'
  35. | 'publisher'
  36. | 'media'
  37. | 'news'
  38. | 'faq'
  39. | 'messages'
  40. | 'downloads'
  41. interface IFrameVideo {
  42. duration: number
  43. currentTime: number
  44. paused: boolean
  45. }
  46. function isPrivacyMode(setting: boolean, ageRestriction?: AgeRestriction) {
  47. return setting || ageRestriction?.id === 5
  48. }
  49. function setPrivacyMode(presenceData: PresenceData) {
  50. presenceData.details = 'Приватный режим'
  51. presenceData.state = 'Вам не следует знать лишнего!'
  52. }
  53. function cleanUrl(location: Location) {
  54. return location.href.replace(location.search, '').replace('/watch', '')
  55. }
  56. let iFrameVideo: IFrameVideo | null = null
  57. let currentDub: string
  58. presence.on('iFrameData', (data: unknown) => {
  59. iFrameVideo = data as typeof iFrameVideo
  60. })
  61. presence.on('UpdateData', async () => {
  62. const presenceData: PresenceData = {
  63. largeImageKey: ActivityAssets.Logo,
  64. type: ActivityType.Watching,
  65. startTimestamp: browsingTimestamp,
  66. largeImageText: 'AnimeLib',
  67. smallImageText: 'AnimeLib',
  68. }
  69. const [privacySetting, buttonsSetting, titleSetting] = await Promise.all([
  70. presence.getSetting<boolean>('privacy'),
  71. presence.getSetting<boolean>('buttons'),
  72. presence.getSetting<boolean>('titleAsPresence'),
  73. ])
  74. const path = document.location.pathname
  75. const route = <RouteName>`${path}/`.split('/')[2]
  76. let animeData: AnimeData,
  77. userData: UserData,
  78. characterData: CharacterData,
  79. peopleData: PersonData,
  80. collectionData: CollectionData,
  81. reviewData: ReviewData,
  82. teamData: TeamData,
  83. publisherData: PublisherData
  84. switch (route) {
  85. case '':
  86. presenceData.details = 'Главная страница'
  87. presenceData.state = 'Так внимательно изучает...'
  88. break
  89. case 'anime':
  90. animeData = await AnimeLib.getAnime(
  91. path,
  92. path.split('/')[3]!.split('-')[0]!,
  93. ).then(response => <AnimeData>response.data)
  94. // Show anime watching in privacy mode if it's enabled, or enforce it when anime is RX rated
  95. if (isPrivacyMode(privacySetting, animeData.ageRestriction)) {
  96. setPrivacyMode(presenceData)
  97. break
  98. }
  99. if (path.endsWith('/watch')) {
  100. if (animeData.toast) {
  101. presenceData.details = 'Смотрит лицензированное аниме'
  102. presenceData.state = 'Информация пока что не доступна'
  103. presenceData.buttons = [
  104. {
  105. label: 'Открыть аниме',
  106. url: cleanUrl(document.location),
  107. },
  108. ]
  109. break
  110. }
  111. const video = document.querySelector('video')
  112. const dub = document
  113. .querySelector('.menu-item.is-active')
  114. ?.querySelector('.menu-item__text')
  115. ?.textContent
  116. ?? document
  117. .querySelector('.btn.is-plain.is-outline')
  118. ?.querySelector('strong')
  119. ?.textContent
  120. const episode = document.querySelector('[id^=\'episode\'][class*=\' \'] > span')
  121. ?.textContent
  122. ?? document
  123. .querySelectorAll('.btn.is-outline')[6]
  124. ?.querySelector('span')
  125. ?.textContent
  126. ?? document
  127. .querySelectorAll('.btn.is-outline')[7]
  128. ?.querySelector('span')
  129. ?.textContent
  130. if (dub || currentDub) {
  131. /**
  132. * This makes sure that the dub will always be defined.
  133. * When user changes menu between dubs/subs, the menu items are different,
  134. * so it's not possible to get the current active item if it's in a different menu.
  135. */
  136. if (dub)
  137. currentDub = dub
  138. const title = animeData.rus_name !== '' ? animeData.rus_name : animeData.name
  139. titleSetting
  140. ? (presenceData.name = title)
  141. : (presenceData.details = title)
  142. presenceData.state = `${
  143. episode ? (episode.includes('эпизод') ? episode : 'Фильм') : 'Фильм'
  144. } | ${currentDub}`
  145. presenceData.largeImageKey = animeData.cover.default
  146. presenceData.largeImageText = title
  147. presenceData.buttons = [
  148. {
  149. label: 'Открыть аниме',
  150. url: cleanUrl(document.location),
  151. },
  152. ]
  153. presenceData.smallImageKey = Assets.Pause
  154. presenceData.smallImageText = 'На паузе'
  155. }
  156. if (video || iFrameVideo) {
  157. if (video) {
  158. [presenceData.startTimestamp, presenceData.endTimestamp] = presence.getTimestampsfromMedia(video)
  159. presenceData.smallImageKey = video.paused
  160. ? Assets.Pause
  161. : Assets.Play
  162. presenceData.smallImageText = video.paused
  163. ? 'На паузе'
  164. : 'Воспроизводится'
  165. iFrameVideo = null
  166. }
  167. else if (iFrameVideo) {
  168. [presenceData.startTimestamp, presenceData.endTimestamp] = presence.getTimestamps(
  169. iFrameVideo.currentTime,
  170. iFrameVideo.duration,
  171. )
  172. presenceData.smallImageKey = iFrameVideo.paused
  173. ? Assets.Pause
  174. : Assets.Play
  175. presenceData.smallImageText = iFrameVideo.paused
  176. ? 'На паузе'
  177. : 'Воспроизводится'
  178. }
  179. if (video?.paused || iFrameVideo?.paused) {
  180. delete presenceData.startTimestamp
  181. delete presenceData.endTimestamp
  182. }
  183. }
  184. }
  185. else {
  186. if (animeData.toast) {
  187. const cover = document.querySelector<HTMLImageElement>('.cover__img')?.src
  188. const title = document.querySelector('h1')?.textContent
  189. const altTitle = document.querySelector('h2')?.textContent
  190. if (cover && title && altTitle) {
  191. presenceData.details = 'Страница лицензированного аниме'
  192. presenceData.state = `${title} (${altTitle})`
  193. presenceData.largeImageKey = cover
  194. presenceData.largeImageText = title
  195. presenceData.buttons = [
  196. {
  197. label: 'Открыть аниме',
  198. url: cleanUrl(document.location),
  199. },
  200. ]
  201. }
  202. break
  203. }
  204. presenceData.details = 'Страница аниме'
  205. presenceData.state = `${
  206. animeData.rus_name !== '' ? animeData.rus_name : animeData.name
  207. } (${animeData.eng_name ?? animeData.name})`
  208. presenceData.largeImageKey = animeData.cover.default
  209. presenceData.largeImageText = animeData.rus_name !== '' ? animeData.rus_name : animeData.name
  210. presenceData.buttons = [
  211. {
  212. label: 'Открыть аниме',
  213. url: cleanUrl(document.location),
  214. },
  215. ]
  216. }
  217. break
  218. case 'characters':
  219. if (path.split('/')[3]) {
  220. if (path.split('/')[3] === 'new') {
  221. presenceData.details = 'Добавляет персонажа'
  222. presenceData.state = 'Очередной аниме персонаж...'
  223. }
  224. else {
  225. characterData = await AnimeLib.getCharacter(
  226. path,
  227. path.split('/')[3]!.split('-')[0]!,
  228. ).then(response => <CharacterData>response.data)
  229. presenceData.details = 'Страница персонажа'
  230. presenceData.state = `${characterData.rus_name} (${characterData.name})`
  231. presenceData.largeImageKey = characterData.cover.default
  232. presenceData.largeImageText = characterData.rus_name
  233. presenceData.smallImageKey = ActivityAssets.Logo
  234. presenceData.buttons = [
  235. {
  236. label: 'Oткрыть персoнажа',
  237. url: cleanUrl(document.location),
  238. },
  239. ]
  240. }
  241. }
  242. else {
  243. presenceData.details = 'Страница персонажей'
  244. presenceData.state = 'Ищет нового фаворита?'
  245. }
  246. break
  247. case 'people':
  248. if (path.split('/')[3]) {
  249. if (path.split('/')[3] === 'create') {
  250. presenceData.details = 'Добавляет человека'
  251. presenceData.state = 'Какая-то известная личность?'
  252. }
  253. else {
  254. peopleData = await AnimeLib.getPerson(
  255. path,
  256. path.split('/')[3]!.split('-')[0]!,
  257. ).then(response => <PersonData>response.data)
  258. const name = peopleData.rus_name !== ''
  259. ? peopleData.rus_name
  260. : peopleData.alt_name !== ''
  261. ? peopleData.alt_name
  262. : peopleData.name
  263. presenceData.details = 'Страница человека'
  264. presenceData.state = `${name} (${peopleData.name})`
  265. presenceData.largeImageKey = peopleData.cover.default
  266. presenceData.largeImageText = name
  267. presenceData.smallImageKey = ActivityAssets.Logo
  268. presenceData.buttons = [
  269. {
  270. label: 'Открыть человека',
  271. url: cleanUrl(document.location),
  272. },
  273. ]
  274. }
  275. }
  276. else {
  277. presenceData.details = 'Страница людей'
  278. presenceData.state = 'Ищет нового фаворита?'
  279. }
  280. break
  281. case 'catalog':
  282. presenceData.details = 'В каталоге'
  283. presenceData.state = 'Что ждёт нас сегодня?'
  284. break
  285. case 'user':
  286. if (path.split('/')[3]) {
  287. if (path.split('/')[3] === 'notifications') {
  288. presenceData.details = 'Страница уведомлений'
  289. presenceData.state = 'Что-то новенькое?'
  290. }
  291. else {
  292. userData = await AnimeLib.getUser(path.split('/')[3]!).then(
  293. response => <UserData>response.data,
  294. )
  295. presenceData.details = 'Страница пользователя'
  296. presenceData.state = userData.username
  297. presenceData.largeImageKey = userData.avatar.url
  298. presenceData.largeImageText = userData.username
  299. presenceData.smallImageKey = ActivityAssets.Logo
  300. presenceData.buttons = [
  301. {
  302. label: 'Открыть профиль',
  303. url: cleanUrl(document.location),
  304. },
  305. ]
  306. }
  307. }
  308. else {
  309. presenceData.details = 'Страница пользователей'
  310. presenceData.state = 'Столько интересных личностей!'
  311. }
  312. break
  313. case 'top-views':
  314. presenceData.details = 'В топе по просмотрам'
  315. presenceData.state = 'Любуется популярными аниме'
  316. break
  317. case 'collections':
  318. if (path.split('/')[3]) {
  319. if (path.split('/')[3] === 'new') {
  320. presenceData.details = 'Создаёт коллекцию'
  321. presenceData.state = 'В ней будет много интересного!'
  322. }
  323. else {
  324. collectionData = await AnimeLib.getCollection(
  325. path.split('/')[3]!,
  326. ).then(response => <CollectionData>response.data)
  327. // Show collection viewing in privacy mode if it's enabled, or enforce it when collection was marked as for adults
  328. if (privacySetting || collectionData.adult) {
  329. setPrivacyMode(presenceData)
  330. break
  331. }
  332. let collectionType: string
  333. switch (collectionData.type) {
  334. case 'titles':
  335. collectionType = 'тайтлам'
  336. break
  337. case 'character':
  338. collectionType = 'персонажам'
  339. break
  340. case 'people':
  341. collectionType = 'людям'
  342. break
  343. }
  344. presenceData.details = `Коллекция по ${collectionType}`
  345. presenceData.state = `${collectionData.name} от ${collectionData.user.username}`
  346. presenceData.largeImageKey = ActivityAssets.Logo
  347. presenceData.smallImageKey = collectionData.user.avatar.url
  348. presenceData.smallImageText = collectionData.user.username
  349. presenceData.buttons = [
  350. {
  351. label: 'Oткрыть кoллекцию',
  352. url: cleanUrl(document.location),
  353. },
  354. ]
  355. }
  356. }
  357. else {
  358. presenceData.details = 'Страница коллекций'
  359. presenceData.state = 'Их так много...'
  360. }
  361. break
  362. case 'reviews':
  363. if (path.split('/')[3]) {
  364. if (path.split('/')[3] === 'new') {
  365. presenceData.details = 'Пишет отзыв'
  366. presenceData.state = 'Излагает свои мысли...'
  367. }
  368. else {
  369. reviewData = await AnimeLib.getReview(path.split('/')[3]!).then(
  370. response => <ReviewData>response.data,
  371. )
  372. // Show review reading in privacy mode if it's enabled, or enforce it when related anime is RX rated
  373. if (
  374. isPrivacyMode(privacySetting, reviewData.related.ageRestriction)
  375. ) {
  376. setPrivacyMode(presenceData)
  377. break
  378. }
  379. presenceData.details = `Отзыв на ${reviewData.related.rus_name}`
  380. presenceData.state = `${reviewData.title} от ${reviewData.user.username}`
  381. presenceData.largeImageKey = reviewData.related.cover.default
  382. presenceData.largeImageText = reviewData.related.rus_name
  383. presenceData.smallImageKey = reviewData.user.avatar.url
  384. presenceData.smallImageText = reviewData.user.username
  385. presenceData.buttons = [
  386. {
  387. label: 'Открыть отзыв',
  388. url: cleanUrl(document.location),
  389. },
  390. ]
  391. }
  392. }
  393. else {
  394. presenceData.details = 'Страница отзывов'
  395. presenceData.state = 'Столько разных мнений!'
  396. }
  397. break
  398. case 'team':
  399. if (path.split('/')[3]) {
  400. if (path.split('/')[3] === 'create') {
  401. presenceData.details = 'Создаёт свою команду'
  402. presenceData.state = 'Она обязательно будет успешной!'
  403. }
  404. else {
  405. teamData = await AnimeLib.getTeam(
  406. path,
  407. path.split('/')[3]!.split('-')[0]!,
  408. ).then(response => <TeamData>response.data)
  409. presenceData.details = 'Страница команды'
  410. presenceData.state = `${teamData.name} (${
  411. teamData.alt_name ?? teamData.name
  412. })`
  413. presenceData.largeImageKey = teamData.cover.default
  414. presenceData.smallImageKey = ActivityAssets.Logo
  415. presenceData.buttons = [
  416. {
  417. label: 'Открыть команду',
  418. url: cleanUrl(document.location),
  419. },
  420. ]
  421. }
  422. }
  423. else {
  424. presenceData.details = 'Страница команд'
  425. presenceData.state = 'Они все такие разные!'
  426. }
  427. break
  428. case 'franchise':
  429. if (path.split('/')[3]) {
  430. const name = document.querySelector('h1')
  431. const altName = document.querySelector('h2')
  432. if (name && altName) {
  433. presenceData.details = 'Страница франшизы'
  434. presenceData.state = `${name.textContent} (${
  435. altName.textContent?.split('/')[0] ?? ''
  436. })`
  437. presenceData.buttons = [
  438. {
  439. label: 'Открыть франшизу',
  440. url: cleanUrl(document.location),
  441. },
  442. ]
  443. }
  444. }
  445. else {
  446. presenceData.details = 'Страница франшиз'
  447. presenceData.state = 'Их так много...'
  448. }
  449. break
  450. case 'publisher':
  451. if (path.split('/')[3]) {
  452. if (path.split('/')[3] === 'new') {
  453. presenceData.details = 'Добавляет издательство'
  454. presenceData.state = 'Да что они там издают?'
  455. }
  456. else {
  457. publisherData = await AnimeLib.getPublisher(
  458. path,
  459. path.split('/')[3]!.split('-')[0]!,
  460. ).then(response => <PublisherData>response.data)
  461. presenceData.details = 'Страница издателя'
  462. presenceData.state = `${
  463. publisherData.rus_name ?? publisherData.name
  464. } (${publisherData.name})`
  465. presenceData.largeImageKey = publisherData.cover.default
  466. presenceData.buttons = [
  467. {
  468. label: 'Открыть издателя',
  469. url: cleanUrl(document.location),
  470. },
  471. ]
  472. }
  473. }
  474. else {
  475. presenceData.details = 'Страница издетелей'
  476. presenceData.state = 'Их так много...'
  477. }
  478. break
  479. case 'media':
  480. if (path.split('/')[3] === 'create') {
  481. presenceData.details = 'Добавляет тайтл'
  482. presenceData.state = 'Он будет самым интересным!'
  483. }
  484. break
  485. case 'news':
  486. if (path.split('/')[3]) {
  487. const avatar = document
  488. .querySelector('.user-inline')
  489. ?.querySelector<HTMLImageElement>('.avatar.is-rounded')
  490. ?.src
  491. const username = document.querySelector(
  492. '.user-inline__username',
  493. )?.textContent
  494. const title = document.querySelector('h1')?.textContent
  495. if (avatar && username && title) {
  496. presenceData.details = 'Читает новость'
  497. presenceData.state = `${title} от ${username}`
  498. presenceData.largeImageKey = ActivityAssets.Logo
  499. presenceData.smallImageKey = avatar
  500. presenceData.smallImageText = username
  501. presenceData.buttons = [
  502. {
  503. label: 'Открыть новость',
  504. url: cleanUrl(document.location),
  505. },
  506. ]
  507. }
  508. }
  509. else {
  510. presenceData.details = 'На странице новостей'
  511. presenceData.state = 'Ищет, чего бы почитать'
  512. }
  513. break
  514. case 'faq':
  515. if (path.split('/')[3]) {
  516. if (document.querySelector('h1')) {
  517. presenceData.details = 'Страница вопросов и ответов'
  518. presenceData.state = document.querySelector('h1')?.textContent ?? ''
  519. presenceData.buttons = [
  520. {
  521. label: 'Открыть страницу',
  522. url: cleanUrl(document.location),
  523. },
  524. ]
  525. }
  526. }
  527. else {
  528. presenceData.details = 'Страница вопросов и ответов'
  529. presenceData.state = 'Ответ на любой вопрос здесь!'
  530. }
  531. break
  532. case 'messages':
  533. presenceData.details = 'В личных сообщениях'
  534. presenceData.state = 'С кем-то общается...'
  535. break
  536. case 'downloads':
  537. presenceData.details = 'Страница загрузок'
  538. presenceData.state = 'Просматривает загруженные материалы'
  539. break
  540. default:
  541. presenceData.details = 'Где-то...'
  542. presenceData.state = 'Не пытайтесь найти!'
  543. break
  544. }
  545. if (!buttonsSetting)
  546. delete presenceData.buttons
  547. presence.setActivity(presenceData)
  548. })