E-Hentai Automated Downloads

Automates downloads through the Doggie Bag Archiver

As of 2016-10-31. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           E-Hentai Automated Downloads
// @description    Automates downloads through the Doggie Bag Archiver
// @include        http://g.e-hentai.org/*
// @include        http://exhentai.org/*
// @include        https://exhentai.org/*
// @grant          GM_xmlhttpRequest
// @run-at         document-start
// @author         etc
// @version        2.0
// @namespace      https://greasyfork.org/users/2168
// ==/UserScript==

/* * * * * promise-polyfill * * * * */

// https://github.com/taylorhakes/promise-polyfill
// Copyright (c) 2014 Taylor Hakes
// Copyright (c) 2014 Forbes Lindesay
// MIT License
!function(e){function n(){}function t(e,n){return function(){e.apply(n,arguments)}}function o(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],s(e,this)}function i(e,n){for(;3===e._state;)e=e._value;return 0===e._state?void e._deferreds.push(n):(e._handled=!0,void o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null===t)return void(1===e._state?r:u)(n.promise,e._value);var o;try{o=t(e._value)}catch(i){return void u(n.promise,i)}r(n.promise,o)}))}function r(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var i=n.then;if(n instanceof o)return e._state=3,e._value=n,void f(e);if("function"==typeof i)return void s(t(i,n),e)}e._state=1,e._value=n,f(e)}catch(r){u(e,r)}}function u(e,n){e._state=2,e._value=n,f(e)}function f(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;n<t;n++)i(e,e._deferreds[n]);e._deferreds=null}function c(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}function s(e,n){var t=!1;try{e(function(e){t||(t=!0,r(n,e))},function(e){t||(t=!0,u(n,e))})}catch(o){if(t)return;t=!0,u(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,t){var o=new this.constructor(n);return i(this,new c(e,t,o)),o},o.all=function(e){var n=Array.prototype.slice.call(e);return new o(function(e,t){function o(r,u){try{if(u&&("object"==typeof u||"function"==typeof u)){var f=u.then;if("function"==typeof f)return void f.call(u,function(e){o(r,e)},t)}n[r]=u,0===--i&&e(n)}catch(c){t(c)}}if(0===n.length)return e([]);for(var i=n.length,r=0;r<n.length;r++)o(r,n[r])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(n,t){for(var o=0,i=e.length;o<i;o++)e[o].then(n,t)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){"undefined"!=typeof console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)},o._setImmediateFn=function(e){o._immediateFn=e},o._setUnhandledRejectionFn=function(e){o._unhandledRejectionFn=e},"undefined"!=typeof module&&module.exports?module.exports=o:e.Promise||(e.Promise=o)}(this);

/* * * * * Resources * * * * */

var icons = {
    download : 'M8.037,11.166L14.5,22.359c0.825,1.43,2.175,1.43,3,0l6.463-11.194c0.826-1.429,0.15-2.598-' +
               '1.5-2.598H9.537C7.886,8.568,7.211,9.737,8.037,11.166z',
    torrent  : 'M22.404,13.585c0-5.319-4.313-9.631-9.632-9.631c-5.32,0-9.632,4.313-9.632,9.631c0,4.1,2.5' +
               '67,7.593,6.177,8.982l-1.818-8.45l-0.514-2.388L6.075,7.505L9.6,6.746l1.303,6.059c0.352,1.636,1.0' +
               '94,2.514,2.316,2.25c0.967-0.208,1.377-0.995,1.487-1.597c0.049-0.228,0.013-0.51-0.047-0.786l-1.4' +
               '43-6.705L16.74,5.21l1.646,7.651c0.662,3.077,2.454,3.548,2.454,3.548s-2.419,0.521-3.433,0.738c-1' +
               '.012,0.219-1.694-1.591-1.694-1.591l-0.07,0.015c-0.288,0.785-0.613,2.06-3.127,2.602c-0.184,0.039' +
               '-0.364,0.064-0.542,0.083l1.064,4.948C18.232,23.063,22.404,18.814,22.404,13.585z',
    picker   : 'M22.727,18.242L4.792,27.208l8.966-8.966l-4.483-4.484l17.933-8.966l-8.966,8.966L22.727,18.242z',
    done     : 'M2.379,14.729 5.208,11.899 12.958,19.648 25.877,6.733 28.707,9.561 12.958,25.308z'
};

var loadingGIF =
        'R0lGODlhEgASAMQaAHl5d66urMXFw3l5dpSUk5WVlKOjoq+vrsbGw6Sko7u7uaWlpbm5t3h4doiIhtLSz4aGhJaWlsbGxNHRzrC' +
        'wr5SUkqKiobq6uNHRz4eHhf///wAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAaACwAAAAAEgASAAAFaq' +
        'AmjmRplstyrkmbrCNFaUZtaFF0HvyhWRZNYVgwBY4BEmFJOB1NlYpJoYBpHI7RZXtZZb4ZEbd7AodFDIYVAjFJJCYA4ISoI0hyu' +
        'UnAF2geDxoDgwMnfBoYiRgaDQ1WiIqPJBMTkpYaIQAAIfkEBQoAGgAsAQABABAAEAAABWSgJo4aRZEoeaxHOiqKFsyBtizopV9y' +
        'nfwJ0o43MhgNKAYjZbGQJBLXKBLRIK4IaWFbEHgFUoKYoPFKRZUK6fFIORwojBxDytgzpDkdANDc8SQTExp8fBoQEGcDiwNnJA0' +
        'NLiEAACH5BAUKABoALAEAAQAQABAAAAVloCaOmqKQKHmtVzpKksa2FIUiOKIxjHb8B5JgKCAFjgHUMHUkPR6u0WKhwVgx0YQ2cc' +
        'W6DGCDZjKJiiwWEgCQikRQ6zWpQC+QBviBxuHQEP4EKA0NGhmGGRoVFWaHiGYjEBAuIQAAIfkEBQoAGgAsAQABABAAEAAABWSgJ' +
        'o6aJJEoiaxIOj6PJsyCpigopmNyff0X0o43AgZJk0mKwSABAK4RhaJ5PqOH7GHAHUQD4ICm0YiKwCSHI7VYoDLwDClBT5Di8khE' +
        'Y+gbUBAQGgWEBRoWFmYEiwRmJBUVLiEAACH5BAUKABoALAEAAQAQABAAAAVloCaO2vOQKImtWDoCgMa2koTCsDZNGuIjpIFwQBI' +
        'YBahGI2UkORyukUKhyVgz0Yv2csW6thcNBBIVMRikSCRFoaAK8ALpQD+QCHiCZrHQBP4BKBUVGgmGCX6BUQaMBmUkFhYuIQAAIf' +
        'kEBQoAGgAsAQABABAAEAAABWagJo4aAJAoaZrp6DjaIA/a86BZnmlNo2FADEm3GwWFJAgkNZmQIpHWSCLRFK4FKWKLIHgJUoFYo' +
        'KlUpCIxabFIKRSohDxButgvJIPeoKFQNHd4JBYWGgeHBxoMDGgBjgFoJI4tIQAAIfkEBQoAGgAsAQABABAAEAAABWSgJo6a45Ao' +
        'ma1ZOkaRxrYAgBZ4oUGQVtckgpBAGhgHqEol1WiQFgvX6PHQJK4JKWaLMXgNWq7GYpGKJhMShZKSSFCH+IGEqCNIgXxAo1BoBIA' +
        'CKHkaF4YXf4JSh4hmIwwMLiEAACH5BAUKABoALAEAAQAQABAAAAVloCaOWhSRKFmsRToui0bMhOY4aKInWlVpmWCGZCgaSMIhyW' +
        'JJQSAkCsU1AgA0h+yBarUGvgHqYDzQfKmiRoOkUKQeD9RlfiFh7hgSvS6RaPB5JAwMGgiGCBoTE2gCjQJoJI0uIQAAOw==';

/* * * * * UI utilities * * * * */

var getIcon = function(name,color) {
    return 'url("data:image/svg+xml,<svg viewBox=\'0 0 30 30\' preserveAspectRatio=\'true\' xmlns=\'http' +
        '://www.w3.org/2000/svg\'><path fill=\'' + color + '\' d=\'' + icons[name] + '\'/></svg>")';
};

var createButton = function(data) {
    var result = document.createElement(data.hasOwnProperty('type') ? data.type : 'a');
    if (data.hasOwnProperty('class')) result.className = data.class;
    if (data.hasOwnProperty('title')) result.title = data.title;
    if (data.hasOwnProperty('onClick')) result.addEventListener('click',data.onClick,false);
    if (data.hasOwnProperty('parent')) data.parent.appendChild(result);
    if (data.hasOwnProperty('target')) result.setAttribute('target',data.target);
    if (data.hasOwnProperty('style'))
        result.style.cssText = Object.keys(data.style).map(function(x) { return x + ': ' + data.style[x] + 'px'; }).join('; ');
    return result;
};

/* * * * * Utilities * * * * */

var xhr = function(data) {
    var request = {
        method: data.method,
        url: data.url,
        onload: data.callback
    };
    if (data.headers) request.headers = data.headers;
    if (data.onerror) request.onerror = data.onerror;
    if (data.body && data.body.constructor == String) request.data = data.body;
    else if (data.body) request.data = JSON.stringify(data.body);
    GM_xmlhttpRequest(request);
};

/* * * * * Download steps * * * * */

var obtainArchiverKey = function(data) {
    return new Promise(function(resolve, reject) {
        xhr({
            method: 'GET',
            url: window.location.protocol + '//' + window.location.host + '/g/' + data.galleryId + '/' +
                data.galleryToken + '?random=' + Date.now(),
            callback: function(response) {
                var div = document.createElement('div');
                div.innerHTML = response.responseText.replace(/src=/g, 'no-src=');
                var target = div.querySelector('[onclick*="archiver.php"]');
                if (!target) data.error = 'could not resolve archiver key';
                else {
                    var tokens = target.getAttribute('onclick').match(/or=([^'"]+)/);
                    if (!tokens) data.error = 'could not resolve archiver key';
                    else data.archiverKey = tokens[1];
                }
                if (data.error) reject(data);
                else resolve(data);
            },
            onerror: function() {
                data.error = 'could not open gallery\'s page';
                reject(data);
            }
        });
    });
};

var obtainTorrentFile = function(data) {
    return new Promise(function(resolve, reject) {
        xhr({
            method: 'GET',
            url: window.location.protocol + '//' + window.location.host +
                '/gallerytorrents.php?gid=' + data.galleryId + '&t=' + data.galleryToken,
            callback: function(response) {
                var div = document.createElement('div');
                div.innerHTML = response.responseText.replace(/src=/g, 'no-src=');
                var forms = div.querySelectorAll('form'), result = null;
                for (var i=0;i<forms.length;++i) {
                    var link = forms[i].querySelector('a');
                    if (!link) continue;
                    var posted = document.evaluate('.//span[contains(text(),"Posted")]', forms[i], null, 9, null).singleNodeValue;
                    var seeds = document.evaluate('.//span[contains(text(),"Seeds")]', forms[i], null, 9, null).singleNodeValue;
                    if (!posted || !seeds) continue;
                    posted = new Date(posted.nextSibling.textContent.trim());
                    seeds = parseInt(seeds.nextSibling.textContent, 10);
                    if (seeds === 0) continue;
                    if (result == null || (result.date - posted < 0))
                        result = { date: posted, link: link.href };
                }
                if (result === null) data.error = 'could not find any seeded torrent';
                else data.fileUrl = result.link;
                if (data.error) reject(data);
                else resolve(data);
            },
            onerror: function() {
                data.error = 'could not obtain torrent list';
                reject(data);
            }
        });
    });
};

var submitDownloadRequest = function(data) {
    return new Promise(function(resolve, reject) {
        if (!data || data.error) {
            resolve(data);
            return;
        }
        xhr({
            method: 'POST',
            url: window.location.protocol + '//' + window.location.host + '/archiver.php?gid=' + data.galleryId +
                '&token=' + data.galleryToken + '&or=' + data.archiverKey.replace(/--/, '-'),
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: 'dltype=org&dlcheck=Download+Original+Archive',
            callback: function(response) {
                var div = document.createElement('div'), url = null;
                div.innerHTML = response.responseText.replace(/src=/g, 'no-src=');
                var target = div.querySelector('#continue > a');
                if (target) url = target.href;
                else {
                    var targets = div.querySelectorAll('script');
                    for (var i=0;i<targets.length;++i) {
                        var match = targets[i].textContent.match(/location\s*=\s*"(.+?)"/);
                        if (!match) continue;
                        url = match[1];
                        break;
                    }
                }
                if (url) data.archiverUrl = url;
                else data.error = 'could not resolve archiver URL';
                if (data.error) reject(data);
                else resolve(data);
            },
            onerror: function() {
                data.error = 'could not access archiver';
                reject(data);
            }

        });
    });
};

var waitForDownloadLink = function(data) {
    return new Promise(function(resolve, reject) {
        if (!data || data.error) {
            resolve(data);
            return;
        }
        xhr({
            method: 'GET',
            url: data.archiverUrl,
            callback: function(response) {
                if (/The file was successfully prepared/i.test(response.responseText)) {
                    var div = document.createElement('div');
                    div.innerHTML = response.responseText.replace(/src=/g, 'no-src=');
                    var target = div.querySelector('#db a');
                    if (target) {
                        var archiverUrl = new URL(data.archiverUrl);
                        data.fileUrl = archiverUrl.protocol + '//' + archiverUrl.host + target.getAttribute('href');
                    } else data.error = 'could not resolve file URL';
                } else
                    data.error = 'archiver did not provide file URL';
                if (data.error) reject(data);
                else resolve(data);
            },
            onerror: function() {
                data.error = 'could not contact archiver';
                if (/https/.test(window.location.protocol)) {
                    data.error += '; this is most likely caused by mixed-content security policies enforced by the' +
                        ' browser that need to be disabled by the user. If you have no clue how to do that, you' +
                        ' should probably Google "how to disable mixed-content blocking".';
                } else {
                    data.error += '; please check whether your browser is not blocking XHR requests towards' +
                        ' 3rd-party URLs';
                }
                reject(data);
            }
        });
    });
};

var downloadFile = function(data) {
    return new Promise(function(resolve, reject) {
        if (!data || data.error) {
            resolve(data);
            return;
        }
        var a = document.createElement('a');
        a.href = data.fileUrl;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        document.body.appendChild(a);
        resolve(data);
    });
};

var updateUI = function(data) {
    if (!data || data.error) return;
    var temp = (data.isTorrent ? torrentQueue[data.galleryId] : archiveQueue[data.galleryId]);
    temp.button.className = temp.button.className.replace(/\s*working/, '') + ' requested';
};

var handleFailure = function(data) {
    if (!data) return;
    var temp = (data.isTorrent ? torrentQueue[data.galleryId] : archiveQueue[data.galleryId]);
    temp.button.className = temp.button.className.replace(/\s*working/, '');
    alert('Could not complete operation.\nReason: ' + (data.error || 'unknown'));
};

/* * * * * State management * * * * */

var archiveQueue = { }, torrentQueue = { };

var requestDownload = function(e) {
    if (/working|requested/.test(e.target.className)) return; 
    e.preventDefault();
    e.target.className += ' working';
    var isTorrent = /torrentLink/.test(e.target.className);
    var tokens = e.target.getAttribute('target').match(/\/g\/(\d+)\/([0-9a-z]+)/i);
    var galleryId = parseInt(tokens[1], 10), galleryToken = tokens[2];
    if (!isTorrent) {
        archiveQueue[galleryId] = { token: galleryToken, button: e.target };
        obtainArchiverKey({ galleryId: galleryId, galleryToken: galleryToken, isTorrent: false })
            .then(submitDownloadRequest, handleFailure)
            .then(waitForDownloadLink, handleFailure)
            .then(downloadFile, handleFailure)
            .then(updateUI, handleFailure);
    } else {
        torrentQueue[galleryId] = { token: galleryToken, button: e.target };
        obtainTorrentFile({ galleryId: galleryId, galleryToken: galleryToken, isTorrent: true })
            .then(downloadFile, handleFailure)
            .then(updateUI, handleFailure);
    }
};

/* * * * * UI setup * * * * */

window.addEventListener('load', function() {

    // button generation (thumbnail list)
    var thumbnails = document.querySelectorAll('.id3 > a'), n = thumbnails.length;
    while (n --> 0) {
        var bottom = Math.max(0,parseInt(thumbnails[n].parentNode.style.height,10) - thumbnails[n].firstChild.height);
        var right  = Math.max(0,0.5 * (200 - thumbnails[n].firstChild.width));
        createButton({ class: 'automatedButton downloadLink', title: 'Automated download', target: thumbnails[n].href,
            style: { bottom: bottom, right: right }, onClick: requestDownload, parent: thumbnails[n] });
        createButton({ class: 'automatedButton torrentLink', title: 'Torrent download', target: thumbnails[n].href,
            style: { bottom: bottom, left: 1 }, onClick: requestDownload, parent: thumbnails[n] });
    }

    // button generation (gallery)
    var bigThumbnail = document.querySelector('#gd1 > img');
    if (bigThumbnail !== null) {
        var bottom = bigThumbnail.parentNode.parentNode.clientHeight - bigThumbnail.offsetTop - bigThumbnail.height - 1;
        var right  = bigThumbnail.parentNode.parentNode.clientWidth - bigThumbnail.offsetLeft - bigThumbnail.width - 2;
        var left   = bigThumbnail.offsetLeft + 1;
        createButton({ class: 'automatedButton downloadLink', title: 'Automated download', target: window.location.href,
            style: { bottom: bottom, right: right }, onClick: requestDownload, parent: bigThumbnail.parentNode });
        createButton({ class: 'automatedButton torrentLink', title: 'Torrent download', target: window.location.href,
            style: { bottom: bottom, left: left }, onClick: requestDownload, parent: bigThumbnail.parentNode });
    }

    // button generation (row list)
    var rows = document.querySelectorAll('.it5 > a'), n = rows.length;
    while (n --> 0) {
        var div = createButton({ type: 'div', class: 'automatedPicker', onClick: requestDownload, parent: rows[n].parentNode });
        var picker = createButton({ type: 'div', parent: div });
        createButton({ type: 'div', class: 'automatedInline torrentLink', title: 'Torrent download', 
            target: rows[n].href, parent: picker });
        createButton({ type: 'div', class: 'automatedInline downloadLink', title: 'Automated download',
            target: rows[n].href, parent: picker });
    }

    // message listener
    window.addEventListener('message',function(message) {
        var data = message.data;
        if (!downloadData.hasOwnProperty(data.nonce)) return;
        if (data.type == 'heartbeat') onHeartbeat(data);
        else if (data.type == 'success') onSuccess(data);
        else onFailure(data);
    },false);

    // document style
    var style = document.createElement('style');
    style.innerHTML =
        '.automatedButton { display: none; position: absolute; text-align: left; cursor: pointer; padding: 8px;' +
        'color: white; margin-right: 1px; font-size: 20px; line-height: 11px; }' +
        '.downloadLink  { background-image: ' + getIcon('download','rgb(0,0,0)') + '; background-color: rgba(98,220,151,1); }' +
        '.torrentLink  { background-image: ' + getIcon('torrent','rgb(0,0,0)') + '; background-color: rgba(98,182,210,1); }' +
        '.torrentLink:not(.requested) { background-position: 2px 2px; }' +
        '.requested  { background-image: ' + getIcon('done','rgb(0,0,0)') + '; }' +
        '.requested, .working { background-color: rgba(255,143,113,1); }' +
        '.automatedButton.downloadLink  { border-radius: 0 0 5px 0 !important; width: 12px; height: 12px; }' +
        '.automatedButton.torrentLink  { border-radius: 0 0 0 5px !important; width: 12px; height: 12px; }' +
        '#gd1 > .automatedButton { border-radius: 0 0 0 0 !important; }' +
        '.working { background-image: url(data:image/gif;base64,' + loadingGIF + ') !important; background-repeat: no-repeat; }' +
        '.automatedInline.working { background-position: 3px 3px; }' +
        '.automatedButton.working { width: 18px; height: 18px; font-size: 0px; background-position: 5px 5px; padding: 5px !important; }' +
        '.automatedPicker { background-image: ' + getIcon('picker','rgb(252,0,97)') + '; width: 16px;' + 
        'height: 16px; float: left; cursor: pointer; }' +
        '.automatedButton:hover, .automatedInline:hover { background-color: rgba(255,199,139,1) !important; color: black !important; }' +
        '*:hover > .automatedButton, .automatedButton.working, .automatedButton.requested { display: block !important; }' +
        '.EHADiframe { width: 0px !important; height: 0px !important; opacity: 0 !important; }' +
        '.automatedPicker > div { display: none; z-index: 2; position: absolute; top: -4px; text-align: center; }' +
        '.automatedPicker:hover > div, .automatedPicker > div:hover { display: block; }' +
        '.automatedInline { padding: 3px; border: 1px solid black; width: 17px; height: 17px; display: inline-block; }' +
        '.automatedInline:first-child { border-right: none !important; }';
    document.head.appendChild(style);

}, false);