ShikiLinker

Редирект-кнопка для Shikimori, которая перенаправляет на Anime365

// ==UserScript==
// @name            ShikiLinker
// @description     Редирект-кнопка для Shikimori, которая перенаправляет на Anime365
// @description:en  Redirect button for Shikimori that redirects to Anime 365
// @icon            https://www.google.com/s2/favicons?sz=64&domain=shikimori.one
// @namespace       https://shikimori.one/animes
// @match           https://shikimori.one/animes/* 
// @connect         smotret-anime.org
// @grant           GM_xmlhttpRequest
// @author          Jogeer
// @license         MIT
// @version         3.4.2
// ==/UserScript==
"use strict";
const DEBUG = true;
const SCRIPT = "ShikiLinker";
const ANIME365 = "smotret-anime.org";
const ORIGIN_KEY = "en";
const PAGEURL = new RegExp(/^https?:\/\/shikimori\.o(?:ne|rg)\/animes\/[A-z]?(\d*)-(.*)$/);
//#region Const
const LOCALE = (navigator.language || navigator.userLanguage).split('-')[0] || ORIGIN_KEY;
const STYLES = {
    class: {
        parent: ".c-about .c-info-right",
        main: `${SCRIPT}`,
        button: `${SCRIPT}-btn`,
        increment: ".item-add.increment",
    },
    container: {
        parent: "display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;margin-top:10px",
        button: "text-align:center;background:#18181b;color:white;margin: 0 10px;user-select:none;",
        buttonTo: "flex:1 1 auto;padding:5px;max-width: 160px;",
        buttonEp: "flex:0 1 auto;padding:5px 15px;",
        span: "width:100%;text-align:center;",
    },
};
const CONST = {
    url: {
        shikimori: `https://${window.location.hostname}/`,
        anime365: `https://${ANIME365}/`,
    },
    apiUrl: {
        shikimori: `https://${window.location.hostname}/api/`,
        anime365: `https://${ANIME365}/api/`,
    },
    headers: {
        default: { "Content-type": "application/json" },
    },
    tags: {
        parentElement: [
            { key: "id", value: STYLES.class.main },
            { key: "class", value: "watch-online" },
            { key: "style", value: STYLES.container.parent },
        ],
        toTitle: [
            { key: "class", value: "link-button" },
            { key: "target", value: "_blank" },
            {
                key: "style",
                value: `${STYLES.container.button}${STYLES.container.buttonTo}`,
            },
        ],
        toEpisode: [
            { key: "class", value: "link-button" },
            { key: "target", value: "_blank" },
            { key: "id", value: STYLES.class.button },
            {
                key: "style",
                value: `${STYLES.container.button}${STYLES.container.buttonEp}`,
            },
        ],
        infoSpan: [{ key: "style", value: STYLES.container.span }],
    },
};
const translations = {
    ru: {
        button: {
            main: {
                text: "Anime 365",
            },
            episode: {
                first: "Первая серия",
                origin: "Серия",
                null: "🚫",
            },
        },
    },
    en: {
        button: {
            main: {
                text: "Anime 365",
            },
            episode: {
                first: "First Episode",
                origin: "Episode",
                null: "🚫",
            },
        },
    },
};
//#endregion
//#region Utils
function LOG(...atrs) {
    const prefix = `%c [${SCRIPT}] `;
    const style = "color:#419541;background:black;";
    DEBUG && console.log(prefix, style, ...atrs);
}
function ns(key) {
    var _a;
    const [lang, _keys] = key.split(":");
    const keys = _keys.split(".");
    const translation = keys.reduce((obj, k) => obj && obj[k], translations[(_a = lang !== null && lang !== void 0 ? lang : ORIGIN_KEY) !== null && _a !== void 0 ? _a : "en"]);
    return translation || key;
}
//#endregion
//#region Main
class ShikiLinker {
    constructor() {
        this._animeId = this._GetAinimeId();
        this._shikiUserData = this._GetUserData();
        this._GetShikimoriApiData();
        this._GetAnime365ApiData();
        return this;
    }
    _GetAinimeId() {
        return PAGEURL.exec(window.location.href)[1];
    }
    _GetUserData() {
        return JSON.parse(document.body.getAttribute("data-user"));
    }
    _GetUserId() {
        return this._shikiUserData.id;
    }
    //#region REQUESTS
    async _MakeRequest(url) {
        return GM.xmlHttpRequest({
            method: "GET",
            headers: CONST.headers.default,
            url: url,
        });
    }
    async _GetShikimoriApiData() {
        LOG("> GetShikimoriApiData > {IN}");
        return this._MakeRequest(`${CONST.apiUrl.shikimori}v2/user_rates?user_id=${this._GetUserId()}&target_id=${this._animeId}&target_type=Anime`).then(async (data) => {
            this._shikiApiData = JSON.parse(data.response)[0];
            LOG("> _GetShikimoriApiData > DATA:", this._shikiApiData);
        }, (error) => {
            console.error(error);
            return null;
        });
    }
    async _GetAnime365ApiData() {
        LOG("> GetAnime365ApiData > {IN}");
        return this._MakeRequest(`${CONST.apiUrl.anime365}series?myAnimeListId=${this._animeId}`).then(async (data) => {
            this._anime365ApiData = JSON.parse(data.response).data[0];
            LOG("> _GetAnime365ApiData > DATA:", this._anime365ApiData);
            this._UpdateGotoButton();
        }, (error) => {
            console.error(error);
            return null;
        });
    }
    //#endregion
    async _UpdateGotoButton() {
        LOG("> UpdateGotoButton > {IN}");
        let element = document.querySelector(`#${STYLES.class.main} a`);
        let timer = setInterval(() => {
            if (this._anime365ApiData) {
                //TODO: Костыль для сервака 365
                let matches = RegExp("(?:https://)(?:.*?/)(.*)").exec(this._anime365ApiData.url);
                element === null || element === void 0 ? void 0 : element.setAttribute("href", `${CONST.url.anime365}${matches[1]}`);
                clearInterval(timer);
            }
        }, 1000);
    }
    async _UpdateEpisodeButton() {
        LOG("> UpdateEpisodeButton > {IN}");
        this.SetupEventListener();
        let element = document.querySelector(`#${STYLES.class.button}`);
        let timer = setInterval(() => {
            let data = { href: "#", text: "--" };
            let _href = `${CONST.url.anime365}episodes/`;
            if (this._anime365ApiData) {
                let episodes = this._anime365ApiData.episodes.filter((ep) => ["ona", "ova", "movie", "tv"].includes(ep.episodeType));
                if (this._shikiApiData) {
                    // FIX: dropped
                    if (["completed"].includes(this._shikiApiData.status)) {
                        LOG("> UpdateEpisodeButton > {INFO}", { ifReason: ["completed"] });
                        data.href = `${_href}${episodes[0].id}`;
                        data.text = ns(`${LOCALE}:button.episode.first`);
                    }
                    else if (episodes.length > this._shikiApiData.episodes) {
                        LOG("> UpdateEpisodeButton > {INFO}", { ifReason: "a365.length > shiki.episodes" });
                        data.href = `${_href}${episodes[this._shikiApiData.episodes].id}`;
                        data.text = `${this._shikiApiData.episodes + 1} ${ns(`${LOCALE}:button.episode.origin`)}`;
                    }
                    else if (episodes.length == this._shikiApiData.episodes) {
                        LOG("> UpdateEpisodeButton > {INFO}", { ifReason: "a365.length == shiki.episodes" });
                        data.href = `${_href}${episodes[this._shikiApiData.episodes - 1].id}`;
                        data.text = `${this._shikiApiData.episodes} ${ns(`${LOCALE}:button.episode.origin`)}`;
                    }
                }
                else {
                    data.href = `${_href}${episodes[0].id}`;
                    data.text = ns(`${LOCALE}:button.episode.first`);
                }
                element.setAttribute("href", data.href);
                element.textContent = data.text;
                clearInterval(timer);
            }
        }, 1000);
    }
    SetupEventListener() {
        var _a;
        //TODO: Следить за запросом, по отправке - обновить, пока костыльком подпёр
        (_a = document.querySelector(STYLES.class.increment)) === null || _a === void 0 ? void 0 : _a.addEventListener("click", async () => {
            LOG("Got refresh event, do refresh...");
            await this._GetShikimoriApiData();
            this._UpdateEpisodeButton();
        });
    }
    //#region Build
    _CreateSubElement(tag, attributes) {
        let element = document.createElement(tag);
        attributes === null || attributes === void 0 ? void 0 : attributes.forEach((attr) => {
            element.setAttribute(attr.key, attr.value);
        });
        return element;
    }
    _CreateChildElements() {
        LOG("(BUILD) > CreateChildElements > {IN}");
        let anime365Button = this._CreateSubElement("a", CONST.tags.toTitle);
        anime365Button.textContent = ns(`${LOCALE}:button.main.text`);
        let goToEpisodeButton = this._CreateSubElement("a", CONST.tags.toEpisode);
        goToEpisodeButton.textContent = ns(`${LOCALE}:button.episode.null`);
        let addonInfoSpan = this._CreateSubElement("span", CONST.tags.infoSpan);
        addonInfoSpan.textContent = STYLES.class.main;
        return [anime365Button, goToEpisodeButton, addonInfoSpan];
    }
    _CreateParentElement() {
        LOG("(BUILD) > CreateParentElement > {IN}");
        let element = this._CreateSubElement("div", CONST.tags.parentElement);
        return element;
    }
    _CreateElement() {
        LOG("(BUILD) > CreateElement > {IN}");
        const target = document.querySelector(STYLES.class.parent);
        const parent = this._CreateParentElement();
        const childs = this._CreateChildElements();
        childs.forEach((ch) => parent.appendChild(ch));
        return target.appendChild(parent);
    }
    //#endregion
    async _Update() {
        LOG("> Update > {IN}");
        this._UpdateEpisodeButton();
        return this;
    }
    async Execute() {
        LOG("> Execute > {IN}");
        const element = document.querySelector(`#${STYLES.class.main}`);
        if (!element) {
            this._CreateElement();
        }
        return this._Update();
    }
}
//#endregion
//#region Support
function GotUpdateReaction(func) {
    document.addEventListener("turbolinks:load", func);
    if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
        func();
    }
    else {
        document.addEventListener("DOMContentLoaded", func);
    }
}
//#endregion
//#region Init
const _init = () => {
    LOG("Script init...");
    const obj = new ShikiLinker();
    obj.Execute();
    LOG("Script inited");
};
GotUpdateReaction(_init);