AnimeBytes Nightly

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     = "&nbsp;&nbsp;&nbsp;".repeat(level) + file // "&nbsp;"" 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   = "&nbsp;&nbsp;&nbsp;".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: /(.* - )(.*?)(&nbsp;&nbsp;\[)/,
    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: /( - )(.*)(&nbsp;&nbsp;\[)/,
    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();
      });
    });
  });
})();