Sleazy Fork is available in English.

Kemono Browser

Kemono links everywhere!

Ajankohdalta 27.12.2023. Katso uusin versio.

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