// ==UserScript==
// @name E-Hentai Automated Downloads
// @description Automates downloads through the Doggie Bag Archiver
// @include http://e-hentai.org/*
// @include https://e-hentai.org/*
// @include http://g.e-hentai.org/*
// @include https://g.e-hentai.org/*
// @include http://exhentai.org/*
// @include https://exhentai.org/*
// @grant GM_xmlhttpRequest
// @run-at document-start
// @author etc
// @version 2.0.2
// @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 });
}
// 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);