Sleazy Fork is available in English.

ViewOnYP

Links various membership platforms to Kemono and Coomer.

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name ViewOnYP
  3. // @name:de ViewOnYP
  4. // @name:en ViewOnYP
  5. // @namespace sun/userscripts
  6. // @version 2.19.2
  7. // @description Links various membership platforms to Kemono and Coomer.
  8. // @description:de Vernetzt verschiedene Mitgliedschaftsplattformen mit Kemono und Coomer.
  9. // @description:en Links various membership platforms to Kemono and Coomer.
  10. // @compatible chrome
  11. // @compatible edge
  12. // @compatible firefox
  13. // @compatible opera
  14. // @compatible safari
  15. // @homepageURL https://forgejo.sny.sh/sun/userscripts
  16. // @supportURL https://forgejo.sny.sh/sun/userscripts/issues
  17. // @contributionURL https://liberapay.com/sun
  18. // @contributionAmount €1.00
  19. // @author Sunny <sunny@sny.sh>
  20. // @include *://afdian.net/a/*
  21. // @include *://boosty.to/*
  22. // @include *://candfans.jp/*
  23. // @include *://discord.com/channels/*
  24. // @include *://discord.com/invite/*
  25. // @include *://www.dlsite.com/*/circle/profile/=/maker_id/*
  26. // @include *://*.fanbox.cc/
  27. // @include *://www.fanbox.cc/@*
  28. // @include *://fansly.com/*
  29. // @include *://fantia.jp/fanclubs/*
  30. // @include *://*.gumroad.com/
  31. // @include *://onlyfans.com/*
  32. // @include *://www.patreon.com/*
  33. // @include *://www.subscribestar.com/*
  34. // @include *://subscribestar.adult/*
  35. // @match *://afdian.net/a/*
  36. // @match *://boosty.to/*
  37. // @match *://candfans.jp/*
  38. // @match *://discord.com/channels/*
  39. // @match *://discord.com/invite/*
  40. // @match *://www.dlsite.com/*/circle/profile/=/maker_id/*
  41. // @match *://*.fanbox.cc/
  42. // @match *://www.fanbox.cc/@*
  43. // @match *://fansly.com/*
  44. // @match *://fantia.jp/fanclubs/*
  45. // @match *://*.gumroad.com/
  46. // @match *://onlyfans.com/*
  47. // @match *://www.patreon.com/*
  48. // @match *://www.subscribestar.com/*
  49. // @match *://subscribestar.adult/*
  50. // @exclude *://boosty.to/app
  51. // @exclude *://boosty.to/app/*
  52. // @exclude *://candfans.jp/
  53. // @exclude *://candfans.jp/auth/*
  54. // @exclude *://www.fanbox.cc/
  55. // @exclude *://fansly.com/
  56. // @exclude *://fansly.com/application
  57. // @exclude *://fansly.com/dmca
  58. // @exclude *://fansly.com/explore/*
  59. // @exclude *://fansly.com/privacy
  60. // @exclude *://fansly.com/tos
  61. // @exclude *://fansly.com/usc2257
  62. // @exclude *://gumroad.com/
  63. // @exclude *://onlyfans.com/
  64. // @exclude *://onlyfans.com/about
  65. // @exclude *://onlyfans.com/antitraffickingstatement
  66. // @exclude *://onlyfans.com/brand
  67. // @exclude *://onlyfans.com/contact
  68. // @exclude *://onlyfans.com/contract
  69. // @exclude *://onlyfans.com/cookies
  70. // @exclude *://onlyfans.com/dmca
  71. // @exclude *://onlyfans.com/help
  72. // @exclude *://onlyfans.com/help/*
  73. // @exclude *://onlyfans.com/legalguide/
  74. // @exclude *://onlyfans.com/legalinquiry
  75. // @exclude *://onlyfans.com/privacy
  76. // @exclude *://onlyfans.com/terms
  77. // @exclude *://onlyfans.com/transparency-center
  78. // @exclude *://onlyfans.com/transparency-center/*
  79. // @exclude *://onlyfans.com/usc2257
  80. // @exclude *://www.patreon.com/
  81. // @exclude *://www.patreon.com/about
  82. // @exclude *://www.patreon.com/apps
  83. // @exclude *://www.patreon.com/brand
  84. // @exclude *://www.patreon.com/careers
  85. // @exclude *://www.patreon.com/create
  86. // @exclude *://www.patreon.com/creators/*
  87. // @exclude *://www.patreon.com/de-DE
  88. // @exclude *://www.patreon.com/de-DE/*
  89. // @exclude *://www.patreon.com/en-GB
  90. // @exclude *://www.patreon.com/en-GB/*
  91. // @exclude *://www.patreon.com/es-ES
  92. // @exclude *://www.patreon.com/es-ES/*
  93. // @exclude *://www.patreon.com/forgot-password
  94. // @exclude *://www.patreon.com/fr-FR
  95. // @exclude *://www.patreon.com/fr-FR/*
  96. // @exclude *://www.patreon.com/home
  97. // @exclude *://www.patreon.com/it-IT
  98. // @exclude *://www.patreon.com/it-IT/*
  99. // @exclude *://www.patreon.com/login
  100. // @exclude *://www.patreon.com/messages?*
  101. // @exclude *://www.patreon.com/mobile
  102. // @exclude *://www.patreon.com/notifications?*
  103. // @exclude *://www.patreon.com/policy
  104. // @exclude *://www.patreon.com/policy/*
  105. // @exclude *://www.patreon.com/press
  106. // @exclude *://www.patreon.com/pricing
  107. // @exclude *://www.patreon.com/product/*
  108. // @exclude *://www.patreon.com/search
  109. // @exclude *://www.patreon.com/search?*
  110. // @exclude *://www.patreon.com/settings/*
  111. // @exclude *://www.subscribestar.com/
  112. // @exclude *://www.subscribestar.com/about
  113. // @exclude *://www.subscribestar.com/api
  114. // @exclude *://www.subscribestar.com/brand
  115. // @exclude *://www.subscribestar.com/contacts
  116. // @exclude *://www.subscribestar.com/dropp
  117. // @exclude *://www.subscribestar.com/features
  118. // @exclude *://www.subscribestar.com/guidelines
  119. // @exclude *://www.subscribestar.com/help
  120. // @exclude *://www.subscribestar.com/login
  121. // @exclude *://www.subscribestar.com/posts/*
  122. // @exclude *://www.subscribestar.com/pricing
  123. // @exclude *://www.subscribestar.com/privacy
  124. // @exclude *://www.subscribestar.com/refund
  125. // @exclude *://www.subscribestar.com/search
  126. // @exclude *://www.subscribestar.com/search?*
  127. // @exclude *://www.subscribestar.com/signup
  128. // @exclude *://www.subscribestar.com/stars
  129. // @exclude *://www.subscribestar.com/stars?*
  130. // @exclude *://www.subscribestar.com/taxes
  131. // @exclude *://www.subscribestar.com/tos
  132. // @exclude *://subscribestar.adult/
  133. // @exclude *://subscribestar.adult/about
  134. // @exclude *://subscribestar.adult/api
  135. // @exclude *://subscribestar.adult/brand
  136. // @exclude *://subscribestar.adult/contacts
  137. // @exclude *://subscribestar.adult/dropp
  138. // @exclude *://subscribestar.adult/features
  139. // @exclude *://subscribestar.adult/guidelines
  140. // @exclude *://subscribestar.adult/help
  141. // @exclude *://subscribestar.adult/login
  142. // @exclude *://subscribestar.adult/posts/*
  143. // @exclude *://subscribestar.adult/pricing
  144. // @exclude *://subscribestar.adult/privacy
  145. // @exclude *://subscribestar.adult/refund
  146. // @exclude *://subscribestar.adult/search
  147. // @exclude *://subscribestar.adult/search?*
  148. // @exclude *://subscribestar.adult/signup
  149. // @exclude *://subscribestar.adult/stars
  150. // @exclude *://subscribestar.adult/stars?*
  151. // @exclude *://subscribestar.adult/taxes
  152. // @exclude *://subscribestar.adult/tos
  153. // @connect coomer.su
  154. // @connect kemono.su
  155. // @connect nekohouse.su
  156. // @connect afdian.net
  157. // @connect api.fanbox.cc
  158. // @connect apiv3.fansly.com
  159. // @connect discord.com
  160. // @run-at document-end
  161. // @inject-into auto
  162. // @grant GM.addStyle
  163. // @grant GM_addStyle
  164. // @grant GM.deleteValue
  165. // @grant GM_deleteValue
  166. // @grant GM.getValue
  167. // @grant GM_getValue
  168. // @grant GM.registerMenuCommand
  169. // @grant GM_registerMenuCommand
  170. // @grant GM.setValue
  171. // @grant GM_setValue
  172. // @grant GM.xmlHttpRequest
  173. // @grant GM_xmlhttpRequest
  174. // @noframes
  175. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  176. // @icon https://forgejo.sny.sh/sun/userscripts/raw/branch/main/icons/ViewOnYP.ico
  177. // @copyright 2020-present, Sunny (https://sny.sh/)
  178. // @license Hippocratic License; https://forgejo.sny.sh/sun/userscripts/src/branch/main/LICENSE.md
  179. // ==/UserScript==
  180.  
  181. (async () => {
  182. if (!(await GM.getValue("cache2"))) await GM.setValue("cache2", {});
  183. const cache = await GM.getValue("cache2");
  184.  
  185. if (await GM.getValue("cache")) {
  186. cache.Kemono = await GM.getValue("cache");
  187. await GM.deleteValue("cache");
  188. }
  189.  
  190. if (await GM.getValue("style")) await GM.deleteValue("style");
  191.  
  192. const sites = [
  193. {
  194. name: "Coomer",
  195. check: "https://coomer.su/api/v1/creators.txt",
  196. cache: (response, x) => {
  197. for (const y of JSON.parse(response.responseText)) {
  198. if (!cache[x.name]) cache[x.name] = {};
  199. if (!cache[x.name][y.service]) cache[x.name][y.service] = [];
  200. cache[x.name][y.service].push(y.id);
  201. }
  202. return cache;
  203. },
  204. get: (response, host, user) => {
  205. return Boolean(
  206. JSON.parse(response.responseText)
  207. .filter((x) => x.service === host)
  208. .filter((x) => x.id === user).length,
  209. );
  210. },
  211. profile: "https://coomer.su/$HOST/user/$USER",
  212. },
  213. {
  214. name: "Kemono",
  215. check: "https://kemono.su/api/v1/creators.txt",
  216. cache: (response, x) => {
  217. for (const y of JSON.parse(response.responseText)) {
  218. if (!cache[x.name]) cache[x.name] = {};
  219. if (!cache[x.name][y.service]) cache[x.name][y.service] = [];
  220. cache[x.name][y.service].push(y.id);
  221. }
  222. return cache;
  223. },
  224. get: (response, host, user) => {
  225. return Boolean(
  226. JSON.parse(response.responseText)
  227. .filter((x) => x.service === host)
  228. .filter((x) => x.id === user).length,
  229. );
  230. },
  231. profile: "https://kemono.su/$HOST/user/$USER",
  232. },
  233. {
  234. name: "Nekohouse",
  235. check: "https://nekohouse.su/api/creators",
  236. cache: (response, x) => {
  237. for (const y of JSON.parse(response.responseText)) {
  238. if (!cache[x.name]) cache[x.name] = {};
  239. if (!cache[x.name][y.service]) cache[x.name][y.service] = [];
  240. cache[x.name][y.service].push(y.user_id);
  241. }
  242. return cache;
  243. },
  244. get: (response, host, user) => {
  245. return Boolean(
  246. JSON.parse(response.responseText)
  247. .filter((x) => x.service === host)
  248. .filter((x) => x.user_id === user).length,
  249. );
  250. },
  251. profile: "https://nekohouse.su/$HOST/user/$USER",
  252. },
  253. ];
  254.  
  255. GM.registerMenuCommand("Populate cache", () => {
  256. for (const x of sites) {
  257. GM.xmlHttpRequest({
  258. url: x.check,
  259. method: "GET",
  260. onload: async (response) => {
  261. const cache = x.cache(response, x);
  262. await GM.setValue("cache2", cache);
  263. alert(`Populated cache for ${x.name}.`);
  264. },
  265. });
  266. }
  267. });
  268.  
  269. GM.registerMenuCommand("Clear cache", () => {
  270. GM.deleteValue("cache2").then(alert("Cache cleared successfully."));
  271. });
  272.  
  273. GM.addStyle(`
  274. #voyp {
  275. background: white;
  276. position: fixed;
  277. top: calc(100vh - 60px);
  278. left: 50%;
  279. transform: translateX(-50%);
  280. border-radius: 5px 5px 0 0;
  281. padding: 25px;
  282. box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.302), 0 1px 3px 1px rgba(60, 64, 67, 0.149);
  283. text-align: center;
  284. z-index: 9999;
  285. }
  286. #voyp:hover {
  287. top: initial;
  288. bottom: 0;
  289. }
  290. #voyp div {
  291. font-size: small;
  292. text-transform: uppercase;
  293. opacity: 0.5;
  294. text-align: center;
  295. }
  296. #voyp div i {
  297. position: absolute;
  298. top: 0;
  299. right: 0;
  300. font-size: 1.5em;
  301. padding: 0.375em 0.5em;
  302. cursor: pointer;
  303. }
  304. `);
  305.  
  306. document
  307. .getElementsByTagName("body")[0]
  308. .insertAdjacentHTML(
  309. "beforeend",
  310. '<div id="voyp"><div><b>ViewOnYP</b> · hover to reveal<i>✕</i></div><span><br>Querying platforms, please wait...</span></div>',
  311. );
  312.  
  313. document.querySelector("#voyp div i").addEventListener("click", () => {
  314. document.getElementById("voyp").style.display = "none";
  315. });
  316.  
  317. const host = window.location.hostname.split(".").slice(-2, -1)[0];
  318. if (!host) return;
  319.  
  320. const p = new Promise((resolve, reject) => {
  321. switch (host) {
  322. case "afdian":
  323. GM.xmlHttpRequest({
  324. url: `https://afdian.net/api/user/get-profile-by-slug?url_slug=${document.location.pathname.split("/")[2]}`,
  325. onload: (response) =>
  326. resolve(JSON.parse(response.responseText).data.user.user_id),
  327. });
  328. break;
  329. case "candfans":
  330. resolve(
  331. new URL(
  332. document
  333. .querySelector("meta[property='og:image']")
  334. .getAttribute("content"),
  335. ).pathname.split("/")[2],
  336. );
  337. break;
  338. case "discord":
  339. switch (document.location.pathname.split("/")[1]) {
  340. case "channels":
  341. resolve(document.location.pathname.split("/")[2]);
  342. break;
  343. case "invite":
  344. GM.xmlHttpRequest({
  345. url: `https://discord.com/api/v10/invites/${document.location.pathname.split("/")[2]}`,
  346. onload: (response) => {
  347. resolve(JSON.parse(response.responseText).guild.id);
  348. },
  349. });
  350. break;
  351. }
  352. break;
  353. case "dlsite":
  354. resolve(document.location.pathname.split("/")[6]);
  355. break;
  356. case "fanbox": {
  357. let creatorId = document.location.hostname.split(".").slice(-3, -2)[0];
  358. if (creatorId === "www")
  359. creatorId = window.location.pathname.split("/")[1].slice(1);
  360. GM.xmlHttpRequest({
  361. url: `https://api.fanbox.cc/creator.get?creatorId=${creatorId}`,
  362. headers: { Origin: "https://fanbox.cc" },
  363. onload: (response) => {
  364. try {
  365. resolve(JSON.parse(response.responseText).body.user.userId);
  366. } catch {
  367. resolve(
  368. new URL(
  369. JSON.parse(
  370. document.querySelector("script[type='application/ld+json']")
  371. .textContent,
  372. )[0].image,
  373. ).pathname.split("/")[7],
  374. );
  375. }
  376. },
  377. });
  378. break;
  379. }
  380. case "fansly": {
  381. GM.xmlHttpRequest({
  382. url: `https://apiv3.fansly.com/api/v1/account?usernames=${document.location.pathname.split("/")[1]}`,
  383. onload: (response) =>
  384. resolve(JSON.parse(response.responseText).response[0].id),
  385. });
  386. break;
  387. }
  388. case "fantia":
  389. if (document.location.pathname.split("/")[1] === "fanclubs") {
  390. resolve(document.location.pathname.split("/")[2]);
  391. } else {
  392. resolve(
  393. document
  394. .querySelector(".fanclub-header a")
  395. .getAttribute("href")
  396. .split("/")[2],
  397. );
  398. }
  399. break;
  400. case "gumroad":
  401. resolve([
  402. document.location.hostname.split(".")[0],
  403. JSON.parse(
  404. document.getElementsByClassName("js-react-on-rails-component")[0]
  405. .textContent,
  406. ).creator_profile.external_id,
  407. ]);
  408. break;
  409. case "patreon":
  410. window.addEventListener("load", () => {
  411. resolve(
  412. unsafeWindow.patreon.bootstrap.campaign.data.relationships.creator
  413. ?.data.id ||
  414. unsafeWindow.patreon.bootstrap.post?.data.relationships.user.data
  415. .id,
  416. );
  417. });
  418. break;
  419. case "boosty":
  420. case "onlyfans":
  421. case "subscribestar":
  422. resolve(document.location.pathname.split("/")[1]);
  423. break;
  424. }
  425. });
  426.  
  427. p.then((u) => {
  428. let users = u;
  429. if (!Array.isArray(users)) users = [users];
  430.  
  431. for (const user of users) {
  432. for (const x of sites) {
  433. if (
  434. cache[x.name] &&
  435. cache[x.name][host] &&
  436. cache[x.name][host].includes(user)
  437. )
  438. return show(x, host, user, true);
  439. GM.xmlHttpRequest({
  440. url: x.check,
  441. method: "GET",
  442. onload: (response) => {
  443. if (x.get(response, host, user)) {
  444. show(x, host, user, true);
  445. } else {
  446. show(x, host, user, false);
  447. }
  448. },
  449. });
  450. }
  451. }
  452. });
  453.  
  454. function show(site, host, user, success) {
  455. const name = site.name;
  456. const url = site.profile.replace("$HOST", host).replace("$USER", user);
  457.  
  458. document.getElementById("voyp").getElementsByTagName("span")[0]?.remove();
  459.  
  460. document
  461. .getElementById("voyp")
  462. .insertAdjacentHTML(
  463. "beforeend",
  464. `<br><b>${name}:</b> ${
  465. success
  466. ? `<a href="${url}">${url}</a>`
  467. : `User ${user} not found in platform's database.`
  468. }`,
  469. );
  470.  
  471. if (!success) return;
  472.  
  473. if (!cache[site.name]) cache[site.name] = {};
  474. if (!cache[site.name][host]) cache[site.name][host] = [];
  475. if (!cache[site.name][host].includes(user))
  476. cache[site.name][host].push(user);
  477. GM.setValue("cache2", cache);
  478. }
  479. })();