Sleazy Fork is available in English.

NHentai Konnichiwa

A simple usercript for downloading doujinshi from NHentai and mirrors

  1. // ==UserScript==
  2. // @name NHentai Konnichiwa
  3. // @author naiymu
  4. // @version 1.1.11
  5. // @license MIT; https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/LICENSE
  6. // @namespace https://github.com/naiymu/nhentai-konnichiwa
  7. // @homepage https://github.com/naiymu/nhentai-konnichiwa
  8. // @supportURL https://github.com/naiymu/nhentai-konnichiwa/issues
  9. // @description A simple usercript for downloading doujinshi from NHentai and mirrors
  10. // @match https://nhentai.net/*
  11. // @match https://nhentai.xxx/*
  12. // @match https://nyahentai.red/*
  13. // @match https://nhentai.to/*
  14. // @match https://nhentai.website/*
  15. // @exclude /https:\/\/n.*hentai.(red|net|xxx|to|website)\/g\/[0-9]*\/[0-9]+\/?$/
  16. // @connect nhentai.xxx
  17. // @connect cdn.nload.xyz
  18. // @connect i3.nhentai.net
  19. // @connect cdn.nhentai.xxx
  20. // @connect nhentai.com
  21. // @connect t.dogehls.xyz
  22. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.0/jszip.min.js
  23. // @require https://unpkg.com/comlink@4.3.1/dist/umd/comlink.min.js
  24. // @grant GM_addStyle
  25. // @grant GM_setClipboard
  26. // @grant GM_xmlhttpRequest
  27. // @grant GM_setValue
  28. // @grant GM_getValue
  29. // @run-at document-end
  30. // @icon https://raw.githubusercontent.com/naiymu/nhentai-konnichiwa/main/assets/icon.png
  31. // ==/UserScript==
  32.  
  33. GM_addStyle (
  34. `
  35. .relative {
  36. position: relative !important;
  37. }
  38.  
  39. .download-check {
  40. position: absolute;
  41. top: 0;
  42. left: 0;
  43. cursor: pointer;
  44. height: 20px;
  45. width: 20px;
  46. accent-color: #ed2553;
  47. }
  48.  
  49. .download-check:focus,
  50. .download-check:hover {
  51. box-shadow: 0 0 10px 0 #ed2553;
  52. }
  53.  
  54. .red-box {
  55. position: relative;
  56. background-color: #ed2553;
  57. color: white;
  58. border: none;
  59. outline: none;
  60. font-size: 16px;
  61. width: 80px;
  62. height: 35px;
  63. border-radius: 5px;
  64. display: flex;
  65. align-items: center;
  66. justify-content: center;
  67. text-align: center;
  68. cursor: pointer;
  69. }
  70.  
  71. .percent {
  72. position: absolute;
  73. top: 50%;
  74. left: 50%;
  75. transform: translate(-50%,-50%);
  76. }
  77.  
  78. .downloading-span,
  79. .compressing-span {
  80. display: inline-block;
  81. position: absolute;
  82. top: 0;
  83. left: 0;
  84. width: 50px;
  85. height: 100%;
  86. border-radius: 5px;
  87. }
  88.  
  89. .downloading-span {
  90. background-color: #03c03c;
  91. }
  92.  
  93. .compressing-span {
  94. background-color: #0047ab;
  95. }
  96.  
  97. .red-box:hover {
  98. background-color: #4d4d4d;
  99. }
  100.  
  101. .red-box>i,
  102. .red-box>.download-check {
  103. margin-right: 5px;
  104. }
  105.  
  106. .download-check-all {
  107. width: 20px;
  108. height: 20px;
  109. cursor: pointer;
  110. accent-color: #1f1f1f;
  111. }
  112.  
  113. .red-box>.download-check:focus,
  114. .red-box>.download-check:hover {
  115. box-shadow: none;
  116. }
  117.  
  118. .red-box:disabled {
  119. background-color: #1f1f1f;
  120. cursor: default;
  121. }
  122.  
  123. .download-div {
  124. position: fixed;
  125. bottom: 0;
  126. left: 0;
  127. display: flex;
  128. flex-direction: column;
  129. gap: 5px;
  130. }
  131.  
  132. .div-horizontal {
  133. flex-direction: row !important;
  134. }
  135.  
  136. .fa-spinner,
  137. .fa-circle-notch {
  138. animation: spin 1.5s infinite linear;
  139. }
  140.  
  141. @keyframes spin {
  142. 100% {transform: rotate(359deg)};
  143. }
  144.  
  145. .config-wrapper {
  146. position: fixed;
  147. top: 0;
  148. left: 0;
  149. width: 100%;
  150. height: 100%;
  151. background-color: rgba(0, 0, 0, 0.8);
  152. z-index: 999999;
  153. visibility: hidden;
  154. }
  155.  
  156. .visible {
  157. visibility: visible;
  158. }
  159.  
  160. .config-div {
  161. position: absolute;
  162. top: 50%;
  163. left: 50%;
  164. width: 100%;
  165. height: 100%;
  166. max-width: 850px;
  167. max-height: 550px;
  168. padding: 50px;
  169. transform: translate(-50%, -50%);
  170. background-color: #1f1f1f;
  171. border-radius: 5px;
  172. display: flex;
  173. flex-direction: column;
  174. align-items: flex-start;
  175. justify-content: center;
  176. overflow: scroll;
  177. }
  178.  
  179. .config-element-div {
  180. width: 100%;
  181. display: inline-grid;
  182. grid: auto / 200px auto;
  183. margin-bottom: 15px
  184. }
  185.  
  186. .config-element-div>input {
  187. color: #000 !important;
  188. }
  189.  
  190. .config-element-div>label {
  191. grid-column-start: 1;
  192. }
  193.  
  194. .config-element-div>input[type='checkbox'] {
  195. justify-self: left;
  196. }
  197.  
  198. .config-element-div>*:not(label) {
  199. grid-column-start: 2;
  200. border-radius: 0;
  201. border: none;
  202. background-color: #d9d9d9 !important;
  203. }
  204.  
  205. .config-btn-div {
  206. display: flex;
  207. flex-direction: row;
  208. gap: 10px;
  209. align-items: center;
  210. }
  211.  
  212. .config-reset:hover {
  213. cursor: pointer;
  214. color: #ed2553;
  215. }
  216.  
  217. .heading {
  218. width: 100%;
  219. border-bottom: solid 1px #d9d9d9;
  220. text-align: left;
  221. }
  222. `
  223. );
  224.  
  225. const netMediaUrl = "https://i3.nhentai.net/galleries/";
  226. const btnStates = {
  227. enabled: "<i class='fa fa-download'></i>",
  228. fetching: "<i id='btn-spinner' class='fa fa-spinner'></i>",
  229. downloading: "<i id='btn-spinner' class='fa fa-circle-notch'></i>",
  230. config: "<i class='fa fa-cog'></i>",
  231. };
  232. const titleFormats = {
  233. PR: "pretty",
  234. EN: "english",
  235. JP: "japanese",
  236. ID: "id",
  237. };
  238. const saveJSONModes = {
  239. NO: "Don't save",
  240. FI: "Save as JSON file",
  241. CB: "Copy to clipboard",
  242. };
  243. const fileNameSeps = {
  244. SP: "Space",
  245. HY: "Hyphen",
  246. US: "Underscore",
  247. };
  248. const btnOrientations = {
  249. VR: "Vertical",
  250. HR: "Horizontal",
  251. };
  252. const CONFIG = {
  253. simulN: {
  254. label: 'Download batch size',
  255. type: 'int',
  256. min: 1,
  257. max: 50,
  258. default: 10,
  259. },
  260. compressionLevel: {
  261. label: 'Compression level',
  262. type: 'int',
  263. min: 0,
  264. max: 9,
  265. default: 0,
  266. },
  267. titleFormat: {
  268. label: 'Title format',
  269. type: 'select',
  270. options: [titleFormats.PR,titleFormats.EN,titleFormats.JP,
  271. titleFormats.ID],
  272. default: titleFormats.PR,
  273. },
  274. fileNamePrep: {
  275. label: 'Filename to prepend',
  276. type: 'text',
  277. size: 30,
  278. default: "",
  279. },
  280. fileNameSep: {
  281. label: 'Filename separator',
  282. type: 'select',
  283. options: [fileNameSeps.SP,fileNameSeps.HY,fileNameSeps.US],
  284. default: fileNameSeps.SP,
  285. },
  286. saveJSONMode: {
  287. label: 'Save JSON',
  288. type: 'select',
  289. options: [saveJSONModes.NO,saveJSONModes.FI,saveJSONModes.CB],
  290. default: saveJSONModes.NO,
  291. },
  292. includeGroups: {
  293. label: 'Include groups in authors',
  294. type: 'checkbox',
  295. default: false,
  296. },
  297. btnOrientation: {
  298. label: 'Button orientation',
  299. type: 'select',
  300. options: [btnOrientations.VR, btnOrientations.HR],
  301. default: btnOrientations.VR,
  302. },
  303. openInNewTab: {
  304. label: 'Open galleries in new tab',
  305. type: 'checkbox',
  306. default: true,
  307. },
  308. autorestart: {
  309. label: 'Auto restart downloads',
  310. type: 'checkbox',
  311. default: true,
  312. }
  313. };
  314.  
  315. const WORKER_THREAD_NUM = ((navigator && navigator.hardwareConcurrency) || 2) - 1;
  316.  
  317. class JSZipWorkerPool {
  318. constructor() {
  319. this.pool = [];
  320. this.WORKER_URL = URL.createObjectURL(
  321. new Blob(
  322. [
  323. `importScripts(
  324. 'https://unpkg.com/comlink/dist/umd/comlink.min.js',
  325. 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js'
  326. );
  327. class JSZipWorker {
  328. constructor() {
  329. this.zip = new JSZip;
  330. }
  331. file(title, name, {data:data}) {
  332. this.zip.folder(title).file(name, data);
  333. }
  334. async generateAsync(options, onUpdate) {
  335. const data = await this.zip.generateAsync(options, onUpdate);
  336. const url = URL.createObjectURL(data);
  337. return Comlink.transfer({url:url});
  338. }
  339. }
  340. Comlink.expose(JSZipWorker);`
  341. ],
  342. {type: 'text/javascript'}
  343. )
  344. );
  345. for(let id=0; id<WORKER_THREAD_NUM; id++) {
  346. this.pool.push({
  347. id,
  348. JSZip: null,
  349. idle: true,
  350. });
  351. }
  352. }
  353. createWorker() {
  354. const worker = new Worker(this.WORKER_URL);
  355. return Comlink.wrap(worker);
  356. }
  357. async generateAsync(files, options, onUpdate) {
  358. const worker = this.pool.find(({idle}) => idle);
  359. if(!worker) throw new Error('No available JSZip worker');
  360. worker.idle = false;
  361. if(!worker.JSZip) worker.JSZip = this.createWorker();
  362. const zip = await new worker.JSZip();
  363. for(const {title, name, data} of files) {
  364. await zip.file(title, name, Comlink.transfer({data}, [data]));
  365. }
  366. return zip
  367. .generateAsync(
  368. options,
  369. Comlink.proxy((data) => onUpdate({workerId: worker.id, ...data}))
  370. )
  371. .then(({url}) => {
  372. worker.idle = true;
  373. return url;
  374. });
  375. }
  376. }
  377.  
  378. const jsZipPool = new JSZipWorkerPool();
  379.  
  380. class JSZip {
  381. constructor() {
  382. this.files = [];
  383. }
  384. file(title, name, data) {
  385. this.files.push({title, name, data});
  386. }
  387. generateAsync(options, onUpdate) {
  388. return jsZipPool.generateAsync(this.files, options, onUpdate);
  389. }
  390. }
  391.  
  392. // nhentai.net API URL
  393. const netAPI = "https://nhentai.net/api/gallery/";
  394. // nhentai.xxx page URL
  395. const xxxPage = "https://nhentai.xxx/g/";
  396. // If we are on nhentai.net
  397. const onNET = location.hostname == 'nhentai.net';
  398. // DOM parser for later
  399. var parser = new DOMParser();
  400. // Saved config options
  401. var configOptions = JSON.parse(GM_getValue('configOptions') || '{}');
  402. // Buttons and Divs
  403. var downloadDiv,
  404. downloadBtn,
  405. downloadPercent,
  406. downloadingSpan,
  407. compressingSpan,
  408. configWrapper,
  409. configDiv;
  410. // Download info
  411. var downloading = false,
  412. cancelled = false,
  413. total = 0,
  414. downloaded = 0,
  415. currentDownloads = 0,
  416. queue = [],
  417. info = JSON.parse(sessionStorage.getItem('info') || '[]');
  418. // Final zip
  419. var zip;
  420.  
  421. function disableButton(btnState) {
  422. downloadingSpan.style.width = 0;
  423. compressingSpan.style.width = 0;
  424. downloadBtn.disabled = true;
  425. downloadPercent.innerHTML = btnState;
  426. }
  427.  
  428. function enableButton() {
  429. downloadingSpan.style.width = 0;
  430. compressingSpan.style.width = 0;
  431. downloadBtn.disabled = false;
  432. downloadPercent.innerHTML = btnStates.enabled;
  433. }
  434.  
  435. function createNode(element, classes=[]) {
  436. var node = document.createElement(element);
  437. for(let cls of classes) {
  438. node.classList.add(cls);
  439. }
  440. return node;
  441. }
  442.  
  443. function createLabel(id, text) {
  444. var label = createNode('label');
  445. label.innerHTML = text;
  446. label.setAttribute('for', id);
  447. return label;
  448. }
  449.  
  450. function getExtension(type) {
  451. switch(type) {
  452. case 'j': return '.jpg';
  453. case 'p': return '.png';
  454. case 'g': return '.gif';
  455. }
  456. }
  457.  
  458. function saveConfig(reset=false) {
  459. const oldOpenInNewTab = configOptions.openInNewTab;
  460. for(const [key, value] of Object.entries(CONFIG)) {
  461. var element = document.getElementById(`config-${key}`);
  462. var configValue;
  463. switch(value.type) {
  464. case 'checkbox':
  465. configValue = element.checked;
  466. break;
  467. case 'int':
  468. configValue = element.value;
  469. if(configValue > value.max) {
  470. configValue = value.max;
  471. }
  472. if(configValue < value.min) {
  473. configValue = value.min;
  474. }
  475. break;
  476. default:
  477. configValue = element.value;
  478. }
  479. configValue = reset ? value.default : configValue;
  480. configOptions[key] = configValue;
  481. element.value = configValue;
  482. if(value.type == 'checkbox') element.checked = configValue;
  483. }
  484. if(configOptions.btnOrientation == btnOrientations.HR) {
  485. downloadDiv.classList.add('div-horizontal');
  486. }
  487. else {
  488. downloadDiv.classList.remove('div-horizontal');
  489. }
  490. GM_setValue('configOptions', JSON.stringify(configOptions));
  491. if(configOptions.openInNewTab != oldOpenInNewTab) {
  492. var gLinks = document.querySelectorAll('.gallery > a, .gallerythumb');
  493. for(let a of gLinks) {
  494. if(configOptions.openInNewTab) {
  495. a.setAttribute('target', '_blank');
  496. }
  497. else {
  498. a.removeAttribute('target');
  499. }
  500. }
  501. }
  502. }
  503.  
  504. function addConfigMenu() {
  505. var saveConfigBtn, closeConfigBtn, cancelAnchor, resetConfigAnchor;
  506.  
  507. var heading = createNode('h3', ['heading']);
  508. heading.innerHTML = "nhentai-konnichiwa";
  509. configDiv.appendChild(heading);
  510.  
  511. for(const [key, value] of Object.entries(CONFIG)) {
  512. var div = createNode('div', ['config-element-div']);
  513. var element, id, label;
  514. switch(value.type) {
  515. case 'select':
  516. element = createNode('select');
  517. for(let i=0; i<value.options.length; i++) {
  518. var optionElement = createNode('option');
  519. var option = value.options[i];
  520. optionElement.text = option;
  521. optionElement.value = option;
  522. element.appendChild(optionElement);
  523. }
  524. break;
  525. case 'int':
  526. element = createNode('input');
  527. element.type = 'number';
  528. if(value.min != undefined) {
  529. element.min = value.min;
  530. }
  531. if(value.max != undefined) {
  532. element.max = value.max;
  533. }
  534. break;
  535. case 'checkbox':
  536. element = createNode('input');
  537. element.type = 'checkbox';
  538. break;
  539. case 'text':
  540. element = createNode('input');
  541. element.type = 'text';
  542. element.maxLength = value.size;
  543. element.defaultValue = value.default;
  544. break;
  545. }
  546. id = `config-${key}`;
  547. element.id = id;
  548. var configValue = (configOptions.hasOwnProperty(key))
  549. ? configOptions[key]
  550. : value.default;
  551. if(value.type == 'checkbox') {
  552. element.checked = configValue;
  553. }
  554. else {
  555. element.value = configValue;
  556. }
  557. // If key does not exist in configOptions, add it
  558. if(!configOptions[key]) {
  559. configOptions[key] = configValue;
  560. }
  561. label = createLabel(id, value.label);
  562.  
  563. div.appendChild(label);
  564. div.appendChild(element);
  565.  
  566. configDiv.appendChild(div);
  567. }
  568.  
  569. var btnDiv = createNode('div', ['config-btn-div']);
  570. saveConfigBtn = createNode('button', ['red-box']);
  571. saveConfigBtn.innerHTML = "Save";
  572. saveConfigBtn.addEventListener('click', () => {
  573. saveConfig();
  574. });
  575.  
  576. closeConfigBtn = createNode('button', ['red-box']);
  577. closeConfigBtn.innerHTML = "Close";
  578. closeConfigBtn.addEventListener('click', () => {
  579. configWrapper.classList.remove('visible');
  580. });
  581.  
  582. cancelAnchor = createNode('a', ['config-reset']);
  583. cancelAnchor.innerHTML = "Cancel downloads";
  584. cancelAnchor.addEventListener('click', () => {
  585. cancelDownload();
  586. });
  587.  
  588. resetConfigAnchor = createNode('a', ['config-reset']);
  589. resetConfigAnchor.innerHTML = 'Reset to default';
  590. resetConfigAnchor.addEventListener('click', () => {
  591. saveConfig(true);
  592. });
  593.  
  594. btnDiv.appendChild(saveConfigBtn);
  595. btnDiv.appendChild(closeConfigBtn);
  596. btnDiv.appendChild(cancelAnchor);
  597. btnDiv.appendChild(resetConfigAnchor);
  598.  
  599. configDiv.appendChild(btnDiv);
  600. }
  601.  
  602. (async function() {
  603. 'use strict';
  604.  
  605. window.addEventListener('beforeunload', (e) => {
  606. if(downloading) {
  607. e.preventDefault();
  608. return '';
  609. }
  610. });
  611.  
  612. var aList, configBtn, checkAllDiv, checkAll;
  613.  
  614. configWrapper = createNode('div', ['config-wrapper']);
  615. configDiv = createNode('div', ['config-div']);
  616. configWrapper.addEventListener('click', () => {
  617. if(event.target != configWrapper) {
  618. return;
  619. }
  620. configWrapper.classList.remove('visible');
  621. });
  622. configWrapper.appendChild(configDiv);
  623. addConfigMenu();
  624.  
  625. aList = document.querySelectorAll(".gallery > a, #cover > a");
  626. for(let a of aList) {
  627. if(configOptions.openInNewTab) a.setAttribute('target', '_blank');
  628. var ref = a.href;
  629. var parent = a.parentElement;
  630. var code;
  631. code = ref.split("/g/")[1];
  632. if(code.endsWith("/")) {
  633. code = code.split("/")[0];
  634. }
  635. var check = createNode('input', ['download-check']);
  636. check.type = 'checkbox';
  637. check.value = code;
  638. parent.classList.add("relative");
  639. parent.appendChild(check);
  640. }
  641. if(configOptions.openInNewTab) {
  642. aList = document.getElementsByClassName("gallerythumb");
  643. for(let a of aList) {
  644. a.setAttribute('target', '_blank');
  645. }
  646. }
  647. let classes = ['download-div'];
  648. if(configOptions.btnOrientation == btnOrientations.HR) {
  649. classes.push('div-horizontal');
  650. }
  651. downloadDiv = createNode('div', classes);
  652.  
  653. downloadBtn = createNode('button', ['red-box']);
  654. downloadPercent = createNode('span', ['percent']);
  655. downloadingSpan = createNode('span', ['downloading-span']);
  656. compressingSpan = createNode('span', ['compressing-span']);
  657. enableButton();
  658. downloadBtn.appendChild(downloadingSpan);
  659. downloadBtn.appendChild(compressingSpan);
  660. downloadBtn.appendChild(downloadPercent);
  661.  
  662. configBtn = createNode('button', ['red-box']);
  663. configBtn.innerHTML = btnStates.config;
  664. configBtn.addEventListener("click", (event) => {
  665. configWrapper.classList.add('visible');
  666. });
  667.  
  668. checkAllDiv = createNode('div', ['red-box', 'relative']);
  669. checkAll = createNode('input', ['download-check-all']);
  670. checkAll.type = 'checkbox';
  671. checkAll.addEventListener('change', () => {
  672. let toCheck = checkAll.checked;
  673. let boxes = document.querySelectorAll('.download-check');
  674. for(let box of boxes) {
  675. if(box === checkAll) {
  676. continue;
  677. }
  678. box.checked = toCheck;
  679. }
  680. });
  681. checkAllDiv.appendChild(checkAll);
  682.  
  683. downloadBtn.addEventListener("click", async () => {
  684. var checked = document.querySelectorAll(".download-check:checked");
  685. if(checked.length > 0) {
  686. disableButton(btnStates.fetching);
  687. }
  688. else {
  689. return;
  690. }
  691. zip = await new JSZip();
  692. switch(configOptions.fileNameSep) {
  693. case fileNameSeps.SP: configOptions.fileNameSep = " "; break;
  694. case fileNameSeps.HY: configOptions.fileNameSep = "-"; break;
  695. case fileNameSeps.US: configOptions.fileNameSep = "_"; break;
  696. }
  697. for(let c of checked) {
  698. c.checked = false;
  699. }
  700. for(const c of checked) {
  701. var code = c.getAttribute("value");
  702. await addInfo(code);
  703. sessionStorage.setItem('info', JSON.stringify(info));
  704. }
  705. startDownload();
  706. });
  707.  
  708. downloadDiv.appendChild(downloadBtn);
  709. downloadDiv.appendChild(configBtn);
  710. downloadDiv.appendChild(checkAllDiv);
  711.  
  712. document.body.appendChild(downloadDiv);
  713. document.body.appendChild(configWrapper);
  714.  
  715. if(info.length > 0) {
  716. if(configOptions.autorestart) {
  717. queue = [];
  718. startDownload();
  719. }
  720. else {
  721. info = [];
  722. sessionStorage.removeItem('info');
  723. }
  724. }
  725. })();
  726.  
  727. function startDownload() {
  728. downloading = true;
  729. cancelled = false;
  730. currentDownloads = 0;
  731. downloaded = 0;
  732. zip = new JSZip();
  733. populateQueue();
  734. total = queue.length;
  735. disableButton(btnStates.downloading);
  736. downloadQueue();
  737. }
  738.  
  739. function cancelDownload() {
  740. info = [];
  741. queue = [];
  742. sessionStorage.removeItem('info');
  743. currentDownloads = 0;
  744. downloading = false;
  745. cancelled = true;
  746. enableButton();
  747. }
  748.  
  749. function cleanString(string) {
  750. string = string.replace(/(\.+$)|(^\.+)|(\|)/g, '');
  751. string = string.replace(/\\\/\:\;/g, configOptions.fileNameSep);
  752. string = string.replace(/\s\s+/, ' ');
  753. string = string.trim();
  754. return string;
  755. }
  756.  
  757. async function makeGetRequest(url, code = null) {
  758. return new Promise((resolve, reject) => {
  759. if(onNET) {
  760. fetch(url, {
  761. method: 'GET',
  762. mode: 'same-origin',
  763. credentials: 'same-origin',
  764. headers: {
  765. 'Content-Type': 'application/json'
  766. },
  767. referrerPolicy: 'same-origin',
  768. })
  769. .then(response => resolve(response.json()));
  770. }
  771. else {
  772. GM_xmlhttpRequest({
  773. method: "GET",
  774. url: url,
  775. onload: (response) => {
  776. resolve(parseXXXResponse(response, code));
  777. },
  778. onerror: (error) => {
  779. reject(error);
  780. }
  781. });
  782. }
  783. });
  784. }
  785.  
  786. function addInfoNET(obj, code) {
  787. var title;
  788. if(configOptions.titleFormat == 'id') {
  789. title = `${obj.id}`;
  790. }
  791. else {
  792. title = obj.title[configOptions.titleFormat];
  793. title = cleanString(title);
  794. }
  795. var pages = obj.num_pages;
  796. var tagList = obj.tags;
  797. var artists = [];
  798. var tags = [];
  799. for(let i=0; i<tagList.length; i++) {
  800. let tagItem = tagList[i];
  801. if(tagItem.type == "artist"
  802. || (configOptions.includeGroups && tagItem.type == "group")) {
  803. artists.push(tagItem.name);
  804. }
  805. if(tagItem.type == "tag") {
  806. tags.push(tagItem.name);
  807. }
  808. }
  809. var mediaUrl = `${netMediaUrl}${obj.media_id}/`;
  810. const constTitleExists = info.some((el) => el.title === title);
  811. if(constTitleExists) {
  812. title += " - "+code;
  813. }
  814. var fileNamePrep = configOptions.fileNamePrep;
  815. fileNamePrep = cleanString(fileNamePrep);
  816. var namePrep = "";
  817. if(fileNamePrep != "") {
  818. namePrep = fileNamePrep + configOptions.fileNameSep;
  819. }
  820. var coverExtension = getExtension(obj.images.pages[0].t);
  821. info.push({
  822. code: code,
  823. title: title,
  824. artists: artists,
  825. tags: tags,
  826. pages: pages,
  827. mediaUrl: mediaUrl,
  828. namePrep: namePrep,
  829. coverExtension: coverExtension,
  830. pagesInfo: obj.images.pages,
  831. });
  832. }
  833.  
  834. async function addInfo(code) {
  835. var apiUrl, obj;
  836. if(onNET) {
  837. apiUrl = netAPI + code;
  838. obj = await makeGetRequest(apiUrl);
  839. addInfoNET(obj, code);
  840. }
  841. else {
  842. apiUrl = xxxPage + code;
  843. obj = await makeGetRequest(apiUrl, code);
  844. info.push(obj);
  845. }
  846. }
  847.  
  848. function parseXXXResponse(response, code) {
  849. var htmlDoc = parser.parseFromString(response.responseText,
  850. 'text/html');
  851. var title, artists = [], tags = [], pages, mediaUrl, pagesInfo = [], coverExtension;
  852. var titleTemplate = string => {
  853. return `${string}.title > span`;
  854. }
  855. const cleanRegex = /(\[[^\]]*\])|(\([^)]*\))|(\{[^}]*\})|([\.\|\~]*)/g;
  856. switch(configOptions.titleFormat) {
  857. case 'english':
  858. title = htmlDoc.querySelector(titleTemplate('h1'));
  859. title = title.textContent;
  860. break;
  861. case 'japanese':
  862. title = htmlDoc.querySelector(titleTemplate('h2'));
  863. title = title.textContent;
  864. break;
  865. case 'pretty':
  866. default:
  867. title = htmlDoc.querySelector(titleTemplate('h1'));
  868. title = title.textContent;
  869. title = title.replace(cleanRegex, '');
  870. title = title.replace(/\s\s+/g, ' ');
  871. title = title.trim();
  872. title = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
  873. }
  874. var tagTemplate = string => {
  875. return `a[href*='${string}/'] > span.name`;
  876. }
  877. var artistSpans = htmlDoc.querySelectorAll(tagTemplate('artist'));
  878. for(var artist of artistSpans) {
  879. artists.push(artist.textContent);
  880. }
  881. if(configOptions.includeGroups) {
  882. var groupSpans = htmlDoc.querySelectorAll(tagTemplate('group'));
  883. for(var group of groupSpans) {
  884. artists.push(group.textContent);
  885. }
  886. }
  887. var tagSpans = htmlDoc.querySelectorAll(tagTemplate('tag'));
  888. for(var tag of tagSpans) {
  889. tags.push(tag.textContent);
  890. }
  891. pages = htmlDoc.querySelector(".tag[href*='#'] > span.name");
  892. pages = parseInt(pages.textContent);
  893. var fileNamePrep = configOptions.fileNamePrep;
  894. fileNamePrep = cleanString(fileNamePrep);
  895. var namePrep = "";
  896. if(fileNamePrep != "") {
  897. namePrep = fileNamePrep + configOptions.fileNameSep;
  898. }
  899. var thumbs = htmlDoc.querySelectorAll("a.gallerythumb > img");
  900. mediaUrl = thumbs[0].src;
  901. mediaUrl = mediaUrl.substring(0, mediaUrl.lastIndexOf("/")+1);
  902. for(var thumb of thumbs) {
  903. var extension = thumb.src.split('.').pop().charAt(0);
  904. pagesInfo.push({t: extension});
  905. }
  906. coverExtension = getExtension(pagesInfo[0].t);
  907. var obj = {
  908. code: code,
  909. title: title,
  910. artists: artists,
  911. tags: tags,
  912. pages: pages,
  913. mediaUrl: mediaUrl,
  914. namePrep: namePrep,
  915. coverExtension: coverExtension,
  916. pagesInfo: pagesInfo,
  917. }
  918. return obj;
  919. }
  920.  
  921. async function addToQueue(item, mediaId=null) {
  922. var pages = item.pages;
  923. var title = item.title;
  924. var namePrep = item.namePrep;
  925. var mediaUrl = item.mediaUrl;
  926. for(let i=0; i<pages; i++) {
  927. var extension = getExtension(item.pagesInfo[i].t);
  928. let page = i + 1;
  929. var imgUrl = `${mediaUrl}${page}${extension}`;
  930. queue.push({
  931. page: page,
  932. url: imgUrl,
  933. title: title,
  934. namePrep: namePrep,
  935. extension: extension,
  936. });
  937. }
  938. }
  939.  
  940. function populateQueue() {
  941. for(const item of info) {
  942. addToQueue(item);
  943. }
  944. }
  945.  
  946. async function downloadQueue() {
  947. while(queue.length > 0) {
  948. if(currentDownloads >= configOptions.simulN) {
  949. await sleep(125);
  950. continue;
  951. }
  952. var item = queue.shift();
  953. download(item);
  954. }
  955. }
  956.  
  957. function saveJSON(fileName) {
  958. var data = [];
  959. for(var item of info) {
  960. data.push({
  961. dirName: item.title,
  962. dirCover: `${item.namePrep}1${item.coverExtension}`,
  963. authors: item.artists,
  964. tags: item.tags,
  965. });
  966. }
  967. var fileContent = {
  968. 'directories': data
  969. };
  970. fileContent = JSON.stringify(fileContent, null, 2);
  971. if(configOptions.saveJSONMode == saveJSONModes.CB) {
  972. GM_setClipboard(fileContent, 'text');
  973. return;
  974. }
  975. fileContent = new TextEncoder().encode(fileContent);
  976. zip.file('', fileName, fileContent.buffer);
  977. }
  978.  
  979. function generateZip() {
  980. var dateName = Date.now();
  981. var compressionType = configOptions.compressionLevel == 0
  982. ? 'STORE'
  983. : 'DEFLATE';
  984. var zipName = `${dateName}.zip`;
  985. var jsonName = `${dateName}.json`;
  986. if(configOptions.saveJSONMode != saveJSONModes.NO) {
  987. saveJSON(jsonName);
  988. }
  989. zip.generateAsync(
  990. {
  991. type: 'blob',
  992. compression: compressionType,
  993. compressionOptions: {
  994. level: configOptions.compressionLevel,
  995. }},
  996. ({workerId, percent, currentFile}) => {
  997. var fraction = percent / 100;
  998. if(fraction == 1) {
  999. enableButton();
  1000. return;
  1001. }
  1002. downloadPercent.innerHTML = `${percent.toFixed(2)}%`;
  1003. compressingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px';
  1004. }
  1005. )
  1006. .then((url) => {
  1007. var a = createNode('a');
  1008. a.download = zipName;
  1009. a.href = url;
  1010. a.click();
  1011. })
  1012. .then(() => {
  1013. info = [];
  1014. sessionStorage.removeItem('info');
  1015. downloading = false;
  1016. });
  1017. }
  1018.  
  1019. function download(item) {
  1020. currentDownloads++;
  1021. const fileName = `${item.namePrep}${item.page}${item.extension}`;
  1022. GM_xmlhttpRequest({
  1023. method: 'GET',
  1024. url: item.url,
  1025. responseType: 'arraybuffer',
  1026. onload: (response) => {
  1027. if(cancelled) return;
  1028. var data = response.response;
  1029. zip.file(item.title, fileName, data);
  1030.  
  1031. currentDownloads--;
  1032.  
  1033. downloaded++;
  1034. var fraction = downloaded/total;
  1035. downloadPercent.innerHTML = `${(fraction * 100).toFixed(2)}%`;
  1036. downloadingSpan.style.width = fraction * downloadBtn.offsetWidth + 'px';
  1037.  
  1038. if(queue.length == 0 && currentDownloads <= 0) {
  1039. disableButton(btnStates.downloading);
  1040. generateZip();
  1041. }
  1042. },
  1043. onerror: (error) => {
  1044. currentDownloads--;
  1045. console.warn(`Could not download '${item.title}' - page ${item.page}`);
  1046. }
  1047. });
  1048. }
  1049.  
  1050. function sleep(ms) {
  1051. return new Promise((resolve) => setTimeout(resolve, ms));
  1052. }