Kemono Browser

Adds a button at the bottom right of all kemono-supported creator websites (+ onlyfans) that redirects to the corresponding kemono/coomer page.

Versión del día 07/01/2024. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Kemono Browser
// @namespace    Violentmonkey Scripts
// @version      1.4.0
// @description  Adds a button at the bottom right of all kemono-supported creator websites (+ onlyfans) that redirects to the corresponding kemono/coomer page.
// @author       zWolfrost
// @license      MIT

// @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://*.onlyfans.com/*

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



const UPDATE_POLLING_MS = 222; //time in milliseconds for the script to update the button. i generally don't recommend setting it to less than 100.
const PROMPT_BTN = document.createElement("a");
initPromptButton();

let lastURL = "";
setInterval(update, UPDATE_POLLING_MS);


function initPromptButton()
{
   const BTN_ANIMATIONS = true; //whether to enable button animations.
   const OPEN_IN_NEW_TAB = true; //whether to open the url in a new tab by default.

   const PROMPT_BTN_ID = "kemono-url-btn";

   const BTN_BASE_CSS = `
   #${PROMPT_BTN_ID}
   {
      display: none;
      position: fixed !important;
      bottom: 0px;
      right: 0px;
      z-index: 10000000 !important;

      min-width: 0 !important;
      min-height: 0 !important;
      max-width: none !important;
      max-height: none !important;

      padding: 4px !important;
      margin: 12px !important;

      font-family: arial !important;
      font-weight: bold !important;
      font-size: 18px !important;
      text-transform: capitalize;

      line-height: normal !important;
      text-decoration: none !important;
      cursor: pointer !important;

      box-shadow: black 0.5px 0.5px, black 1px 1px, black 1.5px 1.5px, black 2px 2px, black 2.5px 2.5px, black 3px 3px, black 3.5px 3.5px, black 4px 4px;
   }

   #${PROMPT_BTN_ID}:hover
   {
      filter: brightness(90%);
   }
   `

   const BTN_ANIMATIONS_CSS = `
   #${PROMPT_BTN_ID}:active
   {
      bottom: -4px;
      right: -4px;

      box-shadow: none;

      filter: brightness(80%);

      animation: press 0.05s ease-in-out;
   }

   @keyframes press
   {
      from
      {
         bottom: 0px;
         right: 0px;
         box-shadow: black 0.5px 0.5px, black 1px 1px, black 1.5px 1.5px, black 2px 2px, black 2.5px 2.5px, black 3px 3px, black 3.5px 3.5px, black 4px 4px;
      }
      to
      {
         bottom: -4px;
         right: -4px;
      }
   }
   `

   appendCSS(BTN_BASE_CSS);
   if (BTN_ANIMATIONS) appendCSS(BTN_ANIMATIONS_CSS);

   PROMPT_BTN.id = PROMPT_BTN_ID;
   PROMPT_BTN.target = OPEN_IN_NEW_TAB ? "_blank" : "_self";

   document.body.prepend(PROMPT_BTN)
}

async function update()
{
   const domain = window.location.hostname.split(".").slice(-2).join(".")

   const url = domainMethods[domain]?.();

   if (url)
   {
      if (url != lastURL)
      {
         //console.log(url)

         lastURL = url

         promptButton(url, buttonPresets["pending"])

         const state = await getCreatorState(url)

         promptButton(url, buttonPresets[state])
      }
   }
   else
   {
      lastURL = null

      PROMPT_BTN.style.display = "none"
   }
}





const buttonPresets = {
   "found": {stateText: "found", textColor: "white", backgroundColor: "green"},
   "incomplete": {stateText: "incomplete", textColor: "black", backgroundColor: "gold"},
   "missing": {stateText: "missing", textColor: "white", backgroundColor: "red"},
   "pending": {stateText: "pending...", textColor: "white", backgroundColor: "gray"},
   "error": {stateText: "unknown (server error)", textColor: "white", backgroundColor: "gray"},
}


function promptButton(url, {stateText, textColor, backgroundColor})
{
   const serviceName = url.split("/")[3];

   PROMPT_BTN.href = url;
   PROMPT_BTN.innerText = `kemono ${serviceName}: ${stateText}`;

   PROMPT_BTN.style.color = textColor;
   PROMPT_BTN.style.backgroundColor = backgroundColor;

   PROMPT_BTN.style.display = "block";
}


function appendCSS(css)
{
   const style = document.createElement("style");
   style.appendChild(document.createTextNode(css));
   document.head.append(style);
}


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

      const redirectUrl = response?.finalUrl;

      if (redirectUrl == url) return "found";
      else if (redirectUrl.includes("user")) return "incomplete";
      else if (redirectUrl.includes("artists")) return "missing";
   }

   return "error"
}





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,
   "onlyfans.com": getRedirectURLFromOnlyFans,
}


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

   return getRedirectURL({
      domain: "kemono.su",
      service: "patreon",
      userID: getPatreonUserID(),
      postID: getPatreonPostID()
   })
}

function getRedirectURLFromFanbox()
{
   function getFanboxUserID() //html
   {
      return getStringBetween(document.documentElement.innerHTML, `creator/`, `/`)
   }
   function getFanboxPostID() //url
   {
      return getURLPathAfter(`posts`)
   }

   return getRedirectURL({
      domain: "kemono.su",
      service: "fanbox",
      userID: getFanboxUserID(),
      postID: getFanboxPostID()
   })
}

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "fanbox",
      userID: getPixivUserID() ?? getPixivUserID2nd()
   })
}

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

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

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "boosty",
      userID: getBoostyUserID(),
      postID: getBoostyPostID()
   })
}

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "dlsite",
      userID: getDLsiteUserID(),
      postID: getDLsitePostID()
   })
}

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "gumroad",
      userID: getGumroadUserID(),
      postID: getGumroadPostID()
   })
}

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "subscribestar",
      userID: getSubScribeStarUserID(),
      postID: getSubScribeStarPostID()
   })
}

function getRedirectURLFromOnlyFans()
{
   function getOnlyFansUserID() //html
   {
      return document.querySelector(`#content a.g-avatar`)?.getAttribute("href").slice(1)
   }
   function getOnlyFansPostID() //url
   {
      return document.querySelector(`#content div.b-post`)?.id.slice(7)
   }

   return getRedirectURL({
      domain: "coomer.su",
      service: "onlyfans",
      userID: getOnlyFansUserID(),
      postID: getOnlyFansPostID()
   })
}



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({domain, service, userID=null, postID=null} = {})
{
   let redirectURL = `https://${domain}/${service}`

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

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

   return redirectURL
}