nHentai Downloader

Download manga on nHentai.

Per 02-08-2020. Zie de nieuwste versie.

  1. // ==UserScript==
  2. // @name nHentai Downloader
  3. // @name:vi nHentai Downloader
  4. // @name:zh nHentai 下载器
  5. // @namespace http://devs.forumvi.com
  6. // @description Download manga on nHentai.
  7. // @description:vi Tải truyện tranh tại NhệnTái.
  8. // @description:zh 在nHentai上下载漫画。
  9. // @version 2.1.1
  10. // @icon http://i.imgur.com/FAsQ4vZ.png
  11. // @author Zzbaivong
  12. // @oujs:author baivong
  13. // @license MIT; https://baivong.mit-license.org/license.txt
  14. // @match http://nhentai.net/g/*
  15. // @match https://nhentai.net/g/*
  16. // @require https://code.jquery.com/jquery-3.5.1.min.js
  17. // @require https://unpkg.com/jszip@3.1.5/dist/jszip.min.js
  18. // @require https://greasyfork.org/scripts/28536-gm-config/code/GM_config.js?version=184529
  19. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?v=a834d46
  20. // @require https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js
  21. // @require https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js
  22. // @noframes
  23. // @connect self
  24. // @supportURL https://github.com/lelinhtinh/Userscript/issues
  25. // @run-at document-idle
  26. // @grant GM.xmlHttpRequest
  27. // @grant GM_xmlhttpRequest
  28. // @grant unsafeWindow
  29. // @grant GM_getValue
  30. // @grant GM_setValue
  31. // @grant GM.getValue
  32. // @grant GM.setValue
  33. // ==/UserScript==
  34.  
  35. /* global streamSaver */
  36. (($, window) => {
  37. 'use strict';
  38.  
  39. const configFrame = document.createElement('div');
  40. $('#info-block').append(configFrame);
  41.  
  42. GM_config.init({
  43. id: 'nHentaiDlConfig',
  44. title: 'Downloader Settings',
  45. fields: {
  46. outputExt: {
  47. options: ['cbz', 'zip'],
  48. label: 'Export as',
  49. type: 'radio',
  50. default: 'cbz',
  51. },
  52. outputName: {
  53. label: 'Filename',
  54. type: 'select',
  55. options: ['pretty', 'english', 'japanese'],
  56. default: 'pretty',
  57. },
  58. threading: {
  59. label: 'Max. conn. number',
  60. type: 'unsigned int',
  61. min: 1,
  62. max: 32,
  63. default: 4,
  64. },
  65. hideTorrentBtn: {
  66. label: 'Hide the download torrent button',
  67. type: 'checkbox',
  68. default: false,
  69. },
  70. },
  71. frame: configFrame,
  72. events: {
  73. save: () => {
  74. outputExt = GM_config.get('outputExt');
  75. outputName = GM_config.get('outputName');
  76. threading = GM_config.get('threading');
  77.  
  78. $download.find('span').text(` as ${outputExt.toUpperCase()}`);
  79.  
  80. if (GM_config.get('hideTorrentBtn') == true) {
  81. $_download.hide();
  82. } else {
  83. $_download.show();
  84. }
  85.  
  86. const $saveBtn = $('#nHentaiDlConfig_saveBtn');
  87. $saveBtn.prop('disabled', true).addClass('saved').text('Saved!');
  88.  
  89. setTimeout(() => {
  90. $saveBtn.prop('disabled', false).removeClass('saved').text('Save');
  91. }, 1500);
  92. },
  93. },
  94. css:
  95. '#nHentaiDlConfig{width:100%!important;position:initial!important;padding:10px!important;background:#0d0d0d;border:1px solid #313131!important;border-radius:5px;text-align:left}#nHentaiDlConfig *{font-family:"Noto Sans",sans-serif}#nHentaiDlConfig .config_header{text-align:left;font-size:17px;font-weight:700;margin-bottom:20px;color:#999}#nHentaiDlConfig .reset_holder{float:left;height:30px;line-height:30px}#nHentaiDlConfig .reset{color:#4d4d4d;text-align:left}#nHentaiDlConfig .saveclose_buttons{margin:0;padding:4px;min-width:100px;height:30px;line-height:14px;border-radius:2px;border:1px solid;cursor:pointer}#nHentaiDlConfig .saveclose_buttons.saved{background:#ffeb3b;border:1px solid #ffc107}#nHentaiDlConfig #nHentaiDlConfig_closeBtn{display:none}#nHentaiDlConfig_buttons_holder{margin-top:20px;border-top:1px dashed #4d4d4d;padding-top:11px}#nHentaiDlConfig .config_var::after{clear:both;content:"";display:block}#nHentaiDlConfig .config_var{position:relative}#nHentaiDlConfig .field_label{font-size:14px;height:26px;line-height:26px;margin:0;padding:0 10px 0 0;width:60%;display:block;float:left}#nHentaiDlConfig .config_var>[type=text],#nHentaiDlConfig .config_var>div,#nHentaiDlConfig .config_var>select,#nHentaiDlConfig .config_var>textarea{width:40%;border-radius:0;display:block;height:26px;line-height:26px;padding:0 10px;float:left}#nHentaiDlConfig .config_var>textarea{height:auto;line-height:14px;padding:10px;min-height:5em}#nHentaiDlConfig .config_var>select{background:#4d4d4d;color:#d9d9d9;padding:0}#nHentaiDlConfig .config_var>select:hover{background:#666}#nHentaiDlConfig .config_var>select:focus{outline:0 none}#nHentaiDlConfig .config_var>div>label{display:inline-block;vertical-align:top;margin-right:5px}#nHentaiDlConfig .config_var>#nHentaiDlConfig_field_outputName{width:150px;text-transform:capitalize}#nHentaiDlConfig .config_var>#nHentaiDlConfig_field_threading{width:70px}#nHentaiDlConfig .config_var>div{padding:0}#nHentaiDlConfig_field_outputExt{text-transform:uppercase}#nHentaiDlConfig_field_outputExt [value=cbz]{margin-right:20px!important}',
  96. });
  97.  
  98. /**
  99. * Output extension
  100. * @type {'cbz'|'zip'}
  101. *
  102. * Tips: Convert .zip to .cbz
  103. * Windows
  104. * $ ren *.zip *.cbz
  105. * Linux
  106. * $ rename 's/\.zip$/\.cbz/' *.zip
  107. */
  108. let outputExt = GM_config.get('outputExt') || 'cbz';
  109.  
  110. /**
  111. * File name
  112. * @type {'pretty'|'english'|'japanese'}
  113. */
  114. let outputName = GM_config.get('outputName') || 'pretty';
  115.  
  116. /**
  117. * Multithreading
  118. * @type {Number} [1 -> 32]
  119. */
  120. let threading = GM_config.get('threading') || 4;
  121.  
  122. /**
  123. * Logging
  124. * @type {Boolean}
  125. */
  126. let debug = false;
  127.  
  128. function end() {
  129. $win.off('beforeunload');
  130.  
  131. if (debug) console.timeEnd('nHentai');
  132. }
  133.  
  134. function getInfo() {
  135. let info = '',
  136. tags = [],
  137. artists = [],
  138. groups = [],
  139. parodies = [],
  140. characters = [],
  141. categories = [],
  142. languages = [];
  143.  
  144. if (gallery.title.english) info += gallery.title.english + '\r\n';
  145. if (gallery.title.japanese) info += gallery.title.japanese + '\r\n';
  146. if (gallery.title.pretty) info += gallery.title.pretty + '\r\n';
  147. info += '#' + gallery.id + '\r\n';
  148.  
  149. if (gallery.tags) {
  150. for (const tag of gallery.tags) {
  151. if (tag.type === 'tag') tags.push(tag.name);
  152. if (tag.type === 'artist') artists.push(tag.name);
  153. if (tag.type === 'category') categories.push(tag.name);
  154. if (tag.type === 'group') groups.push(tag.name);
  155. if (tag.type === 'parody') parodies.push(tag.name);
  156. if (tag.type === 'character') characters.push(tag.name);
  157. if (tag.type === 'language') languages.push(tag.name);
  158. }
  159. }
  160. if (tags.length) info += '\r\n' + 'Tags: ' + tags.join(', ');
  161. if (categories.length) info += '\r\n' + 'Categories: ' + categories.join(', ');
  162. if (groups.length) info += '\r\n' + 'Groups: ' + groups.join(', ');
  163. if (parodies.length) info += '\r\n' + 'Parodies: ' + parodies.join(', ');
  164. if (characters.length) info += '\r\n' + 'Characters: ' + characters.join(', ');
  165. if (languages.length) info += '\r\n' + 'Languages: ' + languages.join(', ');
  166.  
  167. info += '\r\n\r\n' + 'Pages: ' + total;
  168. info += '\r\n' + 'Uploaded at: ' + new Date(gallery.upload_date * 1000).toLocaleString() + '\r\n';
  169.  
  170. if (debug) console.log(info);
  171. return info;
  172. }
  173.  
  174. function genZip() {
  175. const filename = gallery.title[outputName] || gallery.title['english']; // e.g. #321311
  176.  
  177. zip.file('info.txt', getInfo());
  178. zip
  179. .generateAsync(
  180. {
  181. type: 'blob',
  182. compression: 'STORE',
  183. streamFiles: true, // Less memory but less compatibility, https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#streamfiles-option
  184. },
  185. (metadata) => {
  186. $download.html(`<i class="fa fa-file-archive"></i> ${metadata.percent.toFixed(2)} %`);
  187. }
  188. )
  189. .then(
  190. (blob) => {
  191. const zipName = `${filename.replace(/\s+/g, '-').replace(/・/g, '·')}.${comicId}.${outputExt}`;
  192.  
  193. $download
  194. .html('<i class="fa fa-check"></i> Complete')
  195. .css('backgroundColor', hasErr ? 'red' : 'green')
  196. .attr({
  197. href: 'javascript:void(0);',
  198. download: zipName,
  199. });
  200.  
  201. const fileStream = streamSaver.createWriteStream(zipName, {
  202. size: blob.size,
  203. });
  204. const readableStream = blob.stream();
  205.  
  206. window.FSwriter = fileStream.getWriter();
  207. const reader = readableStream.getReader();
  208. const pump = () =>
  209. reader
  210. .read()
  211. .then((res) => (res.done ? window.FSwriter.close() : window.FSwriter.write(res.value).then(pump)));
  212. pump(); // Firefox does not support pipeTo() yet.
  213.  
  214. doc.title = `[⇓] ${filename}`;
  215. if (debug) console.log('COMPLETE');
  216. end();
  217. },
  218. (reason) => {
  219. $download.html('<i class="fa fa-exclamation"></i> Fail').css('backgroundColor', 'red');
  220.  
  221. doc.title = `[x] ${filename}`;
  222. if (debug) console.error(reason, 'ERROR');
  223. end();
  224. }
  225. );
  226. }
  227.  
  228. function dlImg(current, success, error) {
  229. let url = images[current].url,
  230. filename = url.replace(/.*\//g, '');
  231.  
  232. filename = `000${filename}`.slice(-8);
  233. if (debug) console.log(filename, 'progress');
  234.  
  235. GM.xmlHttpRequest({
  236. method: 'GET',
  237. url: url,
  238. responseType: 'arraybuffer',
  239. onload: (response) => {
  240. final++;
  241. success(response, filename);
  242. },
  243. onerror: (err) => {
  244. if (images[current].attempt < 1) {
  245. final++;
  246. error(err, filename);
  247. return;
  248. }
  249.  
  250. setTimeout(() => {
  251. if (debug) console.log(filename, `retry ${images[current].attempt}`);
  252. dlImg(current, success, error);
  253. images[current].attempt--;
  254. }, 2000);
  255. },
  256. });
  257. }
  258.  
  259. function next() {
  260. $download.find('span').text(`${final}/${total}`);
  261. if (debug) console.log(final, current);
  262.  
  263. if (final < current) return;
  264. final < total ? addZip() : genZip();
  265. }
  266.  
  267. function addZip() {
  268. let max = current + threading;
  269. if (max > total) max = total;
  270.  
  271. for (current; current < max; current++) {
  272. if (debug) console.log(images[current].url, 'download');
  273. dlImg(
  274. current,
  275. (response, filename) => {
  276. zip.file(filename, response.response);
  277.  
  278. if (debug) console.log(filename, 'success');
  279. next();
  280. },
  281. (err, filename) => {
  282. hasErr = true;
  283. // zip.file(filename + '_error.txt', err.statusText + '\r\n' + err.finalUrl);
  284. zip.file(`${filename}_${comicId}_error.gif`, 'R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=', {
  285. base64: true,
  286. });
  287. $download.css('backgroundColor', '#FF7F7F');
  288.  
  289. if (debug) console.log(filename, 'error');
  290. next();
  291. }
  292. );
  293. }
  294. if (debug) console.log(current, 'current');
  295. }
  296.  
  297. const gallery = JSON.parse(JSON.stringify(window._gallery));
  298. if (debug) console.log(gallery, 'gallery');
  299. if (!gallery) return;
  300.  
  301. let zip = new JSZip(),
  302. current = 0,
  303. final = 0,
  304. total = gallery.num_pages,
  305. images = gallery.images.pages,
  306. hasErr = false,
  307. $_download = $('#download-torrent, #download'),
  308. $download,
  309. $config,
  310. $configPanel,
  311. doc = document,
  312. $win = $(window),
  313. comicId = gallery.id;
  314.  
  315. if (!$_download.length) return;
  316. GM_config.open();
  317. $configPanel = $('#nHentaiDlConfig');
  318.  
  319. window.URL = window.URL || window.webkitURL;
  320.  
  321. $download = $_download.clone();
  322. $download.removeAttr('id');
  323. $download.removeClass('btn-disabled');
  324. $download.attr('href', '#download');
  325. $download.find('.top').html('No login required<br>No sign up required<i></i>');
  326. $download.append(`<span> as ${outputExt.toUpperCase()}</span>`);
  327.  
  328. $download.insertAfter($_download);
  329. $download.before('\n');
  330.  
  331. $download.css('backgroundColor', 'cornflowerblue').one('click', (e) => {
  332. e.preventDefault();
  333. if (debug) console.time('nHentai');
  334. if (debug) console.log({ outputExt, outputName, threading });
  335.  
  336. if (threading < 1) threading = 1;
  337. if (threading > 32) threading = 32;
  338.  
  339. $win.on('beforeunload', () => {
  340. return 'Progress is running...';
  341. });
  342.  
  343. $download.html('<i class="fa fa-spinner fa-spin"></i> <span>Waiting...</span>').css('backgroundColor', 'orange');
  344.  
  345. images = images.map((img, index) => {
  346. return {
  347. url: `https://i.nhentai.net/galleries/${gallery.media_id}/${index + 1}.${
  348. { j: 'jpg', p: 'png', g: 'gif' }[img.t]
  349. }`,
  350. attempt: 3,
  351. };
  352. });
  353. if (debug) console.log(images, 'images');
  354.  
  355. addZip();
  356. });
  357.  
  358. $configPanel.toggle();
  359. $config = $_download.clone();
  360. $config.removeAttr('id');
  361. $config.removeClass('btn-disabled');
  362. $config.attr('href', 'javascript:void(0);');
  363. $config.css('min-width', '40px');
  364. $config.html('<i class="fa fa-cog"></i>');
  365.  
  366. $config.insertAfter($download);
  367. $config.before('\n');
  368. $config.on('click', () => {
  369. $configPanel.toggle('fast');
  370. });
  371.  
  372. if (GM_config.get('hideTorrentBtn') == true) $_download.hide();
  373. })(jQuery, unsafeWindow);