Shiki OtherAnime

Adds other external anime online player to the shikimori

// ==UserScript==
// @name            Shiki OtherAnime
// @name:ru         Shiki OtherAnime
// @namespace       shikiOtherAnime
// @version         0.2.2
// @description     Adds other external anime online player to the shikimori
// @description:ru  Добавляет возможность смотреть аниме в стороннем плеере в любой озвучке прямо на сайте shikimori
// @author          Blank
// @match           *://shikimori.tld/*
// @match           *://shikimori.one/*
// @match           *://shikimori.me/*
// @run-at          document-end
// @grant           GM.xmlHttpRequest
// @noframes
// ==/UserScript==

// Notes:
// - fully compatible with shikiAnilibria script
// - autopause does not require videoShortcuts script (player handles postMessage itself)
// - videoShortcuts script is recommended for watching boring moments at 1.25 - 16x speed ^_^

(function main() {
  'use strict';

  const log = (...args) => console.log(`${GM.info.script.name}:`, ...args);
  log('start');

  if (!document.querySelector('#watch-online-style')) {
    const style = document.createElement('style');
    style.id = 'watch-online-style';
    style.textContent = `
.watch-online-iframe {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80vw;
  height: 45vw;
  z-index: 9001;
}
.watch-online-line {
  padding-bottom: 5px;
}
#watch-online-overlay {
  display: none;
  position: fixed;
  width: 100%;
  height: 100%;
  z-index: 9000;
  background: rgba(0, 0, 0, 0.85);
}`;
    document.querySelector('head').append(style);
  }

  // xmlHttpRequest
  const request = async (details) => new Promise((resolve, reject) => {
    GM.xmlHttpRequest({
      method: 'GET',
      responseType: 'json',
      anonymous: true,
      ...details,
      onload(responseObject) {
        resolve(responseObject);
      },
      onerror(responseObject) {
        reject(responseObject);
      },
    });
  });

  const reduceResults = ({ results }, title, sId) => {
    let filteredResults;
    let exactMatch = true;
    if (sId) {
      filteredResults = results.filter((a) => a.shikimori_id === sId);
    } else {
      const prepare = (t) => t.toLowerCase().replace(/[ ,:'`]/g, '');
      const origTitle = prepare(title);
      filteredResults = results.filter((a) => prepare(a.title_orig) === origTitle);
      if (!filteredResults.length) {
        exactMatch = false;
        const firstTitle = prepare(results[0].title_orig);
        filteredResults = results.filter((a) => prepare(a.title_orig) === firstTitle);
      }
    }
    filteredResults.sort((a, b) => b.episodes_count - a.episodes_count);
    return { ...filteredResults[0], exactMatch };
  };

  const getOtherResults = async (ogTitle, sId) => {
    const apiUrl = 'https://metamedia.glitch.me/api/search';
    const token = '3o52d19319b8dafoa194c9a77ofc9a3d8obfe1bd3eoe28b7cfd7aacebaaf4f6b';
    const url = `${apiUrl}?${sId ? `shikimori_id=${sId}` : `title=${encodeURI(ogTitle)}`}&sign=${token.replace(/o/g, 0)}`;
    let res;
    try {
      res = await request({ url });
      if (res.status !== 200) {
        log(`response error code ${res.status}: ${url}`);
        return null;
      }
    } catch (er) {
      log(`request error: ${url}`);
      return null;
    }
    const { response } = res;
    if (!response || !response.total || !response.results || !response.results.length) {
      if (sId) {
        log(`not found by shikimori_id (${sId})`);
        if (ogTitle) return getOtherResults(ogTitle);
      } else {
        log(`not found by ogTitle (${ogTitle})`);
      }
      return null;
    }
    return reduceResults(response, ogTitle, sId);
  };

  const addEventListeners = ({ overlay, iframe, otherAnime }) => {
    overlay.addEventListener('click', (e) => {
      if (e.target !== e.currentTarget) return;
      overlay.style.display = 'none';
      iframe.style.display = 'none';
      iframe.contentWindow.postMessage({ key: 'kodik_player_api', value: { method: 'pause' } }, '*');
    }, { passive: true });
    otherAnime.addEventListener('click', () => {
      overlay.style.display = 'block';
      iframe.style.display = 'block';
    }, { passive: true });
  };

  const createElements = ({
    src, count, exact, id, sId,
  }) => {
    let overlay = document.querySelector('#watch-online-overlay');
    if (!overlay) {
      overlay = document.createElement('div');
      overlay.id = 'watch-online-overlay';
      document.body.prepend(overlay);
    }
    let iframe = document.querySelector('#watch-online-iframe-other');
    if (!iframe) {
      iframe = document.createElement('iframe');
      iframe.id = 'watch-online-iframe-other';
      iframe.className = 'watch-online-iframe';
      iframe.src = `${src}?translations=true`;
      iframe.allow = 'fullscreen';
      overlay.append(iframe);
    }
    const contestBlock = document.querySelector('.block.contest_winners');
    if (contestBlock) {
      let contestHeader = document.querySelector('#watch-online-contest-subheadline');
      if (!contestHeader) {
        contestHeader = document.createElement('div');
        contestHeader.id = 'watch-online-contest-subheadline';
        contestHeader.className = 'subheadline';
        contestHeader.textContent = 'Турниры';
        contestBlock.prepend(contestHeader);
      }
    }
    let otherAnime = document.querySelector('#watch-online-a-other');
    if (!otherAnime) {
      let block = document.querySelector('#watch-online-block');
      if (!block) {
        block = document.createElement('div');
        block.id = 'watch-online-block';
        block.className = 'block';
        const subheadline = document.createElement('div');
        subheadline.className = 'subheadline';
        subheadline.textContent = 'Онлайн просмотр';
        block.append(subheadline);
        const targetBlock = document.querySelector('.block[itemprop="aggregateRating"]');
        targetBlock.parentElement.insertBefore(block, targetBlock.nextElementSibling);
      }
      const line = document.createElement('div');
      line.className = 'watch-online-line';
      otherAnime = document.createElement('a');
      otherAnime.id = 'watch-online-a-other';
      otherAnime.className = 'b-link';
      otherAnime.title = `Смотреть (точное совпадение ${exact ? '' : 'не '}найдено)`;
      otherAnime.textContent = `Другой плеер${exact ? '*' : ''} [${count ? `1-${count}` : '1'}]`;
      const aSite = document.createElement('a');
      aSite.className = 'b-link';
      aSite.target = '_blank';
      aSite.title = 'Смотреть на сайте amove';
      aSite.href = `https://amove.my.to/#${sId ? `shikimori-${sId}&${id}` : id}`;
      aSite.textContent = ' ↗ ';
      line.append(otherAnime, aSite);
      block.append(line);
    }
    addEventListeners({ overlay, iframe, otherAnime });
  };

  // new anime handler
  const newAnimeShow = async () => {
    const overlay = document.querySelector('#watch-online-overlay');
    const iframe = document.querySelector('#watch-online-iframe-other');
    const otherAnime = document.querySelector('#watch-online-a-other');
    if (overlay && iframe && otherAnime) {
      addEventListeners({ overlay, iframe, otherAnime });
      return;
    }
    const ogTitle = document.querySelector('head > meta[property = "og:title"]').content;
    const rawId = document.URL.substring(document.URL.lastIndexOf('/') + 1, document.URL.indexOf('-'));
    const sId = Number.isNaN(+rawId[0]) ? rawId.substring(1) : rawId;
    const result = await getOtherResults(ogTitle, sId);
    if (!result) {
      log('other anime not found');
      return;
    }
    createElements({
      src: result.link,
      count: result.episodes_count,
      exact: result.exactMatch,
      id: result.id,
      sId: result.shikimori_id,
    });
  };

  // observer fire when html changes its body
  const observer = new MutationObserver((mutationsList) => {
    mutationsList.forEach((mutationRecord) => mutationRecord.addedNodes.forEach((node) => {
      if (node.classList.contains('p-animes-show')) newAnimeShow();
    }));
  });
  observer.observe(document.querySelector('html'), { childList: true });
  if (document.body.classList.contains('p-animes-show')) newAnimeShow();
}());