Kemono Browser

Kemono links everywhere!

От 27.12.2023. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Kemono Browser
// @namespace    Violentmonkey Scripts
// @version      1.0
// @description  Kemono links everywhere!
// @author       zWolfrost
// @license      MIT

// @match        https://*.vimeo.com/*
// @match        https://*.kemono.su/*
// @match        https://*.patreon.com/*
// @match        https://*.fanbox.cc/*
// @match        https://*.pixiv.net/*
// @match        https://*.fantia.jp/*
// @match        https://*.boosty.to/*
// @match        https://*.dlsite.com/*
// @match        https://*.gumroad.com/*
// @match        https://*.subscribestar.com/*
// @match        https://*.subscribestar.adult/*
// @match        https://*.kemono.su/*

// @icon         https://kemono.su/static/favicon.ico
// @grant        GM.xmlHttpRequest
// ==/UserScript==
"use strict"


const PROMPT_BTN_ID = "kemono-url";
const UPDATE_POLLING_MS = 222;


const badgePresets = {
   found: {stateText: "Found", textColor: "white", backgroundColor: "green"},
   incomplete: {stateText: "Incomplete", textColor: "black", backgroundColor: "gold"},
   missing: {stateText: "Missing", textColor: "white", backgroundColor: "red"},
}


document.onreadystatechange = () =>
{
   if (document.readyState == "complete")
   {
      setInterval(function()
      {
         if (this.lastPathStr !== location.pathname || this.lastQueryStr !== location.search || this.lastPathStr === null || this.lastQueryStr === null)
         {
            this.lastPathStr = location.pathname;
            this.lastQueryStr = location.search;
            main();
         }
      }, UPDATE_POLLING_MS);
   }
}

async function main()
{
   document.getElementById(PROMPT_BTN_ID)?.remove()
   console.log(document.readyState)

   const domain = window.location.hostname.split(".").slice(-2).join(".")

   const url = await Promise.resolve(domainMethods[domain]?.());
   console.log(url)

   const state = await getCreatorState(url);

   promptURL(url, badgePresets[state])
}


function promptURL(url, {stateText, textColor, backgroundColor})
{
   const btn = document.createElement("a")

   btn.id = PROMPT_BTN_ID;
   btn.innerText = "Kemono Creator: " + stateText;

   btn.style.cssText = `
      position: fixed;
      display: block;
      bottom: 5px;
      right: 5px;
      z-index: 9999;

      font-family: arial;
      font-weight: bold;
      font-size: 20px
      line-height: normal;
      border: 2px solid black;
      border-radius: 5px;
      padding: 2px;
      margin: 0px;
      cursor: pointer;
      text-decoration: none;

      color: ${textColor};
      background-color: ${backgroundColor};
   `;

   btn.target = "_blank";
   btn.href = url;

   document.body.prepend(btn)
}



async function getCreatorState(url)
{
   if (url)
   {
      const response = await new Promise(resolve =>
      {
         GM.xmlHttpRequest({
            url: url,
            method: "HEAD",
            onload: resolve
         });
      });

      switch (response.finalUrl)
      {
         case url: return "found";
         case "https://kemono.su/artists": return "missing"
         default: return "incomplete";
      }
   }
}





const domainMethods = {
   "patreon.com": getRedirectURLFromPatreon,
   "fanbox.cc": getRedirectURLFromFanbox,
   "pixiv.net": getRedirectURLFromPixiv,
   "fantia.jp": getRedirectURLFromFantia,
   "boosty.to": getRedirectURLFromBoosty,
   "dlsite.com": getRedirectURLFromDLsite,
   "gumroad.com": getRedirectURLFromGumroad,
   "subscribestar.com": getRedirectURLFromSubscribeStar,
   "subscribestar.adult": getRedirectURLFromSubscribeStar
}



function getRedirectURLFromPatreon()
{
   function getPatreonUserID() //html
   {
      const userIDPrefix = `"creator":{"data":{"id":"`
      const userIDSuffix = `"`
      return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
   }
   function getPatreonPostID() //url
   {
      const postIDPrefix = `posts`
      const postIDInternalPrefix = `-`
      return getURLPathAfter(postIDPrefix)?.split(postIDInternalPrefix).at(-1) ?? null
   }

   return getRedirectURL({
      service: "patreon",
      userID: getPatreonUserID(),
      postID: getPatreonPostID()
   })
}

async function getRedirectURLFromFanbox()
{
   function getFanboxCreatorID() //url
   {
      const creatorIDPrefix1st = `www.fanbox.cc`
      const creatorIDInternalPrefix1st = `@`
      const creatorIDSuffix2nd = `.`

      let creatorID = getURLPathAfter(creatorIDPrefix1st)?.split(creatorIDInternalPrefix1st)[1] ?? window.location.hostname.split(creatorIDSuffix2nd)[0]

      return creatorID == "www" ? null : creatorID
   }


   function getFanboxUserID() //html
   {
      const userIDPrefix = `creator/`
      const userIDSuffix = `/`
      return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
   }
   async function getFanboxUserID2nd() //url (api)
   {
      const creatorID = getFanboxCreatorID()
      if (!creatorID) return null

      const creatorFetch = await new Promise(resolve =>
      {
         GM.xmlHttpRequest({
            url: `https://api.fanbox.cc/creator.get?creatorId=${creatorID}`,
            method: "GET",
            headers: { Origin: "https://fanbox.cc" },
            onload: resolve
         });
      });

      return JSON.parse(creatorFetch.responseText)?.body?.user?.userId ?? null
   }
   function getFanboxPostID() //url
   {
      const postIDPrefix = `posts`
      return getURLPathAfter(postIDPrefix)
   }

   return getRedirectURL({
      service: "fanbox",
      userID: getFanboxUserID() ?? await getFanboxUserID2nd(),
      postID: getFanboxPostID()
   })
}

function getRedirectURLFromPixiv()
{
   function getPixivUserID() //url
   {
      const userIDPrefix = `users`
      return getURLPathAfter(userIDPrefix)
   }
   function getPixivUserID2nd() //html
   {
      const userIDPrefix = `users/`
      const userIDSuffix = `"`
      return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
   }

   return getRedirectURL({
      service: "fanbox",
      userID: getPixivUserID() ?? getPixivUserID2nd()
   })
}

function getRedirectURLFromFantia()
{
   function getFantiaUserID() //url
   {
      const userIDPrefix = `fanclubs`
      return getURLPathAfter(userIDPrefix)
   }
   function getFantiaUserID2nd() //html
   {
      const userIDPrefix = `fanclubs/`
      const userIDSuffix = `"`
      return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
   }
   function getFantiaPostID() //url
   {
      const postIDPrefix = `posts`
      return getURLPathAfter(postIDPrefix)
   }

   return getRedirectURL({
      service: "fantia",
      userID: getFantiaUserID() ?? getFantiaUserID2nd(),
      postID: getFantiaPostID()
   })
}

function getRedirectURLFromBoosty()
{
   function getBoostyUserID() //url
   {
      const userIDPrefix = `boosty.to`
      return getURLPathAfter(userIDPrefix)
   }
   function getBoostyPostID() //url
   {
      const postIDPrefix = `posts`
      return getURLPathAfter(postIDPrefix)
   }

   return getRedirectURL({
      service: "boosty",
      userID: getBoostyUserID(),
      postID: getBoostyPostID()
   })
}

function getRedirectURLFromDLsite()
{
   function getDLsiteUserID() //html
   {
      const userIDPrefix = `maker_id/`
      const userIDSuffix = `.`
      return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
   }
   function getDLsitePostID() //url
   {
      const postIDPrefix = `product_id`
      const postIDMatch = `\\d`
      let postID = getURLPathAfter(postIDPrefix).match(new RegExp(postIDMatch, "g")).join("")
      return postID ? "RE" + postID : null
   }

   return getRedirectURL({
      service: "dlsite",
      userID: getDLsiteUserID(),
      postID: getDLsitePostID()
   })
}

function getRedirectURLFromGumroad()
{
   function getGumroadUserID() //html
   {
      const elementSelector = `script.js-react-on-rails-component`
      const userIDPrefix = `id":"`
      const userIDSuffix = `"`
      return getStringBetween(document.querySelector(elementSelector).innerText, userIDPrefix, userIDSuffix)
   }
   function getGumroadPostID() //url
   {
      const postIDPrefix = `l`
      return getURLPathAfter(postIDPrefix)
   }

   return getRedirectURL({
      service: "gumroad",
      userID: getGumroadUserID(),
      postID: getGumroadPostID()
   })
}

function getRedirectURLFromSubscribeStar()
{
   function getSubScribeStarUserID() //html
   {
      const elementSelector = `img[data-type="avatar"]`
      return document.querySelector(elementSelector)?.alt.toLowerCase() ?? null
   }
   function getSubScribeStarPostID() //url
   {
      const postIDPrefix = `posts`
      return getURLPathAfter(postIDPrefix)
   }

   return getRedirectURL({
      service: "subscribestar",
      userID: getSubScribeStarUserID(),
      postID: getSubScribeStarPostID()
   })
}



function getStringBetween(string, prefix, suffix)
{
   let begIndex = string.indexOf(prefix)
   if (begIndex == -1) return null
   else begIndex += prefix.length

   let endIndex = string.indexOf(suffix, begIndex)
   if (endIndex == -1) endIndex = undefined;
   let result = string.slice(begIndex, endIndex)

   return result
}

function getURLPathAfter(string)
{
   let urlSplit = (window.location.hostname + window.location.pathname).split("/")
   let pathIndex = urlSplit.indexOf(string) + 1

   if (pathIndex == 0) return null

   return urlSplit[pathIndex]
}

function getRedirectURL({service, userID=null, postID=null} = {})
{
   let redirectURL = `https://kemono.su/${service}`

   if (userID)
   {
      redirectURL += `/user/${userID}`

      if (postID)
      {
         redirectURL += `/post/${postID}`
      }
   }
   else return null

   return redirectURL
}