// ==UserScript==
// @name AnimeBytes Nightly
// @description Mitsuki's AnimeBytes Collection
// @namespace ThaUnknown
// @match *://animebytes.tv/*
// @match *://releases.moe/*
// @version 1.1.2
// @author VariousPeeps
// @grant GM_xmlhttpRequest
// @icon http://animebytes.tv/favicon.ico
// @require https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js
// @connect releases.moe
// @license MIT
// @run-at document-idle
// ==/UserScript==
/* global $, switchTabs, GM_xmlhttpRequest */
const TORRENT_ID_REGEX = /&torrentid=(\d+)/i
const seadexEndpoint = tinyRest('https://releases.moe/api/collections/entries/records')
function gmFetchJson (url, opts = {}, timeout = 10000) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
timeout,
...opts,
url: url.toString(),
ontimeout: function () {
reject(new Error(`Request timed out after ${timeout}ms`))
},
onerror: function (err) {
reject(err || new Error('Failed to fetch'))
},
onload: function (response) {
resolve(JSON.parse(response.responseText))
}
})
})
}
// import tinyRest from 'tiny-rest'
// modified for userscripts
function tinyRest (url, options = {}) {
const baseURL = new URL(url)
return (path = './', data = {}) => {
const requestURL = new URL(path, baseURL)
for (const [key, value] of Object.entries(data)) requestURL.searchParams.append(key, value)
requestURL.searchParams.sort() // sort to always have the same order, nicer for caching
return gmFetchJson(requestURL, options)
}
}
async function fetchSeadex (ids) {
const query = ids.map(({ torrentId }) => {
return 'trs.url?~\'%torrentid=' + torrentId + '%\''
}).join('||')
const { items } = await seadexEndpoint('', { filter: `(trs.tracker?='AB' && (${query}))`, expand: 'trs', fields: '*,expand.trs.url,expand.trs.isBest', skipTotal: true })
const linkMap = {}
for (const { alID, notes, comparison, expand } of items) {
for (const { url, isBest } of expand.trs) {
const torrentId = url.match(TORRENT_ID_REGEX)?.[1]
if (torrentId) linkMap[torrentId] = { alID, notes, isBest, comparison: comparison.split(',').filter(i => i) }
}
}
return linkMap
}
// Thanks to https://github.com/momentary0/AB-Userscripts/blob/master/torrent-highlighter/src/tfm_torrent_highlighter.user.js#L470
// for the handy selectors
function torrentsOnPage () {
const torrentPageTorrents = /** @type {Array<{a: HTMLAnchorElement, torrentId: string, separator: string}>} */([...document.querySelectorAll(
(window.location.href.includes('torrents.php') ? '' : '#anime_table ') + '.group_torrent'
)].map(elm => {
/** @type {NodeListOf<HTMLAnchorElement>} */
const links = elm.querySelectorAll('a[href*="&torrentid="]')
if (links.length === 0) return null
const a = links[links.length - 1]
return {
a,
torrentId: a.href.match(TORRENT_ID_REGEX)?.[1],
separator: a.href.includes('torrents.php') && links.length === 1 ? ' | ' : ' / '
}
}).filter((value) => value))
const searchResultTorrents = /** @type {Array<HTMLAnchorElement>} */([...document.querySelectorAll(
'.torrent_properties>a[href*="&torrentid="]'
)]).map(a => ({
a,
torrentId: a.href.match(TORRENT_ID_REGEX)?.[1],
separator: ' | '
}))
return [...torrentPageTorrents, ...searchResultTorrents]
}
function insertTorrentTab (torrentId, tabName, tabId, content) {
// Select
const select = $(`<li><a href="#${torrentId}/${tabId}">${tabName}</a></li>`)
const a = select.find('a')
a.click(() => switchTabs(a))
$(`#tabs_${torrentId}`).append(select)
// Tab it self
const container = $(`<div id="${torrentId}_${tabId}" style="display: none;"></div>`)
container.append(content)
$(`#tabs_${torrentId}`).parent().append(container)
// Load from url hash on page load
let e = window.location.hash
if (e) e = e.substring(1).split('/')
if (e[1] === tabId) switchTabs(a)
}
async function doAnimeBytes () {
try {
const torrents = torrentsOnPage()
// Do a 100 torrents at a time to make url length manageable
for (let i = 0; i < torrents.length; i += 100) {
const sliced = torrents.slice(i, i + 100)
const linkMap = await fetchSeadex(sliced)
for (const torrentLink of sliced) {
const entry = linkMap[torrentLink.torrentId]
if (!entry) continue
// Insert tag
torrentLink.a.append(torrentLink.separator)
let parent = torrentLink.a
if (torrentLink.a.classList.contains('userscript-highlight')) {
// highlight already ran
parent = document.createElement('span')
parent.className = 'userscript-highlight torrent-field'
parent.dataset.seadex = 'SeaDex'
parent.dataset.field = 'SeaDex'
torrentLink.a.append(parent)
}
const img = document.createElement('img')
img.src = entry.isBest
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAJCAYAAABXLP43AAAAAXNSR0IArs4c6QAAAMJJREFUOE9jZBgkgFE1TWwaAwNDJhb3TCdOnHE+K8u/vN9/GLug6qffnvUqC8lcMB+bf5HVgBzyP6skH0PdtJ6JDMSIg9QxMDDOZ2D4nwhSD+EzgD0B49+e9YoR3QKYI2Bq4A6BGoCiHslgnOIINaiOQXMUSD8o1FFCC1kN0SGCy6ewEMUmj2QxONTxqcGaRmCaqB0iWMyFpx+iQwQWN+hph9g0gp4mYKEFM5fiXPP/z//5bBwMROUafDkJIzUPVLECALBqyRj71YzpAAAAAElFTkSuQmCC'
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAJCAYAAABXLP43AAAAAXNSR0IArs4c6QAAALJJREFUOE/NlMEOgkAMRN961Z/kqAfOfoLxA+S4n+lJMd1QUmohTTRRbi2zs9NhSuFPntLBDTgGeoZM/wl1D/0drhN+qHAyvK2O5rUYETKeA9QFyPQFJ2J20AleaqANoXWF4q9QEYqZhUwEC7whXu0rxotxouS8uL5wy2LSjqxNqo5G783FzfUtTJgRPfRtRwLeOT9pR/Tb+OxkM+IzoW4p78db84B6SG7N1ia9pflXv5UXtZlmWNmuM34AAAAASUVORK5CYII='
img.title = entry.notes ? `SeaDex Notes:\n${entry.notes}` : ''
img.alt = 'SeaDex Choice!'
img.dataset.seadex = ''
img.onclick = e => {
e.preventDefault()
e.stopImmediatePropagation()
window.open(`https://releases.moe/${entry.alID}`, '_blank')?.focus()
}
parent.append(img)
// seadex tab
const tab = $('<div></div>')
tab.append(`<div style="margin-bottom: 4px;"><h2><a target="_blank" href="https://releases.moe/${entry.alID}">SeaDex Entry</a></h2></div>`)
tab.append(`<div style="border-top: 1px solid #bbb;"></div>`)
if (entry.notes) {
const span = $('<span style="white-space: pre-wrap;"></span>')
span.text(entry.notes)
const div = $('<div style="margin-top: 16px;"></div>')
div.append('<h2>Notes</h2>', span)
tab.append(div)
}
if (Array.isArray(entry.comparison) && entry.comparison?.length > 0) {
const div = $('<div style="margin-top: 16px;"></div>')
div.append('<h2>Comparisons</h2>')
for (const link of entry.comparison) {
div.append(`<a target="_blank" href="${link}">${link}</a>`, '<br>')
}
tab.append(div)
}
insertTorrentTab(torrentLink.torrentId, 'SeaDex', 'seadex', tab)
}
}
} catch (err) {
console.error(`Failed to fetch seadex for best releases - ${err?.message || err}`)
}
}
function revealABEntries () {
waitForKeyElements('a.pt-button[data-href]', (elm) => {
elm.href = new URL(elm.dataset.href, 'https://animebytes.tv')
elm.classList.remove('pointer-events-none')
elm.removeAttribute('data-href')
elm.childNodes[0].src = '/ab.ico'
if (elm.childNodes[2].textContent.includes('Private')) {
elm.childNodes[2].textContent = 'AnimeBytes'
}
return true
}, false, 50)
waitForKeyElements('button.pt-button', (elm) => {
elm.href = new URL(elm.dataset.href, 'https://animebytes.tv')
elm.childNodes[0].src = '/ab.ico'
elm.childNodes[2].textContent = 'AnimeBytes'
return true
}, false, 50)
}
if (window.location.href.includes('animebytes.tv')) {
doAnimeBytes()
} else if (window.location.href.includes('releases.moe')) {
revealABEntries()
}
// ==UserScript==
// @name AB search autocomplete
// @namespace https://github.com/po5
// @version 0.2.0
// @description :cool:
// @author Eva
// @homepage https://animebytes.tv/forums.php?action=viewthread&threadid=24689
// @icon https://animebytes.tv/favicon.ico
// @updateURL https://gist.github.com/po5/a56f53567eca72f2f32f2545e0236d39/raw/ab-search-autocomplete.user.js
// @downloadURL https://gist.github.com/po5/a56f53567eca72f2f32f2545e0236d39/raw/ab-search-autocomplete.user.js
// @grant none
// @match https://animebytes.tv/*
// @license GPL-3.0
// @run-at document-end
// ==/UserScript==
function debounce(func, timeout=200){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function clearResults(ccomplete) {
while (ccomplete.lastChild) {
ccomplete.removeChild(ccomplete.lastChild);
}
}
async function autocomplete(csearch, ccomplete, ctype) {
let search = csearch.value;
if (search == '') {
select = 0;
return clearResults(ccomplete);
}
csearch.style.background = color + ' url("data:image/gif;base64,R0lGODlhEAAQALMMAKqooJGOhp2bk7e1rZ2bkre1rJCPhqqon8PBudDOxXd1bISCef///wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAAMACwAAAAAEAAQAAAET5DJyYyhmAZ7sxQEs1nMsmACGJKmSaVEOLXnK1PuBADepCiMg/DQ+/2GRI8RKOxJfpTCIJNIYArS6aRajWYZCASDa41Ow+Fx2YMWOyfpTAQAIfkEBQAADAAsAAAAABAAEAAABE6QyckEoZgKe7MEQMUxhoEd6FFdQWlOqTq15SlT9VQM3rQsjMKO5/n9hANixgjc9SQ/CgKRUSgw0ynFapVmGYkEg3v1gsPibg8tfk7CnggAIfkEBQAADAAsAAAAABAAEAAABE2QycnOoZjaA/IsRWV1goCBoMiUJTW8A0XMBPZmM4Ug3hQEjN2uZygahDyP0RBMEpmTRCKzWGCkUkq1SsFOFQrG1tr9gsPc3jnco4A9EQAh+QQFAAAMACwAAAAAEAAQAAAETpDJyUqhmFqbJ0LMIA7McWDfF5LmAVApOLUvLFMmlSTdJAiM3a73+wl5HYKSEET2lBSFIhMIYKRSimFriGIZiwWD2/WCw+Jt7xxeU9qZCAAh+QQFAAAMACwAAAAAEAAQAAAETZDJyRCimFqbZ0rVxgwF9n3hSJbeSQ2rCWIkpSjddBzMfee7nQ/XCfJ+OQYAQFksMgQBxumkEKLSCfVpMDCugqyW2w18xZmuwZycdDsRACH5BAUAAAwALAAAAAAQABAAAARNkMnJUqKYWpunUtXGIAj2feFIlt5JrWybkdSydNNQMLaND7pC79YBFnY+HENHMRgyhwPGaQhQotGm00oQMLBSLYPQ9QIASrLAq5x0OxEAIfkEBQAADAAsAAAAABAAEAAABE2QycmUopham+da1cYkCfZ94UiW3kmtbJuRlGF0E4Iwto3rut6tA9wFAjiJjkIgZAYDTLNJgUIpgqyAcTgwCuACJssAdL3gpLmbpLAzEQA7") right 10px center no-repeat';
last = window.performance.now();
let data = [];
let currcache = 0;
if (cached[ctype + search]) {
currcache = last;
data = cached[ctype + search];
} else {
let response = await fetch('/xhr/ac/search/' + ctype + '?q=' + encodeURIComponent(search) + '&cache=' + last);
currcache = +response.url.split('&cache=')[1];
data = await response.json();
}
if (currcache >= cache || cached[ctype + search]) {
clearResults(ccomplete);
select = 0;
if (currcache === last) csearch.style.background = null;
cached[ctype + search] = data;
if (data.results) {
cache = currcache;
data.results.slice(0, 10).forEach(anime => {
const complete = document.createElement('li');
complete.style = 'display: block !important;border-bottom:1px solid rgba(78, 78, 78, 0.31);padding:4px 3px;white-space: nowrap';
const link = document.createElement('a');
const text = document.createElement('textarea');
text.innerHTML = anime.name;
let title = text.value;
link.href = (ctype == 'anime' ? '/torrents.php?id=' : '/torrents2.php?id=') + anime.id;
if (title.length > 80) {
link.title = title;
title = title.substring(0, 80).trim() + '…';
}
link.appendChild(document.createTextNode(title + (anime.year == '0' ? '' : ' [' + anime.year + ']') + ' - ' + anime.type));
complete.appendChild(link);
ccomplete.appendChild(complete);
});
}
if (Object.keys(cached).length > 200) cached = {}
}
}
function arrowNav(event) {
let dir = 0;
if (event.key == 'ArrowUp') dir = -1;
if (event.key == 'ArrowDown') dir = 1;
const selected = event.target.nextSibling.querySelector(':nth-child(' + (select + dir) + ')');
if (event.key == 'Enter') {
window.location.href = selected.firstChild.href;
event.preventDefault();
}
Array.from(event.target.nextSibling.children).forEach(thing => {
thing.style.outline = 'none';
});
if (selected) {
select = select + dir;
selected.style.outline = '1px dotted grey';
} else if (event.target.nextSibling.firstChild) {
select = 1;
event.target.nextSibling.firstChild.style.outline = '1px dotted grey';
}
}
let search = document.querySelector('form[action$="/series.php"] > .series_search, form[action$="/torrents.php"] > .series_search');
let nsearch = document.querySelector("#series_name_anime");
let search2 = document.querySelector('form[action$="/torrents2.php"] > .series_search');
let nsearch2 = document.querySelector('.inputtext[name="groupname"]');
let last = 0;
let cache = 0;
let cached = {};
let select = 0;
if (search) {
var color = window.getComputedStyle(search).getPropertyValue('background-color');
search.parentElement.style.position = 'relative';
search.autocomplete = 'off';
search2.parentElement.style.position = 'relative';
search2.autocomplete = 'off';
if (nsearch) {
nsearch.parentElement.style.position = 'relative';
nsearch.autocomplete = 'off';
const ntorrentscomplete = document.createElement('ul');
ntorrentscomplete.id = 'ntorrentscomplete';
ntorrentscomplete.className = 'torrentscomplete';
ntorrentscomplete.style = 'position:absolute;background:' + color + ';color:#9a9a9a;overflow:hidden;width:auto;max-width:888px;z-index:1000;min-width:unset;left:0;top:' + (search.clientHeight - 1) + 'px;padding:0;text-align:left;font-size:0.723rem';
const animedb2 = debounce(() => autocomplete(nsearch, ntorrentscomplete, 'anime'));
nsearch.parentNode.insertBefore(ntorrentscomplete, nsearch.nextSibling);
nsearch.addEventListener('input', () => {
nsearch.style.background = color + ' url("data:image/gif;base64,R0lGODlhEAAQALMMAKqooJGOhp2bk7e1rZ2bkre1rJCPhqqon8PBudDOxXd1bISCef///wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAAMACwAAAAAEAAQAAAET5DJyYyhmAZ7sxQEs1nMsmACGJKmSaVEOLXnK1PuBADepCiMg/DQ+/2GRI8RKOxJfpTCIJNIYArS6aRajWYZCASDa41Ow+Fx2YMWOyfpTAQAIfkEBQAADAAsAAAAABAAEAAABE6QyckEoZgKe7MEQMUxhoEd6FFdQWlOqTq15SlT9VQM3rQsjMKO5/n9hANixgjc9SQ/CgKRUSgw0ynFapVmGYkEg3v1gsPibg8tfk7CnggAIfkEBQAADAAsAAAAABAAEAAABE2QycnOoZjaA/IsRWV1goCBoMiUJTW8A0XMBPZmM4Ug3hQEjN2uZygahDyP0RBMEpmTRCKzWGCkUkq1SsFOFQrG1tr9gsPc3jnco4A9EQAh+QQFAAAMACwAAAAAEAAQAAAETpDJyUqhmFqbJ0LMIA7McWDfF5LmAVApOLUvLFMmlSTdJAiM3a73+wl5HYKSEET2lBSFIhMIYKRSimFriGIZiwWD2/WCw+Jt7xxeU9qZCAAh+QQFAAAMACwAAAAAEAAQAAAETZDJyRCimFqbZ0rVxgwF9n3hSJbeSQ2rCWIkpSjddBzMfee7nQ/XCfJ+OQYAQFksMgQBxumkEKLSCfVpMDCugqyW2w18xZmuwZycdDsRACH5BAUAAAwALAAAAAAQABAAAARNkMnJUqKYWpunUtXGIAj2feFIlt5JrWybkdSydNNQMLaND7pC79YBFnY+HENHMRgyhwPGaQhQotGm00oQMLBSLYPQ9QIASrLAq5x0OxEAIfkEBQAADAAsAAAAABAAEAAABE2QycmUopham+da1cYkCfZ94UiW3kmtbJuRlGF0E4Iwto3rut6tA9wFAjiJjkIgZAYDTLNJgUIpgqyAcTgwCuACJssAdL3gpLmbpLAzEQA7") right 10px center no-repeat';
animedb2()
});
}
if (nsearch2) {
nsearch2.parentElement.style.position = 'relative';
nsearch2.autocomplete = 'off';
const ntorrentscomplete2 = document.createElement('ul');
ntorrentscomplete2.id = 'ntorrentscomplete2';
ntorrentscomplete2.className = 'torrentscomplete';
ntorrentscomplete2.style = 'position:absolute;background:' + color + ';color:#9a9a9a;overflow:hidden;width:auto;max-width:888px;z-index:1000;min-width:unset;left:10px;top:' + (search.clientHeight - 1) + 'px;padding:0;text-align:left;font-size:0.723rem';
const musicdb2 = debounce(() => autocomplete(nsearch2, ntorrentscomplete2, 'music'));
nsearch2.parentNode.insertBefore(ntorrentscomplete2, nsearch2.nextSibling);
nsearch2.addEventListener('input', () => {
nsearch2.style.background = color + ' url("data:image/gif;base64,R0lGODlhEAAQALMMAKqooJGOhp2bk7e1rZ2bkre1rJCPhqqon8PBudDOxXd1bISCef///wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAAMACwAAAAAEAAQAAAET5DJyYyhmAZ7sxQEs1nMsmACGJKmSaVEOLXnK1PuBADepCiMg/DQ+/2GRI8RKOxJfpTCIJNIYArS6aRajWYZCASDa41Ow+Fx2YMWOyfpTAQAIfkEBQAADAAsAAAAABAAEAAABE6QyckEoZgKe7MEQMUxhoEd6FFdQWlOqTq15SlT9VQM3rQsjMKO5/n9hANixgjc9SQ/CgKRUSgw0ynFapVmGYkEg3v1gsPibg8tfk7CnggAIfkEBQAADAAsAAAAABAAEAAABE2QycnOoZjaA/IsRWV1goCBoMiUJTW8A0XMBPZmM4Ug3hQEjN2uZygahDyP0RBMEpmTRCKzWGCkUkq1SsFOFQrG1tr9gsPc3jnco4A9EQAh+QQFAAAMACwAAAAAEAAQAAAETpDJyUqhmFqbJ0LMIA7McWDfF5LmAVApOLUvLFMmlSTdJAiM3a73+wl5HYKSEET2lBSFIhMIYKRSimFriGIZiwWD2/WCw+Jt7xxeU9qZCAAh+QQFAAAMACwAAAAAEAAQAAAETZDJyRCimFqbZ0rVxgwF9n3hSJbeSQ2rCWIkpSjddBzMfee7nQ/XCfJ+OQYAQFksMgQBxumkEKLSCfVpMDCugqyW2w18xZmuwZycdDsRACH5BAUAAAwALAAAAAAQABAAAARNkMnJUqKYWpunUtXGIAj2feFIlt5JrWybkdSydNNQMLaND7pC79YBFnY+HENHMRgyhwPGaQhQotGm00oQMLBSLYPQ9QIASrLAq5x0OxEAIfkEBQAADAAsAAAAABAAEAAABE2QycmUopham+da1cYkCfZ94UiW3kmtbJuRlGF0E4Iwto3rut6tA9wFAjiJjkIgZAYDTLNJgUIpgqyAcTgwCuACJssAdL3gpLmbpLAzEQA7") right 10px center no-repeat';
musicdb2()
});
}
const torrentscomplete = document.createElement('ul');
torrentscomplete.id = 'torrentscomplete';
torrentscomplete.className = 'torrentscomplete';
torrentscomplete.style = 'position:absolute;background:' + color + ';color:#9a9a9a;overflow:hidden;width:auto;max-width:888px;z-index:1000;min-width:unset;left:0;top:' + (search.clientHeight - 1) + 'px;padding:0;text-align:left;font-size:0.723rem';
const animedb = debounce(() => autocomplete(search, torrentscomplete, 'anime'));
search.parentNode.insertBefore(torrentscomplete, search.nextSibling);
const torrentscompletedb = () => {
search.style.background = color + ' url("data:image/gif;base64,R0lGODlhEAAQALMMAKqooJGOhp2bk7e1rZ2bkre1rJCPhqqon8PBudDOxXd1bISCef///wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAAMACwAAAAAEAAQAAAET5DJyYyhmAZ7sxQEs1nMsmACGJKmSaVEOLXnK1PuBADepCiMg/DQ+/2GRI8RKOxJfpTCIJNIYArS6aRajWYZCASDa41Ow+Fx2YMWOyfpTAQAIfkEBQAADAAsAAAAABAAEAAABE6QyckEoZgKe7MEQMUxhoEd6FFdQWlOqTq15SlT9VQM3rQsjMKO5/n9hANixgjc9SQ/CgKRUSgw0ynFapVmGYkEg3v1gsPibg8tfk7CnggAIfkEBQAADAAsAAAAABAAEAAABE2QycnOoZjaA/IsRWV1goCBoMiUJTW8A0XMBPZmM4Ug3hQEjN2uZygahDyP0RBMEpmTRCKzWGCkUkq1SsFOFQrG1tr9gsPc3jnco4A9EQAh+QQFAAAMACwAAAAAEAAQAAAETpDJyUqhmFqbJ0LMIA7McWDfF5LmAVApOLUvLFMmlSTdJAiM3a73+wl5HYKSEET2lBSFIhMIYKRSimFriGIZiwWD2/WCw+Jt7xxeU9qZCAAh+QQFAAAMACwAAAAAEAAQAAAETZDJyRCimFqbZ0rVxgwF9n3hSJbeSQ2rCWIkpSjddBzMfee7nQ/XCfJ+OQYAQFksMgQBxumkEKLSCfVpMDCugqyW2w18xZmuwZycdDsRACH5BAUAAAwALAAAAAAQABAAAARNkMnJUqKYWpunUtXGIAj2feFIlt5JrWybkdSydNNQMLaND7pC79YBFnY+HENHMRgyhwPGaQhQotGm00oQMLBSLYPQ9QIASrLAq5x0OxEAIfkEBQAADAAsAAAAABAAEAAABE2QycmUopham+da1cYkCfZ94UiW3kmtbJuRlGF0E4Iwto3rut6tA9wFAjiJjkIgZAYDTLNJgUIpgqyAcTgwCuACJssAdL3gpLmbpLAzEQA7") right 10px center no-repeat';
animedb();
}
search.addEventListener('input', torrentscompletedb);
const torrentscomplete2 = document.createElement('ul');
torrentscomplete2.id = 'torrentscomplete2';
torrentscomplete2.className = 'torrentscomplete';
torrentscomplete2.style = 'position:absolute;background:' + color + ';color:#9a9a9a;overflow:hidden;width:auto;max-width:888px;z-index:1000;min-width:unset;left:0;top:' + (search.clientHeight - 1) + 'px;padding:0;text-align:left;font-size:0.723rem';
const musicdb = debounce(() => autocomplete(search2, torrentscomplete2, 'music'));
search2.parentNode.insertBefore(torrentscomplete2, search2.nextSibling);
const torrentscomplete2db = () => {
search2.style.background = color + ' url("data:image/gif;base64,R0lGODlhEAAQALMMAKqooJGOhp2bk7e1rZ2bkre1rJCPhqqon8PBudDOxXd1bISCef///wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAAMACwAAAAAEAAQAAAET5DJyYyhmAZ7sxQEs1nMsmACGJKmSaVEOLXnK1PuBADepCiMg/DQ+/2GRI8RKOxJfpTCIJNIYArS6aRajWYZCASDa41Ow+Fx2YMWOyfpTAQAIfkEBQAADAAsAAAAABAAEAAABE6QyckEoZgKe7MEQMUxhoEd6FFdQWlOqTq15SlT9VQM3rQsjMKO5/n9hANixgjc9SQ/CgKRUSgw0ynFapVmGYkEg3v1gsPibg8tfk7CnggAIfkEBQAADAAsAAAAABAAEAAABE2QycnOoZjaA/IsRWV1goCBoMiUJTW8A0XMBPZmM4Ug3hQEjN2uZygahDyP0RBMEpmTRCKzWGCkUkq1SsFOFQrG1tr9gsPc3jnco4A9EQAh+QQFAAAMACwAAAAAEAAQAAAETpDJyUqhmFqbJ0LMIA7McWDfF5LmAVApOLUvLFMmlSTdJAiM3a73+wl5HYKSEET2lBSFIhMIYKRSimFriGIZiwWD2/WCw+Jt7xxeU9qZCAAh+QQFAAAMACwAAAAAEAAQAAAETZDJyRCimFqbZ0rVxgwF9n3hSJbeSQ2rCWIkpSjddBzMfee7nQ/XCfJ+OQYAQFksMgQBxumkEKLSCfVpMDCugqyW2w18xZmuwZycdDsRACH5BAUAAAwALAAAAAAQABAAAARNkMnJUqKYWpunUtXGIAj2feFIlt5JrWybkdSydNNQMLaND7pC79YBFnY+HENHMRgyhwPGaQhQotGm00oQMLBSLYPQ9QIASrLAq5x0OxEAIfkEBQAADAAsAAAAABAAEAAABE2QycmUopham+da1cYkCfZ94UiW3kmtbJuRlGF0E4Iwto3rut6tA9wFAjiJjkIgZAYDTLNJgUIpgqyAcTgwCuACJssAdL3gpLmbpLAzEQA7") right 10px center no-repeat';
musicdb();
}
search2.addEventListener('input', torrentscomplete2db);
document.querySelectorAll('form[action$="/series.php"] > .series_search, form[action$="/torrents.php"] > .series_search, form[action$="/torrents2.php"] > .series_search, #series_name_anime, .inputtext[name="groupname"]').forEach(input => {
input.addEventListener('keydown', arrowNav);
input.addEventListener('focus', (e) => {
select = 0;
Array.from(e.target.nextSibling.children).forEach(thing => {
thing.style.outline = 'none';
});
})
});
const style = document.createElement("style");
style.innerHTML = '.torrentscomplete {display:none} .torrentscomplete > li {outline-offset:-1px} .torrentscomplete > li:last-child {border-bottom:none !important} form[action$="/series.php"]:focus-within > #torrentscomplete, form[action$="/torrents.php"]:focus-within > #torrentscomplete, form[action$="/torrents2.php"]:focus-within > #torrentscomplete2, #series_names_anime:focus-within > #ntorrentscomplete, #ui-id-2 dd[style]:focus-within > #ntorrentscomplete2 {display:block} .torrentscomplete a {display:block;width:100%}';
document.head.appendChild(style);
}
// ==UserScript==
// @name AB Anilist Links
// @version 1.1
// @description Anilist AB linker
// @author Chosensilver
// @icon https://animebytes.tv/favicon.ico
// @grant none
// @match https://animebytes.tv/torrents.php*
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
function insertAfter(newNode, existingNode) {
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
let header = document.querySelectorAll('h3 a');
header = header[header.length - 1];
let seriesName = document.querySelectorAll('h2 a')[0].innerText;
function createnewLink(linku,link, name, blank){
linku = document.createElement("a");
linku.innerHTML = " | " + name;
linku.href = link + seriesName;
linku.style.fontWeight = "900";
linku.style.fontSize = "15px";
//anilistLink.style.color = "#7FFFD4"
if(blank){
$(linku).attr('target', '_blank');
}
$(linku).insertAfter(header);
};
createnewLink("anilistLink","https://anilist.co/search/anime?search=","Anilist",false);
// ==UserScript==
// @name AB - Tree Style Filelist
// @version 1.1
// @author neesrod
// @description Changes filelist to be in tree style, heavily inspired by U2's filelist and based on WeebDataHoarder's "Filelist Improvements"
// @match https://animebytes.tv/torrents.php?*id=*
// @match https://animebytes.tv/torrents2.php?*id=*
// @exclude https://animebytes.tv/torrents*groupid=*
// @icon https://animebytes.tv/favicon.ico
// @grant none
// @run-at document-end
// ==/UserScript==
// locale, changes the number style, i.e.:
//
// "us" - 1,056.12
// "xh" - 1 056.12
// "rm" - 1'056.12
// "ee" - 1056.12
// "pt" - 1.056,12
// "fr" - 1 056,12
// "et" - 1056,12
//
let locale = "xh" // I prefer the space on xh's
var id_count = 6001 // this is used for unique ids, page-wide
function size_in_bytes(text) {
// obviously rounded, turns "1,056.12 MiB" into 1107296256
// split "1,056.12 MiB" into ["1,056.12", "MiB"], remove comma, parse as float, and multiply with unit conversion
var size = parseFloat(text.split(" ")[0].replace(",", ""))
var unit = text.split(" ")[1]
switch (unit) {
case "B":
return size
case "KiB":
return size * 1024
case "MiB":
return size * 1024 ** 2
case "GiB":
return size * 1024 ** 3
case "TiB":
return size * 1024 ** 4
}
return -1
}
function bytes_to_text(size) {
// turns 1107296256 into "1 056.12 MiB"
var value = 0
// due to reasons, multiply by 100 to save the first two decimals, then divide by 100 to put them back
switch (true) {
case (size >= 1024 ** 4):
value = Math.round((size * 100) / (1024 ** 4)) / 100
return value.toLocaleString(locale) + " TiB"
case (size >= 1024 ** 3):
value = Math.round((size * 100) / (1024 ** 3)) / 100
return value.toLocaleString(locale) + " GiB"
case (size >= 1024 ** 2):
value = Math.round((size * 100) / (1024 ** 2)) / 100
return value.toLocaleString(locale) + " MiB"
case (size >= 1024):
value = Math.round((size * 100) / 1024) / 100
return value.toLocaleString(locale) + " KiB"
case (size < 1024):
return size.toLocaleString(locale) + " B"
}
return -1
}
function toggle_expand(start_id, end_id) {
var toggle = document.getElementById("filetree_toggle_" + start_id)
for (var i = start_id; i < end_id; i++) {
var folder = document.getElementById("filetree_" + i)
if (folder.getAttribute("filetree_status") === toggle.getAttribute("filetree_status")) {
folder.children[0].onclick()
}
}
if (toggle.getAttribute("filetree_status") === "closed") {
toggle.setAttribute("filetree_status", "opened")
toggle.textContent = "[-]"
} else {
toggle.setAttribute("filetree_status", "closed")
toggle.textContent = "[+]"
}
}
function toggle_folder(children, id) {
var row = document.getElementById("filetree_" + id)
// toggles parent's status
if (row.getAttribute("filetree_status") === "closed") {
row.setAttribute("filetree_status", "opened")
} else {
row.setAttribute("filetree_status", "closed")
}
// goes through the children and clicks the opened ones to toggle too, this is recursive
for (var i = 0; i < children.length; i++) {
var child_row = document.getElementById("filetree_" + children[i])
if (child_row.getAttribute("filetree_status") === "opened") {
child_row.children[0].onclick()
}
child_row.style.display = (child_row.style.display == "table-row" ? "none" : "table-row");
}
}
// obviously, to make a global function just inject it into a script element
let global_function = document.createElement('script')
global_function.innerHTML = toggle_folder.toString() + "\n" + toggle_expand.toString(); // I had no clue you could do this shit, JS is a hell of a drug
document.head.appendChild(global_function)
function create_folder_recursive(folder, folders, table) {
// this creates a row, fills the name and size, adds an onclick and tag, then recurses to create the children
var row = document.createElement("tr")
var name = document.createElement("td")
var size = document.createElement("td")
var data = folders[folder]
row.setAttribute("id", "filetree_" + data.id)
row.style.display = "none"
if (!(data.folders.length + data.files.length)) {
// no children, not a folder
name.innerHTML = "<code style='font-family: monospace; font-size: 1.2em'>" + data.name + "</code>"
size.innerHTML = "<span style='opacity: 100%'>" + bytes_to_text(data.size) + "</span>"
row.appendChild(name)
row.appendChild(size)
table.appendChild(row)
return;
} else {
// it's a folder
row.setAttribute("filetree_status", "closed")
name.setAttribute("onclick", "toggle_folder([" + data.folders + (data.folders.length ? "," : "") + data.files + "], " + data.id + ")")
name.innerHTML = "<code style='font-family: monospace; font-size: 1.2em; font-weight: bold'>" + data.name + "</code>"
size.innerHTML = "<span style='opacity: 60%'>[" + bytes_to_text(data.size) + "]</span>"
row.appendChild(name)
row.appendChild(size)
table.appendChild(row)
// loop through children folders, then the children files
data.folders.concat(data.files).forEach(child => {
create_folder_recursive(folders["."][child], folders, table)
})
}
}
// table nodes whose id start with "filelist", such as "filelist_990556"
document.querySelectorAll("table[id^='filelist']").forEach( (el) => {
var rows = Array.from(el.getElementsByTagName("tr"));
var folders = { ".": {} } // dot stores the inverse, instead of names to ids, it's ids to names
var top_level = { "folders" : [], "files" : [] } // first stores top folders, second stores top files, just for aesthetic sorting
// this adds the file count onto the title
var title = rows[0].children[0]
var num_files = document.createElement("span")
num_files.style.float = "right"
num_files.textContent = (rows.length - 1) + " File" + ((rows.length - 1) === 1 ? "" : "s")
title.appendChild(num_files)
// this adds an expand/collapse button, the JS is added after the rows loop
title = rows[0].children[1]
num_files = document.createElement("span")
num_files.style.float = "right"
num_files.textContent = "[+]"
num_files.style.fontSize = "1.2em"
num_files.style.fontFamily = "monospace"
num_files.setAttribute("filetree_status", "closed")
title.appendChild(num_files)
// exclude title row, parses the rows, creates the tree data, and removes the original
rows.slice(1).forEach(row => {
// first column, filename
var filename = row.children[0].textContent // i.e. "BDMV/STREAM/00005.m2ts"
// second column, size
var size_text = row.children[1].textContent // i.e. "1,056.12 MiB"
var pathlist = filename.split("/") // i.e. ["BDMV", "STREAM", "00005.m2ts"]
var level = pathlist.length - 1
var file = pathlist.slice(level).join("/") // file, i.e. 00005.m2ts
var fname = " ".repeat(level) + file // " "" is a space that won't get ignored, this makes " 00005.m2ts"
// for loop for each level below to add the file size
// if folder doesn't exist, create it and add id to parent
// else simply add the size
var parent = 0 // used to track the parent of the folder
for (let p = 1; p < pathlist.length; p++) {
// this will loop through all levels below, i.e. "BDMV" then "BDMV/STREAM" then "BDMV/STREAM/..."
var folder = pathlist.slice(0, p).join("/")
var name = " ".repeat(p - 1) + pathlist.slice(p-1, p).join("/")
if (!(folder in folders)) {
// doesn't exist yet
id_count += 1
// separation between children files and folders is to properly sort them, just more visually appealing
folders[folder] = { "id": id_count, "size": size_in_bytes(size_text), "name": name, "folders": [], "files": [] }
folders["."][id_count] = folder
// parent doesn't exist? then it's a root folder
if (parent) {
folders[parent].folders.push(id_count)
} else {
top_level.folders.push(id_count)
}
} else {
folders[folder].size += size_in_bytes(size_text)
}
parent = folder
}
// create current file, same as a folder
id_count += 1
folders[filename] = { "id": id_count, "size": size_in_bytes(size_text), "name": fname, "folders": [], "files": [] }
folders["."][id_count] = filename
// parent doesn't exist? then it's a root file
if (parent) {
folders[parent].files.push(id_count)
} else {
top_level.files.push(id_count)
}
row.remove()
});
// adds expand toggle's logic, or removes the toggle if there aren't folders
if (top_level.folders.length) {
num_files.setAttribute("id", "filetree_toggle_" + top_level.folders[0])
num_files.setAttribute("onclick", "toggle_expand(" + top_level.folders[0] + "," + id_count + ")")
} else {
num_files.remove()
}
// create the top folders, all others are built recursively, then the top files
top_level.folders.concat(top_level.files).forEach(folder_id => {
create_folder_recursive(folders["."][folder_id], folders, rows[0].parentElement)
document.getElementById("filetree_" + folder_id).style.display = "table-row"
})
});
})();
// ==UserScript==
// @name eva's v2 highlighter script
// @namespace https://animebytes.tv/
// @version 2.0
// @description none
// @match https://animebytes.tv/
// @grant none
// ==/UserScript==
const config = [{
name: "AB highlight - Printed Media",
site: ["animebytes.tv"],
siteregex: false,
selector: "a[href*='torrents.php?id='][href*='&torrentid=']:not([href*='#']):not([title])",
regex: {
translation: /(^»?\s*|\] \[)(Raw|Translated)()/,
group: /(<span data-translation=".*?">.*?<\/span> \()(.*?)(\))/,
digital: /( \| )(Digital)()/,
ongoing: /( \| )(Ongoing)()/,
fileType: /(<span data-translation=".*?">.*?<\/span> (?:\s+\(<span data-group=".*?">.*?<\/span>\) )?\| )([^<]+?(?: Scans)?)( |\]|$)/,
hentai: /()(<img src="\/static\/common\/hentaic?.png" alt="Hentai" title="This torrent is of (?:un)?censored hentai \(18\+\) material!">)()/,
},
parent: true,
authors: ["eva"],
version: "0.1.0"
},{
name: "AB highlight - Games",
site: ["animebytes.tv"],
siteregex: false,
selector: "a[href*='torrents.php?id='][href*='&torrentid=']:not([href*='#']):not([title])",
regex: {
type: /(^»?\s*|\] \[)(Game|Patch|DLC)()/,
platform: /(<span data-type=".*?">.*?<\/span> \| )(.*?)( \| )/,
archived: /( \| )(Archived|Unarchived)()/,
region: /(<span data-platform=".*?">.*?<\/span> \| )(.*?)( \| <span data-archived=".*?">.*?<\/span>)/,
scene: /( \| )(Scene)()/,
},
parent: true,
authors: ["eva"],
version: "0.1.0"
},{
name: "AB highlight - Anime",
site: ["animebytes.tv"],
siteregex: false,
selector: "a[href*='torrents.php?id='][href*='&torrentid=']:not([href*='#']):not([title])",
regex: {
series: /^()(.*)( - )/,
category: /(.* - )(.*?)( \[)/,
year: /(\[)(\d+)(\])/,
source: /(^»?\s*|\] \[)([^ ]+)( \| )/,
container: /(<span data-source=".*?">.*?<\/span> \| )(.*?)( \| )/,
region: /(<span data-container=".*?">.*?<\/span> \()(.*?)(\))/,
aspectRatio: /(<span data-region=".*?">.*?<\/span>\) \| )(.*?)( )/,
subbing: /( \| )(RAW|Hardsubs|Softsubs)()/,
freeleech: /()(<img src="\/static\/common\/flicon.png" alt="Freeleech!" title="This torrent is freeleech. Remember to seed!">)()/,
remaster: /()(<img src="\/static\/common\/rmstr.png" alt="Remastered" title="This torrent is from a remastered source!">)()/,
dualAudio: /()(Dual Audio)(.* \| <span data-subbing=".*?">.*?<\/span>)/,
audioChannels: /( )([^ ]+?)( \| <span data-(?:subbing|remaster|dual-audio)=".*?">.*?<\/span>)/,
audioCodec: /(\| )([^|]+?)( <span data-audio-channels=".*?">.*?<\/span>)/,
resolution: /( )([^ ]+?)( \| <span data-audio-codec=".*?">.*?<\/span>)/,
codec: /(<span data-container=".*?">.*?<\/span> \| )([^<]+?(?: 10-bit)?)( )/,
group: /(<span data-subbing=".*?">.*?<\/span> \()(.*?)(\))/,
episode: /(<span data-group=".*?">.*?<\/span>\) \| Episode )(.*?)( )/,
exclusive: /()(<font color="#990000">Exclusive!<\/font>)()/,
snatched: /( - )(Snatched)($)/
},
parent: true,
authors: ["eva"],
version: "0.1.0"
},{
name: "AB highlight - Music",
site: ["animebytes.tv"],
siteregex: /^https?:\/\/animebytes\.tv\//,
selector: "a[href*='torrents2.php?id='][href*='&torrentid=']:not([href*='#']):not([title])",
regex: {
artist: /^()(.*?)( - )/,
title: /( - )(.*)( \[)/,
year: /(\[)(\d+)(\])/,
encoding: /(^»?\s*|\] \[)(FLAC|MP3|AAC)( )/,
bitrate: /(<span data-encoding=".*?">.*?<\/span> . )([^\|\/]+?)( . )/,
media: /(<span data-bitrate=".*?">.*?<\/span> . )([^\|\/]+?)( . |$|\]$)/,
log: /(\|| \/ )(Log)(\]?)/,
cue: /(\|| \/ )(Cue)(\]?)/,
source: /(^»?\s*|\] \[)([^<][^\|\/]+)( . )/,
container: /(<span data-source=".*?">.*?<\/span> . )(.*?)( . )/,
region: /(<span data-container=".*?">.*?<\/span> \()(.*?)(\))/,
aspectRatio: /(<span data-region=".*?">.*?<\/span>\) . )(.*?)( )/,
subbing: /( . )(RAW|Hardsubs|Softsubs)()/,
freeleech: /()(<img src="\/static\/common\/flicon.png" alt="Freeleech!" title="This torrent is freeleech. Remember to seed!">)()/,
remaster: /()(<img src="\/static\/common\/rmstr.png" alt="Remastered" title="This torrent is from a remastered source!">)()/,
dualAudio: /()(Dual Audio)(.* . <span data-subbing=".*?">.*?<\/span>)/,
audioChannels: /( )([^ ]+?)( . <span data-(?:subbing|remaster|dual-audio)=".*?">.*?<\/span>)/,
audioCodec: /(. )([^\|\/]+?)( <span data-audio-channels=".*?">.*?<\/span>)/,
resolution: /( )([^ ]+?)( . <span data-audio-codec=".*?">.*?<\/span>)/,
codec: /(<span data-container=".*?">.*?<\/span> . )([^<]+?(?: 10-bit)?)( )/,
exclusive: /()(<font color="#990000">Exclusive!<\/font>)()/,
snatched: /( - )(Snatched)($)/
},
parent: true,
authors: ["eva"],
version: "0.1.0"
},{
name: "AR highlight",
site: ["alpharatio.cc"],
siteregex: false,
selector: "a[href*='torrents.php?id'][href*='&torrentid=']",
regex: {
group: /(.*)-(.*)()/
},
parent: true,
authors: ["eva"],
version: "0.1.0"
}];
config.forEach(function(filter) {
if(filter.site.indexOf(window.location.hostname) != -1) {
if(filter.siteregex == false || window.location.href.match(filter.siteregex)) {
const links = document.querySelectorAll(filter.selector);
links.forEach(function(link) {
link.classList.add("userscript-highlight");
for(const name in filter.regex) {
const matches = link.innerHTML.match(filter.regex[name]);
if(matches) {
if(filter.parent) {
link.dataset[name] = matches[2];
}
link.innerHTML = link.innerHTML.replace(filter.regex[name], '$1<span data-'+name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`)+'=\''+matches[2]+'\'>$2</span>$3');
}
}
});
}
}
});
// ==UserScript==
// @name Easteregg
// @namespace https://animebytes.tv/
// @version 2.4
// @description Girl walks across bottom of homepage at an intervall
// @match https://animebytes.tv/
// @grant none
// ==/UserScript==
(function() {
'use strict';
if (window.location.pathname !== '/') return;
let wrapper = document.getElementById('wrapper-for-walking-girl');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.id = 'wrapper-for-walking-girl';
while (document.body.firstChild) {
wrapper.appendChild(document.body.firstChild);
}
wrapper.style.position = 'relative';
wrapper.style.overflowX = 'hidden';
wrapper.style.width = '100vw';
document.body.appendChild(wrapper);
}
const container = document.createElement('div');
container.style.position = 'relative';
container.style.height = '0px';
container.style.width = '100%';
container.style.overflow = 'visible';
const girl = document.createElement('img');
girl.src = 'https://mei.kuudere.pw/qslYQMkwWdO.gif';
girl.alt = 'Walking Girl';
girl.style.position = 'absolute';
girl.style.bottom = '0px';
girl.style.right = '-120px';
girl.style.width = '100px';
girl.style.height = 'auto';
girl.style.zIndex = '10000';
girl.style.pointerEvents = 'none';
container.appendChild(girl);
wrapper.appendChild(container);
const walkDuration = 20000; // 20 seconds walk (half speed)
const intervalDuration = 60000; // 30 seconds interval
const style = document.createElement('style');
style.textContent = `
@keyframes walk-across-once {
from { right: -120px; }
to { right: 100%; }
}
`;
document.head.appendChild(style);
function walkOnce() {
girl.style.animation = 'none';
girl.style.right = '-120px';
void girl.offsetWidth;
girl.style.animation = `walk-across-once ${walkDuration}ms linear forwards`;
}
walkOnce();
setInterval(walkOnce, intervalDuration);
})();
// ==UserScript==
// @name Highlight Non-Zero Seeders
// @namespace animebytes
// @version 1.0
// @description Highlight non-zero seeders in red
// @match *://animebytes.tv/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
// 1. Inject CSS
const style = document.createElement('style');
style.textContent = `
tr.group_torrent td.non-zero-cell {
color: #b82625 !important;
}
`;
document.head.appendChild(style);
// 2. Wait for the DOM and check cells
function highlightNonZeroCells() {
document.querySelectorAll('tr.group_torrent').forEach(row => {
const cell = row.querySelector('td:nth-child(5)');
if (cell && cell.textContent.trim() !== '0') {
cell.classList.add('non-zero-cell');
}
});
}
// Run immediately if DOM is ready, or wait for load
if (document.readyState === 'complete' || document.readyState === 'interactive') {
highlightNonZeroCells();
} else {
document.addEventListener('DOMContentLoaded', highlightNonZeroCells);
}
})();
// ==UserScript==
// @name Smooth Glow with Hover Fade & Sync Fix
// @match https://animebytes.tv/user*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const selectors = [
'td.center:nth-child(1) > img:nth-child(1)',
'td.center:nth-child(2) > img:nth-child(1)',
'td.center:nth-child(3) > img:nth-child(1)',
'td.center:nth-child(4) > img:nth-child(1)'
];
function waitForElements(selectors, callback) {
let elements = [];
for (const selector of selectors) {
const el = document.querySelector(selector);
if (!el) {
setTimeout(() => waitForElements(selectors, callback), 300);
return;
} else {
elements.push(el);
}
}
callback(elements);
}
waitForElements(selectors, (imgs) => {
const keyframes = [
{ filter: 'drop-shadow(0 0 12px rgba(255, 163, 0, 0.8))', offset: 0 },
{ filter: 'drop-shadow(0 0 18px rgba(255, 163, 0, 0.9))', offset: 0.35 },
{ filter: 'drop-shadow(0 0 24px rgba(255, 163, 0, 1))', offset: 0.5 },
{ filter: 'drop-shadow(0 0 24px rgba(255, 163, 0, 1))', offset: 0.6 },
{ filter: 'drop-shadow(0 0 18px rgba(255, 163, 0, 0.9))', offset: 0.75 },
{ filter: 'drop-shadow(0 0 12px rgba(255, 163, 0, 0.8))', offset: 1 }
];
const options = {
duration: 1500,
iterations: Infinity,
easing: 'ease-in-out'
};
const animations = imgs.map(img => img.animate(keyframes, options));
animations.forEach(anim => anim.playbackRate = 1);
// Sync all animations' currentTime to a value
function syncAllCurrentTime(time) {
animations.forEach(anim => {
anim.currentTime = time % anim.effect.getTiming().duration;
});
}
function fadeOthers(hoveredIndex) {
imgs.forEach((img, i) => {
if (i !== hoveredIndex) {
animations[i].pause();
img.style.transition = 'opacity 0.3s ease';
img.style.opacity = '0.4';
}
});
}
function unfadeOthers() {
// Calculate synced currentTime as average of all paused animations + hovered animation
// But hovered animation is playing faster so just use hovered animation's time mod duration for sync
// Find hovered animation (the one running faster)
const hoveredAnim = animations.find(anim => anim.playbackRate > 1) || animations[0];
const syncTime = hoveredAnim.currentTime % hoveredAnim.effect.getTiming().duration;
// Resume all animations at synced time and normal speed
animations.forEach(anim => {
anim.currentTime = syncTime;
anim.playbackRate = 1;
anim.play();
});
imgs.forEach(img => {
img.style.transition = 'opacity 0.3s ease';
img.style.opacity = '1';
});
}
imgs.forEach((img, idx) => {
const anim = animations[idx];
img.addEventListener('mouseenter', () => {
anim.playbackRate = 2.5;
fadeOthers(idx);
});
img.addEventListener('mouseleave', () => {
anim.playbackRate = 1;
unfadeOthers();
});
});
});
})();