e-hentai retriever

e-hentai & exhentai image url retriever

  1. // ==UserScript==
  2. // @name e-hentai retriever
  3. // @namespace https://github.com/s25g5d4/e-hentai-retriever
  4. // @description e-hentai & exhentai image url retriever
  5. // @include /^https?://e-hentai.org/s/.*/
  6. // @include /^https?://exhentai.org/s/.*/
  7. // @version 4.4.1
  8. // @author s25g5d4
  9. // @homepageURL https://github.com/s25g5d4/e-hentai-retriever
  10. // @grant GM_xmlhttpRequest
  11. // @grant unsafeWindow
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. function styleInject(css, ref) {
  18. if ( ref === void 0 ) ref = {};
  19. var insertAt = ref.insertAt;
  20.  
  21. if (!css || typeof document === 'undefined') { return; }
  22.  
  23. var head = document.head || document.getElementsByTagName('head')[0];
  24. var style = document.createElement('style');
  25. style.type = 'text/css';
  26.  
  27. if (insertAt === 'top') {
  28. if (head.firstChild) {
  29. head.insertBefore(style, head.firstChild);
  30. } else {
  31. head.appendChild(style);
  32. }
  33. } else {
  34. head.appendChild(style);
  35. }
  36.  
  37. if (style.styleSheet) {
  38. style.styleSheet.cssText = css;
  39. } else {
  40. style.appendChild(document.createTextNode(css));
  41. }
  42. }
  43.  
  44. var css_248z = "#i3 a {\n display: inline-block;\n position: relative;\n}\n\n#i3.force-img-full-height a img {\n\twidth:auto !important;\n\theight:100vh !important;\n}\n\n.close,\n.swap,\n.page-number {\n\tposition: absolute;\n\twidth: 32px;\n\theight: 32px;\n\tmargin: 8px;\n\tz-index: 999;\n opacity: 0;\n transition: opacity 0.25s;\n\tbackground-color: rgba(255, 255, 255, 0.3);\n}\n\n.close {\n\ttop: 0;\n\tright: 0;\n\tbackground-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAmElEQVRYhc2USQ6AMAwD+/9PhyuRAs3igVrqCTR2m2UtL1u8Hj3sdkjz0MOCQ5o7j+iDOsTWgwyRZhMhykxliDZLEWLMmABkr9gByfuoAsQmKQPGd8mbAW7eDYHoV/NsCFxH3/6I+h8xAZ/tgMo/mDkWogOUhZiAxiEUt2gzlHUss4hOTjPJWd6y8UXSDaFWqQyUUo1Iy3lcN6p2mB7qGCwAAAAASUVORK5CYII=);\n}\n\n.swap {\n\ttop: 0;\n\tleft: 0;\n\tbackground-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAdklEQVRYhe3TwQ0AIQhE0enA7b9JS2BPezExERb9HpiEI/KiIlUqZ2I3AFCECUaYYMQI+IXokwMjhQNCCPwGmqTHWal/IJKrtsDVlA3Y17Bwnns4/lb4TzX5161lDo9UJ4eHAZmIMGCGOB4cMCKw4IAPUalszwvTfbCPlftI4QAAAABJRU5ErkJggg==);\n}\n\n.page-number {\n\tbottom: 0;\n\tright: 0;\n\tfont-size: 16px;\n\tline-height: 32px;\n\tcolor: black;\n}\n\n.close:hover,\n.swap:hover,\n.page-number:hover {\n opacity: 1;\n}\n\n.hidden {\n\tdisplay: none !important;\n}\n\n.show-hidden {\n\tfont-size: larger;\n\tmargin-left: 5px;\n}\n";
  45. styleInject(css_248z);
  46.  
  47. class Queue {
  48. constructor(limit, timeout = 0, delay = 0) {
  49. this.limit = limit;
  50. this.timeout = timeout;
  51. this.delay = delay;
  52. this.slot = [];
  53. this.q = [];
  54. }
  55. queue(executor, name) {
  56. const job = new Promise((resolve, reject) => {
  57. this.q.push({
  58. name,
  59. run: executor,
  60. resolve,
  61. reject,
  62. isTimeout: false,
  63. timeoutId: undefined
  64. });
  65. });
  66. // console.log(`queue: job ${name} queued`);
  67. this.dequeue();
  68. return job;
  69. }
  70. dequeue() {
  71. const { q, slot, limit, timeout, delay } = this;
  72. if (slot.length < limit && q.length >= 1) {
  73. const job = q.shift();
  74. slot.push(job);
  75. // console.log(`queue: job ${job.name} started`);
  76. if (timeout) {
  77. job.timeoutId = window.setTimeout(() => this.jobTimeout(job), timeout);
  78. }
  79. const onFulfilled = (data) => {
  80. if (job.isTimeout) {
  81. return;
  82. }
  83. this.removeJob(job);
  84. window.setTimeout(() => this.dequeue(), delay); // force dequeue() run after current dequeue()
  85. if (job.timeoutId) {
  86. window.clearTimeout(job.timeoutId);
  87. }
  88. // console.log(`queue: job ${job.name} resolved`);
  89. job.resolve(data);
  90. };
  91. const onRejected = (reason) => {
  92. if (job.isTimeout) {
  93. return;
  94. }
  95. this.removeJob(job);
  96. setTimeout(() => this.dequeue(), delay);
  97. if (job.timeoutId) {
  98. window.clearTimeout(job.timeoutId);
  99. }
  100. // console.log(`queue: job ${job.name} rejected`);
  101. job.reject(reason);
  102. };
  103. job.run(onFulfilled, onRejected);
  104. }
  105. }
  106. jobTimeout(job) {
  107. this.removeJob(job);
  108. // console.log(`queue: job ${job.name} timeout`);
  109. job.reject(new Error(`queue: job ${job.name} timeout`));
  110. job = null;
  111. }
  112. removeJob(job) {
  113. let index = this.slot.indexOf(job);
  114. if (index >= 0) {
  115. this.slot.splice(index, 1);
  116. return;
  117. }
  118. index = this.q.indexOf(job);
  119. if (index >= 0) {
  120. this.q.splice(index, 1);
  121. }
  122. }
  123. }
  124.  
  125. // Origin from window.fetch polyfill
  126. // https://github.com/github/fetch
  127. // License https://github.com/github/fetch/blob/master/LICENSE
  128. const COFetch = (input, init = {}) => {
  129. let request;
  130. if (Request.prototype.isPrototypeOf(input) && !init) {
  131. request = input;
  132. }
  133. else {
  134. request = new Request(input, init);
  135. }
  136. const headers = Object.assign({}, request.headers);
  137. let anonymous = true;
  138. if (request.credentials === 'include') {
  139. anonymous = false;
  140. }
  141. const onload = (resolve, reject, gmxhr) => {
  142. const init = {
  143. url: gmxhr.finalUrl || request.url,
  144. status: gmxhr.status,
  145. statusText: gmxhr.statusText,
  146. headers: undefined
  147. };
  148. try {
  149. const rawHeaders = gmxhr.responseHeaders.trim().replace(/\r\n(\s+)/g, '$1').split('\r\n').map(e => e.split(/:/));
  150. const header = new Headers();
  151. rawHeaders.forEach(e => {
  152. header.append(e[0].trim(), e[1].trim());
  153. });
  154. init.headers = header;
  155. const res = new Response(gmxhr.response, init);
  156. resolve(res);
  157. }
  158. catch (e) {
  159. reject(e);
  160. }
  161. };
  162. const onerror = (resolve, reject, gmxhr) => {
  163. reject(new TypeError('Network request failed'));
  164. };
  165. return new Promise((resolve, reject) => {
  166. GM_xmlhttpRequest({
  167. anonymous,
  168. method: request.method,
  169. url: request.url,
  170. headers: headers,
  171. responseType: 'blob',
  172. data: init.body,
  173. onload: onload.bind(null, resolve, reject),
  174. onerror: onerror.bind(null, resolve, reject)
  175. });
  176. });
  177. };
  178.  
  179. var domain;
  180.  
  181. // This constructor is used to store event handlers. Instantiating this is
  182. // faster than explicitly calling `Object.create(null)` to get a "clean" empty
  183. // object (tested with v8 v4.9).
  184. function EventHandlers() {}
  185. EventHandlers.prototype = Object.create(null);
  186.  
  187. function EventEmitter() {
  188. EventEmitter.init.call(this);
  189. }
  190.  
  191. // nodejs oddity
  192. // require('events') === require('events').EventEmitter
  193. EventEmitter.EventEmitter = EventEmitter;
  194.  
  195. EventEmitter.usingDomains = false;
  196.  
  197. EventEmitter.prototype.domain = undefined;
  198. EventEmitter.prototype._events = undefined;
  199. EventEmitter.prototype._maxListeners = undefined;
  200.  
  201. // By default EventEmitters will print a warning if more than 10 listeners are
  202. // added to it. This is a useful default which helps finding memory leaks.
  203. EventEmitter.defaultMaxListeners = 10;
  204.  
  205. EventEmitter.init = function() {
  206. this.domain = null;
  207. if (EventEmitter.usingDomains) {
  208. // if there is an active domain, then attach to it.
  209. if (domain.active ) ;
  210. }
  211.  
  212. if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
  213. this._events = new EventHandlers();
  214. this._eventsCount = 0;
  215. }
  216.  
  217. this._maxListeners = this._maxListeners || undefined;
  218. };
  219.  
  220. // Obviously not all Emitters should be limited to 10. This function allows
  221. // that to be increased. Set to zero for unlimited.
  222. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
  223. if (typeof n !== 'number' || n < 0 || isNaN(n))
  224. throw new TypeError('"n" argument must be a positive number');
  225. this._maxListeners = n;
  226. return this;
  227. };
  228.  
  229. function $getMaxListeners(that) {
  230. if (that._maxListeners === undefined)
  231. return EventEmitter.defaultMaxListeners;
  232. return that._maxListeners;
  233. }
  234.  
  235. EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
  236. return $getMaxListeners(this);
  237. };
  238.  
  239. // These standalone emit* functions are used to optimize calling of event
  240. // handlers for fast cases because emit() itself often has a variable number of
  241. // arguments and can be deoptimized because of that. These functions always have
  242. // the same number of arguments and thus do not get deoptimized, so the code
  243. // inside them can execute faster.
  244. function emitNone(handler, isFn, self) {
  245. if (isFn)
  246. handler.call(self);
  247. else {
  248. var len = handler.length;
  249. var listeners = arrayClone(handler, len);
  250. for (var i = 0; i < len; ++i)
  251. listeners[i].call(self);
  252. }
  253. }
  254. function emitOne(handler, isFn, self, arg1) {
  255. if (isFn)
  256. handler.call(self, arg1);
  257. else {
  258. var len = handler.length;
  259. var listeners = arrayClone(handler, len);
  260. for (var i = 0; i < len; ++i)
  261. listeners[i].call(self, arg1);
  262. }
  263. }
  264. function emitTwo(handler, isFn, self, arg1, arg2) {
  265. if (isFn)
  266. handler.call(self, arg1, arg2);
  267. else {
  268. var len = handler.length;
  269. var listeners = arrayClone(handler, len);
  270. for (var i = 0; i < len; ++i)
  271. listeners[i].call(self, arg1, arg2);
  272. }
  273. }
  274. function emitThree(handler, isFn, self, arg1, arg2, arg3) {
  275. if (isFn)
  276. handler.call(self, arg1, arg2, arg3);
  277. else {
  278. var len = handler.length;
  279. var listeners = arrayClone(handler, len);
  280. for (var i = 0; i < len; ++i)
  281. listeners[i].call(self, arg1, arg2, arg3);
  282. }
  283. }
  284.  
  285. function emitMany(handler, isFn, self, args) {
  286. if (isFn)
  287. handler.apply(self, args);
  288. else {
  289. var len = handler.length;
  290. var listeners = arrayClone(handler, len);
  291. for (var i = 0; i < len; ++i)
  292. listeners[i].apply(self, args);
  293. }
  294. }
  295.  
  296. EventEmitter.prototype.emit = function emit(type) {
  297. var er, handler, len, args, i, events, domain;
  298. var doError = (type === 'error');
  299.  
  300. events = this._events;
  301. if (events)
  302. doError = (doError && events.error == null);
  303. else if (!doError)
  304. return false;
  305.  
  306. domain = this.domain;
  307.  
  308. // If there is no 'error' event listener then throw.
  309. if (doError) {
  310. er = arguments[1];
  311. if (domain) {
  312. if (!er)
  313. er = new Error('Uncaught, unspecified "error" event');
  314. er.domainEmitter = this;
  315. er.domain = domain;
  316. er.domainThrown = false;
  317. domain.emit('error', er);
  318. } else if (er instanceof Error) {
  319. throw er; // Unhandled 'error' event
  320. } else {
  321. // At least give some kind of context to the user
  322. var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
  323. err.context = er;
  324. throw err;
  325. }
  326. return false;
  327. }
  328.  
  329. handler = events[type];
  330.  
  331. if (!handler)
  332. return false;
  333.  
  334. var isFn = typeof handler === 'function';
  335. len = arguments.length;
  336. switch (len) {
  337. // fast cases
  338. case 1:
  339. emitNone(handler, isFn, this);
  340. break;
  341. case 2:
  342. emitOne(handler, isFn, this, arguments[1]);
  343. break;
  344. case 3:
  345. emitTwo(handler, isFn, this, arguments[1], arguments[2]);
  346. break;
  347. case 4:
  348. emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
  349. break;
  350. // slower
  351. default:
  352. args = new Array(len - 1);
  353. for (i = 1; i < len; i++)
  354. args[i - 1] = arguments[i];
  355. emitMany(handler, isFn, this, args);
  356. }
  357.  
  358. return true;
  359. };
  360.  
  361. function _addListener(target, type, listener, prepend) {
  362. var m;
  363. var events;
  364. var existing;
  365.  
  366. if (typeof listener !== 'function')
  367. throw new TypeError('"listener" argument must be a function');
  368.  
  369. events = target._events;
  370. if (!events) {
  371. events = target._events = new EventHandlers();
  372. target._eventsCount = 0;
  373. } else {
  374. // To avoid recursion in the case that type === "newListener"! Before
  375. // adding it to the listeners, first emit "newListener".
  376. if (events.newListener) {
  377. target.emit('newListener', type,
  378. listener.listener ? listener.listener : listener);
  379.  
  380. // Re-assign `events` because a newListener handler could have caused the
  381. // this._events to be assigned to a new object
  382. events = target._events;
  383. }
  384. existing = events[type];
  385. }
  386.  
  387. if (!existing) {
  388. // Optimize the case of one listener. Don't need the extra array object.
  389. existing = events[type] = listener;
  390. ++target._eventsCount;
  391. } else {
  392. if (typeof existing === 'function') {
  393. // Adding the second element, need to change to array.
  394. existing = events[type] = prepend ? [listener, existing] :
  395. [existing, listener];
  396. } else {
  397. // If we've already got an array, just append.
  398. if (prepend) {
  399. existing.unshift(listener);
  400. } else {
  401. existing.push(listener);
  402. }
  403. }
  404.  
  405. // Check for listener leak
  406. if (!existing.warned) {
  407. m = $getMaxListeners(target);
  408. if (m && m > 0 && existing.length > m) {
  409. existing.warned = true;
  410. var w = new Error('Possible EventEmitter memory leak detected. ' +
  411. existing.length + ' ' + type + ' listeners added. ' +
  412. 'Use emitter.setMaxListeners() to increase limit');
  413. w.name = 'MaxListenersExceededWarning';
  414. w.emitter = target;
  415. w.type = type;
  416. w.count = existing.length;
  417. emitWarning(w);
  418. }
  419. }
  420. }
  421.  
  422. return target;
  423. }
  424. function emitWarning(e) {
  425. typeof console.warn === 'function' ? console.warn(e) : console.log(e);
  426. }
  427. EventEmitter.prototype.addListener = function addListener(type, listener) {
  428. return _addListener(this, type, listener, false);
  429. };
  430.  
  431. EventEmitter.prototype.on = EventEmitter.prototype.addListener;
  432.  
  433. EventEmitter.prototype.prependListener =
  434. function prependListener(type, listener) {
  435. return _addListener(this, type, listener, true);
  436. };
  437.  
  438. function _onceWrap(target, type, listener) {
  439. var fired = false;
  440. function g() {
  441. target.removeListener(type, g);
  442. if (!fired) {
  443. fired = true;
  444. listener.apply(target, arguments);
  445. }
  446. }
  447. g.listener = listener;
  448. return g;
  449. }
  450.  
  451. EventEmitter.prototype.once = function once(type, listener) {
  452. if (typeof listener !== 'function')
  453. throw new TypeError('"listener" argument must be a function');
  454. this.on(type, _onceWrap(this, type, listener));
  455. return this;
  456. };
  457.  
  458. EventEmitter.prototype.prependOnceListener =
  459. function prependOnceListener(type, listener) {
  460. if (typeof listener !== 'function')
  461. throw new TypeError('"listener" argument must be a function');
  462. this.prependListener(type, _onceWrap(this, type, listener));
  463. return this;
  464. };
  465.  
  466. // emits a 'removeListener' event iff the listener was removed
  467. EventEmitter.prototype.removeListener =
  468. function removeListener(type, listener) {
  469. var list, events, position, i, originalListener;
  470.  
  471. if (typeof listener !== 'function')
  472. throw new TypeError('"listener" argument must be a function');
  473.  
  474. events = this._events;
  475. if (!events)
  476. return this;
  477.  
  478. list = events[type];
  479. if (!list)
  480. return this;
  481.  
  482. if (list === listener || (list.listener && list.listener === listener)) {
  483. if (--this._eventsCount === 0)
  484. this._events = new EventHandlers();
  485. else {
  486. delete events[type];
  487. if (events.removeListener)
  488. this.emit('removeListener', type, list.listener || listener);
  489. }
  490. } else if (typeof list !== 'function') {
  491. position = -1;
  492.  
  493. for (i = list.length; i-- > 0;) {
  494. if (list[i] === listener ||
  495. (list[i].listener && list[i].listener === listener)) {
  496. originalListener = list[i].listener;
  497. position = i;
  498. break;
  499. }
  500. }
  501.  
  502. if (position < 0)
  503. return this;
  504.  
  505. if (list.length === 1) {
  506. list[0] = undefined;
  507. if (--this._eventsCount === 0) {
  508. this._events = new EventHandlers();
  509. return this;
  510. } else {
  511. delete events[type];
  512. }
  513. } else {
  514. spliceOne(list, position);
  515. }
  516.  
  517. if (events.removeListener)
  518. this.emit('removeListener', type, originalListener || listener);
  519. }
  520.  
  521. return this;
  522. };
  523. // Alias for removeListener added in NodeJS 10.0
  524. // https://nodejs.org/api/events.html#events_emitter_off_eventname_listener
  525. EventEmitter.prototype.off = function(type, listener){
  526. return this.removeListener(type, listener);
  527. };
  528.  
  529. EventEmitter.prototype.removeAllListeners =
  530. function removeAllListeners(type) {
  531. var listeners, events;
  532.  
  533. events = this._events;
  534. if (!events)
  535. return this;
  536.  
  537. // not listening for removeListener, no need to emit
  538. if (!events.removeListener) {
  539. if (arguments.length === 0) {
  540. this._events = new EventHandlers();
  541. this._eventsCount = 0;
  542. } else if (events[type]) {
  543. if (--this._eventsCount === 0)
  544. this._events = new EventHandlers();
  545. else
  546. delete events[type];
  547. }
  548. return this;
  549. }
  550.  
  551. // emit removeListener for all listeners on all events
  552. if (arguments.length === 0) {
  553. var keys = Object.keys(events);
  554. for (var i = 0, key; i < keys.length; ++i) {
  555. key = keys[i];
  556. if (key === 'removeListener') continue;
  557. this.removeAllListeners(key);
  558. }
  559. this.removeAllListeners('removeListener');
  560. this._events = new EventHandlers();
  561. this._eventsCount = 0;
  562. return this;
  563. }
  564.  
  565. listeners = events[type];
  566.  
  567. if (typeof listeners === 'function') {
  568. this.removeListener(type, listeners);
  569. } else if (listeners) {
  570. // LIFO order
  571. do {
  572. this.removeListener(type, listeners[listeners.length - 1]);
  573. } while (listeners[0]);
  574. }
  575.  
  576. return this;
  577. };
  578.  
  579. EventEmitter.prototype.listeners = function listeners(type) {
  580. var evlistener;
  581. var ret;
  582. var events = this._events;
  583.  
  584. if (!events)
  585. ret = [];
  586. else {
  587. evlistener = events[type];
  588. if (!evlistener)
  589. ret = [];
  590. else if (typeof evlistener === 'function')
  591. ret = [evlistener.listener || evlistener];
  592. else
  593. ret = unwrapListeners(evlistener);
  594. }
  595.  
  596. return ret;
  597. };
  598.  
  599. EventEmitter.listenerCount = function(emitter, type) {
  600. if (typeof emitter.listenerCount === 'function') {
  601. return emitter.listenerCount(type);
  602. } else {
  603. return listenerCount.call(emitter, type);
  604. }
  605. };
  606.  
  607. EventEmitter.prototype.listenerCount = listenerCount;
  608. function listenerCount(type) {
  609. var events = this._events;
  610.  
  611. if (events) {
  612. var evlistener = events[type];
  613.  
  614. if (typeof evlistener === 'function') {
  615. return 1;
  616. } else if (evlistener) {
  617. return evlistener.length;
  618. }
  619. }
  620.  
  621. return 0;
  622. }
  623.  
  624. EventEmitter.prototype.eventNames = function eventNames() {
  625. return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
  626. };
  627.  
  628. // About 1.5x faster than the two-arg version of Array#splice().
  629. function spliceOne(list, index) {
  630. for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
  631. list[i] = list[k];
  632. list.pop();
  633. }
  634.  
  635. function arrayClone(arr, i) {
  636. var copy = new Array(i);
  637. while (i--)
  638. copy[i] = arr[i];
  639. return copy;
  640. }
  641.  
  642. function unwrapListeners(arr) {
  643. var ret = new Array(arr.length);
  644. for (var i = 0; i < ret.length; ++i) {
  645. ret[i] = arr[i].listener || arr[i];
  646. }
  647. return ret;
  648. }
  649.  
  650. class EhRetriever extends EventEmitter {
  651. constructor(url, html) {
  652. super();
  653. const testEXHentaiUrl = /^https?:\/\/(?:e-|ex)hentai\.org\//;
  654. if (typeof url !== 'string') {
  655. throw new TypeError('invalid `url`, expected a string');
  656. }
  657. if (!testEXHentaiUrl.test(url)) {
  658. throw new TypeError(`invalid url: ${url}`);
  659. }
  660. this.url = url;
  661. this.html = html;
  662. this.gallery = { gid: '', token: '' };
  663. this.referer = url;
  664. this.showkey = '';
  665. this.ehentaiHost = testEXHentaiUrl.exec(url)[0].slice(0, -1);
  666. this.q = new Queue(3, 3000, 1000);
  667. this.pages = this.init();
  668. this.pages.then(() => this.emit('ready'));
  669. }
  670. async init() {
  671. if (!this.html) {
  672. this.html = await this.fetch(this.url).then(res => res.text());
  673. }
  674. const galleryURL = this.html.match(/hentai\.org\/g\/(\d+)\/([a-z0-9]+)/i);
  675. const showkey = this.html.match(/showkey="([^"]+)"/i);
  676. if (galleryURL) {
  677. this.gallery.gid = galleryURL[1];
  678. this.gallery.token = galleryURL[2];
  679. }
  680. else {
  681. throw new Error("Can't get gallery URL");
  682. }
  683. if (showkey) {
  684. this.showkey = showkey[1];
  685. }
  686. else {
  687. throw new Error("Can't get showkey");
  688. }
  689. return await this.getAllPageURL();
  690. }
  691. async getAllPageURL() {
  692. const { ehentaiHost, gallery: { gid, token } } = this;
  693. const firstPage = await this.fetch(`${ehentaiHost}/g/${gid}/${token}`).then(res => res.text());
  694. let pageNum;
  695. const pageLinksTable = firstPage.match(/<table[^>]*class="ptt"[^>]*>((?:[^<]*)(?:<(?!\/table>)[^<]*)*)<\/table>/);
  696. if (pageLinksTable) {
  697. const pageLinks = pageLinksTable[1].match(/g\/[^/]+\/[^/]+\/\?p=\d+/g);
  698. if (pageLinks) {
  699. pageNum = Math.max.apply(null, pageLinks.map(e => parseInt(/\d+$/.exec(e)[0], 10)));
  700. }
  701. else {
  702. pageNum = 0;
  703. }
  704. }
  705. else {
  706. throw new Error('Cant get page numbers');
  707. }
  708. const allPages = await Promise.all(Array(pageNum).fill(undefined).map((e, i) => {
  709. return this.fetch(`${ehentaiHost}/g/${gid}/${token}/?p=${i + 1}`).then(res => res.text());
  710. }));
  711. allPages.unshift(firstPage);
  712. return allPages
  713. .map(p => this.parsePage(p))
  714. .reduce((p, c) => p.concat(c), []); // 2d array to 1d
  715. }
  716. parsePage(page) {
  717. const gdtMatcher = /<div[^>]*id="gdt"[^>]*>/g;
  718. gdtMatcher.exec(page);
  719. const gtbMatcher = /<div[^>]*class="gtb"[^>]*>/g;
  720. gtbMatcher.lastIndex = gdtMatcher.lastIndex;
  721. gtbMatcher.exec(page);
  722. const gdtContent = page.substring(gdtMatcher.lastIndex, gtbMatcher.lastIndex);
  723. return Array.from(gdtContent.matchAll(/s\/(\w+)\/\d+-(\d+)/g)).map(([, imgkey, page]) => ({ imgkey, page: parseInt(page, 10) }));
  724. }
  725. fetch(url, options = {}) {
  726. if (typeof url !== 'string') {
  727. return Promise.reject(new TypeError('invalid `url`, expected a string'));
  728. }
  729. if (url.search(/^https?:\/\//) < 0) {
  730. return Promise.reject(new TypeError(`invalid url: ${url}`));
  731. }
  732. const cofetchOptions = {
  733. method: 'GET',
  734. credentials: 'include',
  735. headers: {
  736. 'User-Agent': navigator.userAgent,
  737. Referer: this.referer
  738. }
  739. };
  740. for (const key of Object.keys(options)) {
  741. if (key === 'headers') {
  742. Object.assign(cofetchOptions.headers, options.headers);
  743. }
  744. else {
  745. cofetchOptions[key] = options[key];
  746. }
  747. }
  748. return this.q.queue((resolve, reject) => {
  749. COFetch(url, cofetchOptions).then(resolve).catch(reject);
  750. }, `Fetch ${url} ${JSON.stringify(cofetchOptions)}`);
  751. }
  752. async retrieve(start = 0, stop = -1) {
  753. const pages = await this.pages;
  754. if (start < 0 || start >= pages.length || isNaN(start)) {
  755. throw new RangeError(`invalid start number: ${start}`);
  756. }
  757. if (stop < 0) {
  758. stop = pages.length - 1;
  759. }
  760. else if (stop < start || stop >= pages.length || isNaN(stop)) {
  761. throw new RangeError(`invalid stop number: ${stop}, start: ${start}`);
  762. }
  763. const retrievePages = pages.slice(start, stop + 1);
  764. const loadPage = async (e) => {
  765. if (e.imgsrc && e.filename) {
  766. return Promise.resolve(e);
  767. }
  768. const fetchPage = await this.fetch(`${this.ehentaiHost}/api.php`, {
  769. method: 'POST',
  770. headers: { 'Content-Type': 'application/json' },
  771. // assign e = {'imgkey': ..., 'page': ...} to object literal {'method': ..., 'gid': ..., 'showkey': ...}
  772. // does not modify e
  773. body: JSON.stringify(Object.assign({
  774. method: 'showpage',
  775. gid: this.gallery.gid,
  776. showkey: this.showkey
  777. }, e))
  778. }).then(res => res.json());
  779. this.emit('load', {
  780. current: e.page - start,
  781. total: stop - start + 1
  782. });
  783. return fetchPage;
  784. };
  785. const imagePages = await Promise.all(retrievePages.map(loadPage));
  786. imagePages.forEach((e, i) => {
  787. retrievePages[i].filename = e.i.match(/>([^:]+):/)[1].trim();
  788. retrievePages[i].imgsrc = e.i3.match(/src="([^"]+)"/)[1];
  789. retrievePages[i].failnl = new Set([e.i6.match(/nl\('([^']+)'/)[1]]);
  790. retrievePages[i].style = e.i3.match(/style="([^"]+)"/)[1];
  791. retrievePages[i].url = e.s;
  792. });
  793. return retrievePages;
  794. }
  795. async fail(index) {
  796. const pages = await this.pages;
  797. const failPage = pages[index - 1];
  798. const failnl = [...failPage.failnl.values()].map(e => `nl=${e}`).join('&');
  799. const res = await this.fetch(`${this.ehentaiHost}/${failPage.url}?${failnl}`).then(res => res.text());
  800. const parsed = res.match(/<img[^>]*id="img"[^>]*src="([^"]+)"[^>]*.*onclick="return nl\('([^']+)'\)/i);
  801. if (parsed) {
  802. failPage.imgsrc = parsed[1];
  803. failPage.failnl.add(parsed[2]);
  804. return failPage;
  805. }
  806. return null;
  807. }
  808. }
  809.  
  810. const LoadTimeout = 10000;
  811. // helper functions
  812. const $ = (selector) => document.querySelector(selector);
  813. const $$ = (selector) => Array.from(document.querySelectorAll(selector));
  814. const buttonsFragment = document.createDocumentFragment();
  815. const buttonReverse = document.createElement('button');
  816. const buttonDoubleFrame = document.createElement('button');
  817. const buttonRetrieve = document.createElement('button');
  818. const buttonRange = document.createElement('button');
  819. const buttonFullHeight = document.createElement('button');
  820. buttonsFragment.appendChild(buttonReverse);
  821. buttonsFragment.appendChild(buttonDoubleFrame);
  822. buttonsFragment.appendChild(buttonFullHeight);
  823. buttonsFragment.appendChild(buttonRetrieve);
  824. buttonsFragment.appendChild(buttonRange);
  825. buttonReverse.textContent = 'Reverse';
  826. buttonDoubleFrame.textContent = 'Double Frame';
  827. buttonRetrieve.textContent = 'Retrieve!';
  828. buttonRange.textContent = 'Set Range';
  829. buttonFullHeight.textContent = 'View Height';
  830. $('#i1').insertBefore(buttonsFragment, $('#i2'));
  831. let ehentaiResize;
  832. let originalWidth;
  833. let ehr;
  834. let showHiddenImageLink = false;
  835. const reload = (event) => {
  836. event.stopPropagation();
  837. event.preventDefault();
  838. const target = event.target;
  839. if (target.dataset.locked === 'true') {
  840. return;
  841. }
  842. target.dataset.locked = 'true';
  843. ehr.fail(parseInt(target.dataset.page, 10)).then(imgInfo => {
  844. target.src = imgInfo.imgsrc;
  845. target.parentElement.href = imgInfo.imgsrc;
  846. target.dataset.locked = 'false';
  847. });
  848. };
  849. const showImage = (event) => {
  850. event.stopPropagation();
  851. event.preventDefault();
  852. $$('#i3 a').forEach(e => {
  853. e.classList.remove('hidden');
  854. });
  855. event.target.remove();
  856. showHiddenImageLink = false;
  857. };
  858. const hideImage = (event) => {
  859. event.stopPropagation();
  860. event.preventDefault();
  861. event.target.parentElement.classList.add('hidden');
  862. if (!showHiddenImageLink) {
  863. const showHiddenImage = document.createElement('a');
  864. showHiddenImage.href = '';
  865. showHiddenImage.textContent = 'show hidden image';
  866. showHiddenImage.classList.add('show-hidden');
  867. showHiddenImage.addEventListener('click', showImage);
  868. buttonRetrieve.insertAdjacentElement('afterend', showHiddenImage);
  869. showHiddenImageLink = true;
  870. }
  871. };
  872. const swapImage = (event) => {
  873. event.stopPropagation();
  874. event.preventDefault();
  875. const right = event.target.parentElement;
  876. const left = right.previousElementSibling;
  877. if (left) {
  878. left.parentElement.insertBefore(right, left);
  879. }
  880. };
  881. const scrollNextImage = (event) => {
  882. if (event.keyCode !== 9) {
  883. return;
  884. }
  885. event.preventDefault();
  886. const bodyOffset = document.body.getBoundingClientRect().top;
  887. const pageY = window.pageYOffset;
  888. const imgs = Array.from($$('#i3 a img'));
  889. const isLast = !imgs.some((e, i) => {
  890. const imgOffset = e.getBoundingClientRect().top - bodyOffset;
  891. if (pageY - imgOffset < -1) {
  892. window.scrollTo(0, imgOffset);
  893. return true;
  894. }
  895. });
  896. if (isLast) {
  897. window.scrollTo(0, imgs[0].getBoundingClientRect().top - bodyOffset);
  898. }
  899. };
  900. buttonReverse.addEventListener('click', event => {
  901. const i3 = $('#i3');
  902. const imgs = $$('#i3 > a[data-page]');
  903. if (buttonReverse.textContent === 'Reverse') {
  904. buttonReverse.textContent = 'Original Order';
  905. imgs
  906. .sort((a, b) => a.offsetTop - b.offsetTop)
  907. .reduce((p, c) => {
  908. const l = p.at(-1);
  909. if (!l || l.at(-1).offsetTop !== c.offsetTop) {
  910. return [...p, [c]];
  911. }
  912. l.push(c);
  913. return p;
  914. }, [])
  915. .flatMap(e => e.reverse())
  916. .forEach(e => i3.appendChild(e));
  917. }
  918. else {
  919. buttonReverse.textContent = 'Reverse';
  920. imgs
  921. .sort((a, b) => parseInt(a.dataset.page, 10) - parseInt(b.dataset.page, 10))
  922. .forEach(e => i3.appendChild(e));
  923. }
  924. });
  925. buttonDoubleFrame.addEventListener('click', event => {
  926. if (!ehentaiResize) {
  927. try {
  928. ehentaiResize = unsafeWindow.onresize;
  929. }
  930. catch (e) {
  931. console.log(e);
  932. }
  933. }
  934. const imgWidths = $$('#i3 a:not(.hidden) img').map(e => e.getBoundingClientRect().width);
  935. const avg = imgWidths.reduce((p, c) => p + c) / imgWidths.length;
  936. const filtered = imgWidths.filter(v => (v < avg * 1.5 && v > avg * 0.5));
  937. const filteredMax = Math.max.apply(null, filtered);
  938. if (!originalWidth) {
  939. originalWidth = parseInt($('#i1').style.width, 10);
  940. }
  941. if (buttonDoubleFrame.textContent === 'Double Frame') {
  942. buttonDoubleFrame.textContent = 'Reset Frame';
  943. try {
  944. unsafeWindow.onresize = null;
  945. }
  946. catch (e) {
  947. console.log(e);
  948. }
  949. $('#i1').style.maxWidth = (filteredMax * 2 + 20) + 'px';
  950. $('#i1').style.width = (filteredMax * 2 + 20) + 'px';
  951. }
  952. else {
  953. buttonDoubleFrame.textContent = 'Double Frame';
  954. try {
  955. unsafeWindow.onresize = ehentaiResize;
  956. ehentaiResize();
  957. }
  958. catch (e) {
  959. console.log(e);
  960. $('#i1').style.maxWidth = originalWidth + 'px';
  961. $('#i1').style.width = originalWidth + 'px';
  962. }
  963. }
  964. });
  965. buttonRetrieve.addEventListener('click', event => {
  966. buttonRetrieve.setAttribute('disabled', '');
  967. buttonRange.setAttribute('disabled', '');
  968. buttonRetrieve.textContent = 'Initializing...';
  969. if (!ehr) {
  970. ehr = new EhRetriever(location.href, document.body.innerHTML);
  971. }
  972. ehr.on('ready', () => {
  973. buttonRetrieve.textContent = `Ready to retrieve`;
  974. });
  975. ehr.on('load', (progress) => {
  976. buttonRetrieve.textContent = `Retrieving ${progress.current}/${progress.total}`;
  977. });
  978. let retrieve;
  979. if ($('#ehrstart')) {
  980. const start = parseInt($('#ehrstart').value, 10);
  981. const stop = parseInt($('#ehrstop').value, 10);
  982. const pageNumMax = parseInt($('div.sn').textContent.match(/\/\s*(\d+)/)[1], 10);
  983. if (stop < start || start <= 0 || start > pageNumMax || stop > pageNumMax) {
  984. window.alert(`invalid range: ${start} - ${stop}, accepted range: 1 - ${pageNumMax}`);
  985. buttonRetrieve.textContent = 'Retrieve!';
  986. buttonRetrieve.removeAttribute('disabled');
  987. return;
  988. }
  989. retrieve = ehr.retrieve(start - 1, stop - 1);
  990. $('#ehrsetrange').remove();
  991. }
  992. else {
  993. retrieve = ehr.retrieve();
  994. buttonRange.remove();
  995. }
  996. retrieve.then(pages => {
  997. $('#i3 a').remove();
  998. const template = document.createElement('template');
  999. template.innerHTML = pages
  1000. .map(e => `
  1001. <a href="${e.imgsrc}" data-page="${e.page}">
  1002. <img src="${e.imgsrc}" style="${e.style}" data-page="${e.page}" data-locked="false" />
  1003. <div class="close"></div>
  1004. <div class="swap"></div>
  1005. <div class="page-number">${e.page}</div>
  1006. </a>`)
  1007. .join('');
  1008. template.content.querySelectorAll('img').forEach(e => {
  1009. e.addEventListener('error', function onError(event) {
  1010. e.removeEventListener('error', onError);
  1011. reload(event);
  1012. });
  1013. let timeout;
  1014. {
  1015. timeout = window.setTimeout(() => {
  1016. console.log(`timeout: page ${e.dataset.page}`);
  1017. const clickEvent = new MouseEvent('click', {
  1018. bubbles: true,
  1019. cancelable: true,
  1020. view: window
  1021. });
  1022. e.dispatchEvent(clickEvent);
  1023. }, LoadTimeout);
  1024. e.addEventListener('load', function onload() {
  1025. e.removeEventListener('load', onload);
  1026. clearTimeout(timeout);
  1027. });
  1028. }
  1029. });
  1030. $('#i3').appendChild(template.content);
  1031. $('#i3').addEventListener('click', event => {
  1032. if (event.target.nodeName === 'IMG') {
  1033. reload(event);
  1034. }
  1035. else if (event.target.classList.contains('close')) {
  1036. hideImage(event);
  1037. }
  1038. else if (event.target.classList.contains('swap')) {
  1039. swapImage(event);
  1040. }
  1041. else if (event.target.classList.contains('page-number')) {
  1042. event.preventDefault();
  1043. event.stopPropagation();
  1044. }
  1045. });
  1046. buttonRetrieve.textContent = 'Done!';
  1047. buttonDoubleFrame.removeAttribute('disabled');
  1048. buttonFullHeight.removeAttribute('disabled');
  1049. document.onkeydown = null;
  1050. document.addEventListener('keydown', scrollNextImage);
  1051. }).catch(e => { console.log(e); });
  1052. });
  1053. buttonRange.addEventListener('click', event => {
  1054. // override e-hentai's viewing shortcut
  1055. document.onkeydown = undefined;
  1056. const pageNum = $('div.sn').textContent.match(/(\d+)\s*\/\s*(\d+)/).slice(1);
  1057. buttonRange.insertAdjacentHTML('afterend', `<span id="ehrsetrange"><input type="number" id="ehrstart" value="${pageNum[0]}" min="1" max="${pageNum[1]}"> - <input type="number" id="ehrstop" value="${pageNum[1]}" min="1" max="${pageNum[1]}"></span>`);
  1058. buttonRange.remove();
  1059. });
  1060. buttonFullHeight.addEventListener('click', event => {
  1061. $('#i3').classList.toggle('force-img-full-height');
  1062. });
  1063.  
  1064. })();