您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==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/[email protected] // @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://gf.qytechs.cn/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(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址