Sleazy Fork is available in English.

E-Hentai Automated Downloads

Automates downloads through the Doggie Bag Archiver

  1. // ==UserScript==
  2. // @name E-Hentai Automated Downloads
  3. // @description Automates downloads through the Doggie Bag Archiver
  4. // @include http://e-hentai.org/*
  5. // @include https://e-hentai.org/*
  6. // @include http://g.e-hentai.org/*
  7. // @include https://g.e-hentai.org/*
  8. // @include http://exhentai.org/*
  9. // @include https://exhentai.org/*
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM.xmlHttpRequest
  12. // @grant GM_openInTab
  13. // @grant GM.openInTab
  14. // @run-at document-start
  15. // @author etc
  16. // @version 2.2.0
  17. // @namespace https://greasyfork.org/users/2168
  18. // ==/UserScript==
  19.  
  20. if (typeof(Promise) === 'undefined') {
  21. console.warn('Browser does not support promises, aborting.');
  22. return;
  23. }
  24.  
  25. /*-----------------------
  26. Assets (icons and GIFs)
  27. -----------------------*/
  28.  
  29. var ASSETS = {
  30. downloadIcon: generateSvgIcon(1500, 'rgb(0,0,0)', 'M370.333 0h200q21 0 35.5 14.5t14.5 35.5v550h291q21 0 26 11.5t-8 27.5l-427 522q-13 16-32 16t-32-16l-427-522q-13-16-8-27.5t26-11.5h291V50q0-21 14.5-35.5t35.5-14.5z'),
  31. torrentIcon: generateSvgIcon(1300, 'rgb(0,0,0)', 'M932 12.667l248 230q14 14 14 35t-14 35l-248 230q-14 14-24.5 10t-10.5-25v-150H497v-200h400v-150q0-21 10.5-25t24.5 10zm-735 365h-50q-21 0-35.5-14.5t-14.5-35.5v-100q0-21 14.5-35.5t35.5-14.5h50v200zm200 0H297v-200h100v200zm-382 365l247-230q14-14 24.5-10t10.5 25v150h400v200H297v150q0 21-10.5 25t-24.5-10l-247-230q-15-14-15-35t15-35zm882 135H797v-200h100v200zm100-200h51q20 0 34.5 14.5t14.5 35.5v100q0 21-14.5 35.5t-34.5 14.5h-51v-200z'),
  32. doneIcon: generateSvgIcon(1800, 'rgb(0,0,0)', 'M1412 734q0-28-18-46l-91-90q-19-19-45-19t-45 19l-408 407-226-226q-19-19-45-19t-45 19l-91 90q-18 18-18 46 0 27 18 45l362 362q19 19 45 19 27 0 46-19l543-543q18-18 18-45zm252 162q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z'),
  33. loadingIcon: generateSvgIcon(1900, 'rgb(0,0,0)', 'M462 1394q0 53-37.5 90.5T334 1522q-52 0-90-38t-38-90q0-53 37.5-90.5T334 1266t90.5 37.5T462 1394zm498 206q0 53-37.5 90.5T832 1728t-90.5-37.5T704 1600t37.5-90.5T832 1472t90.5 37.5T960 1600zM256 896q0 53-37.5 90.5T128 1024t-90.5-37.5T0 896t37.5-90.5T128 768t90.5 37.5T256 896zm1202 498q0 52-38 90t-90 38q-53 0-90.5-37.5T1202 1394t37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zM494 398q0 66-47 113t-113 47-113-47-47-113 47-113 113-47 113 47 47 113zm1170 498q0 53-37.5 90.5T1536 1024t-90.5-37.5T1408 896t37.5-90.5T1536 768t90.5 37.5T1664 896zm-640-704q0 80-56 136t-136 56-136-56-56-136 56-136T832 0t136 56 56 136zm530 206q0 93-66 158.5T1330 622q-93 0-158.5-65.5T1106 398q0-92 65.5-158t158.5-66q92 0 158 66t66 158z')
  34. };
  35.  
  36. /*---------
  37. Utilities
  38. ---------*/
  39.  
  40. function generateSvgIcon(size, color, data) {
  41. return format('url("data:image/svg+xml,' +
  42. '<svg width=\'{0}\' height=\'{0}\' viewBox=\'0 0 {0} {0}\' xmlns=\'http://www.w3.org/2000/svg\'>' +
  43. '<path fill=\'{1}\' d=\'{2}\'/></svg>")', size, color, data);
  44. }
  45.  
  46. function createButton(data) {
  47. var result = document.createElement(data.hasOwnProperty('type') ? data.type : 'a');
  48. if (data.hasOwnProperty('className')) result.className = data.className;
  49. if (data.hasOwnProperty('title')) result.title = data.title;
  50. if (data.hasOwnProperty('onClick')) {
  51. result.addEventListener('mousedown', data.onClick, false);
  52. result.addEventListener('click', function(e) { e.preventDefault(); }, false);
  53. result.addEventListener('contextmenu', function(e) { e.preventDefault(); }, false);
  54. }
  55. if (data.hasOwnProperty('parent')) data.parent.appendChild(result);
  56. if (data.hasOwnProperty('target')) result.setAttribute('target',data.target);
  57. if (data.hasOwnProperty('style'))
  58. result.style.cssText = Object.keys(data.style).map(function(x) { return x + ': ' + data.style[x] + 'px'; }).join('; ');
  59. return result;
  60. }
  61.  
  62. function format(varargs) {
  63. var pattern = arguments[0];
  64. for (var i=1;i<arguments.length;++i)
  65. pattern = pattern.replace(new RegExp('\\{' + (i-1) + '\\}', 'g'), arguments[i]);
  66. return pattern;
  67. }
  68.  
  69. function xhr(data) {
  70. return new Promise(function(resolve, reject) {
  71. var request = {
  72. method: data.method,
  73. url: data.url,
  74. onload: function() { resolve.apply(this, arguments); },
  75. onerror: function() { reject.apply(this, arguments); }
  76. };
  77. if (data.headers) request.headers = data.headers;
  78. if (data.body && data.body.constructor == String) request.data = data.body;
  79. else if (data.body) request.data = JSON.stringify(data.body);
  80. if (typeof(GM_xmlhttpRequest) !== 'undefined') GM_xmlhttpRequest(request);
  81. else if (typeof(GM) !== 'undefined' && GM.xmlHttpRequest) GM.xmlHttpRequest(request);
  82. else reject(new Error('Could not submit XHR request'));
  83. });
  84. }
  85.  
  86. function parseHTML(html) {
  87. var div = document.createElement('div');
  88. div.innerHTML = html.replace(/src=/g, 'no-src=');
  89. return div;
  90. }
  91.  
  92. function updateUI(data) {
  93. if (!data || data.error) return;
  94. var temp = (data.isTorrent ? torrentQueue[data.galleryId] : archiveQueue[data.galleryId]);
  95. temp.button.className = temp.button.className.replace(/\s*working/, '') + ' requested';
  96. }
  97.  
  98. function handleFailure(data) {
  99. if (!data) return;
  100. var temp = (data.isTorrent ? torrentQueue[data.galleryId] : archiveQueue[data.galleryId]);
  101. temp.button.className = temp.button.className.replace(/\s*working/, '');
  102. if (data.error !== 'aborted')
  103. alert('Could not complete operation.\nReason: ' + (data.error || 'unknown'));
  104. }
  105.  
  106. function xpathFind(root, nodeType, text) {
  107. return document.evaluate('.//' + (nodeType || '*') + '[contains(text(), "' + text + '")]', root, null, 9, null).singleNodeValue;
  108. }
  109.  
  110. function pickTorrent(candidates, lastUpdateDate) {
  111. var currentScore = 0, currentCandidate = null;
  112. // Get max values
  113. var maxSeeds = candidates.reduce(function(p,n) { return Math.max(p, n.seeds); }, 0);
  114. var maxSize = candidates.reduce(function(p,n) { return Math.max(p, n.size); }, 0);
  115. var newest = candidates.reduce(function(p,n) { return Math.max(p, n.date.valueOf()); }, 0);
  116. // Calculate scores
  117. candidates.forEach(function(candidate) {
  118. var seedScore = candidate.seeds / maxSeeds;
  119. var sizeScore = candidate.size / maxSize;
  120. var dateScore = 1;
  121. if (lastUpdateDate && newest > lastUpdateDate) {
  122. dateScore = (candidate.date.valueOf() - lastUpdateDate) / (newest - lastUpdateDate);
  123. if (dateScore < 0) dateScore = 0.1; // galleries posted before the last update automatically get 0.1
  124. }
  125. // Total score
  126. var score = seedScore * sizeScore * dateScore;
  127. if (currentScore >= score) return;
  128. currentScore = score;
  129. currentCandidate = candidate;
  130. });
  131. return currentCandidate;
  132. }
  133.  
  134. /*--------------
  135. Download Steps
  136. --------------*/
  137.  
  138.  
  139. function obtainTorrentFile(data) {
  140. return xhr({
  141. method: 'GET',
  142. url: format('{0}//{1}/gallerytorrents.php?gid={2}&t={3}',
  143. window.location.protocol, window.location.host, data.galleryId, data.galleryToken)
  144. }).then(function(response) {
  145. var div = parseHTML(response.responseText);
  146. var forms = div.querySelectorAll('form'), candidates = [ ];
  147. var findValue = function(text) {
  148. var target = xpathFind(forms[i], 'span', text);
  149. return (target ? target.nextSibling.textContent.trim() : null);
  150. };
  151. for (var i=0;i<forms.length;++i) {
  152. var link = forms[i].querySelector('a');
  153. if (!link) continue;
  154. // Gather torrent data
  155. var posted = new Date(findValue('Posted')), size = findValue('Size'),
  156. seeds = parseInt(findValue('Seeds'), 10) || 0;
  157. size = parseFloat(size, 10) * (/MB/i.test(size) ? 1024 : (/GB/i.test(size) ? 1024 * 1024 : 1));
  158. if (size !== 0) candidates.push({ link: link.href, date: posted, size: size, seeds: seeds });
  159. }
  160. if (candidates.length === 0) data.error = 'could not find any suitable torrent';
  161. else data.fileUrl = pickTorrent(candidates, data.date).link;
  162. if (data.error) return Promise.reject(data);
  163. else return data;
  164. });
  165. }
  166.  
  167. function confirmDownloadRequest(data) {
  168. return xhr({
  169. method: 'GET',
  170. url: format('{0}//{1}/archiver.php?gid={2}&token={3}',
  171. window.location.protocol, window.location.host, data.galleryId, data.galleryToken)
  172. }).then(function(response) {
  173. var div = parseHTML(response.responseText);
  174. var costLabel = xpathFind(div, '*', 'Download Cost:');
  175. var sizeLabel = xpathFind(div, '*', 'Estimated Size:');
  176. if (!costLabel || !sizeLabel)
  177. return data;
  178. var cost = costLabel.textContent.replace(/^.+:/, '').trim();
  179. var size = sizeLabel.textContent.replace(/^.+:/, '').trim();
  180. var proceed = confirm(format('Size: {0}\nCost: {1}\n\nProceed?', size, cost));
  181. if (proceed) return data;
  182. data.error = 'aborted';
  183. return Promise.reject(data);
  184. });
  185. }
  186.  
  187. function submitDownloadRequest(data) {
  188. return xhr({
  189. method: 'POST',
  190. url: format('{0}//{1}/archiver.php?gid={2}&token={3}',
  191. window.location.protocol, window.location.host, data.galleryId, data.galleryToken),
  192. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  193. body: 'dltype=org&dlcheck=Download+Original+Archive',
  194. }).then(function(response) {
  195. var div = parseHTML(response.responseText);
  196. var url, target = div.querySelector('#continue > a');
  197. if (target) url = target.href;
  198. else {
  199. var targets = div.querySelectorAll('script');
  200. for (var i=0;i<targets.length;++i) {
  201. var match = targets[i].textContent.match(/location\s*=\s*"(.+?)"/);
  202. if (!match) continue;
  203. url = match[1];
  204. break;
  205. }
  206. }
  207. if (url) data.archiverUrl = url;
  208. else data.error = 'could not resolve archiver URL';
  209. if (data.error) return Promise.reject(data);
  210. else return data;
  211. });
  212. }
  213.  
  214. function waitForDownloadLink(data) {
  215. return xhr({
  216. method: 'GET',
  217. url: data.archiverUrl
  218. }).then(function(response) {
  219. if (/The file was successfully prepared/i.test(response.responseText)) {
  220. var div = parseHTML(response.responseText);
  221. var target = div.querySelector('#db a');
  222. if (target) {
  223. var archiverUrl = new URL(data.archiverUrl);
  224. data.fileUrl = archiverUrl.protocol + '//' + archiverUrl.host + target.getAttribute('href');
  225. } else data.error = 'could not resolve file URL';
  226. } else
  227. data.error = 'archiver did not provide file URL';
  228. if (data.error) return Promise.reject(data);
  229. else return data;
  230. }).catch(function() {
  231. if (data.error) return Promise.reject(data);
  232. data.error = 'could not contact archiver';
  233. if (/https/.test(window.location.protocol)) {
  234. data.error += '; this is most likely caused by mixed-content security policies enforced by the' +
  235. ' browser that need to be disabled by the user. If you have no clue how to do that, you' +
  236. ' should probably Google "how to disable mixed-content blocking".';
  237. } else {
  238. data.error += '; please check whether your browser is not blocking XHR requests towards' +
  239. ' 3rd-party URLs';
  240. }
  241. return Promise.reject(data);
  242. });
  243. }
  244.  
  245. function downloadFile(data) {
  246. downloadQueue = downloadQueue.then(function() {
  247. if (typeof(GM_openInTab) !== 'undefined') GM_openInTab(data.fileUrl, true);
  248. else if (typeof(GM) !== 'undefined' && GM.openInTab) GM.openInTab(data.fileUrl, true);
  249. else {
  250. var a = document.createElement('a');
  251. a.href = data.fileUrl;
  252. document.body.appendChild(a);
  253. a.click();
  254. document.body.removeChild(a);
  255. }
  256. return new Promise(function(resolve) { setTimeout(resolve, 500); });
  257. });
  258. return Promise.resolve(data);
  259. }
  260.  
  261. /*----------------
  262. State Management
  263. ----------------*/
  264.  
  265. var archiveQueue = { }, torrentQueue = { };
  266. var downloadQueue = Promise.resolve();
  267.  
  268. function requestDownload(e) {
  269. var isTorrent = /torrentLink/.test(e.target.className);
  270. if (/working|requested/.test(e.target.className)) return;
  271. if (isTorrent && e.which !== 1) return;
  272. e.preventDefault();
  273. e.stopPropagation();
  274. e.target.className += ' working';
  275. var tokens = e.target.getAttribute('target').match(/\/g\/(\d+)\/([0-9a-z]+)/i);
  276. var galleryId = parseInt(tokens[1], 10), galleryToken = tokens[2];
  277. var askConfirmation = (!isTorrent && e.which === 3);
  278. if (!isTorrent) {
  279. archiveQueue[galleryId] = { token: galleryToken, button: e.target };
  280. var promise = Promise.resolve({ galleryId: galleryId, galleryToken: galleryToken, isTorrent: false });
  281. if (askConfirmation) promise = promise.then(confirmDownloadRequest);
  282. promise
  283. .then(submitDownloadRequest)
  284. .then(waitForDownloadLink)
  285. .then(downloadFile)
  286. .then(updateUI)
  287. .catch(handleFailure);
  288. } else {
  289. // Try to find out gallery's last update date if possible
  290. var galleryDate = xpathFind(document, 'td', 'Posted:'); // gallery page
  291. if (galleryDate) galleryDate = galleryDate.nextSibling;
  292. else // thumbnail mode
  293. galleryDate = document.evaluate('./ancestor::tr/td[@class="itd"]', e.target, null, 9, null).singleNodeValue;
  294. if (galleryDate !== null) galleryDate = new Date(galleryDate.textContent.trim());
  295. // Gather data
  296. torrentQueue[galleryId] = { token: galleryToken, button: e.target };
  297. obtainTorrentFile({ galleryId: galleryId, galleryToken: galleryToken, isTorrent: true, date: galleryDate })
  298. .then(downloadFile)
  299. .then(updateUI)
  300. .catch(handleFailure);
  301. }
  302. return false;
  303. }
  304.  
  305. /*--------
  306. UI Setup
  307. --------*/
  308.  
  309. window.addEventListener('load', function() {
  310.  
  311. // button generation (thumbnail / extended)
  312. var thumbnails = document.querySelectorAll('.gl3t, .gl1e > div'), n = thumbnails.length;
  313. while (n-- > 0) {
  314. createButton({
  315. title: 'Automated download',
  316. target: thumbnails[n].querySelector('a').href,
  317. className: 'automatedButton downloadLink',
  318. onClick: requestDownload,
  319. style: { bottom: 0, right: -2 },
  320. parent: thumbnails[n]
  321. });
  322. createButton({
  323. title: 'Torrent download',
  324. target: thumbnails[n].querySelector('a').href,
  325. className: 'automatedButton torrentLink',
  326. onClick: requestDownload,
  327. style: { bottom: 0, left: -1 },
  328. parent: thumbnails[n]
  329. });
  330. }
  331.  
  332. // button generation (gallery)
  333. var bigThumbnail = document.querySelector('#gd1 > div');
  334. if (bigThumbnail !== null) {
  335. createButton({
  336. title: 'Automated download',
  337. target: window.location.href,
  338. className: 'automatedButton downloadLink',
  339. onClick: requestDownload,
  340. style: { bottom: 0, right: 0 },
  341. parent: bigThumbnail
  342. });
  343. createButton({
  344. title: 'Torrent download',
  345. target: window.location.href,
  346. className: 'automatedButton torrentLink',
  347. onClick: requestDownload,
  348. style: { bottom: 0, left: 0 },
  349. parent: bigThumbnail
  350. });
  351. }
  352.  
  353. // document style
  354. var style = document.createElement('style');
  355. style.innerHTML =
  356. // Icons and colors
  357. '.downloadLink:not(.working) { background-image: ' + ASSETS.downloadIcon + '; background-color: rgb(220,98,98); background-position: 7px 7px; }' +
  358. '.torrentLink:not(.working) { background-image: ' + ASSETS.torrentIcon + '; background-color: rgb(98,182,210); background-position: 5px 6px; }' +
  359. '.requested { background-image: ' + ASSETS.doneIcon + ' !important; background-position: 4px 5px !important; }' +
  360. '.requested { background-color: rgba(128,226,126,1) !important; }' +
  361. '.working { background-color: rgba(255,128,192,1) !important; }' +
  362. '.working:before {' +
  363. 'content: ""; top: 1px; left: 0; width: 28px; height: 28px; position: absolute; animation: eh-spin 2s linear infinite;' +
  364. 'background-image: ' + ASSETS.loadingIcon + '; background-size: 20px 20px; background-position: 5px 5px; background-repeat: no-repeat;' +
  365. '}' +
  366. '.automatedButton:hover { background-color: rgba(255,199,139,1) }' +
  367. // Positioning
  368. '#gd1 > div, .gl3t, .gl1e > div { position: relative; }' +
  369. // Backgrounds
  370. '.automatedButton { background-size: 20px 20px; background-repeat: no-repeat; }' +
  371. // Others (thumbnail mode)
  372. '.automatedButton {' +
  373. 'display: none; position: absolute; text-align: left; cursor: pointer;' +
  374. 'color: white; margin-right: 1px; font-size: 20px; line-height: 11px; width: 28px; height: 28px;' +
  375. '}' +
  376. '.automatedButton.downloadLink { border-radius: 0 0 5px 0 !important; }' +
  377. '.automatedButton.torrentLink { border-radius: 0 0 0 5px !important; }' +
  378. '#gd1 > div > .automatedButton { border-radius: 0 0 0 0 !important; }' +
  379. '.automatedButton.working { font-size: 0px; }' +
  380. '#gd1 > div:hover .automatedButton, .gl3t:hover .automatedButton, .gl1e > div:hover .automatedButton, .automatedButton.working, .automatedButton.requested { display: block !important; }' +
  381. '@keyframes eh-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }' +
  382. '@-webkit-keyframes eh-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }' +
  383. '@-moz-keyframes eh-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
  384. document.head.appendChild(style);
  385.  
  386. }, false);