- // ==UserScript==
- // @name AnilistBytes
- // @match https://anilist.co/*
- // @match https://animebytes.tv/*
- // @run-at document-end
- // @version 1.9.7
- // @author notmarek
- // @icon https://anilistbytes.notmarek.com/AB.svg
- // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
- // @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.
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.xmlHttpRequest
- // @namespace https://greasyfork.org/users/1095705
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- 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}";
-
- let passkey = null; // You still can change this manually
- let username = null; // Same here
-
- // Get passkey and username from local storage
-
- if (unsafeWindow.location.href.match(/animebytes\.tv/))
- // check which site we are on to run the correct script
- animebytes();else anilist();
- document.head.append(VM.m(VM.h("style", null, css_248z)));
- async function animebytes() {
- passkey = await GM.getValue('passkey', null);
- username = await GM.getValue('username', null);
- const save = async e => {
- e.preventDefault();
- let passkey = document.querySelector("link[type='application/rss+xml']").href.match(/\/feed\/rss_torrents_all\/(.*)/)[1];
- let username = document.querySelector('.username').innerText;
- await GM.setValue('passkey', passkey);
- await GM.setValue('username', username);
- alert('Passkey and username set you can now go to anilist!');
- return false;
- };
- let element = VM.h("div", null, VM.h("h3", null, "AnilistBytes"), VM.h("ul", {
- class: "nobullet"
- }, VM.h("li", null, VM.h("a", {
- href: "#",
- onclick: save,
- id: "anilistbytes"
- }, !passkey && !username ? 'Set Passkey & Username' : 'Update Passkey & Username'))));
- document.querySelector('#footer_inner').appendChild(VM.m(element));
- }
- async function getMALId(id, type, isAdult = false) {
- let query = {
- query: 'query media($id: Int, $type: MediaType, $isAdult: Boolean) { Media(id: $id, type: $type, isAdult: $isAdult) { idMal }}',
- variables: {
- id,
- type,
- isAdult
- }
- };
- let res = await fetch('https://anilist.co/graphql', {
- body: JSON.stringify(query),
- headers: {
- 'content-type': 'application/json',
- 'x-csrf-token': unsafeWindow.al_token
- },
- method: 'POST'
- });
- return (await res.json()).data.Media.idMal;
- }
- async function anilist() {
- passkey = await GM.getValue('passkey', null);
- username = await GM.getValue('username', null);
- if (passkey === null || username === null) {
- alert('Make sure to press the button in the footer of animebytes or edit the script to set your passkey and username!');
- }
-
- // stolen from https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
- function formatBytes(a, b = 2) {
- if (!+a) return '0 Bytes';
- const c = 0 > b ? 0 : b,
- d = Math.floor(Math.log(a) / Math.log(1024));
- return `${parseFloat((a / Math.pow(1024, d)).toFixed(c))} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][d]}`;
- }
- const createTorrentEntry = (link, name, size, l, s, snatch, downMultipler) => {
- let st = VM.h(VM.Fragment, null, "\xA0|\xA0", VM.h("a", {
- style: "color:gray;",
- href: "",
- onclick: e => {
- unsafeWindow._addTo(link);
- e.target.innerText = 'Added!';
- return false;
- }
- }, "ST"));
- let flicon = VM.h("img", {
- src: "https://anilistbytes.notmarek.com/flicon.png",
- alt: "| Freeleech"
- });
- let sneedexicon = VM.h("img", {
- style: "margin-left: 5px;",
- src: "https://anilistbytes.notmarek.com/sndx.png",
- alt: "| Sneedex"
- });
- let anime = name.includes('| Freeleech') ? VM.h(VM.Fragment, null, name.replace('| Freeleech', ''), flicon) : VM.h(VM.Fragment, null, name);
- anime = sneedex.includes(link.match(/torrent\/(\d+)\/download/)[1]) ? VM.h(VM.Fragment, null, anime, sneedexicon) : VM.h(VM.Fragment, null, anime);
- return VM.h(VM.Fragment, null, VM.h("h2", null, VM.h("span", null, "[", VM.h("a", {
- href: link,
- style: "color:gray;"
- }, "\xA0DL"), unsafeWindow._addTo ? st : null, "\xA0]\xA0"), VM.h("span", null, anime)), VM.h("h2", {
- class: "animebytes stats"
- }, VM.h("span", null, formatBytes(size)), VM.h("span", null, String(snatch)), VM.h("span", null, String(s)), VM.h("span", null, String(l))));
- };
-
- // function to decode html entities in strings (e.g. & -> &)
- const getDecodedString = str => {
- const txt = document.createElement('textarea');
- txt.innerHTML = str;
- return txt.value;
- };
-
- // function using GM.xmlHttpRequest to make the xmlhttprequest closer to fetch
- const GM_get = async url => {
- return new Promise((resolve, reject) => {
- GM.xmlHttpRequest({
- method: 'GET',
- url,
- headers: {
- Accept: 'application/json'
- },
- onload: res => {
- resolve({
- json: async () => JSON.parse(res.responseText)
- });
- },
- onerror: err => {
- reject(err);
- },
- onabort: err => {
- reject(err);
- }
- });
- });
- };
- const cacheSneedex = async () => {
- let res = await GM_get('https://sneedex.moe/api/public/ab');
- let data = await res.json();
- data = data.map(e => e.permLinks.map(e => e.match(/torrentid=(\d+)/)[1])).flat();
- await GM.setValue('sneedexv2', data);
- return data;
- };
- let sneedex = await GM.getValue('sneedexv2', await cacheSneedex());
- const formats = {
- MANGA: 'Manga',
- NOVEL: 'Light Novel'
- };
- const createTorrentList = async (perfectMatch = true, title_type = 0, mal_id = null) => {
- // Cleanup exising elements
- try {
- document.querySelectorAll('.animebytes').forEach(e => e.remove());
- } catch (_unused) {
- }
- let type = unsafeWindow.location.pathname.match(/\/(anime|manga)\/[0-9]/);
- if (type === null) {
- return;
- }
- type = type[1];
- let vueMyBeloved;
- try {
- vueMyBeloved = document.getElementById('app').__vue__.$children.find(e => e.media);
- } catch (_unused2) {
- setTimeout(createTorrentList, 500);
- return;
- }
- const containerEl = document.querySelector('.content div.overview');
- if (containerEl) {
- var _vueMyBeloved$media$e, _vueMyBeloved$media$s, _vueMyBeloved$media$s2;
- const types = ['romaji', 'userPreferred', 'english', 'native'];
- let seriesName;
- try {
- seriesName = vueMyBeloved.media.title[types[title_type]].replaceAll(/[\]\[]/g, '');
- } catch (_unused3) {
- setTimeout(createTorrentList, 500);
- return;
- }
- const hentai = vueMyBeloved.media.isAdult;
- const epcount = (_vueMyBeloved$media$e = vueMyBeloved.media.episodes) != null ? _vueMyBeloved$media$e : 'manga';
- 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;
- let clonableEl = containerEl.querySelector('div .description-wrap');
- if (clonableEl === null) setTimeout(createTorrentList, 500);
- 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' : ''}`;
- if (!perfectMatch) endpoint = `https://animebytes.tv/scrape.php?torrent_pass=${passkey}&username=${username}&hentai=2&type=anime&searchstr=${encodeURIComponent(seriesName)}`;
- console.log(`[AnilistBytes] Using api endpoint: ${endpoint}`);
- let res = await GM_get(endpoint);
- if (!mal_id) {
- mal_id = await getMALId(vueMyBeloved.media.id, vueMyBeloved.media.type, vueMyBeloved.media.isAdult);
- }
- let ab_groups = (await res.json()).Groups;
- if (!ab_groups) {
- if (perfectMatch && title_type < 3) {
- console.log(`[AnilistBytes] Perfect match for ${types[title_type]} title failed, trying ${types[title_type + 1]} title`);
- return await createTorrentList(true, title_type + 1, mal_id);
- } else if (!perfectMatch && title_type < 3) {
- console.log(`[AnilistBytes] Imperfect match for ${types[title_type]} title failed, trying ${types[title_type + 1]} title`);
- return await createTorrentList(false, title_type + 1, mal_id);
- } else if (perfectMatch) {
- console.log(`[AnilistBytes] Perfect match for all titles failed, trying imperfect match.`);
- return await createTorrentList(false, 0);
- } else {
- console.log('[AnilistBytes] No match found giving up.');
- vueMyBeloved.$children.find(e => e.$options._componentTag == 'external-links')._props.links.push({
- color: '#ed106a',
- site: 'AnimeBytes [Search]',
- url: `https://animebytes.tv/torrents.php?searchstr=${encodeURIComponent(vueMyBeloved.media.title[types[1]].replaceAll(/[\]\[]/g, ''))}`,
- icon: 'https://anilistbytes.notmarek.com/AB.svg'
- });
- return;
- }
- }
- let data = null;
- for (let match of ab_groups) {
- if (!match.Links.MAL) {
- continue;
- }
- let mid = match.Links.MAL.match(/(\d+)/)[1];
- if (mid == mal_id) {
- data = match;
- break;
- }
- }
- console.log(data);
- if (!data && !perfectMatch) {
- data = ab_groups[0];
- } else if (!data) {
- return await createTorrentList(false, 0, mal_id);
- } else {
- perfectMatch = true;
- }
- vueMyBeloved.$children.find(e => e.$options._componentTag == 'external-links')._props.links.push({
- color: '#ed106a',
- site: 'AnimeBytes',
- url: `https://animebytes.tv/torrents.php?id=${data.ID}`,
- icon: 'https://anilistbytes.notmarek.com/AB.svg'
- });
- let entries = await Promise.all(data.Torrents.map(async torrent => {
- return await createTorrentEntry(torrent.Link, torrent.Property, torrent.Size, torrent.Leechers, torrent.Seeders, torrent.Snatched, torrent.RawDownMultiplier);
- }));
- let element = VM.h("div", {
- class: "animebytes"
- }, VM.h("h2", null, "AnilistBytes"), VM.h("p", {
- class: "description content-wrap"
- }, VM.h("h2", null, getDecodedString(data.FullName), "\xA0[", VM.h("a", {
- href: `https://animebytes.tv/torrents.php?id=${data.ID}`,
- style: "color: gray;",
- target: "_blank"
- }, "AB"), "]\xA0", perfectMatch ? null : VM.h("span", {
- style: "cursor: help; color: #ffaa00;",
- 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."
- }, "(imperfect match)")), VM.h("h2", {
- class: "animebytes stats"
- }, VM.h("span", null, "Size"), VM.h("span", null, VM.h("img", {
- src: "https://anilistbytes.notmarek.com/snatched.svg",
- alt: "Snatches"
- })), VM.h("span", null, VM.h("img", {
- src: "https://anilistbytes.notmarek.com/seeders.svg",
- alt: "Seeders"
- })), VM.h("span", null, VM.h("img", {
- src: "https://anilistbytes.notmarek.com/leechers.svg",
- alt: "Leechers"
- }))), entries));
- containerEl.insertBefore(VM.m(element), clonableEl);
- } else {
- // check every 500ms if the page has loaded, so we can load our data
- setTimeout(() => createTorrentList(), 500);
- }
- };
-
- // hijack the window.history.pushState function to do shit for us on navigation
- (function (history) {
- var pushState = history.pushState;
- history.pushState = function (_state) {
- const res = pushState.apply(history, arguments);
- unsafeWindow.dispatchEvent(new Event('popstate'));
- return res;
- };
- })(unsafeWindow.history);
- unsafeWindow.addEventListener('popstate', () => {
- console.log(`[AnilistBytes] Soft navigated to ${unsafeWindow.location.pathname}`);
- setTimeout(createTorrentList, 500);
- });
- createTorrentList();
- }
-
- })();