您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a button at the bottom right of all kemono & coomer supported creator websites (but fansly) that redirects to the corresponding kemono/coomer page.
当前为
// ==UserScript== // @name Kemono Browser // @namespace Violentmonkey Scripts // @version 1.7.1 // @description Adds a button at the bottom right of all kemono & coomer supported creator websites (but fansly) 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/* // @match https://*.candfans.jp/* // @connect kemono.su // @connect coomer.su // @icon https://kemono.su/static/favicon.ico // @grant GM.xmlHttpRequest // @grant GM.getResourceUrl // @resource kemonoWhiteIcon https://i.postimg.cc/D0K6jqjV/icon.png // ==/UserScript== "use strict"; ///////////////// OPTIONS ///////////////// // time in milliseconds for the script to update the button. i generally don't recommend setting it to less than 100. const KEMONO_BTN_POLLING_MS = 222; // whether to open the url in a new tab by default. note that ctrl+clicking the button does the opposite of the default behavior. const OPEN_IN_NEW_TAB = true; // default button classes. remove them to customize the button! const KEMONO_BTN_CLASSES = ["include-icon", "include-text", "animate-click"] // kemono button css const KEMONO_BTN_ID = "kemono-btn"; const KEMONO_BTN_CSS = ` #${KEMONO_BTN_ID} { display: none !important; 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 !important; line-height: normal !important; text-decoration: none !important; cursor: pointer !important; } #${KEMONO_BTN_ID}[data-status-text] { display: flex !important; align-items: center !important; justify-content: center !important; gap: 2px !important; } #${KEMONO_BTN_ID}:hover { filter: brightness(90%); } #${KEMONO_BTN_ID}:active { filter: brightness(80%); } #${KEMONO_BTN_ID} img { display: none !important; } #${KEMONO_BTN_ID}.include-icon img { display: inline-block !important; width: 24px !important; height: 24px !important; margin: 0px 2px !important; } #${KEMONO_BTN_ID}.animate-click { 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; } #${KEMONO_BTN_ID}.animate-click:active { bottom: -4px; right: -4px; box-shadow: none; transition-property: bottom, right, box-shadow; transition-duration: 0.05s; transition-timing-function: ease-in-out; } #${KEMONO_BTN_ID}[data-status-text=found] { background-color: green; color: white; } #${KEMONO_BTN_ID}.include-text[data-status-text=found]::after { content: "creator: found"; } #${KEMONO_BTN_ID}[data-status-text=incomplete] { background-color: gold; color: black; } #${KEMONO_BTN_ID}[data-status-text=incomplete] img { filter: brightness(1) invert(1); } #${KEMONO_BTN_ID}.include-text[data-status-text=incomplete]::after { content: "creator: incomplete"; } #${KEMONO_BTN_ID}[data-status-text=missing] { background-color: red; color: white; } #${KEMONO_BTN_ID}.include-text[data-status-text=missing]::after { content: "creator: missing"; } #${KEMONO_BTN_ID}[data-status-text=pending] { background-color: gray; color: white; } #${KEMONO_BTN_ID}.include-text[data-status-text=pending]::after { content: "creator: pending..."; } #${KEMONO_BTN_ID}[data-status-text=error] { background-color: #444444; color: white; } #${KEMONO_BTN_ID}[data-status-text=error]::after { content: "creator: unknown (error " attr(data-status-code) ")"; } ` /////////////////////////////////////////// let lastKemonoURL = ""; const KEMONO_BTN = document.createElement("a"); initKemonoButton(); setInterval(updateKemonoButton, KEMONO_BTN_POLLING_MS); /////////// KEMONO BUTTON STUFF /////////// // initialize kemono button function initKemonoButton() { // append css to head const appendCSS = css => document.head.appendChild(document.createElement("style")).innerHTML = css; appendCSS(KEMONO_BTN_CSS); // set button attributes KEMONO_BTN.id = KEMONO_BTN_ID; KEMONO_BTN.target = OPEN_IN_NEW_TAB ? "_blank" : "_self"; KEMONO_BTN.draggable = false; // set button classes KEMONO_BTN.classList.add(...KEMONO_BTN_CLASSES); // add kemono icon const KEMONO_ICON = document.createElement("img"); GM.getResourceUrl("kemonoWhiteIcon").then(url => KEMONO_ICON.src = url); KEMONO_ICON.alt = "🐺"; KEMONO_BTN.prepend(KEMONO_ICON); // add ctrl+click event listener KEMONO_BTN.addEventListener("click", function(e) { if (e.ctrlKey) { e.preventDefault(); window.open(this.href, this.target == "_blank" ? "_self" : "_blank"); } }); // append button to body document.body.prepend(KEMONO_BTN); } // update kemono button function updateKemonoButton() { // get page domain const domain = window.location.hostname.split(".").slice(-2).join("."); // get the respective kemono creator url using the correct domain method (see "domainMethods" object) const url = domainMethods[domain]?.(); if (url) { if (url != lastKemonoURL) { // cache the url to prevent unnecessary requests lastKemonoURL = url; // set the button to the pending status, while waiting for kemono to respond KEMONO_BTN.href = url; Object.assign(KEMONO_BTN.dataset, {statusText: "pending", statusCode: 202}); // check if the creator exists on kemono & set the button status accordingly getCreatorStatus(url).then(status => Object.assign(KEMONO_BTN.dataset, status)); } } else { lastKemonoURL = null; delete KEMONO_BTN.dataset.statusText; delete KEMONO_BTN.dataset.statusCode; } } ///////// WEBPAGE INSPECTION STUFF ///////// const domainMethods = { "patreon.com": getKemonoURLFromPatreon, "fanbox.cc": getKemonoURLFromFanbox, "pixiv.net": getKemonoURLFromPixiv, "fantia.jp": getKemonoURLFromFantia, "boosty.to": getKemonoURLFromBoosty, "dlsite.com": getKemonoURLFromDLsite, "gumroad.com": getKemonoURLFromGumroad, "subscribestar.com": getKemonoURLFromSubscribeStar, "subscribestar.adult": getKemonoURLFromSubscribeStar, "onlyfans.com": getKemonoURLFromOnlyFans, "candfans.jp": getKemonoURLFromCandFans } // create the creator url with the given parameters function getKemonoURL({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; } function getKemonoURLFromPatreon() { return getKemonoURL({ domain: "kemono.su", service: "patreon", userID: getStringBetween(document.documentElement.innerHTML, `"creator":{"data":{"id":"`, `"`), postID: getURLNextPath(`posts`)?.split(`-`).at(-1) }) } function getKemonoURLFromFanbox() { return getKemonoURL({ domain: "kemono.su", service: "fanbox", userID: getStringBetween(document.querySelector(".styled__StyledUserIcon-sc-1upaq18-10[style]")?.style.backgroundImage, `user/`, `/`), postID: getURLNextPath(`posts`) }) } function getKemonoURLFromPixiv() { return getKemonoURL({ domain: "kemono.su", service: "fanbox", userID: getURLNextPath(`users`) ?? document.querySelector("button[data-gtm-user-id]")?.getAttribute("data-gtm-user-id") }) } function getKemonoURLFromFantia() { return getKemonoURL({ domain: "kemono.su", service: "fantia", userID: getURLNextPath(`fanclubs`) ?? getStringBetween(document.querySelector(".fanclub-header").innerHTML, `fanclubs/`, `"`), postID: getURLNextPath(`posts`) ?? getURLNextPath(`products`) }) } function getKemonoURLFromBoosty() { return getKemonoURL({ domain: "kemono.su", service: "boosty", userID: getURLNextPath(`boosty.to`), postID: getURLNextPath(`posts`) }) } function getKemonoURLFromDLsite() { let addRE = str => str ? "RE" + str : null return getKemonoURL({ domain: "kemono.su", service: "dlsite", userID: getStringBetween(document.documentElement.innerHTML, `maker_id/`, `.`), postID: addRE(getURLNextPath(`product_id`)?.replace(/\D/g, "")) }) } function getKemonoURLFromGumroad() { const idText = document.querySelector(`script.js-react-on-rails-component`)?.innerText return getKemonoURL({ domain: "kemono.su", service: "gumroad", userID: getStringBetween(idText, `"external_id":"`, `"`) ?? getStringBetween(idText, `"seller":{"id":"`, `"`), postID: document.querySelector(`meta[property="product:retailer_item_id"]`)?.content }) } function getKemonoURLFromSubscribeStar() { return getKemonoURL({ domain: "kemono.su", service: "subscribestar", userID: document.querySelector(`img[data-type="avatar"]`)?.alt.toLowerCase(), postID: getURLNextPath(`posts`) }) } function getKemonoURLFromOnlyFans() { return getKemonoURL({ domain: "coomer.su", service: "onlyfans", userID: document.querySelector(`#content .g-avatar[href]`)?.getAttribute("href").split(`/`)[1], postID: document.querySelector(`div.b-post:not(.is-not-post-page)`)?.id.replace(/\D/g, "") }) } function getKemonoURLFromCandFans() { return getKemonoURL({ domain: "coomer.su", service: "candfans", userID: getStringBetween(document.querySelector(`.v-main__wrap`).innerHTML, `user/`, `/`), postID: getURLNextPath(`show`) }) } // get string between a prefix and a suffix function getStringBetween(string, prefix, suffix) { if (string == null) return null; 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; } // get the url next path after a path. e.g.: if url is "https://example.com/after/this", getURLNextPath("after") === "this" function getURLNextPath(string) { let urlSplit = (window.location.hostname + window.location.pathname).split("/"); let pathIndex = urlSplit.indexOf(string); if (pathIndex == -1) return null; return urlSplit[pathIndex + 1]; } // check if the creator exists on kemono async function getCreatorStatus(url) { if (url) { const response = await request({method: "HEAD", url: url}); const redirectUrl = response?.finalUrl; //console.log(response); if (response.status == 200) { if (redirectUrl == url) return {statusText: "found", statusCode: 200}; else if (redirectUrl.includes("user")) return {statusText: "incomplete", statusCode: 303}; else if (redirectUrl.includes("artists")) return {statusText: "missing", statusCode: 404}; } else return {statusText: "error", statusCode: response.status}; } return {statusText: "error", statusCode: 400}; } // make a request function request(details) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ ...details, onload: resolve, onerror: reject }); }); }