AniOleg / ShikiAirTime

// ==UserScript==
// @name         ShikiAirTime
// @version      2.11
// @description  Добавляет на главную страницу блок "Ваше аниме", в котором отображается таймер до выхода новых серий онгоингов пользователя (заменяет собой блок "Сейчас выходит")
// @author       AniOleg
// @match        https://shikimori.one/*
// @match        https://shikimori.org/*
// @match        https://shikimori.me/*
// @match        http://shikimori.one/*
// @match        http://shikimori.org/*
// @match        http://shikimori.me/*
// @icon         https://www.google.com/s2/favicons?domain=shikimori.me
// @homepageURL  https://openuserjs.org/scripts/AniOleg/ShikiAirTime
// @updateURL    https://openuserjs.org/meta/AniOleg/ShikiAirTime.meta.js
// @license      MIT
// @grant        none
// ==/UserScript==

//НАСТРАИВАЕМЫЕ ПАРАМЕТРЫ
const cGap = 11.4,                   //отступ между обложками аниме (px)
      cGridItemWidth = 96,           //ширина обложки аниме (px)
      clOngoing = 'GreenYellow',     //цвет линии просматриваемого онгоинга
      clOngoingLaze = 'red',         //цвет линии просматриваемого онгоинга (есть отставание по сериям)
      clOngoingPlanned = 'orange',   //цвет линии запланированного онгоинга
      bSize = 2,                     //толщина линии подчёркивания тайтла (px)
      cTooltip = true,               //включить отображение карточек при наведении на обложку аниме
      showAiredAnime = true,         //отображать уже вышедшее аниме, или аниме для которого неизвестна дата выхода эпизода
      showPlannedAirAnime = false,   //отображать онгоинги из списка "Запланировано"
      finalEpisodeDate = true,       //подсчитывать предполагаемую дату окончания онгоинга и отображать во всплывающей карточке (при наведении)
      gDebul = false;                //выводить отладочную информацию в консоль DevTools
//

const host = location.protocol + '//' + location.host;
const lang_data = {ru: {header: 'Ваше аниме',
                        episode: 'Эпизод',
                        progress: 'Прогресс:',
                        day: 'д',
                        hour: 'ч',
                        min: 'м',
                        emptyList: 'Список аниме пуст',
                        errorCatch: 'При обработке данных произошла ошибка. Попробуйте обновить страницу',
                        episodeIncrementProgress: 'Отправка...',
                        cacheResetAlert: 'Кэш расписания сброшен. Обновите страницу',
                        finalEpisode: 'Финальный эпизод'
                       },
                   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',
                        finalEpisode: 'Final episode'
                       },
                  };
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 == '/') {
        if ($("._ShikiAirTime__CustomStyles").length < 1) {
            var d = document.createElement('style');
            d.classList = "_ShikiAirTime__CustomStyles";
            d.innerHTML = `._ShikiAirTime__CalendarGridEntry .cover .image-decor>.text:before {background: none !important}
            ._ShikiAirTime__CalendarGridEntry .cover .image-decor>.text {text-align: center !important}
            ._ShikiAirTime__PreAirDate {display: none !important;}
            ._ShikiAirTime__PreAirDate.Show {display: block !important;}
            ._ShikiAirTime__AddEpisode {display: none !important; padding-top: 5px !important; padding-bottom: 5px; font-size: 14px !important;}
            ._ShikiAirTime__AddEpisode:hover > .plus {font-weight: bold !important;}
            ._ShikiAirTime__AddEpisode.Show {display: block !important}
            ._ShikiAirTime__CalendarGridContainer {display: grid; gap: ` + cGap + `px; grid-template-columns: repeat(auto-fill, ` + cGridItemWidth + `px);}`;
            $('body')[0].appendChild(d);
        }

        if ($("._ShikiAirTime__CalendarTooltip").length < 1) {
            var g = document.createElement('div');
            g.classList = '_ShikiAirTime__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].classList.add("_ShikiAirTime__MainContainer");
        $("._ShikiAirTime__MainContainer").find(".subheadline").addClass("_ShikiAirTime__HeaderLinkContainer");

        $('.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';
        HideCustomTooltip();
    }
}

function InitDataFetch() {
    try{
        AnimeList.sort(compareNames); //2.9 сортируем по названию
        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' || AnimeList[a].anime.status == 'anons') {
                    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' || AnimeList[i].anime.status == 'anons') {
                        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;
        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' || AnimeList[b].anime.status == 'anons')) {
                    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 _ShikiAirTime__CalendarGridEntry" itemtype="http://schema.org/Movie" style="border-bottom: ` + bSize + `px solid ` + bColor + `; position: relative" title_name="`
                            + AnimeList[b].anime.name.replace(/["]/g, '&#34;') + `" title_ru="` + AnimeList[b].anime.russian.replace(/["]/g, '&#34;') + `" ` +
                            `episodes_watched="` + AnimeList[b].episodes + `" episodes="` + AnimeList[b].anime.episodes + `" episodes_aired="` +
                            (CalendarList[a].data.Media.nextAiringEpisode.episode != null ? (CalendarList[a].data.Media.nextAiringEpisode.episode - 1) : 10000) + `" next_episode_airing_at="` +
                            (CalendarList[a].data.Media.nextAiringEpisode.airingAt != null ? (CalendarList[a].data.Media.nextAiringEpisode.airingAt - 1) : 0) +`"
                            anime_rate_id="` + AnimeList[b].id + `" clOngoing="` + clOngoing + `" clOngoingLaze="` + clOngoingLaze + `" is_ongoing=true>
                            <a class="cover anime-tooltip-processed" href="` + host + AnimeList[b].anime.url + `">
                                <span class="image-decor">
                                    <span class="image-cutter _ShikiAirTime__CalendatGridEntryImageContainer">
                                        <img alt="` + (TitleLang == 'ru' ? AnimeList[b].anime.russian.replace(/["]/g, '&#34;') : AnimeList[b].anime.name.replace(/["]/g, '&#34;')) + `" src="` + AnimeList[b].anime.image.preview + `">
                                    </span>
                                    <div class="text _ShikiAirTime__PreAirDate Show">` + lang_data[SiteLang].episode + ` ` + CalendarList[a].data.Media.nextAiringEpisode.episode + `<br>` + CalcEndTime(tTime) + `</div>
                                    <div class="text _ShikiAirTime__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++;

                    //2.7
                    var episodes_aired = 0;
                    if (AnimeList[c].anime.status == 'ongoing') {
                        episodes_aired = AnimeList[c].anime.episodes_aired == null ? 0 : AnimeList[c].anime.episodes_aired;
                    } else {
                        episodes_aired = AnimeList[c].anime.episodes == 0 ? '?' : AnimeList[c].anime.episodes;
                    }

                    tInner += `
                        <article class="b-catalog_entry _ShikiAirTime__CalendarGridEntry" itemtype="http://schema.org/Movie" style=" position: relative" title_name="`
                        + AnimeList[c].anime.name.replace(/["]/g, '&#34;') + `" title_ru="` + AnimeList[c].anime.russian.replace(/["]/g, '&#34;') + `" ` +
                        `episodes_watched="` + AnimeList[c].episodes + `" episodes="` + AnimeList[c].anime.episodes + `" episodes_aired="` + episodes_aired + `"
                            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="` + (TitleLang == 'ru' ? AnimeList[c].anime.russian.replace(/["]/g, '&#34;') : AnimeList[c].anime.name.replace(/["]/g, '&#34;')) + `" src="` + AnimeList[c].anime.image.preview + `">
                                    </span>
                                    <div class="text _ShikiAirTime__AddEpisode">` + AnimeList[c].episodes + ` <span class="plus">+</span></div>
                                </span>
                            </a>
                        </article>
                        `;
                }
            }
        }

        if (allCount > 0) {
            $('.block2')[0].childNodes[1].classList = '_ShikiAirTime__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 = $('._ShikiAirTime__CalendarGridEntry');
            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('._ShikiAirTime__AddEpisode').addClass('Show');
                        $(e.target.parentElement.parentElement).find('._ShikiAirTime__PreAirDate').removeClass('Show');
                    },
                    function (e) {
                        HideCustomTooltip();
                        $(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode').removeClass('Show');
                        $(e.target.parentElement.parentElement).find('._ShikiAirTime__PreAirDate').addClass('Show');
                    }
                )
            }
        }
        $('._ShikiAirTime__AddEpisode').click(function (e) {
            e.preventDefault();
            incEpisode($(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode')[0].parentElement.parentElement.parentElement.getAttribute('anime_rate_id'), $(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode')[0]);
        })
    }
}

function ShowCustomTooltip(e) {
    var AnimeElement = e.tagName == 'IMG' ? e.parentElement.parentElement.parentElement.parentElement : e.parentElement.parentElement.parentElement;
    var TooltipElement = $('._ShikiAirTime__CalendarTooltip')[0];
    TooltipElement.style.display = 'block';
    TooltipElement.style.top = AnimeElement.getBoundingClientRect().y + document.documentElement.scrollTop + 'px';

    TooltipElement.childNodes[0].innerHTML = (TitleLang == 'ru' ? AnimeElement.attributes.title_ru.nodeValue : AnimeElement.attributes.title_name.nodeValue) + '<br><br>';
    var EpisodesBehind = AnimeElement.attributes.episodes_aired.nodeValue - AnimeElement.attributes.episodes_watched.nodeValue;
    if (EpisodesBehind > 0) {
        TooltipElement.childNodes[0].innerHTML += (SiteLang == 'ru' ?
                                                             'Отставание на ' + EpisodesBehind + ' ' + GetLocalizedEpisodeHint(EpisodesBehind)
                                                             : EpisodesBehind + ' episode' + (EpisodesBehind > 1 ? 's' : '') + ' behind') + '<br><br>';
    }
    TooltipElement.childNodes[0].innerHTML += lang_data[SiteLang].progress + ' ' + AnimeElement.attributes.episodes_watched.nodeValue + '/' + (AnimeElement.attributes.episodes.nodeValue != 0 ? AnimeElement.attributes.episodes.nodeValue : '?');
    if (AnimeElement.getBoundingClientRect().x + AnimeElement.getBoundingClientRect().width + 2 + TooltipElement.childNodes[0].getBoundingClientRect().width + 10 < $(window).width()) {
        TooltipElement.style.left = AnimeElement.getBoundingClientRect().x + AnimeElement.getBoundingClientRect().width + 2 + 'px';
    } else {
        TooltipElement.style.left = AnimeElement.getBoundingClientRect().x - 2 - TooltipElement.childNodes[0].getBoundingClientRect().width + 'px';
    }

    //2.8
    if (finalEpisodeDate && AnimeElement.attributes.episodes.nodeValue != 0 && AnimeElement.attributes.is_ongoing.nodeValue == "true") {
        var endDate = new Date(
                (
                    GlobalServerTime + (60 * 60 * 24 * 7 * (AnimeElement.attributes.episodes.nodeValue - AnimeElement.attributes.episodes_aired.nodeValue - 1) + parseInt(AnimeElement.attributes.next_episode_airing_at.nodeValue) - GlobalServerTime)
                ) * 1000
            );
        TooltipElement.childNodes[0].innerHTML += "<br><br>" + lang_data[SiteLang].finalEpisode + " " + endDate.getDate() + "." + (endDate.getMonth() + 1).toString().padStart(2, "0") + "." + endDate.getFullYear();
    }
}

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() {
    $('._ShikiAirTime__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 compareNames(a, b) { //2.9 сортировка списка аниме по имени (для неонгоингов или без даты след. серии)
    a = (TitleLang == 'ru' ? a.anime.russian : a.anime.name);
    b = (TitleLang == 'ru' ? b.anime.russian : b.anime.name);
    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') {
                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)