Sleazy Fork is available in English.

Kemono Browser

Kemono links everywhere!

נכון ליום 29-12-2023. ראה הגרסה האחרונה.

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