Sleazy Fork is available in English.

Kemono Browser

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

ของเมื่อวันที่ 05-01-2024 ดู เวอร์ชันล่าสุด

  1. // ==UserScript==
  2. // @name Kemono Browser
  3. // @namespace Violentmonkey Scripts
  4. // @version 1.3.1
  5. // @description Adds a button at the bottom right of all kemono-supported creator websites (+ onlyfans) that redirects to the corresponding kemono/coomer page.
  6. // @author zWolfrost
  7. // @license MIT
  8.  
  9. // @match https://*.patreon.com/*
  10. // @match https://*.fanbox.cc/*
  11. // @match https://*.pixiv.net/*
  12. // @match https://*.fantia.jp/*
  13. // @match https://*.boosty.to/*
  14. // @match https://*.dlsite.com/*
  15. // @match https://*.gumroad.com/*
  16. // @match https://*.subscribestar.com/*
  17. // @match https://*.subscribestar.adult/*
  18. // @match https://*.onlyfans.com/*
  19.  
  20. // @icon https://kemono.su/static/favicon.ico
  21. // @grant GM.xmlHttpRequest
  22. // ==/UserScript==
  23. "use strict"
  24.  
  25.  
  26.  
  27. 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.
  28. const PROMPT_BTN = document.createElement("a");
  29. initPromptButton();
  30.  
  31. let lastURL = "";
  32. setInterval(update, UPDATE_POLLING_MS);
  33.  
  34.  
  35. function initPromptButton()
  36. {
  37. const BTN_ANIMATIONS = true; //whether to enable button animations.
  38. const OPEN_IN_NEW_TAB = true; //whether to open the url in a new tab by default.
  39.  
  40. const PROMPT_BTN_ID = "kemono-url-btn";
  41.  
  42. const BTN_BASE_CSS = `
  43. #${PROMPT_BTN_ID}
  44. {
  45. display: none;
  46. position: fixed !important;
  47. bottom: 0px;
  48. right: 0px;
  49. z-index: 9999 !important;
  50.  
  51. min-width: 0 !important;
  52. min-height: 0 !important;
  53. max-width: none !important;
  54. max-height: none !important;
  55.  
  56. padding: 4px !important;
  57. margin: 12px !important;
  58.  
  59. font-family: arial !important;
  60. font-weight: bold !important;
  61. font-size: 18px !important;
  62.  
  63. line-height: normal !important;
  64. text-decoration: none !important;
  65. cursor: pointer !important;
  66.  
  67. 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;
  68. }
  69.  
  70. #${PROMPT_BTN_ID}:hover
  71. {
  72. filter: brightness(90%);
  73. }
  74. `
  75.  
  76. const BTN_ANIMATIONS_CSS = `
  77. #${PROMPT_BTN_ID}:active
  78. {
  79. bottom: -4px;
  80. right: -4px;
  81.  
  82. box-shadow: none;
  83.  
  84. filter: brightness(80%);
  85.  
  86. animation: press 0.05s ease-in-out;
  87. }
  88.  
  89. @keyframes press
  90. {
  91. from
  92. {
  93. bottom: 0px;
  94. right: 0px;
  95. 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;
  96. }
  97. to
  98. {
  99. bottom: -4px;
  100. right: -4px;
  101. }
  102. }
  103. `
  104.  
  105. appendCSS(BTN_BASE_CSS);
  106. if (BTN_ANIMATIONS) appendCSS(BTN_ANIMATIONS_CSS);
  107.  
  108. PROMPT_BTN.id = PROMPT_BTN_ID;
  109. PROMPT_BTN.target = OPEN_IN_NEW_TAB ? "_blank" : "_self";
  110.  
  111. document.body.prepend(PROMPT_BTN)
  112. }
  113.  
  114. async function update()
  115. {
  116. const domain = window.location.hostname.split(".").slice(-2).join(".")
  117.  
  118. const url = domainMethods[domain]?.();
  119.  
  120. if (url)
  121. {
  122. if (url != lastURL)
  123. {
  124. //console.log(url)
  125.  
  126. lastURL = url
  127.  
  128. promptButton(url, buttonPresets["pending"])
  129.  
  130. const state = await getCreatorState(url)
  131.  
  132. promptButton(url, buttonPresets[state])
  133. }
  134. }
  135. else
  136. {
  137. lastURL = null
  138.  
  139. PROMPT_BTN.style.display = "none"
  140. }
  141. }
  142.  
  143.  
  144.  
  145.  
  146.  
  147. const buttonPresets = {
  148. "found": {stateText: "Found", textColor: "white", backgroundColor: "green"},
  149. "incomplete": {stateText: "Incomplete", textColor: "black", backgroundColor: "gold"},
  150. "missing": {stateText: "Missing", textColor: "white", backgroundColor: "red"},
  151. "pending": {stateText: "Pending...", textColor: "white", backgroundColor: "gray"},
  152. }
  153.  
  154.  
  155. function promptButton(url, {stateText, textColor, backgroundColor})
  156. {
  157. PROMPT_BTN.href = url;
  158. PROMPT_BTN.innerText = "Kemono Creator: " + stateText;
  159.  
  160. PROMPT_BTN.style.color = textColor;
  161. PROMPT_BTN.style.backgroundColor = backgroundColor;
  162.  
  163. PROMPT_BTN.style.display = "block";
  164. }
  165.  
  166.  
  167. function appendCSS(css)
  168. {
  169. const style = document.createElement("style");
  170. style.appendChild(document.createTextNode(css));
  171. document.head.append(style);
  172. }
  173.  
  174.  
  175. async function getCreatorState(url)
  176. {
  177. if (url)
  178. {
  179. const response = await new Promise(resolve =>
  180. {
  181. GM.xmlHttpRequest({
  182. url: url,
  183. method: "HEAD",
  184. onload: resolve
  185. });
  186. });
  187.  
  188. switch (response.finalUrl)
  189. {
  190. case url: return "found";
  191. case "https://kemono.su/artists": case "https://coomer.su/artists": return "missing";
  192. default: return "incomplete";
  193. }
  194. }
  195. }
  196.  
  197.  
  198.  
  199.  
  200.  
  201. const domainMethods = {
  202. "patreon.com": getRedirectURLFromPatreon,
  203. "fanbox.cc": getRedirectURLFromFanbox,
  204. "pixiv.net": getRedirectURLFromPixiv,
  205. "fantia.jp": getRedirectURLFromFantia,
  206. "boosty.to": getRedirectURLFromBoosty,
  207. "dlsite.com": getRedirectURLFromDLsite,
  208. "gumroad.com": getRedirectURLFromGumroad,
  209. "subscribestar.com": getRedirectURLFromSubscribeStar,
  210. "subscribestar.adult": getRedirectURLFromSubscribeStar,
  211. "onlyfans.com": getRedirectURLFromOnlyFans,
  212. }
  213.  
  214.  
  215. function getRedirectURLFromPatreon()
  216. {
  217. function getPatreonUserID() //html
  218. {
  219. const userIDPrefix = `"creator":{"data":{"id":"`
  220. const userIDSuffix = `"`
  221. return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
  222. }
  223. function getPatreonPostID() //url
  224. {
  225. const postIDPrefix = `posts`
  226. const postIDInternalPrefix = `-`
  227. return getURLPathAfter(postIDPrefix)?.split(postIDInternalPrefix).at(-1) ?? null
  228. }
  229.  
  230. return getRedirectURL({
  231. domain: "kemono.su",
  232. service: "patreon",
  233. userID: getPatreonUserID(),
  234. postID: getPatreonPostID()
  235. })
  236. }
  237.  
  238. function getRedirectURLFromFanbox()
  239. {
  240. function getFanboxUserID() //html
  241. {
  242. const userIDPrefix = `creator/`
  243. const userIDSuffix = `/`
  244. return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
  245. }
  246. function getFanboxPostID() //url
  247. {
  248. const postIDPrefix = `posts`
  249. return getURLPathAfter(postIDPrefix)
  250. }
  251.  
  252. return getRedirectURL({
  253. domain: "kemono.su",
  254. service: "fanbox",
  255. userID: getFanboxUserID(),
  256. postID: getFanboxPostID()
  257. })
  258. }
  259.  
  260. function getRedirectURLFromPixiv()
  261. {
  262. function getPixivUserID() //url
  263. {
  264. const userIDPrefix = `users`
  265. return getURLPathAfter(userIDPrefix)
  266. }
  267. function getPixivUserID2nd() //html
  268. {
  269. const userIDPrefix = `users/`
  270. const userIDSuffix = `"`
  271. return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
  272. }
  273.  
  274. return getRedirectURL({
  275. domain: "kemono.su",
  276. service: "fanbox",
  277. userID: getPixivUserID() ?? getPixivUserID2nd()
  278. })
  279. }
  280.  
  281. function getRedirectURLFromFantia()
  282. {
  283. function getFantiaUserID() //url
  284. {
  285. const userIDPrefix = `fanclubs`
  286. return getURLPathAfter(userIDPrefix)
  287. }
  288. function getFantiaUserID2nd() //html
  289. {
  290. const userIDPrefix = `fanclubs/`
  291. const userIDSuffix = `"`
  292. return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
  293. }
  294. function getFantiaPostID() //url
  295. {
  296. const postIDPrefix = `posts`
  297. return getURLPathAfter(postIDPrefix)
  298. }
  299.  
  300. return getRedirectURL({
  301. domain: "kemono.su",
  302. service: "fantia",
  303. userID: getFantiaUserID() ?? getFantiaUserID2nd(),
  304. postID: getFantiaPostID()
  305. })
  306. }
  307.  
  308. function getRedirectURLFromBoosty()
  309. {
  310. function getBoostyUserID() //url
  311. {
  312. const userIDPrefix = `boosty.to`
  313. return getURLPathAfter(userIDPrefix)
  314. }
  315. function getBoostyPostID() //url
  316. {
  317. const postIDPrefix = `posts`
  318. return getURLPathAfter(postIDPrefix)
  319. }
  320.  
  321. return getRedirectURL({
  322. domain: "kemono.su",
  323. service: "boosty",
  324. userID: getBoostyUserID(),
  325. postID: getBoostyPostID()
  326. })
  327. }
  328.  
  329. function getRedirectURLFromDLsite()
  330. {
  331. function getDLsiteUserID() //html
  332. {
  333. const userIDPrefix = `maker_id/`
  334. const userIDSuffix = `.`
  335. return getStringBetween(document.documentElement.innerHTML, userIDPrefix, userIDSuffix)
  336. }
  337. function getDLsitePostID() //url
  338. {
  339. const postIDPrefix = `product_id`
  340. const postIDMatch = `\\d`
  341. let postID = getURLPathAfter(postIDPrefix).match(new RegExp(postIDMatch, "g")).join("")
  342. return postID ? "RE" + postID : null
  343. }
  344.  
  345. return getRedirectURL({
  346. domain: "kemono.su",
  347. service: "dlsite",
  348. userID: getDLsiteUserID(),
  349. postID: getDLsitePostID()
  350. })
  351. }
  352.  
  353. function getRedirectURLFromGumroad()
  354. {
  355. function getGumroadUserID() //html
  356. {
  357. const elementSelector = `script.js-react-on-rails-component`
  358. const userIDPrefix = `id":"`
  359. const userIDSuffix = `"`
  360. return getStringBetween(document.querySelector(elementSelector)?.innerText, userIDPrefix, userIDSuffix)
  361. }
  362. function getGumroadPostID() //url
  363. {
  364. const postIDPrefix = `l`
  365. return getURLPathAfter(postIDPrefix)
  366. }
  367.  
  368. return getRedirectURL({
  369. domain: "kemono.su",
  370. service: "gumroad",
  371. userID: getGumroadUserID(),
  372. postID: getGumroadPostID()
  373. })
  374. }
  375.  
  376. function getRedirectURLFromSubscribeStar()
  377. {
  378. function getSubScribeStarUserID() //html
  379. {
  380. const elementSelector = `img[data-type="avatar"]`
  381. return document.querySelector(elementSelector)?.alt.toLowerCase() ?? null
  382. }
  383. function getSubScribeStarPostID() //url
  384. {
  385. const postIDPrefix = `posts`
  386. return getURLPathAfter(postIDPrefix)
  387. }
  388.  
  389. return getRedirectURL({
  390. domain: "kemono.su",
  391. service: "subscribestar",
  392. userID: getSubScribeStarUserID(),
  393. postID: getSubScribeStarPostID()
  394. })
  395. }
  396.  
  397. function getRedirectURLFromOnlyFans()
  398. {
  399. function getOnlyFansUserID() //html
  400. {
  401. const elementSelector = `#content a.g-avatar`
  402. return document.querySelector(elementSelector)?.getAttribute("href").slice(1)
  403. }
  404. function getOnlyFansPostID() //url
  405. {
  406. const elementSelector = `#content div.b-post`
  407. return document.querySelector(elementSelector)?.id.slice(7)
  408. }
  409.  
  410. return getRedirectURL({
  411. domain: "coomer.su",
  412. service: "onlyfans",
  413. userID: getOnlyFansUserID(),
  414. postID: getOnlyFansPostID()
  415. })
  416. }
  417.  
  418.  
  419.  
  420. function getStringBetween(string, prefix, suffix)
  421. {
  422. let begIndex = string.indexOf(prefix)
  423. if (begIndex == -1) return null
  424. else begIndex += prefix.length
  425.  
  426. let endIndex = string.indexOf(suffix, begIndex)
  427. if (endIndex == -1) endIndex = undefined;
  428. let result = string.slice(begIndex, endIndex)
  429.  
  430. return result
  431. }
  432.  
  433. function getURLPathAfter(string)
  434. {
  435. let urlSplit = (window.location.hostname + window.location.pathname).split("/")
  436. let pathIndex = urlSplit.indexOf(string) + 1
  437.  
  438. if (pathIndex == 0) return null
  439.  
  440. return urlSplit[pathIndex]
  441. }
  442.  
  443. function getRedirectURL({domain, service, userID=null, postID=null} = {})
  444. {
  445. let redirectURL = `https://${domain}/${service}`
  446.  
  447. if (userID)
  448. {
  449. redirectURL += `/user/${userID}`
  450.  
  451. if (postID)
  452. {
  453. redirectURL += `/post/${postID}`
  454. }
  455. }
  456. else return null
  457.  
  458. return redirectURL
  459. }