// ==UserScript==
// @name AnilistBytes
// @match https://anilist.co/*
// @match https://animebytes.tv/*
// @run-at document-end
// @version 1.9.6
// @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.');
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();
}
})();