MjKey / All in One for Shikimori

// ==UserScript==
// @name         All in One for Shikimori
// @version      0.4
// @description  Всё в одном.
// @author       MjKey and more..
// @match        https://shikimori.org/*
// @match        https://shikimori.one/*
// @match        http://shikimori.one/*
// @match        http://shikimori.org/*
// @match        https://shikimori.me/*
// @match        http://shikimori.me/*
// @updateURL    https://openuserjs.org/meta/MjKey/All_in_One_for_Shikimori.meta.js
// @downloadURL  https://openuserjs.org/install/MjKey/All_in_One_for_Shikimori.user.js
// @connect      myanimelist.net
// @require      https://github.com/qt-kaneko/Shikiplayer/releases/latest/download/manifest.user.js
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

// OP/ED by ShaDream & Chortowod
const youtubeLinks = true;
let debug = 1;
const insertAfter = (elem, refElem) => refElem.parentNode.insertBefore(elem, refElem.nextSibling);

function log(message) {
  debug && console.log(message)
}

function get_anime_id() {
  let full_url = window.location.href;
  let start = full_url.lastIndexOf('/') + 1;
  for (; isNaN(parseInt(full_url[start]));) start++;
  let number = "";
  for (; !isNaN(parseInt(full_url[start]));) {
    number += full_url[start];
    start++;
  }
  return number;
}

function createMusic(elements, placeToAppend) {
  elements.forEach(element => {
    let sound = document.createElement("span");
    let fst = element[0];
    let check = element.indexOf('(ep');
    let substringS = element.substring((fst == "#" ? 4 : 0), check);
    let substringF = element.substring(fst == "#" ? 4 : 0);
    sound.innerText = element;
    sound.className = "value sound";
    placeToAppend.appendChild(sound);
    if (youtubeLinks) sound.appendChild(getYoutubeLink(sound, check, substringS, substringF));
  })
}

function getYoutubeLink(sound, check, substringS, substringF) {
  let sound2 = document.createElement("a");
  sound2.innerText = " -> YouTube";
  sound2.href = "https://www.youtube.com/results?search_query=" + (check != -1 ? substringS : substringF);
  return sound2;
}

function musicConstructor(main, text, items) {
  let container = document.createElement("div");
  let addedClassName = "OP'S" === text ? " op" : " ed";
  container.className = "sound-container" + addedClassName;
  main.appendChild(container);
  let title = document.createElement("div");
  title.innerText = text;
  title.className = "subheadline m5";
  container.appendChild(title);
  createMusic(items, container);
}

function createOPEDList(op, ed) {
  log(op);
  if (0 != op.length || 0 != ed.length) {
    // Create main div and paste in in a right place
    let main = document.createElement("div");
    let paste_after = document.getElementsByClassName("b-db_entry")[0];
    //Create open button if needed
    if (main.className = "main-sound-container", insertAfter(main, paste_after),
      //Create content
      musicConstructor(main, "OP'S", op), musicConstructor(main, "ED'S", ed), op.length > 4 || ed.length > 4) {
      main.style.maxHeight = '150px';
      let expand_container = document.createElement("div");
      insertAfter(expand_container, main);
      expand_container.className = "b-height_shortener open-music";
      let shade = document.createElement("div");
      shade.className = "shade";
      expand_container.appendChild(shade);
      let expander = document.createElement("div");
      expander.className = "expand";
      expand_container.appendChild(expander);
      let span = document.createElement('span');
      span.innerText = "Развернуть";
      expander.appendChild(span);
      expand_container.onclick = function () {
        expand_container.parentNode.removeChild(expand_container);
        main.style.animation = 'height 15s cubic-bezier(.19,1,.22,1) forwards';
      }
    }
    //apply styles
    createStyle();
  }
}

function createStyle() {
  // Here you can change style of all new elements
  GM_addStyle(".main-sound-container{margin-bottom:15px; overflow:hidden}.sound-container{display:inline-block; vertical-align:top; width: 48%;}.op{margin-right:3%;}.sound{padding-top:5px; margin:5px; display: block;}.open-music{margin-bottom:15px;}@keyframes height {from{max-height:150px;} to {max-height: 2000px;}}");
}

function getMusic(doc, class_name) {
  let music = doc.getElementsByClassName("theme-songs js-theme-songs " + class_name)[0];
  log(music);
  let childs = [];
  let authors = music.getElementsByClassName("theme-song-artist");
  let counter = 1;
  for (var i = 0; i < authors.length; i++) {
    let td = authors[i].parentNode;
    let episodes = td.getElementsByClassName("theme-song-episode")[0];
    if (episodes) episodes = episodes.textContent;
    else episodes = '';
    let songName = td.getElementsByClassName("theme-song-title")[0];
    if (songName) {
      log(songName.textContent + authors[i].textContent + ' ' + episodes);
      childs.push('#' + counter + ' ' + songName.textContent + authors[i].textContent + ' ' + episodes);
    }
    else {
      for (let songName2 of td.childNodes) {
        if (songName2.nodeType === 3) {
          log(songName2.textContent + authors[i].textContent);
          childs.push('#' + counter + ' ' + songName2.textContent + authors[i].textContent + ' ' + episodes);
          break;
        }
      }
    }
    counter++;
  }
  log(childs);
  return childs;
}

function loadMalt() {
  "use strict";
  if (!isAnimePage() || isAdded()) return;
  let url = "https://myanimelist.net/anime/" + get_anime_id();
  log("Finding OP/ED.");
  GM_xmlhttpRequest({
    method: "GET",
    url: url,
    onload: function (response) {
      let doc = (new DOMParser).parseFromString(response.responseText, 'text/html');
      log(doc);
      createOPEDList(getMusic(doc, "opnening"), getMusic(doc, "ending"));
      log("OP/ED finded!");
    }
  })
}

function isAnimePage() {
  return 0 === window.location.href.replace(/http.?:\/\/shikimori\..*\/animes\/[^\/]*/, "").length
}

function isAdded() {
  return document.getElementsByClassName("main-sound-container").length > 0
}

function ready0(fn) {
  if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") fn();
  else document.addEventListener('DOMContentLoaded', fn);
}

ready0(loadMalt);

//CustomListMark by AniOleg

const clOngoing = 'GreenYellow';
const clOngoingLaze = 'red';
const clAnnounce = 'orange';

const markTitle = (sColor, EntriesList, a, b) => {
  EntriesList[a].childNodes[b].childNodes[0].style.borderLeft = `1px solid ${sColor}`;
};

const cReDraw = () => {
  if (!(location.pathname.includes('/list/anime') || location.pathname.includes('/list/manga'))) return;

  try {
    const EntriesList = document.getElementsByClassName('entries');

    for (let a = 0; a < EntriesList.length; a++) {
      for (let b = 0; b < EntriesList[a].childNodes.length; b++) {
        const ongoingElem = EntriesList[a].childNodes[b].childNodes[1].getElementsByClassName('ongoing')[0];
        if (ongoingElem) {
          ongoingElem.remove();
          const miscValue = EntriesList[a].childNodes[b].childNodes[3].getElementsByClassName('misc-value')[0].childNodes[1].nodeValue;
          const currentValue = EntriesList[a].childNodes[b].childNodes[3].getElementsByClassName('current-value')[0].childNodes[0].childNodes[0].nodeValue;
          if (miscValue === '?' || miscValue - currentValue <= 0) {
            markTitle(clOngoing, EntriesList, a, b);
          }
          else {
            markTitle(clOngoingLaze, EntriesList, a, b);
          }
        }
        else {
          const anonsElem = EntriesList[a].childNodes[b].childNodes[1].getElementsByClassName('anons')[0];
          if (anonsElem) {
            anonsElem.remove();
            markTitle(clAnnounce, EntriesList, a, b);
          }
        }
      }
    }
  }
  catch (error) {}
};

const observeDOM = (function () {
  const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  const eventListenerSupported = window.addEventListener;

  return function (obj, callback) {
    if (MutationObserver) {
      const obs = new MutationObserver((mutations, observer) => {
        if (mutations[0].addedNodes.length || mutations[0].removedNodes.length) {
          callback();
        }
      });
      obs.observe(obj, {
        childList: true,
        subtree: true
      });
    }
    else if (eventListenerSupported) {
      obj.addEventListener('DOMNodeInserted', callback, false);
    }
  };
})();

observeDOM(document.querySelector('html'), cReDraw);

//ShikiAirTime by AniOleg

const cGap = 11.4, //отступ между обложками аниме (px) //
  cGridItemWidth = 96, //цвет линии просматриваемого онгоинга (есть отставание по сериям)
  clOngoingPlanned = 'orange', //цвет линии запланированного онгоинга
  bSize = 2, //толщина линии подчёркивания тайтла (px)
  cTooltip = true, //включить отображение карточек при наведении на обложку аниме
  showAiredAnime = true, //отображать уже вышедшее аниме, или аниме для которого неизвестна дата выхода эпизода
  showPlannedAirAnime = false, //отображать онгоинги из списка "Запланировано"
  gDebul = false; //выводить отладочную информацию в консоль DevTools
//

const host = location.protocol + '//' + location.host;
const lang_data = {
  ru: {
    header: 'Ваше аниме',
    episode: 'Эпизод',
    progress: 'Прогресс:',
    day: 'д',
    hour: 'ч',
    min: 'м',
    emptyList: 'Список аниме пуст',
    errorCatch: 'При обработке данных произошла ошибка. Попробуйте обновить страницу',
    episodeIncrementProgress: 'Отправка...',
    cacheResetAlert: 'Кэш расписания сброшен. Обновите страницу',
  },
  en: {
    header: 'Your anime',
    episode: 'Ep',
    progress: 'Progress:',
    day: 'd',
    hour: 'h',
    min: 'm',
    emptyList: 'Anime list is empty',
    errorCatch: 'An error occurred while processing the data. Try refreshing the page',
    episodeIncrementProgress: 'Sending...',
    cacheResetAlert: 'Schedule cache was reset. Please, refresh page',
  },
};
var CalendarList = [],
  AnimeList = [],
  GlobalServerTime = null,
  SiteLang, TitleLang;

function InitScript(fn) {
  document.addEventListener('page:load', fn);
  document.addEventListener('turbolinks:load', fn);
  if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
    fn();

    var ctrlDown = false,
      shiftDown = false,
      ctrlKey = 17,
      cmdKey = 91,
      shiftKey = 16,
      fKey = 70,
      prevKey = 0;

    $(document).keydown(function (e) {
      if (e.keyCode == ctrlKey || e.keyCode == cmdKey) ctrlDown = true;
      if (e.keyCode == shiftKey) shiftDown = true;
      if (ctrlDown && shiftDown && e.keyCode == fKey && location.pathname == '/' && GetUserId() != null) {
        localStorage.setItem('__AniListCalendar', '[]');
        alert(lang_data[SiteLang].cacheResetAlert);
        if (gDebul) console.log("ShikiAirTime: Кэш календаря сброшен пользователем");
      }
    }).keyup(function (e) {
      if (e.keyCode == ctrlKey || e.keyCode == cmdKey) ctrlDown = false;
      if (e.keyCode == shiftKey) shiftDown = false;
    });
  }
  else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

function InitListFetch() {
  if (localStorage.getItem('__AniListCalendar') == null) {
    localStorage.setItem('__AniListCalendar', '[]');
  }
  if (location.pathname == '/' && GetUserId() != null) {
    GlobalServerTime = GetServerTime();
    SiteLang = $('body')[0].attributes['data-locale'].nodeValue;
    TitleLang = $('body')[0].attributes['data-localized_names'].nodeValue;
    if (gDebul) console.log('ShikiAirTime: Запрос списка пользователя...');

    InitListGrid();

    fetch(host + '/api/users/' + GetUserId() + '/anime_rates?status=watching&limit=5000', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        }
      })
      .then(r => r.json())
      .then(data => {
        AnimeList = data;
        if (gDebul) console.log('ShikiAirTime: Список аниме пользователя получен');

        //2.5
        if (showPlannedAirAnime) {
          fetch(host + '/api/users/' + GetUserId() + '/anime_rates?status=planned&limit=5000', {
              method: 'GET',
              headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
              }
            })
            .then(r => r.json())
            .then(data => {
              if (gDebul) console.log('ShikiAirTime: Список аниме пользователя получен (для опции "Отображать запланированное аниме")');
              for (var a = 0; a < data.length; a++) {
                if (data[a].anime.status == "ongoing") {
                  AnimeList.push(data[a]);
                }
              }
              InitDataFetch();
            })
            .catch(error => {
              if (gDebul) console.log('ShikiAirTime: Ошибка получения списка аниме пользователя (для опции "Отображать запланированное аниме") ' + error);
              $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>';
            })
        }
        else {
          InitDataFetch();
        }
      })
      .catch(error => {
        if (gDebul) console.log('ShikiAirTime: Ошибка получения списка аниме пользователя ' + error);
        $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>';
      })

  }
}

function InitListGrid() {
  if (location.pathname == '/') {
    var d = document.createElement('style');
    d.innerHTML = `.b-catalog_entry.__Calendar .cover .image-decor>.text:before {background: none !important}
        .b-catalog_entry.__Calendar .cover .image-decor>.text {text-align: center !important}
        .preAirData {display: none !important;}
        .preAirData.Show {display: block !important;}
        .addEpisode {display: none !important; padding-top: 5px !important; padding-bottom: 5px; font-size: 14px !important;}
        .addEpisode:hover > .plus {font-weight: bold !important;}
        .addEpisode.Show {display: block !important}
        .calendarGridContainer {display: grid; gap: ` + cGap + `px; grid-template-columns: repeat(auto-fill, ` + cGridItemWidth + `px);}`;
    $('body')[0].appendChild(d);

    var g = document.createElement('div');
    g.id = 'CalendarTooltip';
    g.style = 'display: none; position: absolute; left: 0px; top: 0px; pointer-events: none !important; margin: 0';
    g.innerHTML = '<div class="tooltip-inner" style="width: 240px !important; min-height: 50px !important; box-shadow: none !important; margin: 0px !important"></div>';

    $('body')[0].appendChild(g);
    $('.block2')[0].childNodes[1].style = '';
    $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><div class="b-ajax"></div>';
    $('.block2')[0].childNodes[1].classList = '';
    $('.block2')[0].childNodes[0].childNodes[0].childNodes[0].nodeValue = lang_data[SiteLang].header;
    $('.block2')[0].childNodes[0].childNodes[0].href = GetUserLink() + '/list/anime/mylist/watching,rewatching';
  }
}

function InitDataFetch() {
  try {
    CalendarList = [];
    CalendarList = JSON.parse(localStorage.getItem('__AniListCalendar'));
    var Queue = [];
    if (CalendarList != null && CalendarList.length > 0) {
      if (gDebul) console.log('ShikiAirTime: В localStorage обнаружены данные календаря');
      for (var a = 0; a < AnimeList.length; a++) {
        if (AnimeList[a].anime.status == 'ongoing') {
          var IsFinded = false;
          for (var b = CalendarList.length - 1; b >= 0; b--) { //перебор календаря (сохранённого)
            if (AnimeList[a].anime.id == CalendarList[b].data.Media.idMal) { //2.2
              IsFinded = true;
              if (CalendarList[b].data.Media.nextAiringEpisode.airingAt - GlobalServerTime < 0) { //если у аниме эпизод уже вышел, то обновляем информацию
                CalendarList.splice(b, 1);
                Queue.push(AnimeList[a].anime.id);
              }
            }
          }
          if (!IsFinded) //если не нашли нужного аниме, то запрашиваем инфу
            Queue.push(AnimeList[a].anime.id);
        }
      }
    }
    else { //если календарь не был сохранён, то запрашиваем инфу по всем своим онгоингам
      if (gDebul) console.log('ShikiAirTime: Запрос новых данных по всему списку...')
      CalendarList = [];
      if (AnimeList.length > 0) {
        for (var i = 0; i < AnimeList.length; i++) {
          if (AnimeList[i].anime.status == 'ongoing') {
            Queue.push(AnimeList[i].anime.id);
          }
        }
      }
    }
    if (Queue.length > 0) {
      GrabAniList(Queue, 1);
    }
    else {
      CalendarList.sort(compareNumeric);
      DrawListGrid();
    }
  }
  catch (e) {
    if (gDebul) console.log('ShikiAirTime: Ошибка ' + e + '. Кэш календаря будет сброшен')
    $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>';
    localStorage.setItem('__AniListCalendar', '[]');
  }
}

function GrabAniList(Queue, Page) {
  const ApiUrl = 'https://graphql.anilist.co';
  const QueryStruct = `
query($idMal_in: [Int], $page: Int) {
  Page(page: $page) {
    media(idMal_in: $idMal_in) {
      idMal
      nextAiringEpisode {
        airingAt
        episode
      }
    }
    pageInfo {
      perPage
      hasNextPage
      total
    }
  }
}`;
  var xhr = new XMLHttpRequest();
  xhr.open("POST", ApiUrl);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      data = JSON.parse(xhr.responseText);
      for (var i = 0; i < data.data.Page.media.length; i++) {
        if (data.data.Page.media[i].nextAiringEpisode != null) {
          CalendarList.push({
            data: {
              Media: data.data.Page.media[i]
            }
          });
        }
      }
      localStorage.setItem('__AniListCalendar', JSON.stringify(CalendarList)); //сохраняем календарь в локальное хранилище
      if (data.data.Page.pageInfo.hasNextPage == true) {
        GrabAniList(Queue, Page + 1)
      }
      else {
        CalendarList.sort(compareNumeric);
        DrawListGrid();
      }
    }
  };
  var data = JSON.stringify({
    query: QueryStruct,
    variables: {
      idMal_in: Queue
    }
  });
  xhr.send(data);
}

function DrawListGrid() {
  if (location.pathname == '/') {
    var tInner = '';
    var allCount = 0;

    function datediff(first, second) {
      return Math.round((second - first) / (1000 * 60 * 60 * 24));
    }
    var findedArray = [];
    for (var a = 0; a < CalendarList.length; a++) {
      for (var b = 0; b < AnimeList.length; b++) {
        if (CalendarList[a].data.Media.idMal == AnimeList[b].anime.id && AnimeList[b].anime.status == "ongoing") {
          findedArray.push(b);

          var tTime = CalendarList[a].data.Media.nextAiringEpisode.airingAt - GlobalServerTime;

          if (tTime > -60) { //придерживаем серию ещё минуту после её выхода
            allCount++;

            var bColor = AnimeList[b].episodes + 1 < CalendarList[a].data.Media.nextAiringEpisode.episode ? clOngoingLaze : clOngoing;
            if (AnimeList[b].episodes == 0) bColor = clOngoingPlanned;

            tInner += `
                        <article class="b-catalog_entry __Calendar" itemtype="http://schema.org/Movie" style="border-bottom: ` + bSize + `px solid ` + bColor + `; position: relative" title_name="` +
              AnimeList[b].anime.name + `" title_ru="` + AnimeList[b].anime.russian + `" ` +
              `episodes_watched="` + AnimeList[b].episodes + `" episodes="` + AnimeList[b].anime.episodes + `" episodes_aired="` + (CalendarList[a].data.Media.nextAiringEpisode.episode - 1) + `"
                            anime_rate_id="` + AnimeList[b].id + `" clOngoing="` + clOngoing + `" clOngoingLaze="` + clOngoingLaze + `" is_ongoing=true>
                            <a class="cover anime-tooltip-processed" data-delay="150" href="` + host + AnimeList[b].anime.url + `">
                                <span class="image-decor">
                                    <span class="image-cutter">
                                        <img alt="` + AnimeList[b].anime.name + `" src="` + AnimeList[b].anime.image.x96 + `">
                                    </span>
                                    <div class="text preAirData Show">` + lang_data[SiteLang].episode + ` ` + CalendarList[a].data.Media.nextAiringEpisode.episode + `<br>` + CalcEndTime(tTime) + `</div>
                                    <div class="text addEpisode">` + AnimeList[b].episodes + ` <span class="plus">+</span></div>
                                </span>
                            </a>
                        </article>
                        `;
          }
        }
      }
    }

    if (showAiredAnime) {
      for (var c = 0; c < AnimeList.length; c++) {
        if (findedArray.indexOf(c) == -1) {
          allCount++;
          tInner += `
                        <article class="b-catalog_entry __Calendar" itemtype="http://schema.org/Movie" style=" position: relative" title_name="` +
            AnimeList[c].anime.name + `" title_ru="` + AnimeList[c].anime.russian + `" ` +
            `episodes_watched="` + AnimeList[c].episodes + `" episodes="` + AnimeList[c].anime.episodes + `" episodes_aired="` + (AnimeList[c].anime.episodes == 0 ? "?" : AnimeList[c].anime.episodes) + `"
                            anime_rate_id="` + AnimeList[c].id + `" clOngoing="` + clOngoing + `" clOngoingLaze="` + clOngoingLaze + `" is_ongoing=false>
                            <a class="cover anime-tooltip-processed" data-delay="150" href="` + host + AnimeList[c].anime.url + `">
                                <span class="image-decor">
                                    <span class="image-cutter">
                                        <img alt="` + AnimeList[c].anime.name + `" src="` + AnimeList[c].anime.image.x96 + `">
                                    </span>
                                    <div class="text addEpisode">` + AnimeList[c].episodes + ` <span class="plus">+</span></div>
                                </span>
                            </a>
                        </article>
                        `;
        }
      }
    }

    if (allCount > 0) {
      $('.block2')[0].childNodes[1].classList = 'calendarGridContainer';
      $('.block2')[0].childNodes[1].innerHTML = tInner;
    }
    else {
      $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].emptyList + '</span></div>';
    }

    //очистка завершенных онгоингов из кэша календаря
    for (var x = CalendarList.length - 1; x >= 0; x--) {
      var IsFinded = false;
      for (var d = 0; d < AnimeList.length; d++) {
        if (CalendarList[x].data.Media.idMal == AnimeList[d].anime.id) {
          IsFinded = true;
        }
      }
      if (!IsFinded) {
        CalendarList.splice(x, 1);
      }
    }

    localStorage.setItem('__AniListCalendar', JSON.stringify(CalendarList)); //сохраняем календарь в локальное хранилище

    if (cTooltip) {
      var elements = $('.__Calendar');
      for (var i = 0; i < elements.length; i++) {
        $(elements[i].childNodes[1].childNodes[1]).hover(
          function (e) {
            ShowCustomTooltip(e.target);
            $(e.target.parentElement.parentElement).find(".addEpisode").addClass("Show");
            $(e.target.parentElement.parentElement).find(".preAirData").removeClass("Show");
          },
          function (e) {
            HideCustomTooltip();
            $(e.target.parentElement.parentElement).find(".addEpisode").removeClass("Show");
            $(e.target.parentElement.parentElement).find(".preAirData").addClass("Show");
          }
        )
      }
    }
    $(".addEpisode").click(function (e) {
      e.preventDefault();
      incEpisode($(e.target.parentElement.parentElement).find(".addEpisode")[0].parentElement.parentElement.parentElement.getAttribute("anime_rate_id"), $(e.target.parentElement.parentElement).find(".addEpisode")[0]);
    })
  }
}

function ShowCustomTooltip(e) {
  e = e.tagName == 'IMG' ? e.parentElement.parentElement.parentElement.parentElement : e.parentElement.parentElement.parentElement;
  $('#CalendarTooltip')[0].style.display = 'block';
  $('#CalendarTooltip')[0].style.top = e.getBoundingClientRect().y + document.documentElement.scrollTop + 'px';
  $('#CalendarTooltip')[0].childNodes[0].innerHTML = (TitleLang == 'ru' ? e.attributes.title_ru.nodeValue : e.attributes.title_name.nodeValue) + '<br><br>';
  var EpisodesBehind = e.attributes.episodes_aired.nodeValue - e.attributes.episodes_watched.nodeValue;
  if (EpisodesBehind > 0) {
    $('#CalendarTooltip')[0].childNodes[0].innerHTML += (SiteLang == 'ru' ?
      'Отставание на ' + EpisodesBehind + ' ' + GetLocalizedEpisodeHint(EpisodesBehind) :
      EpisodesBehind + ' episode' + (EpisodesBehind > 1 ? 's' : '') + ' behind') + '<br><br>';
  }
  $('#CalendarTooltip')[0].childNodes[0].innerHTML += lang_data[SiteLang].progress + ' ' + e.attributes.episodes_watched.nodeValue + '/' + (e.attributes.episodes.nodeValue != 0 ? e.attributes.episodes.nodeValue : '?');
  if (e.getBoundingClientRect().x + e.getBoundingClientRect().width + 2 + $('#CalendarTooltip')[0].childNodes[0].getBoundingClientRect().width + 10 < $(window).width()) {
    $('#CalendarTooltip')[0].style.left = e.getBoundingClientRect().x + e.getBoundingClientRect().width + 2 + 'px';
  }
  else {
    $('#CalendarTooltip')[0].style.left = e.getBoundingClientRect().x - 2 - $('#CalendarTooltip')[0].childNodes[0].getBoundingClientRect().width + 'px';
  }
}

function GetLocalizedEpisodeHint(e) {
  var variants = ['эпизод', 'эпизода', 'эпизодов'];
  e = Math.abs(e) % 100;
  var n1 = e % 10;
  if (e > 10 && e < 20) {
    return variants[2];
  }
  if (n1 > 1 && n1 < 5) {
    return variants[1];
  }
  if (n1 == 1) {
    return variants[0];
  }
  return variants[2];
}

function HideCustomTooltip() {
  $('#CalendarTooltip')[0].style.display = 'none';
}

function GetUserId() {
  return JSON.parse($('body')[0].attributes['data-user'].value).id;
}

function GetUserLink() {
  return JSON.parse($('body')[0].attributes['data-user'].value).url;
}

function GetServerTime() {
  try {
    return new Date($('body')[0].attributes['data-server_time'].nodeValue).getTime() / 1000;
  }
  catch (e) {
    if (gDebul) console.log('ShikiAirTime: Ошибка запроса серверного времени');
    return null;
  }
}

function compareNumeric(a, b) { //сортировка AniList по времени до выхода эпизода
  a = a.data.Media.nextAiringEpisode.airingAt;
  b = b.data.Media.nextAiringEpisode.airingAt;
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

function CalcEndTime(tUTime) {
  var t = parseInt(tUTime);
  if (t < 60) {
    return '<1' + lang_data[SiteLang].min;
  }
  else {
    var days = parseInt(t / 86400);
    t = t - (days * 86400);
    var hours = parseInt(t / 3600);
    t = t - (hours * 3600);
    var minutes = parseInt(t / 60);
    var content = '';
    if (days) {
      content += days + lang_data[SiteLang].day;
    }
    if (hours > 0) {
      if (hours || days) {
        if (content) {
          content += ' ';
        }
        content += hours + lang_data[SiteLang].hour;
      }
    }
    if (minutes) {
      if (content) {
        content += ' ';
      }
      content += minutes + lang_data[SiteLang].min;
    }
    return content;
  }
}

function incEpisode(rate_id, addEpisodeElement) {
  var prevHTML = addEpisodeElement.innerHTML;
  addEpisodeElement.innerHTML = lang_data[SiteLang].episodeIncrementProgress;
  fetch(host + '/api/v2/user_rates/' + rate_id + '/increment', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      }
    })
    .then(r => r.json())
    .then(data => {
      if (gDebul) console.log('ShikiAirTime: Количество просмотренных эпизодов увеличено на 1');
      addEpisodeElement.innerHTML = data.episodes + ' <span class="plus">+</span>';
      addEpisodeElement.parentElement.parentElement.parentElement.setAttribute("episodes_watched", data.episodes);
      HideCustomTooltip();
      if (addEpisodeElement.parentElement.parentElement.parentElement.getAttribute("is_ongoing") == "true") {
        console.log(data.episodes, addEpisodeElement.parentElement.parentElement.parentElement.getAttribute("episodes_aired"));
        var bColor = data.episodes < addEpisodeElement.parentElement.parentElement.parentElement.getAttribute("episodes_aired") ? clOngoingLaze : clOngoing;
        addEpisodeElement.parentElement.parentElement.parentElement.style.borderBottom = bSize + `px solid ` + bColor;
      }
      if (addEpisodeElement.parentElement.parentElement.parentElement.getAttribute("episodes") > 0 &&
        addEpisodeElement.parentElement.parentElement.parentElement.getAttribute("episodes") == data.episodes) {
        addEpisodeElement.parentElement.parentElement.parentElement.remove();
      }
    })
    .catch(error => {
      if (gDebul) console.log('ShikiAirTime: Ошибка увеличения кол-ва эпизодов');
      addEpisodeElement.innerHTML = prevHTML;
    })
}

InitScript(InitListFetch)

//Shiki Rating by ImoutoChan

function getLocale() {
  return document.querySelector('body').getAttribute('data-locale');
}

function needAddRating(urlpart) {
  return urlpart === "/animes" ||
    urlpart === "/mangas" ||
    urlpart === "/ranobe";
}

function removeLastClass(domElement) {
  var classes = domElement.classList;
  classes.remove(classes.item(classes.length - 1));
}

function setNoData(domElement) {
  var noData = document.createElement('p');
  noData.classList.add('b-nothing_here');
  noData.innerText = getLocale() === 'ru' ?
    `Недостаточно данных` :
    `Insufficient data`;

  domElement.innerHTML = '';
  domElement.appendChild(noData);

  domElement.style.textAlign = 'center';
  domElement.style.color = '#7b8084';
  domElement.style.marginTop = '15px';
}

function appendShikiRating() {
  'use strict';

  var urlpart = window.location.pathname.substring(0, 7);
  log(urlpart);

  if (!needAddRating(urlpart)) {
    log('wrong page');
    return;
  }

  if (document.querySelector("#shiki-score") !== null) {
    log('already created');
    return;
  }

  if (document.querySelector(".scores > .b-rate") === null) {
    log("can't find default rating");
    return;
  }

  // get current rating element
  var malRate = document.querySelector(".scores > .b-rate");
  malRate.setAttribute('id', 'mal-score');

  // clone it to new element
  var newShikiRate = malRate.cloneNode(true);
  newShikiRate.setAttribute('id', 'shiki-score');

  // append cloned rating to parent container
  var rateContainer = document.querySelector(".scores");
  rateContainer.appendChild(newShikiRate);

  // load scores stats
  var scoreDataJson = document.querySelector("#rates_scores_stats").getAttribute("data-stats");
  var scoreData = JSON.parse(scoreDataJson);
  log(scoreDataJson);

  // set no data lable
  if (scoreData.length === 0) {
    setNoData(newShikiRate);
    return;
  }

  // calculate shiki rating
  var sumScore = 0;
  var totalCount = 0;
  for (var i = 0; i < scoreData.length; i++) {
    sumScore += scoreData[i][1] * scoreData[i][0];
    totalCount += scoreData[i][1] * 1;
  }
  var shikiScore = sumScore / totalCount;
  var shikiScoreDigit = Math.round(shikiScore);
  log(shikiScore);

  // set number value
  var scoreElement = newShikiRate.querySelector("div.text-score > div.score-value");
  scoreElement.innerHTML = shikiScore.toFixed(2);
  removeLastClass(scoreElement);
  scoreElement.classList.add("score-" + shikiScoreDigit);

  // set stars calue
  var starElement = newShikiRate.querySelector("div.stars-container > div.stars.score");
  removeLastClass(starElement);
  starElement.style.color = '#456';
  starElement.classList.add("score-" + shikiScoreDigit);

  // load labels
  var labelData = getLocale() === 'ru' ? {
    "0": "",
    "1": "Хуже некуда",
    "2": "Ужасно",
    "3": "Очень плохо",
    "4": "Плохо",
    "5": "Более-менее",
    "6": "Нормально",
    "7": "Хорошо",
    "8": "Отлично",
    "9": "Великолепно",
    "10": "Эпик вин!"
  } : {
    "0": "",
    "1": "Worst Ever",
    "2": "Terrible",
    "3": "Very Bad",
    "4": "Bad",
    "5": "So-so",
    "6": "Fine",
    "7": "Good",
    "8": "Excellent",
    "9": "Great",
    "10": "Masterpiece!"
  };

  // set label under score
  newShikiRate.querySelector("div.text-score > div.score-notice").textContent = labelData[shikiScoreDigit];

  // set mal description label
  var malLabel = getLocale() === 'ru' ? 'На основе оценок mal' : 'From MAL users';
  malRate.insertAdjacentHTML('afterend', '<p class="score-source">' + malLabel + '</p>');

  // set shiki description label
  var shikiCountLabel = '<strong>' + totalCount + '</strong>';
  shikiCountLabel = (getLocale() === 'ru') ?
    'На основе ' + shikiCountLabel + ' оценок shiki' :
    'From ' + shikiCountLabel + ' shiki users';
  newShikiRate.insertAdjacentHTML('afterend', '<p class="score-counter">' + shikiCountLabel + '</p>');

  // set style for mal description label
  var malScoreLabelElement = document.querySelector('.score-source');
  malScoreLabelElement.style.marginBottom = '15px';
  malScoreLabelElement.style.textAlign = 'center';
  malScoreLabelElement.style.color = '#7b8084';

  // set style for shiki description label
  var shikiScoreLabelElement = document.querySelector('.score-counter');
  shikiScoreLabelElement.style.textAlign = 'center';
  shikiScoreLabelElement.style.color = '#7b8084';
}

function ready(fn) {
  document.addEventListener('page:load', fn);
  document.addEventListener('turbolinks:load', fn);

  if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
    fn();
  }
  else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

ready(appendShikiRating);

//Shikimori more comments loader by BoberMod

const commentsPerClick = 100; //НЕ СТОИТ ДЕЛАТЬ ЗНАЧЕНИЕ БОЛЬШЕ 150-200!

function change() {
  "use strict";

  let loader = document.getElementsByClassName("comments-loader")[0];
  if (!loader) {
    return false;
  }
  let numberOfComments = loader.getAttribute("data-count");
  let skipLink = loader.getAttribute("data-clickloaded-url-template").replace(/SKIP\/\d{1,2}/g, `SKIP/${commentsPerClick}`);

  loader.setAttribute("data-limit", commentsPerClick);
  loader.setAttribute("data-clickloaded-url-template", skipLink);

  if (numberOfComments <= commentsPerClick) {
    loader.innerText = `Загрузить ${numberOfComments} из ${numberOfComments} комментариев`;
  }
  else {
    loader.innerText = `Загрузить ещё ${commentsPerClick} из ${numberOfComments} комментариев`;
  }
}

ready(change);
//