您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mitsuki's AnimeBytes Collection
// ==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(); }); }); }); })();