- // ==UserScript==
- // @name Kemono Browser
- // @namespace Violentmonkey Scripts
- // @version 1.3.1
- // @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: 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]?.();
-
- 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"},
- }
-
-
- 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
- }