// ==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);