Shiki Anilibria

Adds original anilibria online player to the shikimori

// ==UserScript==
// @name            Shiki Anilibria
// @name:ru         Shiki Anilibria
// @namespace       shikiAnilibria
// @version         0.1.2
// @description     Adds original anilibria 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 shikiOtherAnime script
// - for autopause to work require videoShortcuts script (it handles 'pleasePauseVideo' postMessage)
// - videoShortcuts script is also 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 requestAnilibriaAPI = (title) => {
    const formData = new FormData();
    const data = {
      query: 'search',
      search: title,
      filter: 'id,names,code,series',
    };
    Object.entries(data).forEach(([key, value]) => formData.append(key, value));
    return request({
      method: 'POST',
      url: 'https://www.anilibria.tv/public/api/index.php',
      data: formData,
    });
  };

  // sort by weight desc and title asc
  const sort = (results) => Object.values(results).sort((a, b) => {
    if (b.weight - a.weight) {
      return b.weight - a.weight;
    }
    return a.title.localeCompare(b.title);
  });

  // collect and weighing results from anilibria.tv
  const getAnilibriaResults = async ({ titles, utitle }) => {
    let rawResults;
    try {
      const reqPromises = titles.map((title) => requestAnilibriaAPI(title));
      rawResults = await Promise.all(reqPromises);
    } catch (er) {
      log('request error: anilibria.tv not available (blocking by censorship?)');
      return [];
    }
    const results = {};
    // results handling and weighing
    rawResults.forEach((res) => {
      if (res.status !== 200) {
        log('response error code:', res.status);
        return;
      }
      const { response } = res;
      if (!response.status) {
        log('some internal anilibria.tv error:', response);
        return;
      }
      response.data.forEach(({
        id, code, names, series,
      }) => {
        if (results[id]) {
          results[id].weight += 1; // for each duplicate result weight +1
        } else { // initial record
          let weight = 1;
          let exact = false;
          if (code === utitle) {
            weight += 9; // if titles in url complete match weight +9
            exact = true;
          } else if (utitle.includes(code) || code.includes(utitle)) {
            weight += 1; // if one title in url include other weight +1
          }
          if (titles[0] === names[0]) {
            weight += 5; // if local titles complete match weight +5
          } else if (titles[0].includes(names[0]) || names[0].includes(titles[0])) {
            weight += 1; // if one local title include other weight +1
          }
          if (titles[1] === names[1]) {
            weight += 5; // if official titles complete match weight +5
          } else if (titles[1].includes(names[1]) || names[1].includes(titles[1])) {
            weight += 1; // if one official title include other weight +1
          }
          results[id] = { // add new record
            id, title: names[0], code, series, weight, exact,
          };
        }
      });
    });
    return sort(results);
  };

  // collect titles from page
  const getTitles = () => {
    const titles = document.querySelector('.head>h1').textContent.split(' / ');
    const ogTitle = document.querySelector('head > meta[property = "og:title"]').content;
    if (!titles.includes(ogTitle)) titles.push(ogTitle);
    // ../animes/31964-boku-no-pico-academia => boku-no-pico-academia
    const utitle = document.URL.substring(document.URL.indexOf('-') + 1);
    return { titles, utitle };
  };

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

  const createElements = ({
    id, series, exact, code,
  }) => {
    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-alib');
    if (!iframe) {
      iframe = document.createElement('iframe');
      iframe.id = 'watch-online-iframe-alib';
      iframe.className = 'watch-online-iframe';
      iframe.src = `https://www.anilibria.tv/public/iframe.php?id=${id}`;
      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 anilibria = document.querySelector('#watch-online-a-alib');
    if (!anilibria) {
      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';
      anilibria = document.createElement('a');
      anilibria.id = 'watch-online-a-alib';
      anilibria.className = 'b-link';
      anilibria.title = `Смотреть (точное совпадение ${exact ? '' : 'не '}найдено)`;
      anilibria.textContent = `Анилибрия${exact ? '*' : ''} [${series}]`;
      const aSite = document.createElement('a');
      aSite.className = 'b-link';
      aSite.target = '_blank';
      aSite.title = 'Смотреть на сайте Анилибрии';
      aSite.href = `https://www.anilibria.tv/release/${code}.html`;
      aSite.textContent = ' ↗ ';
      line.append(anilibria, aSite);
      block.append(line);
    }
    addEventListeners({ overlay, iframe, anilibria });
  };

  // change body handler
  const newAnimeShow = async () => {
    const overlay = document.querySelector('#watch-online-overlay');
    const iframe = document.querySelector('#watch-online-iframe-alib');
    const anilibria = document.querySelector('#watch-online-a-alib');
    if (overlay && iframe && anilibria) {
      addEventListeners({ overlay, iframe, anilibria });
      return;
    }
    const titlesAndUtitle = getTitles();
    const results = await getAnilibriaResults(titlesAndUtitle);
    if (results.length === 0) {
      log('anime not found on anilibria');
      return;
    }
    createElements(results[0]);
  };

  // observer fires 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();
}());