Kemono Browser

Kemono links everywhere!

As of 28/12/2023. See the latest version.

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