您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Редирект-кнопка для 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);