Su-san

Untitled

Jul 24th, 2026
2,112
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.09 KB | None | 0 0
  1. // ==UserScript==
  2. // @name VoiceLinks 2026
  3. // @namespace Sanya
  4. // @description Makes RJ codes more usefuler.
  5. // @include *://*/*
  6. // @version 2.1.0
  7. // @grant GM.xmlHttpRequest
  8. // @grant GM_xmlhttpRequest
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14. const RJ_REGEX = new RegExp("R[JE][0-9]{6}[0-9]*", "gi");
  15. const VOICELINK_CLASS = 'voicelink';
  16. const RJCODE_ATTRIBUTE = 'rjcode';
  17. const css = `
  18. .voicepopup {
  19. min-width: 600px !important;
  20. z-index: 50000 !important;
  21. max-width: 80% !important;
  22. position: fixed !important;
  23. line-height: 1.4em;
  24. font-size: 1.1em;
  25. margin-bottom: 10px;
  26. box-shadow: 0 0 .125em 0 rgba(0,0,0,.5);
  27. border-radius: 0.5em;
  28. background-color: inherit;
  29. padding: 10px;
  30. }
  31.  
  32. .voicepopup img {
  33. width: 270px;
  34. height: auto;
  35. margin: 3px 15px 3px 3px;
  36. }
  37.  
  38. .voice-title {
  39. font-size: 1.4em;
  40. font-weight: bold;
  41. text-align: center;
  42. margin: 5px 10px 0 0;
  43. display: block;
  44. }
  45.  
  46. .rjcode {
  47. text-align: center;
  48. font-size: 1.2em;
  49. font-style: italic;
  50. opacity: 0.3;
  51. }
  52.  
  53. .error {
  54. height: 210px;
  55. line-height: 210px;
  56. text-align: center;
  57. }
  58.  
  59. .discord-dark {
  60. background-color: #36393f;
  61. color: #dcddde;
  62. font-size: 0.9375rem;
  63. }
  64. `
  65.  
  66. function getAdditionalPopupClasses() {
  67. const hostname = document.location.hostname;
  68. switch (hostname) {
  69. case "boards.4chan.org": return "post reply";
  70. case "discordapp.com": return "discord-dark";
  71. default: return null;
  72. }
  73. }
  74.  
  75. function getXmlHttpRequest() {
  76. return (typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : GM_xmlhttpRequest);
  77. }
  78.  
  79. const Parser = {
  80. walkNodes: function (elem) {
  81. const rjNodeTreeWalker = document.createTreeWalker(
  82. elem,
  83. NodeFilter.SHOW_TEXT,
  84. {
  85. acceptNode: function (node) {
  86. if (node.parentElement.classList.contains(VOICELINK_CLASS))
  87. return NodeFilter.FILTER_ACCEPT;
  88. if (node.nodeValue.match(RJ_REGEX))
  89. return NodeFilter.FILTER_ACCEPT;
  90. }
  91. },
  92. false,
  93. );
  94. while (rjNodeTreeWalker.nextNode()) {
  95. const node = rjNodeTreeWalker.currentNode;
  96. if (node.parentElement.classList.contains(VOICELINK_CLASS))
  97. Parser.rebindEvents(node.parentElement);
  98. else
  99. Parser.linkify(node);
  100. }
  101. },
  102.  
  103. wrapRJCode: function (rjCode) {
  104. var e;
  105. e = document.createElement("a");
  106. e.classList = VOICELINK_CLASS;
  107. e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`
  108. e.innerHTML = rjCode;
  109. e.target = "_blank";
  110. e.rel = "noreferrer";
  111. e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase());
  112. e.addEventListener("mouseover", Popup.over);
  113. e.addEventListener("mouseout", Popup.out);
  114. e.addEventListener("mousemove", Popup.move);
  115. return e;
  116. },
  117.  
  118. linkify: function (textNode) {
  119. const nodeOriginalText = textNode.nodeValue;
  120. const matches = [];
  121.  
  122. let match;
  123. while (match = RJ_REGEX.exec(nodeOriginalText)) {
  124. matches.push({
  125. index: match.index,
  126. value: match[0],
  127. });
  128. }
  129.  
  130. // Keep text in text node until first RJ code
  131. textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index);
  132.  
  133. // Insert rest of text while linkifying RJ codes
  134. let prevNode = null;
  135. for (let i = 0; i < matches.length; ++i) {
  136. // Insert linkified RJ code
  137. const rjLinkNode = Parser.wrapRJCode(matches[i].value);
  138. textNode.parentNode.insertBefore(
  139. rjLinkNode,
  140. prevNode ? prevNode.nextSibling : textNode.nextSibling,
  141. );
  142.  
  143. // Insert text after if there is any
  144. let upper;
  145. if (i === matches.length - 1)
  146. upper = undefined;
  147. else
  148. upper = matches[i + 1].index;
  149. let substring;
  150. if (substring = nodeOriginalText.substring(matches[i].index + matches[i].value.length, upper)) {
  151. const subtextNode = document.createTextNode(substring);
  152. textNode.parentNode.insertBefore(
  153. subtextNode,
  154. rjLinkNode.nextElementSibling,
  155. );
  156. prevNode = subtextNode;
  157. }
  158. else {
  159. prevNode = rjLinkNode;
  160. }
  161. }
  162. },
  163.  
  164. rebindEvents: function (elem) {
  165. if (elem.nodeName === "A") {
  166. elem.addEventListener("mouseover", Popup.over);
  167. elem.addEventListener("mouseout", Popup.out);
  168. elem.addEventListener("mousemove", Popup.move);
  169. }
  170. else {
  171. const voicelinks = elem.querySelectorAll("." + VOICELINK_CLASS);
  172. for (var i = 0, ii = voicelinks.length; i < ii; i++) {
  173. const voicelink = voicelinks[i];
  174. voicelink.addEventListener("mouseover", Popup.over);
  175. voicelink.addEventListener("mouseout", Popup.out);
  176. voicelink.addEventListener("mousemove", Popup.move);
  177. }
  178. }
  179. },
  180.  
  181. }
  182.  
  183. const Popup = {
  184. makePopup: function (e, rjCode) {
  185. const popup = document.createElement("div");
  186. popup.className = "voicepopup " + (getAdditionalPopupClasses() || '');
  187. popup.id = "voice-" + rjCode;
  188. popup.style = "display: flex";
  189. document.body.appendChild(popup);
  190. DLsite.request(rjCode, e, function (workInfo, rj) {
  191. if (workInfo === null)
  192. popup.innerHTML = "<div class='error'>Work not found.</span>";
  193. else {
  194. workInfo.rj = rj;
  195.  
  196. const imgContainer = document.createElement("div")
  197. const img = document.createElement("img");
  198. img.src = workInfo.img;
  199. imgContainer.appendChild(img);
  200.  
  201. let html = `
  202. <div>
  203. <div class='voice-title'>${workInfo.title}</div>
  204. <div class='rjcode'>[${workInfo.rj}]</div>
  205. <br />
  206. `;
  207.  
  208. if(workInfo.circle)
  209. {
  210. html += `Circle: <a>${workInfo.circle}</a> <br />`
  211. }
  212.  
  213. if (workInfo.date)
  214. html += `Release: <a>${workInfo.date}</a> <br />`;
  215. else if (workInfo.dateAnnounce)
  216. html += `Scheduled Release: <a>${workInfo.dateAnnounce}</a> <br />`;
  217.  
  218. if (workInfo.rating)
  219. html += `Age rating: <a>${workInfo.rating}</a> <br />`
  220.  
  221. if (workInfo.cv)
  222. html += `CV: <a>${workInfo.cv}</a> <br />`;
  223.  
  224. if (workInfo.tags)
  225. {
  226. html += `Tags: <a>`
  227. workInfo.tags.forEach(tag => {
  228. html += tag + "\u3000";
  229. });
  230. html += "</a> <br />";
  231. }
  232.  
  233. if (workInfo.filesize)
  234. html += `File size: ${workInfo.filesize}<br />`;
  235.  
  236. html += "</div>"
  237. popup.innerHTML = html;
  238.  
  239. popup.insertBefore(imgContainer, popup.childNodes[0]);
  240. }
  241.  
  242. Popup.move(e);
  243. });
  244. },
  245.  
  246. over: function (e) {
  247. const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE);
  248. const popup = document.querySelector("div#voice-" + rjCode);
  249. if (popup) {
  250. const style = popup.getAttribute("style").replace("none", "flex");
  251. popup.setAttribute("style", style);
  252. }
  253. else {
  254. Popup.makePopup(e, rjCode);
  255. }
  256. },
  257.  
  258. out: function (e) {
  259. const rjCode = e.target.getAttribute("rjcode");
  260. const popup = document.querySelector("div#voice-" + rjCode);
  261. if (popup) {
  262.  
  263. const style = popup.getAttribute("style").replace("flex", "none");;
  264. popup.setAttribute("style", style);
  265. }
  266. },
  267.  
  268. move: function (e) {
  269. const rjCode = e.target.getAttribute("rjcode");
  270. const popup = document.querySelector("div#voice-" + rjCode);
  271. if (popup) {
  272. if (popup.offsetWidth + e.clientX + 10 < window.innerWidth - 10) {
  273. popup.style.left = (e.clientX + 10) + "px";
  274. }
  275. else {
  276. popup.style.left = (window.innerWidth - popup.offsetWidth - 10) + "px";
  277. }
  278.  
  279. if (popup.offsetHeight + e.clientY + 50 > window.innerHeight) {
  280. popup.style.top = (e.clientY - popup.offsetHeight - 8) + "px";
  281. }
  282. else {
  283. popup.style.top = (e.clientY + 20) + "px";
  284. }
  285. }
  286. },
  287. }
  288.  
  289. const DLsite = {
  290. parseWorkDOMDlsite: function (dom) {
  291. // workInfo: {
  292. // rj: any;
  293. // img: string;
  294. // title: any;
  295. // circle: any;
  296. // date: any;
  297. // rating: any;
  298. // tags: any[];
  299. // cv: any;
  300. // filesize: any;
  301. // dateAnnounce: any;
  302. // }
  303. const workInfo = {};
  304. workInfo.title = dom.getElementById("work_name").innerText;
  305. workInfo.circle = dom.querySelector("span.maker_name").innerText;
  306. const table_outline = dom.querySelector("table#work_outline");
  307. for (var i = 0, ii = table_outline.rows.length; i < ii; i++) {
  308. const row = table_outline.rows[i];
  309. const row_header = row.cells[0].innerText;
  310. const row_data = row.cells[1];
  311. switch (true) {
  312. case (row_header.includes("販売日")):
  313. workInfo.date = row_data.innerText;
  314. break;
  315. case (row_header.includes("年齢指定")):
  316. workInfo.rating = row_data.innerText;
  317. break;
  318. case (row_header.includes("ジャンル")):
  319. const tag_nodes = row_data.querySelectorAll("a");
  320. workInfo.tags = [...tag_nodes].map(a => { return a.innerText });
  321. break;
  322. case (row_header.includes("声優")):
  323. workInfo.cv = row_data.innerText;
  324. break;
  325. case (row_header.includes("ファイル容量")):
  326. workInfo.filesize = row_data.innerText.replace("総計", "").trim();
  327. break;
  328. default:
  329. break;
  330. }
  331. }
  332. const work_date_ana = dom.querySelector("strong.work_date_ana");
  333. if (work_date_ana) {
  334. workInfo.dateAnnounce = work_date_ana.innerText;
  335. }
  336. workInfo.img = DLsite.getDlsiteImage(dom);
  337. return workInfo;
  338. },
  339.  
  340. parseWorkDOMChobit: function (dom) {
  341. const workInfo = {};
  342.  
  343. const workNameTag = dom.getElementsByClassName("work-name");
  344. if (workNameTag && workNameTag[0])
  345. workInfo.title = workNameTag[0].innerText;
  346.  
  347. const circleTag = dom.getElementsByClassName("maker-name");
  348. if (circleTag && circleTag[0])
  349. workInfo.circle = circleTag[0].innerText;
  350.  
  351. const dateTag = dom.querySelector("span.date");
  352. if (dateTag)
  353. workInfo.date = dateTag.innerText;
  354.  
  355. const dateRegistTag = dom.getElementsByClassName("regist-date");
  356. if(dateRegistTag && dateRegistTag[0] && dateRegistTag[0].innerText.includes("発売予定"))
  357. workInfo.dateAnnounce = workInfo.date;
  358.  
  359. const ratingTag = dom.querySelector("span.work-tag");
  360. if(ratingTag)
  361. workInfo.rating = ratingTag.innerText;
  362.  
  363. const tagsTag = dom.getElementsByClassName("tags");
  364. if(tagsTag && tagsTag[0])
  365. {
  366. const tag_nodes = tagsTag[0].querySelectorAll("a");
  367. workInfo.tags = [...tag_nodes].map(a => { return a.innerText });
  368. }
  369.  
  370. return workInfo;
  371. },
  372.  
  373. request: function (rjCode, e, callback) {
  374. const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
  375. getXmlHttpRequest()({
  376. method: "GET",
  377. url,
  378. headers: {
  379. "Accept": "text/xml",
  380. "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
  381. },
  382. onload: function (resp) {
  383. if (resp.readyState === 4 && resp.status === 200) {
  384. const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
  385. if(resp.responseText.includes("SORRY...")) {
  386. DLsite.requestChobit(rjCode, e, callback, DLsite.getDlsiteImage(dom));
  387. }
  388. else {
  389. const workInfo = DLsite.parseWorkDOMDlsite(dom);
  390. callback(workInfo, rjCode);
  391. }
  392. }
  393. else if (resp.readyState === 4 && resp.status === 404)
  394. DLsite.requestAnnounce(rjCode, e, callback);
  395. },
  396. });
  397. },
  398.  
  399. requestAnnounce: function (rjCode, e, callback) {
  400. const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
  401. e.target.href = url;
  402. getXmlHttpRequest()({
  403. method: "GET",
  404. url,
  405. headers: {
  406. "Accept": "text/xml",
  407. "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
  408. },
  409. onload: function (resp) {
  410. if (resp.readyState === 4 && resp.status === 200) {
  411. const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
  412. if(resp.responseText.includes("SORRY...")) {
  413. DLsite.requestChobit(rjCode, e, callback, DLsite.getDlsiteImage(dom));
  414. }
  415. else {
  416. const workInfo = DLsite.parseWorkDOMDlsite(dom);
  417. callback(workInfo, rjCode);
  418. }
  419. }
  420. else if (resp.readyState === 4 && resp.status === 404)
  421. DLsite.requestChobit(rjCode, e, callback);
  422. },
  423. });
  424. },
  425.  
  426. requestChobit: function (rjCode, e, callback, imgSrc) {
  427. const apiUrl = `https://chobit.cc/api/v1/dlsite/embed?workno=${encodeURIComponent(rjCode)}`;
  428.  
  429. getXmlHttpRequest()({
  430. method: "GET",
  431. url: apiUrl,
  432. headers: {
  433. "Accept": "application/json",
  434. "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
  435. },
  436. onload: function (resp) {
  437. if (resp.readyState !== 4)
  438. return;
  439.  
  440. if (resp.status === 200) {
  441. let data;
  442.  
  443. try {
  444. data = JSON.parse(resp.responseText);
  445. } catch (err) {
  446. callback(null, rjCode);
  447. return;
  448. }
  449.  
  450. if (!data.works || data.works.length === 0) {
  451. const workInfo = {
  452. title: "",
  453. circle: "",
  454. img: imgSrc
  455. };
  456. callback(workInfo, rjCode);
  457. return;
  458. }
  459.  
  460. const workId = data.works[0].work_id;
  461. const url = `https://chobit.cc/${workId}/`;
  462.  
  463. e.target.href = url;
  464.  
  465. getXmlHttpRequest()({
  466. method: "GET",
  467. url,
  468. headers: {
  469. "Accept": "text/html",
  470. "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
  471. },
  472. onload: function (resp) {
  473. if (resp.readyState !== 4)
  474. return;
  475.  
  476. if (resp.status === 200) {
  477. const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
  478. const workInfo = DLsite.parseWorkDOMChobit(dom);
  479. workInfo.img = imgSrc;
  480. callback(workInfo, rjCode);
  481. }
  482. else if (resp.status === 404) {
  483. callback(null, rjCode);
  484. }
  485. }
  486. });
  487. }
  488. else if (resp.status === 404) {
  489. callback(null, rjCode);
  490. }
  491. }
  492. });
  493. },
  494.  
  495. getDlsiteImage: function (dom) {
  496. const metaTag = dom.querySelector('meta[property="og:image"]');
  497. if (metaTag)
  498. {
  499. return metaTag.getAttribute("content");
  500. }
  501. },
  502. }
  503.  
  504.  
  505. document.addEventListener("DOMContentLoaded", function () {
  506. const style = document.createElement("style");
  507. style.innerHTML = css;
  508. document.head.appendChild(style);
  509.  
  510. Parser.walkNodes(document.body);
  511.  
  512. const observer = new MutationObserver(function (m) {
  513. for (let i = 0; i < m.length; ++i) {
  514. let addedNodes = m[i].addedNodes;
  515.  
  516. for (let j = 0; j < addedNodes.length; ++j) {
  517. Parser.walkNodes(addedNodes[j]);
  518. }
  519. }
  520. });
  521.  
  522. document.addEventListener("securitypolicyviolation", function (e) {
  523. if (e.blockedURI.includes("img.dlsite.jp")) {
  524. const img = document.querySelector(`img[src="${e.blockedURI}"]`);
  525. img.remove();
  526. }
  527. });
  528.  
  529. observer.observe(document.body, { childList: true, subtree: true })
  530. });
  531. })();
Add Comment
Please, Sign In to add comment