- // ==UserScript==
- // @name e-hentai retriever
- // @namespace https://github.com/s25g5d4/e-hentai-retriever
- // @description e-hentai & exhentai image url retriever
- // @include /^https?://e-hentai.org/s/.*/
- // @include /^https?://exhentai.org/s/.*/
- // @version 4.4.1
- // @author s25g5d4
- // @homepageURL https://github.com/s25g5d4/e-hentai-retriever
- // @grant GM_xmlhttpRequest
- // @grant unsafeWindow
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- function styleInject(css, ref) {
- if ( ref === void 0 ) ref = {};
- var insertAt = ref.insertAt;
-
- if (!css || typeof document === 'undefined') { return; }
-
- var head = document.head || document.getElementsByTagName('head')[0];
- var style = document.createElement('style');
- style.type = 'text/css';
-
- if (insertAt === 'top') {
- if (head.firstChild) {
- head.insertBefore(style, head.firstChild);
- } else {
- head.appendChild(style);
- }
- } else {
- head.appendChild(style);
- }
-
- if (style.styleSheet) {
- style.styleSheet.cssText = css;
- } else {
- style.appendChild(document.createTextNode(css));
- }
- }
-
- 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";
- styleInject(css_248z);
-
- class Queue {
- constructor(limit, timeout = 0, delay = 0) {
- this.limit = limit;
- this.timeout = timeout;
- this.delay = delay;
- this.slot = [];
- this.q = [];
- }
- queue(executor, name) {
- const job = new Promise((resolve, reject) => {
- this.q.push({
- name,
- run: executor,
- resolve,
- reject,
- isTimeout: false,
- timeoutId: undefined
- });
- });
- // console.log(`queue: job ${name} queued`);
- this.dequeue();
- return job;
- }
- dequeue() {
- const { q, slot, limit, timeout, delay } = this;
- if (slot.length < limit && q.length >= 1) {
- const job = q.shift();
- slot.push(job);
- // console.log(`queue: job ${job.name} started`);
- if (timeout) {
- job.timeoutId = window.setTimeout(() => this.jobTimeout(job), timeout);
- }
- const onFulfilled = (data) => {
- if (job.isTimeout) {
- return;
- }
- this.removeJob(job);
- window.setTimeout(() => this.dequeue(), delay); // force dequeue() run after current dequeue()
- if (job.timeoutId) {
- window.clearTimeout(job.timeoutId);
- }
- // console.log(`queue: job ${job.name} resolved`);
- job.resolve(data);
- };
- const onRejected = (reason) => {
- if (job.isTimeout) {
- return;
- }
- this.removeJob(job);
- setTimeout(() => this.dequeue(), delay);
- if (job.timeoutId) {
- window.clearTimeout(job.timeoutId);
- }
- // console.log(`queue: job ${job.name} rejected`);
- job.reject(reason);
- };
- job.run(onFulfilled, onRejected);
- }
- }
- jobTimeout(job) {
- this.removeJob(job);
- // console.log(`queue: job ${job.name} timeout`);
- job.reject(new Error(`queue: job ${job.name} timeout`));
- job = null;
- }
- removeJob(job) {
- let index = this.slot.indexOf(job);
- if (index >= 0) {
- this.slot.splice(index, 1);
- return;
- }
- index = this.q.indexOf(job);
- if (index >= 0) {
- this.q.splice(index, 1);
- }
- }
- }
-
- // Origin from window.fetch polyfill
- // https://github.com/github/fetch
- // License https://github.com/github/fetch/blob/master/LICENSE
- const COFetch = (input, init = {}) => {
- let request;
- if (Request.prototype.isPrototypeOf(input) && !init) {
- request = input;
- }
- else {
- request = new Request(input, init);
- }
- const headers = Object.assign({}, request.headers);
- let anonymous = true;
- if (request.credentials === 'include') {
- anonymous = false;
- }
- const onload = (resolve, reject, gmxhr) => {
- const init = {
- url: gmxhr.finalUrl || request.url,
- status: gmxhr.status,
- statusText: gmxhr.statusText,
- headers: undefined
- };
- try {
- const rawHeaders = gmxhr.responseHeaders.trim().replace(/\r\n(\s+)/g, '$1').split('\r\n').map(e => e.split(/:/));
- const header = new Headers();
- rawHeaders.forEach(e => {
- header.append(e[0].trim(), e[1].trim());
- });
- init.headers = header;
- const res = new Response(gmxhr.response, init);
- resolve(res);
- }
- catch (e) {
- reject(e);
- }
- };
- const onerror = (resolve, reject, gmxhr) => {
- reject(new TypeError('Network request failed'));
- };
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- anonymous,
- method: request.method,
- url: request.url,
- headers: headers,
- responseType: 'blob',
- data: init.body,
- onload: onload.bind(null, resolve, reject),
- onerror: onerror.bind(null, resolve, reject)
- });
- });
- };
-
- var domain;
-
- // This constructor is used to store event handlers. Instantiating this is
- // faster than explicitly calling `Object.create(null)` to get a "clean" empty
- // object (tested with v8 v4.9).
- function EventHandlers() {}
- EventHandlers.prototype = Object.create(null);
-
- function EventEmitter() {
- EventEmitter.init.call(this);
- }
-
- // nodejs oddity
- // require('events') === require('events').EventEmitter
- EventEmitter.EventEmitter = EventEmitter;
-
- EventEmitter.usingDomains = false;
-
- EventEmitter.prototype.domain = undefined;
- EventEmitter.prototype._events = undefined;
- EventEmitter.prototype._maxListeners = undefined;
-
- // By default EventEmitters will print a warning if more than 10 listeners are
- // added to it. This is a useful default which helps finding memory leaks.
- EventEmitter.defaultMaxListeners = 10;
-
- EventEmitter.init = function() {
- this.domain = null;
- if (EventEmitter.usingDomains) {
- // if there is an active domain, then attach to it.
- if (domain.active ) ;
- }
-
- if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
- this._events = new EventHandlers();
- this._eventsCount = 0;
- }
-
- this._maxListeners = this._maxListeners || undefined;
- };
-
- // Obviously not all Emitters should be limited to 10. This function allows
- // that to be increased. Set to zero for unlimited.
- EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
- if (typeof n !== 'number' || n < 0 || isNaN(n))
- throw new TypeError('"n" argument must be a positive number');
- this._maxListeners = n;
- return this;
- };
-
- function $getMaxListeners(that) {
- if (that._maxListeners === undefined)
- return EventEmitter.defaultMaxListeners;
- return that._maxListeners;
- }
-
- EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
- return $getMaxListeners(this);
- };
-
- // These standalone emit* functions are used to optimize calling of event
- // handlers for fast cases because emit() itself often has a variable number of
- // arguments and can be deoptimized because of that. These functions always have
- // the same number of arguments and thus do not get deoptimized, so the code
- // inside them can execute faster.
- function emitNone(handler, isFn, self) {
- if (isFn)
- handler.call(self);
- else {
- var len = handler.length;
- var listeners = arrayClone(handler, len);
- for (var i = 0; i < len; ++i)
- listeners[i].call(self);
- }
- }
- function emitOne(handler, isFn, self, arg1) {
- if (isFn)
- handler.call(self, arg1);
- else {
- var len = handler.length;
- var listeners = arrayClone(handler, len);
- for (var i = 0; i < len; ++i)
- listeners[i].call(self, arg1);
- }
- }
- function emitTwo(handler, isFn, self, arg1, arg2) {
- if (isFn)
- handler.call(self, arg1, arg2);
- else {
- var len = handler.length;
- var listeners = arrayClone(handler, len);
- for (var i = 0; i < len; ++i)
- listeners[i].call(self, arg1, arg2);
- }
- }
- function emitThree(handler, isFn, self, arg1, arg2, arg3) {
- if (isFn)
- handler.call(self, arg1, arg2, arg3);
- else {
- var len = handler.length;
- var listeners = arrayClone(handler, len);
- for (var i = 0; i < len; ++i)
- listeners[i].call(self, arg1, arg2, arg3);
- }
- }
-
- function emitMany(handler, isFn, self, args) {
- if (isFn)
- handler.apply(self, args);
- else {
- var len = handler.length;
- var listeners = arrayClone(handler, len);
- for (var i = 0; i < len; ++i)
- listeners[i].apply(self, args);
- }
- }
-
- EventEmitter.prototype.emit = function emit(type) {
- var er, handler, len, args, i, events, domain;
- var doError = (type === 'error');
-
- events = this._events;
- if (events)
- doError = (doError && events.error == null);
- else if (!doError)
- return false;
-
- domain = this.domain;
-
- // If there is no 'error' event listener then throw.
- if (doError) {
- er = arguments[1];
- if (domain) {
- if (!er)
- er = new Error('Uncaught, unspecified "error" event');
- er.domainEmitter = this;
- er.domain = domain;
- er.domainThrown = false;
- domain.emit('error', er);
- } else if (er instanceof Error) {
- throw er; // Unhandled 'error' event
- } else {
- // At least give some kind of context to the user
- var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
- err.context = er;
- throw err;
- }
- return false;
- }
-
- handler = events[type];
-
- if (!handler)
- return false;
-
- var isFn = typeof handler === 'function';
- len = arguments.length;
- switch (len) {
- // fast cases
- case 1:
- emitNone(handler, isFn, this);
- break;
- case 2:
- emitOne(handler, isFn, this, arguments[1]);
- break;
- case 3:
- emitTwo(handler, isFn, this, arguments[1], arguments[2]);
- break;
- case 4:
- emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
- break;
- // slower
- default:
- args = new Array(len - 1);
- for (i = 1; i < len; i++)
- args[i - 1] = arguments[i];
- emitMany(handler, isFn, this, args);
- }
-
- return true;
- };
-
- function _addListener(target, type, listener, prepend) {
- var m;
- var events;
- var existing;
-
- if (typeof listener !== 'function')
- throw new TypeError('"listener" argument must be a function');
-
- events = target._events;
- if (!events) {
- events = target._events = new EventHandlers();
- target._eventsCount = 0;
- } else {
- // To avoid recursion in the case that type === "newListener"! Before
- // adding it to the listeners, first emit "newListener".
- if (events.newListener) {
- target.emit('newListener', type,
- listener.listener ? listener.listener : listener);
-
- // Re-assign `events` because a newListener handler could have caused the
- // this._events to be assigned to a new object
- events = target._events;
- }
- existing = events[type];
- }
-
- if (!existing) {
- // Optimize the case of one listener. Don't need the extra array object.
- existing = events[type] = listener;
- ++target._eventsCount;
- } else {
- if (typeof existing === 'function') {
- // Adding the second element, need to change to array.
- existing = events[type] = prepend ? [listener, existing] :
- [existing, listener];
- } else {
- // If we've already got an array, just append.
- if (prepend) {
- existing.unshift(listener);
- } else {
- existing.push(listener);
- }
- }
-
- // Check for listener leak
- if (!existing.warned) {
- m = $getMaxListeners(target);
- if (m && m > 0 && existing.length > m) {
- existing.warned = true;
- var w = new Error('Possible EventEmitter memory leak detected. ' +
- existing.length + ' ' + type + ' listeners added. ' +
- 'Use emitter.setMaxListeners() to increase limit');
- w.name = 'MaxListenersExceededWarning';
- w.emitter = target;
- w.type = type;
- w.count = existing.length;
- emitWarning(w);
- }
- }
- }
-
- return target;
- }
- function emitWarning(e) {
- typeof console.warn === 'function' ? console.warn(e) : console.log(e);
- }
- EventEmitter.prototype.addListener = function addListener(type, listener) {
- return _addListener(this, type, listener, false);
- };
-
- EventEmitter.prototype.on = EventEmitter.prototype.addListener;
-
- EventEmitter.prototype.prependListener =
- function prependListener(type, listener) {
- return _addListener(this, type, listener, true);
- };
-
- function _onceWrap(target, type, listener) {
- var fired = false;
- function g() {
- target.removeListener(type, g);
- if (!fired) {
- fired = true;
- listener.apply(target, arguments);
- }
- }
- g.listener = listener;
- return g;
- }
-
- EventEmitter.prototype.once = function once(type, listener) {
- if (typeof listener !== 'function')
- throw new TypeError('"listener" argument must be a function');
- this.on(type, _onceWrap(this, type, listener));
- return this;
- };
-
- EventEmitter.prototype.prependOnceListener =
- function prependOnceListener(type, listener) {
- if (typeof listener !== 'function')
- throw new TypeError('"listener" argument must be a function');
- this.prependListener(type, _onceWrap(this, type, listener));
- return this;
- };
-
- // emits a 'removeListener' event iff the listener was removed
- EventEmitter.prototype.removeListener =
- function removeListener(type, listener) {
- var list, events, position, i, originalListener;
-
- if (typeof listener !== 'function')
- throw new TypeError('"listener" argument must be a function');
-
- events = this._events;
- if (!events)
- return this;
-
- list = events[type];
- if (!list)
- return this;
-
- if (list === listener || (list.listener && list.listener === listener)) {
- if (--this._eventsCount === 0)
- this._events = new EventHandlers();
- else {
- delete events[type];
- if (events.removeListener)
- this.emit('removeListener', type, list.listener || listener);
- }
- } else if (typeof list !== 'function') {
- position = -1;
-
- for (i = list.length; i-- > 0;) {
- if (list[i] === listener ||
- (list[i].listener && list[i].listener === listener)) {
- originalListener = list[i].listener;
- position = i;
- break;
- }
- }
-
- if (position < 0)
- return this;
-
- if (list.length === 1) {
- list[0] = undefined;
- if (--this._eventsCount === 0) {
- this._events = new EventHandlers();
- return this;
- } else {
- delete events[type];
- }
- } else {
- spliceOne(list, position);
- }
-
- if (events.removeListener)
- this.emit('removeListener', type, originalListener || listener);
- }
-
- return this;
- };
-
- // Alias for removeListener added in NodeJS 10.0
- // https://nodejs.org/api/events.html#events_emitter_off_eventname_listener
- EventEmitter.prototype.off = function(type, listener){
- return this.removeListener(type, listener);
- };
-
- EventEmitter.prototype.removeAllListeners =
- function removeAllListeners(type) {
- var listeners, events;
-
- events = this._events;
- if (!events)
- return this;
-
- // not listening for removeListener, no need to emit
- if (!events.removeListener) {
- if (arguments.length === 0) {
- this._events = new EventHandlers();
- this._eventsCount = 0;
- } else if (events[type]) {
- if (--this._eventsCount === 0)
- this._events = new EventHandlers();
- else
- delete events[type];
- }
- return this;
- }
-
- // emit removeListener for all listeners on all events
- if (arguments.length === 0) {
- var keys = Object.keys(events);
- for (var i = 0, key; i < keys.length; ++i) {
- key = keys[i];
- if (key === 'removeListener') continue;
- this.removeAllListeners(key);
- }
- this.removeAllListeners('removeListener');
- this._events = new EventHandlers();
- this._eventsCount = 0;
- return this;
- }
-
- listeners = events[type];
-
- if (typeof listeners === 'function') {
- this.removeListener(type, listeners);
- } else if (listeners) {
- // LIFO order
- do {
- this.removeListener(type, listeners[listeners.length - 1]);
- } while (listeners[0]);
- }
-
- return this;
- };
-
- EventEmitter.prototype.listeners = function listeners(type) {
- var evlistener;
- var ret;
- var events = this._events;
-
- if (!events)
- ret = [];
- else {
- evlistener = events[type];
- if (!evlistener)
- ret = [];
- else if (typeof evlistener === 'function')
- ret = [evlistener.listener || evlistener];
- else
- ret = unwrapListeners(evlistener);
- }
-
- return ret;
- };
-
- EventEmitter.listenerCount = function(emitter, type) {
- if (typeof emitter.listenerCount === 'function') {
- return emitter.listenerCount(type);
- } else {
- return listenerCount.call(emitter, type);
- }
- };
-
- EventEmitter.prototype.listenerCount = listenerCount;
- function listenerCount(type) {
- var events = this._events;
-
- if (events) {
- var evlistener = events[type];
-
- if (typeof evlistener === 'function') {
- return 1;
- } else if (evlistener) {
- return evlistener.length;
- }
- }
-
- return 0;
- }
-
- EventEmitter.prototype.eventNames = function eventNames() {
- return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
- };
-
- // About 1.5x faster than the two-arg version of Array#splice().
- function spliceOne(list, index) {
- for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
- list[i] = list[k];
- list.pop();
- }
-
- function arrayClone(arr, i) {
- var copy = new Array(i);
- while (i--)
- copy[i] = arr[i];
- return copy;
- }
-
- function unwrapListeners(arr) {
- var ret = new Array(arr.length);
- for (var i = 0; i < ret.length; ++i) {
- ret[i] = arr[i].listener || arr[i];
- }
- return ret;
- }
-
- class EhRetriever extends EventEmitter {
- constructor(url, html) {
- super();
- const testEXHentaiUrl = /^https?:\/\/(?:e-|ex)hentai\.org\//;
- if (typeof url !== 'string') {
- throw new TypeError('invalid `url`, expected a string');
- }
- if (!testEXHentaiUrl.test(url)) {
- throw new TypeError(`invalid url: ${url}`);
- }
- this.url = url;
- this.html = html;
- this.gallery = { gid: '', token: '' };
- this.referer = url;
- this.showkey = '';
- this.ehentaiHost = testEXHentaiUrl.exec(url)[0].slice(0, -1);
- this.q = new Queue(3, 3000, 1000);
- this.pages = this.init();
- this.pages.then(() => this.emit('ready'));
- }
- async init() {
- if (!this.html) {
- this.html = await this.fetch(this.url).then(res => res.text());
- }
- const galleryURL = this.html.match(/hentai\.org\/g\/(\d+)\/([a-z0-9]+)/i);
- const showkey = this.html.match(/showkey="([^"]+)"/i);
- if (galleryURL) {
- this.gallery.gid = galleryURL[1];
- this.gallery.token = galleryURL[2];
- }
- else {
- throw new Error("Can't get gallery URL");
- }
- if (showkey) {
- this.showkey = showkey[1];
- }
- else {
- throw new Error("Can't get showkey");
- }
- return await this.getAllPageURL();
- }
- async getAllPageURL() {
- const { ehentaiHost, gallery: { gid, token } } = this;
- const firstPage = await this.fetch(`${ehentaiHost}/g/${gid}/${token}`).then(res => res.text());
- let pageNum;
- const pageLinksTable = firstPage.match(/<table[^>]*class="ptt"[^>]*>((?:[^<]*)(?:<(?!\/table>)[^<]*)*)<\/table>/);
- if (pageLinksTable) {
- const pageLinks = pageLinksTable[1].match(/g\/[^/]+\/[^/]+\/\?p=\d+/g);
- if (pageLinks) {
- pageNum = Math.max.apply(null, pageLinks.map(e => parseInt(/\d+$/.exec(e)[0], 10)));
- }
- else {
- pageNum = 0;
- }
- }
- else {
- throw new Error('Cant get page numbers');
- }
- const allPages = await Promise.all(Array(pageNum).fill(undefined).map((e, i) => {
- return this.fetch(`${ehentaiHost}/g/${gid}/${token}/?p=${i + 1}`).then(res => res.text());
- }));
- allPages.unshift(firstPage);
- return allPages
- .map(p => this.parsePage(p))
- .reduce((p, c) => p.concat(c), []); // 2d array to 1d
- }
- parsePage(page) {
- const gdtMatcher = /<div[^>]*id="gdt"[^>]*>/g;
- gdtMatcher.exec(page);
- const gtbMatcher = /<div[^>]*class="gtb"[^>]*>/g;
- gtbMatcher.lastIndex = gdtMatcher.lastIndex;
- gtbMatcher.exec(page);
- const gdtContent = page.substring(gdtMatcher.lastIndex, gtbMatcher.lastIndex);
- return Array.from(gdtContent.matchAll(/s\/(\w+)\/\d+-(\d+)/g)).map(([, imgkey, page]) => ({ imgkey, page: parseInt(page, 10) }));
- }
- fetch(url, options = {}) {
- if (typeof url !== 'string') {
- return Promise.reject(new TypeError('invalid `url`, expected a string'));
- }
- if (url.search(/^https?:\/\//) < 0) {
- return Promise.reject(new TypeError(`invalid url: ${url}`));
- }
- const cofetchOptions = {
- method: 'GET',
- credentials: 'include',
- headers: {
- 'User-Agent': navigator.userAgent,
- Referer: this.referer
- }
- };
- for (const key of Object.keys(options)) {
- if (key === 'headers') {
- Object.assign(cofetchOptions.headers, options.headers);
- }
- else {
- cofetchOptions[key] = options[key];
- }
- }
- return this.q.queue((resolve, reject) => {
- COFetch(url, cofetchOptions).then(resolve).catch(reject);
- }, `Fetch ${url} ${JSON.stringify(cofetchOptions)}`);
- }
- async retrieve(start = 0, stop = -1) {
- const pages = await this.pages;
- if (start < 0 || start >= pages.length || isNaN(start)) {
- throw new RangeError(`invalid start number: ${start}`);
- }
- if (stop < 0) {
- stop = pages.length - 1;
- }
- else if (stop < start || stop >= pages.length || isNaN(stop)) {
- throw new RangeError(`invalid stop number: ${stop}, start: ${start}`);
- }
- const retrievePages = pages.slice(start, stop + 1);
- const loadPage = async (e) => {
- if (e.imgsrc && e.filename) {
- return Promise.resolve(e);
- }
- const fetchPage = await this.fetch(`${this.ehentaiHost}/api.php`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- // assign e = {'imgkey': ..., 'page': ...} to object literal {'method': ..., 'gid': ..., 'showkey': ...}
- // does not modify e
- body: JSON.stringify(Object.assign({
- method: 'showpage',
- gid: this.gallery.gid,
- showkey: this.showkey
- }, e))
- }).then(res => res.json());
- this.emit('load', {
- current: e.page - start,
- total: stop - start + 1
- });
- return fetchPage;
- };
- const imagePages = await Promise.all(retrievePages.map(loadPage));
- imagePages.forEach((e, i) => {
- retrievePages[i].filename = e.i.match(/>([^:]+):/)[1].trim();
- retrievePages[i].imgsrc = e.i3.match(/src="([^"]+)"/)[1];
- retrievePages[i].failnl = new Set([e.i6.match(/nl\('([^']+)'/)[1]]);
- retrievePages[i].style = e.i3.match(/style="([^"]+)"/)[1];
- retrievePages[i].url = e.s;
- });
- return retrievePages;
- }
- async fail(index) {
- const pages = await this.pages;
- const failPage = pages[index - 1];
- const failnl = [...failPage.failnl.values()].map(e => `nl=${e}`).join('&');
- const res = await this.fetch(`${this.ehentaiHost}/${failPage.url}?${failnl}`).then(res => res.text());
- const parsed = res.match(/<img[^>]*id="img"[^>]*src="([^"]+)"[^>]*.*onclick="return nl\('([^']+)'\)/i);
- if (parsed) {
- failPage.imgsrc = parsed[1];
- failPage.failnl.add(parsed[2]);
- return failPage;
- }
- return null;
- }
- }
-
- const LoadTimeout = 10000;
- // helper functions
- const $ = (selector) => document.querySelector(selector);
- const $$ = (selector) => Array.from(document.querySelectorAll(selector));
- const buttonsFragment = document.createDocumentFragment();
- const buttonReverse = document.createElement('button');
- const buttonDoubleFrame = document.createElement('button');
- const buttonRetrieve = document.createElement('button');
- const buttonRange = document.createElement('button');
- const buttonFullHeight = document.createElement('button');
- buttonsFragment.appendChild(buttonReverse);
- buttonsFragment.appendChild(buttonDoubleFrame);
- buttonsFragment.appendChild(buttonFullHeight);
- buttonsFragment.appendChild(buttonRetrieve);
- buttonsFragment.appendChild(buttonRange);
- buttonReverse.textContent = 'Reverse';
- buttonDoubleFrame.textContent = 'Double Frame';
- buttonRetrieve.textContent = 'Retrieve!';
- buttonRange.textContent = 'Set Range';
- buttonFullHeight.textContent = 'View Height';
- $('#i1').insertBefore(buttonsFragment, $('#i2'));
- let ehentaiResize;
- let originalWidth;
- let ehr;
- let showHiddenImageLink = false;
- const reload = (event) => {
- event.stopPropagation();
- event.preventDefault();
- const target = event.target;
- if (target.dataset.locked === 'true') {
- return;
- }
- target.dataset.locked = 'true';
- ehr.fail(parseInt(target.dataset.page, 10)).then(imgInfo => {
- target.src = imgInfo.imgsrc;
- target.parentElement.href = imgInfo.imgsrc;
- target.dataset.locked = 'false';
- });
- };
- const showImage = (event) => {
- event.stopPropagation();
- event.preventDefault();
- $$('#i3 a').forEach(e => {
- e.classList.remove('hidden');
- });
- event.target.remove();
- showHiddenImageLink = false;
- };
- const hideImage = (event) => {
- event.stopPropagation();
- event.preventDefault();
- event.target.parentElement.classList.add('hidden');
- if (!showHiddenImageLink) {
- const showHiddenImage = document.createElement('a');
- showHiddenImage.href = '';
- showHiddenImage.textContent = 'show hidden image';
- showHiddenImage.classList.add('show-hidden');
- showHiddenImage.addEventListener('click', showImage);
- buttonRetrieve.insertAdjacentElement('afterend', showHiddenImage);
- showHiddenImageLink = true;
- }
- };
- const swapImage = (event) => {
- event.stopPropagation();
- event.preventDefault();
- const right = event.target.parentElement;
- const left = right.previousElementSibling;
- if (left) {
- left.parentElement.insertBefore(right, left);
- }
- };
- const scrollNextImage = (event) => {
- if (event.keyCode !== 9) {
- return;
- }
- event.preventDefault();
- const bodyOffset = document.body.getBoundingClientRect().top;
- const pageY = window.pageYOffset;
- const imgs = Array.from($$('#i3 a img'));
- const isLast = !imgs.some((e, i) => {
- const imgOffset = e.getBoundingClientRect().top - bodyOffset;
- if (pageY - imgOffset < -1) {
- window.scrollTo(0, imgOffset);
- return true;
- }
- });
- if (isLast) {
- window.scrollTo(0, imgs[0].getBoundingClientRect().top - bodyOffset);
- }
- };
- buttonReverse.addEventListener('click', event => {
- const i3 = $('#i3');
- const imgs = $$('#i3 > a[data-page]');
- if (buttonReverse.textContent === 'Reverse') {
- buttonReverse.textContent = 'Original Order';
- imgs
- .sort((a, b) => a.offsetTop - b.offsetTop)
- .reduce((p, c) => {
- const l = p.at(-1);
- if (!l || l.at(-1).offsetTop !== c.offsetTop) {
- return [...p, [c]];
- }
- l.push(c);
- return p;
- }, [])
- .flatMap(e => e.reverse())
- .forEach(e => i3.appendChild(e));
- }
- else {
- buttonReverse.textContent = 'Reverse';
- imgs
- .sort((a, b) => parseInt(a.dataset.page, 10) - parseInt(b.dataset.page, 10))
- .forEach(e => i3.appendChild(e));
- }
- });
- buttonDoubleFrame.addEventListener('click', event => {
- if (!ehentaiResize) {
- try {
- ehentaiResize = unsafeWindow.onresize;
- }
- catch (e) {
- console.log(e);
- }
- }
- const imgWidths = $$('#i3 a:not(.hidden) img').map(e => e.getBoundingClientRect().width);
- const avg = imgWidths.reduce((p, c) => p + c) / imgWidths.length;
- const filtered = imgWidths.filter(v => (v < avg * 1.5 && v > avg * 0.5));
- const filteredMax = Math.max.apply(null, filtered);
- if (!originalWidth) {
- originalWidth = parseInt($('#i1').style.width, 10);
- }
- if (buttonDoubleFrame.textContent === 'Double Frame') {
- buttonDoubleFrame.textContent = 'Reset Frame';
- try {
- unsafeWindow.onresize = null;
- }
- catch (e) {
- console.log(e);
- }
- $('#i1').style.maxWidth = (filteredMax * 2 + 20) + 'px';
- $('#i1').style.width = (filteredMax * 2 + 20) + 'px';
- }
- else {
- buttonDoubleFrame.textContent = 'Double Frame';
- try {
- unsafeWindow.onresize = ehentaiResize;
- ehentaiResize();
- }
- catch (e) {
- console.log(e);
- $('#i1').style.maxWidth = originalWidth + 'px';
- $('#i1').style.width = originalWidth + 'px';
- }
- }
- });
- buttonRetrieve.addEventListener('click', event => {
- buttonRetrieve.setAttribute('disabled', '');
- buttonRange.setAttribute('disabled', '');
- buttonRetrieve.textContent = 'Initializing...';
- if (!ehr) {
- ehr = new EhRetriever(location.href, document.body.innerHTML);
- }
- ehr.on('ready', () => {
- buttonRetrieve.textContent = `Ready to retrieve`;
- });
- ehr.on('load', (progress) => {
- buttonRetrieve.textContent = `Retrieving ${progress.current}/${progress.total}`;
- });
- let retrieve;
- if ($('#ehrstart')) {
- const start = parseInt($('#ehrstart').value, 10);
- const stop = parseInt($('#ehrstop').value, 10);
- const pageNumMax = parseInt($('div.sn').textContent.match(/\/\s*(\d+)/)[1], 10);
- if (stop < start || start <= 0 || start > pageNumMax || stop > pageNumMax) {
- window.alert(`invalid range: ${start} - ${stop}, accepted range: 1 - ${pageNumMax}`);
- buttonRetrieve.textContent = 'Retrieve!';
- buttonRetrieve.removeAttribute('disabled');
- return;
- }
- retrieve = ehr.retrieve(start - 1, stop - 1);
- $('#ehrsetrange').remove();
- }
- else {
- retrieve = ehr.retrieve();
- buttonRange.remove();
- }
- retrieve.then(pages => {
- $('#i3 a').remove();
- const template = document.createElement('template');
- template.innerHTML = pages
- .map(e => `
- <a href="${e.imgsrc}" data-page="${e.page}">
- <img src="${e.imgsrc}" style="${e.style}" data-page="${e.page}" data-locked="false" />
- <div class="close"></div>
- <div class="swap"></div>
- <div class="page-number">${e.page}</div>
- </a>`)
- .join('');
- template.content.querySelectorAll('img').forEach(e => {
- e.addEventListener('error', function onError(event) {
- e.removeEventListener('error', onError);
- reload(event);
- });
- let timeout;
- {
- timeout = window.setTimeout(() => {
- console.log(`timeout: page ${e.dataset.page}`);
- const clickEvent = new MouseEvent('click', {
- bubbles: true,
- cancelable: true,
- view: window
- });
- e.dispatchEvent(clickEvent);
- }, LoadTimeout);
- e.addEventListener('load', function onload() {
- e.removeEventListener('load', onload);
- clearTimeout(timeout);
- });
- }
- });
- $('#i3').appendChild(template.content);
- $('#i3').addEventListener('click', event => {
- if (event.target.nodeName === 'IMG') {
- reload(event);
- }
- else if (event.target.classList.contains('close')) {
- hideImage(event);
- }
- else if (event.target.classList.contains('swap')) {
- swapImage(event);
- }
- else if (event.target.classList.contains('page-number')) {
- event.preventDefault();
- event.stopPropagation();
- }
- });
- buttonRetrieve.textContent = 'Done!';
- buttonDoubleFrame.removeAttribute('disabled');
- buttonFullHeight.removeAttribute('disabled');
- document.onkeydown = null;
- document.addEventListener('keydown', scrollNextImage);
- }).catch(e => { console.log(e); });
- });
- buttonRange.addEventListener('click', event => {
- // override e-hentai's viewing shortcut
- document.onkeydown = undefined;
- const pageNum = $('div.sn').textContent.match(/(\d+)\s*\/\s*(\d+)/).slice(1);
- 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>`);
- buttonRange.remove();
- });
- buttonFullHeight.addEventListener('click', event => {
- $('#i3').classList.toggle('force-img-full-height');
- });
-
- })();