AnilistBytes

adds torrents from animebytes to the anime view. Make sure to change the passkey and username from null or click the new button in the animebytes footer.

安装此脚本?
作者推荐脚本

您可能也喜欢SendToClient

安装此脚本
  1. // ==UserScript==
  2. // @name AnilistBytes
  3. // @match https://anilist.co/*
  4. // @match https://animebytes.tv/*
  5. // @run-at document-end
  6. // @version 1.9.7
  7. // @author notmarek
  8. // @icon https://anilistbytes.notmarek.com/AB.svg
  9. // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
  10. // @description adds torrents from animebytes to the anime view. Make sure to change the passkey and username from null or click the new button in the animebytes footer.
  11. // @grant GM.getValue
  12. // @grant GM.setValue
  13. // @grant GM.xmlHttpRequest
  14. // @namespace https://greasyfork.org/users/1095705
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. var css_248z = "#footer_inner .footer_column{width:180px}.animebytes>p{display:grid;grid-auto-columns:1fr;grid-template-columns:1fr .5fr}h2.animebytes.stats{grid-column-gap:1rem;display:grid;grid-template-columns:1fr .5fr .5fr .5fr;text-align:center}";
  21.  
  22. let passkey = null; // You still can change this manually
  23. let username = null; // Same here
  24.  
  25. // Get passkey and username from local storage
  26.  
  27. if (unsafeWindow.location.href.match(/animebytes\.tv/))
  28. // check which site we are on to run the correct script
  29. animebytes();else anilist();
  30. document.head.append(VM.m(VM.h("style", null, css_248z)));
  31. async function animebytes() {
  32. passkey = await GM.getValue('passkey', null);
  33. username = await GM.getValue('username', null);
  34. const save = async e => {
  35. e.preventDefault();
  36. let passkey = document.querySelector("link[type='application/rss+xml']").href.match(/\/feed\/rss_torrents_all\/(.*)/)[1];
  37. let username = document.querySelector('.username').innerText;
  38. await GM.setValue('passkey', passkey);
  39. await GM.setValue('username', username);
  40. alert('Passkey and username set you can now go to anilist!');
  41. return false;
  42. };
  43. let element = VM.h("div", null, VM.h("h3", null, "AnilistBytes"), VM.h("ul", {
  44. class: "nobullet"
  45. }, VM.h("li", null, VM.h("a", {
  46. href: "#",
  47. onclick: save,
  48. id: "anilistbytes"
  49. }, !passkey && !username ? 'Set Passkey & Username' : 'Update Passkey & Username'))));
  50. document.querySelector('#footer_inner').appendChild(VM.m(element));
  51. }
  52. async function getMALId(id, type, isAdult = false) {
  53. let query = {
  54. query: 'query media($id: Int, $type: MediaType, $isAdult: Boolean) { Media(id: $id, type: $type, isAdult: $isAdult) { idMal }}',
  55. variables: {
  56. id,
  57. type,
  58. isAdult
  59. }
  60. };
  61. let res = await fetch('https://anilist.co/graphql', {
  62. body: JSON.stringify(query),
  63. headers: {
  64. 'content-type': 'application/json',
  65. 'x-csrf-token': unsafeWindow.al_token
  66. },
  67. method: 'POST'
  68. });
  69. return (await res.json()).data.Media.idMal;
  70. }
  71. async function anilist() {
  72. passkey = await GM.getValue('passkey', null);
  73. username = await GM.getValue('username', null);
  74. if (passkey === null || username === null) {
  75. alert('Make sure to press the button in the footer of animebytes or edit the script to set your passkey and username!');
  76. }
  77.  
  78. // stolen from https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
  79. function formatBytes(a, b = 2) {
  80. if (!+a) return '0 Bytes';
  81. const c = 0 > b ? 0 : b,
  82. d = Math.floor(Math.log(a) / Math.log(1024));
  83. return `${parseFloat((a / Math.pow(1024, d)).toFixed(c))} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][d]}`;
  84. }
  85. const createTorrentEntry = (link, name, size, l, s, snatch, downMultipler) => {
  86. let st = VM.h(VM.Fragment, null, "\xA0|\xA0", VM.h("a", {
  87. style: "color:gray;",
  88. href: "",
  89. onclick: e => {
  90. unsafeWindow._addTo(link);
  91. e.target.innerText = 'Added!';
  92. return false;
  93. }
  94. }, "ST"));
  95. let flicon = VM.h("img", {
  96. src: "https://anilistbytes.notmarek.com/flicon.png",
  97. alt: "| Freeleech"
  98. });
  99. let sneedexicon = VM.h("img", {
  100. style: "margin-left: 5px;",
  101. src: "https://anilistbytes.notmarek.com/sndx.png",
  102. alt: "| Sneedex"
  103. });
  104. let anime = name.includes('| Freeleech') ? VM.h(VM.Fragment, null, name.replace('| Freeleech', ''), flicon) : VM.h(VM.Fragment, null, name);
  105. anime = sneedex.includes(link.match(/torrent\/(\d+)\/download/)[1]) ? VM.h(VM.Fragment, null, anime, sneedexicon) : VM.h(VM.Fragment, null, anime);
  106. return VM.h(VM.Fragment, null, VM.h("h2", null, VM.h("span", null, "[", VM.h("a", {
  107. href: link,
  108. style: "color:gray;"
  109. }, "\xA0DL"), unsafeWindow._addTo ? st : null, "\xA0]\xA0"), VM.h("span", null, anime)), VM.h("h2", {
  110. class: "animebytes stats"
  111. }, VM.h("span", null, formatBytes(size)), VM.h("span", null, String(snatch)), VM.h("span", null, String(s)), VM.h("span", null, String(l))));
  112. };
  113.  
  114. // function to decode html entities in strings (e.g. & -> &)
  115. const getDecodedString = str => {
  116. const txt = document.createElement('textarea');
  117. txt.innerHTML = str;
  118. return txt.value;
  119. };
  120.  
  121. // function using GM.xmlHttpRequest to make the xmlhttprequest closer to fetch
  122. const GM_get = async url => {
  123. return new Promise((resolve, reject) => {
  124. GM.xmlHttpRequest({
  125. method: 'GET',
  126. url,
  127. headers: {
  128. Accept: 'application/json'
  129. },
  130. onload: res => {
  131. resolve({
  132. json: async () => JSON.parse(res.responseText)
  133. });
  134. },
  135. onerror: err => {
  136. reject(err);
  137. },
  138. onabort: err => {
  139. reject(err);
  140. }
  141. });
  142. });
  143. };
  144. const cacheSneedex = async () => {
  145. let res = await GM_get('https://sneedex.moe/api/public/ab');
  146. let data = await res.json();
  147. data = data.map(e => e.permLinks.map(e => e.match(/torrentid=(\d+)/)[1])).flat();
  148. await GM.setValue('sneedexv2', data);
  149. return data;
  150. };
  151. let sneedex = await GM.getValue('sneedexv2', await cacheSneedex());
  152. const formats = {
  153. MANGA: 'Manga',
  154. NOVEL: 'Light Novel'
  155. };
  156. const createTorrentList = async (perfectMatch = true, title_type = 0, mal_id = null) => {
  157. // Cleanup exising elements
  158. try {
  159. document.querySelectorAll('.animebytes').forEach(e => e.remove());
  160. } catch (_unused) {
  161. }
  162. let type = unsafeWindow.location.pathname.match(/\/(anime|manga)\/[0-9]/);
  163. if (type === null) {
  164. return;
  165. }
  166. type = type[1];
  167. let vueMyBeloved;
  168. try {
  169. vueMyBeloved = document.getElementById('app').__vue__.$children.find(e => e.media);
  170. } catch (_unused2) {
  171. setTimeout(createTorrentList, 500);
  172. return;
  173. }
  174. const containerEl = document.querySelector('.content div.overview');
  175. if (containerEl) {
  176. var _vueMyBeloved$media$e, _vueMyBeloved$media$s, _vueMyBeloved$media$s2;
  177. const types = ['romaji', 'userPreferred', 'english', 'native'];
  178. let seriesName;
  179. try {
  180. seriesName = vueMyBeloved.media.title[types[title_type]].replaceAll(/[\]\[]/g, '');
  181. } catch (_unused3) {
  182. setTimeout(createTorrentList, 500);
  183. return;
  184. }
  185. const hentai = vueMyBeloved.media.isAdult;
  186. const epcount = (_vueMyBeloved$media$e = vueMyBeloved.media.episodes) != null ? _vueMyBeloved$media$e : 'manga';
  187. const seriesYear = (_vueMyBeloved$media$s = vueMyBeloved.media.seasonYear) != null ? _vueMyBeloved$media$s : (_vueMyBeloved$media$s2 = vueMyBeloved.media.startDate) == null ? void 0 : _vueMyBeloved$media$s2.year;
  188. let clonableEl = containerEl.querySelector('div .description-wrap');
  189. if (clonableEl === null) setTimeout(createTorrentList, 500);
  190. let endpoint = `https://animebytes.tv/scrape.php?torrent_pass=${passkey}&username=${username}&hentai=${Number(hentai)}&epcount=${epcount}&year=${seriesYear}&type=anime&searchstr=${encodeURIComponent(seriesName)}${type == 'manga' ? '&printedtype[' + formats[vueMyBeloved.media.format] + ']=1' : ''}`;
  191. if (!perfectMatch) endpoint = `https://animebytes.tv/scrape.php?torrent_pass=${passkey}&username=${username}&hentai=2&type=anime&searchstr=${encodeURIComponent(seriesName)}`;
  192. console.log(`[AnilistBytes] Using api endpoint: ${endpoint}`);
  193. let res = await GM_get(endpoint);
  194. if (!mal_id) {
  195. mal_id = await getMALId(vueMyBeloved.media.id, vueMyBeloved.media.type, vueMyBeloved.media.isAdult);
  196. }
  197. let ab_groups = (await res.json()).Groups;
  198. if (!ab_groups) {
  199. if (perfectMatch && title_type < 3) {
  200. console.log(`[AnilistBytes] Perfect match for ${types[title_type]} title failed, trying ${types[title_type + 1]} title`);
  201. return await createTorrentList(true, title_type + 1, mal_id);
  202. } else if (!perfectMatch && title_type < 3) {
  203. console.log(`[AnilistBytes] Imperfect match for ${types[title_type]} title failed, trying ${types[title_type + 1]} title`);
  204. return await createTorrentList(false, title_type + 1, mal_id);
  205. } else if (perfectMatch) {
  206. console.log(`[AnilistBytes] Perfect match for all titles failed, trying imperfect match.`);
  207. return await createTorrentList(false, 0);
  208. } else {
  209. console.log('[AnilistBytes] No match found giving up.');
  210. vueMyBeloved.$children.find(e => e.$options._componentTag == 'external-links')._props.links.push({
  211. color: '#ed106a',
  212. site: 'AnimeBytes [Search]',
  213. url: `https://animebytes.tv/torrents.php?searchstr=${encodeURIComponent(vueMyBeloved.media.title[types[1]].replaceAll(/[\]\[]/g, ''))}`,
  214. icon: 'https://anilistbytes.notmarek.com/AB.svg'
  215. });
  216. return;
  217. }
  218. }
  219. let data = null;
  220. for (let match of ab_groups) {
  221. if (!match.Links.MAL) {
  222. continue;
  223. }
  224. let mid = match.Links.MAL.match(/(\d+)/)[1];
  225. if (mid == mal_id) {
  226. data = match;
  227. break;
  228. }
  229. }
  230. console.log(data);
  231. if (!data && !perfectMatch) {
  232. data = ab_groups[0];
  233. } else if (!data) {
  234. return await createTorrentList(false, 0, mal_id);
  235. } else {
  236. perfectMatch = true;
  237. }
  238. vueMyBeloved.$children.find(e => e.$options._componentTag == 'external-links')._props.links.push({
  239. color: '#ed106a',
  240. site: 'AnimeBytes',
  241. url: `https://animebytes.tv/torrents.php?id=${data.ID}`,
  242. icon: 'https://anilistbytes.notmarek.com/AB.svg'
  243. });
  244. let entries = await Promise.all(data.Torrents.map(async torrent => {
  245. return await createTorrentEntry(torrent.Link, torrent.Property, torrent.Size, torrent.Leechers, torrent.Seeders, torrent.Snatched, torrent.RawDownMultiplier);
  246. }));
  247. let element = VM.h("div", {
  248. class: "animebytes"
  249. }, VM.h("h2", null, "AnilistBytes"), VM.h("p", {
  250. class: "description content-wrap"
  251. }, VM.h("h2", null, getDecodedString(data.FullName), "\xA0[", VM.h("a", {
  252. href: `https://animebytes.tv/torrents.php?id=${data.ID}`,
  253. style: "color: gray;",
  254. target: "_blank"
  255. }, "AB"), "]\xA0", perfectMatch ? null : VM.h("span", {
  256. style: "cursor: help; color: #ffaa00;",
  257. title: "Imperfect match means that the found anime may not be what you are looking for or that year/episode count/age rating simply don't match between anilist and AB."
  258. }, "(imperfect match)")), VM.h("h2", {
  259. class: "animebytes stats"
  260. }, VM.h("span", null, "Size"), VM.h("span", null, VM.h("img", {
  261. src: "https://anilistbytes.notmarek.com/snatched.svg",
  262. alt: "Snatches"
  263. })), VM.h("span", null, VM.h("img", {
  264. src: "https://anilistbytes.notmarek.com/seeders.svg",
  265. alt: "Seeders"
  266. })), VM.h("span", null, VM.h("img", {
  267. src: "https://anilistbytes.notmarek.com/leechers.svg",
  268. alt: "Leechers"
  269. }))), entries));
  270. containerEl.insertBefore(VM.m(element), clonableEl);
  271. } else {
  272. // check every 500ms if the page has loaded, so we can load our data
  273. setTimeout(() => createTorrentList(), 500);
  274. }
  275. };
  276.  
  277. // hijack the window.history.pushState function to do shit for us on navigation
  278. (function (history) {
  279. var pushState = history.pushState;
  280. history.pushState = function (_state) {
  281. const res = pushState.apply(history, arguments);
  282. unsafeWindow.dispatchEvent(new Event('popstate'));
  283. return res;
  284. };
  285. })(unsafeWindow.history);
  286. unsafeWindow.addEventListener('popstate', () => {
  287. console.log(`[AnilistBytes] Soft navigated to ${unsafeWindow.location.pathname}`);
  288. setTimeout(createTorrentList, 500);
  289. });
  290. createTorrentList();
  291. }
  292.  
  293. })();