Kemono Browser

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

Από την 05/01/2024. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Kemono Browser
// @namespace    Violentmonkey Scripts
// @version      1.3.0
// @description  Adds a button at the bottom right of all kemono-supported creator websites (+ onlyfans) that redirects to the corresponding kemono 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: 9999 !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;

      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]?.(); /* await Promise.resolve(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
   {
      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"},
}


function promptButton(url, {stateText, textColor, backgroundColor})
{
   PROMPT_BTN.href = url;
   PROMPT_BTN.innerText = "Kemono Creator: " + 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
         });
      });

      switch (response.finalUrl)
      {
         case url: return "found";
         case "https://kemono.su/artists": case "https://coomer.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,
   "onlyfans.com": getRedirectURLFromOnlyFans,
}


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({
      domain: "kemono.su",
      service: "patreon",
      userID: getPatreonUserID(),
      postID: getPatreonPostID()
   })
}

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

   return getRedirectURL({
      domain: "kemono.su",
      service: "fanbox",
      userID: getFanboxUserID(),
      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({
      domain: "kemono.su",
      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({
      domain: "kemono.su",
      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({
      domain: "kemono.su",
      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({
      domain: "kemono.su",
      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({
      domain: "kemono.su",
      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({
      domain: "kemono.su",
      service: "subscribestar",
      userID: getSubScribeStarUserID(),
      postID: getSubScribeStarPostID()
   })
}

function getRedirectURLFromOnlyFans()
{
   function getOnlyFansUserID() //html
   {
      const elementSelector = `#content a.g-avatar`
      return document.querySelector(elementSelector)?.getAttribute("href").slice(1)
   }
   function getOnlyFansPostID() //url
   {
      const elementSelector = `#content div.b-post`
      return document.querySelector(elementSelector)?.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
}