ShikiLinker

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

2024-10-26 기준 버전입니다. 최신 버전을 확인하세요.

// ==UserScript==
// @name            ShikiLinker
// @description     Редирект-кнопка для Шикимори, которая перенаправляет на Anime365
// @description:en  Redirect button for Shikimori that redirects to Anime 365
// @namespace       https://shikimori.one/animes
// @match           https://shikimori.one/animes/*
// @connect         smotret-anime.online
// @grant           GM_xmlhttpRequest
// @icon            https://www.google.com/s2/favicons?domain=shikimori.me
// @author          Jogeer
// @license         MIT
// @version         2.3.0
// ==/UserScript==
"use strict"

const DEBUG = true
const PAGEURL = new RegExp(
  /^https?:\/\/shikimori\.o(?:ne|rg)\/animes\/[A-z]?(\d*)-(.*)$/
)

const A365URL = "https://smotret-anime.online/"
const A365API = `${A365URL}api/`
const SHIKIAPI = `https://${window.location.hostname}/api/`

const NYAASI = "https://nyaa.land/?"
const DISTRIB = "Erai-raws"

const PARENSSTYLES =
  "display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;margin-top:10px"
const CHILDSTYLES =
  "flex:1 1 auto;text-align:center;padding:5px;background:#18181b;color:white;margin: 0 10px"
const SPANSTYLES = "width:100%;text-align:center;"
const BASICLINKATTRS = [
  { attribute: "class", value: "link-button" },
  { attribute: "target", value: "_balnk" },
  { attribute: "style", value: CHILDSTYLES }
]

//#endregion

class ShikiLinker extends EventTarget {
  //#region Supports
  static BuildElement(element) {
    let attrs = ""

    element.attributes?.forEach(el => {
      let _val = ""

      el.value ? (_val = `=\"${el.value}\"`) : null

      attrs += `${el.attribute}${_val} `
    })

    return `<${element.tag} ${attrs}>${element.text}</${element.tag}>`
  }

  static ParseUserData() {
    return JSON.parse(document.querySelector("body").getAttribute("data-user"))
  }

  //#endregion

  //#region Key
  static async MakeRequest(url) {
    return await new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        headers: { "Content-type": "application/json" },
        url: `${url}`,
        onload: async data => {
          data = await JSON.parse(data.response)
          DEBUG ? console.log(`Request to ${url}`, data) : null
          resolve(data)
        },
        onerror: async data => {
          DEBUG ? console.error(`Request to ${url}`, data) : null
          reject(null)
        }
      })
    })
  }

  static async MakeApiRequests() {
    let matches = PAGEURL.exec(window.location.href)
    let userData = ShikiLinker.ParseUserData()
    DEBUG ? console.log("Parsing done:", matches, userData) : null

    let first = await ShikiLinker.MakeRequest(
      `${A365API}series?myAnimeListId=${matches[1]}`
    )
    let second = await ShikiLinker.MakeRequest(
      `${SHIKIAPI}v2/user_rates?user_id=${userData.id}&target_id=${matches[1]}&target_type=Anime`
    )
    DEBUG ? console.log("Req done:", first, second) : null

    return [first, second]
  }
  //#endregion

  static async AddParentContainer(toSelector) {
    if (document.querySelector("#shikilinker-inject")) {
      DEBUG ? console.log("Parent container already exist") : null
      return false
    }

    let documentDom = document.querySelector(toSelector)
    documentDom?.insertAdjacentHTML(
      "beforeend",
      ShikiLinker.BuildElement({
        tag: "div",
        attributes: [
          { attribute: "class", value: "watch-online" },
          { attribute: "id", value: "shikilinker-inject" },
          { attribute: "style", value: PARENSSTYLES }
        ],
        text: ""
      })
    )
    DEBUG ? console.log("Parent container has been added") : null
    return true
  }

  static async AddElement(element) {
    let documentDom = document.querySelector("#shikilinker-inject")
    DEBUG ? console.log("Add:", element, " to:", documentDom) : null
    documentDom?.insertAdjacentHTML(
      "beforeend",
      ShikiLinker.BuildElement(element)
    )
  }

  static async HaveNonWatchedEpisode(a365Data, shikiData) {
    DEBUG ? console.log("API data:", a365Data, shikiData) : null

    if (!shikiData || !shikiData.episodes) {
      shikiData = JSON.parse('{"status": "none", "episodes": 0}')
    }

    if (
      ["completed", "dropped"].includes(shikiData.status) ||
      shikiData.episodes >= a365Data.episodes.length
    ) {
      return false
    }

    return true
  }

  static SetupEventListeners() {
    let target = document.querySelector(".rate-number > span.item-add")
    target?.addEventListener("click", () => {
      DEBUG ? console.log("Got refresh event, do refresh...") : null
      setTimeout(() => {
        ShikiLinker.RefreshGoToEpisodeButton()
      }, 100)
    })

    DEBUG ? console.log("Setted events") : null
  }

  static async RefreshGoToEpisodeButton() {
    let button = document.querySelector("#shikilinker-a365-gtebtn")
    let datas = await ShikiLinker.MakeApiRequests()

    let _a365Data = datas[0]
    let _shikiData = datas[1]

    if (
      await ShikiLinker.HaveNonWatchedEpisode(_a365Data.data[0], _shikiData[0])
    ) {
      button.innerHTML = ShikiLinker.BuildElement({
        tag: "a",
        attributes: [
          {
            attribute: "href",
            value: `${A365URL}episodes/${
              _a365Data.data[0].episodes[_shikiData[0].episodes].id
            }`
          },
          { attribute: "id", value: "shikilinker-a365-gtebtn" }
        ].concat(BASICLINKATTRS),
        text: `${_shikiData[0].episodes + 1} ep`
      })
    } else {
      button.remove()
    }

    DEBUG ? console.log("Refreshed") : null
    ShikiLinker.SetupEventListeners()
  }

  static async Execute() {
    ShikiLinker.SetupEventListeners()

    if (!(await ShikiLinker.AddParentContainer(".c-info-right"))) {
      DEBUG ? console.log("Block already exist") : null
      return
    }

    let domObject = document.querySelector("#shikilinker-inject")
    let datas = await ShikiLinker.MakeApiRequests()

    let _a365Data = datas[0]
    let _shikiData = datas[1]

    let elements = [
      {
        tag: "a",
        attributes: [
          { attribute: "href", value: _a365Data.data[0].url }
        ].concat(BASICLINKATTRS),
        text: "Anime 365"
      }
    ]

    if (
      await ShikiLinker.HaveNonWatchedEpisode(_a365Data.data[0], _shikiData[0])
    ) {
      DEBUG ? console.log("Have nonwatched ep") : null
      try {
        elements.push({
          tag: "a",
          attributes: [
            {
              attribute: "href",
              value: `${A365URL}episodes/${
                _a365Data.data[0].episodes[_shikiData[0].episodes].id
              }`
            },
            { attribute: "id", value: "shikilinker-a365-gtebtn" }
          ].concat(BASICLINKATTRS),
          text: `${_shikiData[0].episodes + 1} ep`
        })
      } catch (error) {
        DEBUG ? console.log("Have NWE, but:", error) : null
      }
    }
    elements.push({
      tag: "a",
      attributes: [
        {
          attribute: "href",
          value: `${NYAASI}u=${DISTRIB}&q=${
            document.querySelector('meta[property="og:title"]').content
          }`
        }
      ].concat(BASICLINKATTRS),
      text: "Nyaa.si"
    })
    elements.push({
      tag: "span",
      attributes: [{ attribute: "style", value: SPANSTYLES }],
      text: "ShikiLinker"
    })

    elements.forEach(element => {
      ShikiLinker.AddElement(element)
    })
  }
}

function ready(func) {
  //var document = document;
  document.addEventListener("turbolinks:load", func)

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

ready(ShikiLinker.Execute)

// to js on: https://www.typescriptlang.org/