Sleazy Fork is available in English.

ExResurrect

Resurrect E/Ex gallery listings

目前為 2020-10-10 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ExResurrect
  3. // @namespace https://sleazyfork.org/en/users/285675-hauffen
  4. // @version 1.18.3
  5. // @description Resurrect E/Ex gallery listings
  6. // @author Hauffen
  7. // @runat document-start
  8. // @require https://code.jquery.com/jquery-3.3.1.min.js
  9. // @include /https?:\/\/(e-|ex)hentai\.org\/.*/
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. let $ = window.jQuery;
  14. var spl = document.URL.split('/');
  15. var dead, claim, language, translated;
  16. const category = {doujinshi: 'ct2', manga: 'ct3', artistcg: 'ct4', gamecg: 'ct5', western: 'cta', nonh: 'ct9', imageset: 'ct6', cosplay: 'ct7', asianporn: 'ct8', misc: 'ct1'};
  17. const fileSizeLabels = [ "B", "KB", "MB", "GB" ];
  18. const defaultNamespace = "misc";
  19. const tags = {};
  20.  
  21. function gotonext () {
  22. // Override the default function to prevent redirect
  23. }
  24.  
  25. addJS_Node (gotonext); // Inject the override function before doing anything to avoid the timeout redirect
  26.  
  27. function addJS_Node(text, s_URL, funcToRun, runOnLoad) {
  28. var scriptNode = document.createElement('script');
  29. if (runOnLoad) {
  30. scriptNode.addEventListener("load", runOnLoad, false);
  31. }
  32. scriptNode.type = "text/javascript";
  33. if (text) scriptNode.textContent = text;
  34. if (s_URL) scriptNode.src = s_URL;
  35. if (funcToRun) scriptNode.textContent = '(' + funcToRun.toString() + ')()';
  36.  
  37. var targ = document.getElementsByTagName('head')[0] || document.body || document.documentElement;
  38. targ.appendChild(scriptNode);
  39. }
  40. // There's a better critera for this probably
  41. if ($('.d').length) {
  42. dead = true;
  43. if ($('.d').text().indexOf("copyright") > 0) claim = $('.d').text().split('by')[1].split('.')[0].trim();
  44. $('.d').remove(); // Leave us with an entirely blank page to build up
  45. }
  46. generateRequest();
  47.  
  48. /**
  49. * Generate the JSON request for the E-H API
  50. */
  51. function generateRequest() {
  52. var reqList = [[spl[4], spl[5]]]; // We use an array for our gidlist, since the API can handle up to 25 galleries per request
  53. var request = {"method": "gdata", "gidlist": reqList, "namespace": 1};
  54.  
  55. var req = new XMLHttpRequest();
  56. req.onreadystatechange = e => {
  57. if (req.readyState == 4) {
  58. if (req.status == 200) {
  59. var apirsp = JSON.parse(req.responseText);
  60. for (var i = 0; i < apirsp.gmetadata.length; i++) {
  61. if (dead) generateListing(apirsp.gmetadata[i]);
  62. else generateLiveListing(apirsp.gmetadata[i]);
  63. }
  64. } else {
  65. console.error();
  66. }
  67. }
  68. }
  69. req.open("POST", document.location.origin + "/api.php", true); // Due to CORS, we need to use the API on the same domain as the script
  70. req.send(JSON.stringify(request));
  71. }
  72.  
  73. function generateListing(glisting) {
  74. var d = new Date(glisting.posted * 1000);
  75. document.title = glisting.title;
  76. generateTags(glisting);
  77. // There's a better way to do this, but I suck
  78. var listing = $(`
  79. <div id="nb" class="nose1">
  80. <div><a href="` + document.location.origin + `">Front<span class="nbw1"> Page</span></a></div>
  81. <div><a href="` + document.location.origin + `/watched">Watched</a></div>
  82. <div><a href="` + document.location.origin + `/popular">Popular</a></div>
  83. <div><a href="` + document.location.origin + `/torrents.php">Torrents</a></div>
  84. <div><a href="` + document.location.origin + `/favorites.php">Fav<span class="nbw1">orite</span>s</a></div>
  85. <div><a href="` + document.location.origin + `/uconfig.php">Settings</a></div>
  86. <div><a href="` + document.location.origin + `/upload/manage.php"><span class="nbw2">My </span>Uploads</a></div>
  87. <div><a href="` + document.location.origin + `/mytags">My Tags</a></div>
  88. </div>
  89. <div class="gm">
  90. <div id="gleft">
  91. <div id="gd1">
  92. <div style="width:250px; height:354px; background:transparent url(` + glisting.thumb.substring(0, glisting.thumb.length-5) + `250.jpg) 0 0 no-repeat"></div>
  93. </div>
  94. </div>
  95. <div id="gd2">
  96. <h1 id="gn">` + glisting.title + `</h1>
  97. <h1 id="gj">` + glisting.title_jpn + `</h1>
  98. </div>
  99. <div id="gmid">
  100. <div id="gd3">
  101. <div id="gdc">
  102. <div class="cs ` + category[glisting.category.toLowerCase().replace(/ /g, '').replace(/-/g, '')] + `" onclick="document.location='` + document.location.origin + '/' + glisting.category.toLowerCase().replace(/ /g, '') + `'">` + glisting.category + `</div>
  103. </div>
  104. <div id="gdn">
  105. <a href="` + document.location.origin + '/uploader/' + glisting.uploader + '">' + glisting.uploader + `</a>
  106. </div>
  107. <div id="gdd">
  108. <table>
  109. <tbody>
  110. <tr><td class="gdt1">Posted:</td><td class="gdt2">` + d.getUTCFullYear().toString() + '-' + (d.getUTCMonth() + 1).toString().padStart(2, '0') + '-' + d.getUTCDate().toString().padStart(2, '0') + ' ' + d.getUTCHours().toString().padStart(2, '0') + ':' + d.getUTCMinutes().toString().padStart(2, '0') + `</td></tr>
  111. <tr><td class="gdt1">Visible:</td><td class="gdt2">` + (glisting.expunged ? 'No' : 'Yes') + `</td></tr>
  112. <tr><td class="gdt1">Language:</td><td class="gdt2">` + (translated ? language + `&nbsp <span class="halp" title="This gallery has been translated from the original language text.">TR</span>` : language) + `</td></tr>
  113. <tr><td class="gdt1">File Size:</td><td class="gdt2">` + getPrettyFileSize(glisting.filesize) + `</td></tr>
  114. <tr><td class="gdt1">Length:</td><td class="gdt2">` + glisting.filecount + ` pages</td></tr>
  115. <tr><td class="gdt1">Removal:</td><td class="gdt2">` + claim + `</td></tr>
  116. </tbody>
  117. </table>
  118. </div>
  119. <div id="gdr">
  120. <table>
  121. <tbody>
  122. <tr>
  123. <td class="grt1">Rating:</td>
  124. <td class="grt2">
  125. <div id="rating_image" class="ir" style="background-position:` + getStarNumber(glisting.rating, true) + `"></div>
  126. </td>
  127. </tr>
  128. <tr><td id="rating_label" colspan="2">Average: ` + glisting.rating + `</td></tr>
  129. </tbody>
  130. </table>
  131. </div>
  132. </div>
  133. <div id="gd4">
  134. <div id="taglist">
  135. </div>
  136. </div>
  137. <div id="gd5">
  138. <p class="g3 gsp">
  139. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  140. <a href="">Gallery Unavailable</a>
  141. </p>
  142. <p class="g2 gsp">
  143. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  144. <a id="dl_eze" href="#">EZE JSON</a>
  145. </p>
  146. <p class="g2">
  147. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  148. <a id="dl_ehdl">E-HDL JSON</a>
  149. </p>
  150. <p class="g2">
  151. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  152. <a id="dl_gdl">Gallery-DL JSON</a>
  153. </p>
  154. </div>
  155. <div class="c"></div>
  156. </div>
  157. <div class="c"></div>
  158. </div>
  159. `);
  160. $('body').append(listing);
  161. // Generate taglist table
  162. var taglist = "<table><tbody>";
  163. for (const namespace in tags) {
  164. taglist += `<tr><td class="tc">${namespace}:</td><td>`;
  165. for (var i = 0; i < tags[namespace].length; i++) {
  166. taglist += `<div id="td_${namespace}:${tags[namespace][i]}" class="gt" style="opacity:1.0"><a id="ta_${namespace}:${tags[namespace][i]}" href="${document.location.origin}/tag/${namespace}:${tags[namespace][i]}">${tags[namespace][i]}</a></div>`;
  167. }
  168. taglist += "</td></tr>";
  169. }
  170. taglist += "</tbody></table>";
  171. $('#taglist').append(taglist);
  172. // I want to make these smaller, but the 'this' call prevents me from doing so
  173. $('#dl_eze').on('click', function() {
  174. var json = JSON.stringify(toEze(glisting), null, " "),
  175. blob = new Blob([json], {type: "octet/stream"}),
  176. url = window.URL.createObjectURL(blob);
  177. this.href = url;
  178. this.target = '_blank';
  179. this.download = 'info.json';
  180. });
  181. $('#dl_ehdl').on('click', function() {
  182. var blob = new Blob([toEhDl(glisting)], {type: "text/plain"}),
  183. url = window.URL.createObjectURL(blob);
  184. this.href = url;
  185. this.target = '_blank';
  186. this.download = 'info.json';
  187. });
  188. $('#dl_gdl').on('click', function() {
  189. var json = JSON.stringify(toGalleryDl(glisting), null, " "),
  190. blob = new Blob([json], {type: "octet/stream"}),
  191. url = window.URL.createObjectURL(blob);
  192. this.href = url;
  193. this.target = '_blank';
  194. this.download = 'info.json';
  195. });
  196. // Try to generate torrent links
  197. if (glisting.torrentcount > 0) {
  198. $(`<div id="torrents" style="border-top:1px solid #000; padding: 10px 10px 5px 10px;"><p><span class="halp" title="If these links don't work, this gallery likely had a parent gallery">Possible Torrents:</span></p></div>`).appendTo('.gm');
  199. for (var j = 0; j < glisting.torrentcount; j++) {
  200. $(`<div><img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" /><a style="padding-left:3px;margin-right:1 5px;" href="` + window.location.origin + "/torrents/" + glisting.gid + "/" + glisting.torrents[j].hash + `.torrent">` + glisting.torrents[j].name + `</a>&#9;` + getPrettyFileSize(glisting.torrents[j].fsize) + `<span style="float:right">Added ` + getTimestampDateString(glisting.torrents[j].added * 1000) + `</span></p>`).appendTo('#torrents');
  201. }
  202. }
  203. }
  204.  
  205. function generateLiveListing(glisting) {
  206. generateTags(glisting);
  207. var listing = $(`
  208. <p class="g2">
  209. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  210. <a id="dl_eze" href="#">EZE JSON</a>
  211. </p>
  212. <p class="g2">
  213. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  214. <a id="dl_ehdl">E-HDL JSON</a>
  215. </p>
  216. <p class="g2">
  217. <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
  218. <a id="dl_gdl">Gallery-DL JSON</a>
  219. </p>`);
  220. $('#gd5').append(listing);
  221. $('#dl_eze').on('click', function() {
  222. var json = JSON.stringify(toEze(glisting), null, " "),
  223. blob = new Blob([json], {type: "octet/stream"}),
  224. url = window.URL.createObjectURL(blob);
  225. this.href = url;
  226. this.target = '_blank';
  227. this.download = 'info.json';
  228. });
  229. $('#dl_ehdl').on('click', function() {
  230. var blob = new Blob([toEhDl(glisting)], {type: "text/plain"}),
  231. url = window.URL.createObjectURL(blob);
  232. this.href = url;
  233. this.target = '_blank';
  234. this.download = 'info.json';
  235. });
  236. $('#dl_gdl').on('click', function() {
  237. var json = JSON.stringify(toGalleryDl(glisting), null, " "),
  238. blob = new Blob([json], {type: "octet/stream"}),
  239. url = window.URL.createObjectURL(blob);
  240. this.href = url;
  241. this.target = '_blank';
  242. this.download = 'info.json';
  243. });
  244. }
  245.  
  246. function generateTags(glisting) {
  247. if (Array.isArray(glisting.tags)) {
  248. for (const jsonTag of glisting.tags) {
  249. const stringTag = getJsonString(jsonTag);
  250. if (stringTag === null) { continue; }
  251.  
  252. const {tag, namespace} = getTagAndNamespace(stringTag);
  253.  
  254. let namespaceTags;
  255. if (tags.hasOwnProperty(namespace)) {
  256. namespaceTags = tags[namespace];
  257. } else {
  258. namespaceTags = [];
  259. tags[namespace] = namespaceTags;
  260. }
  261.  
  262. namespaceTags.push(tag);
  263. }
  264. }
  265.  
  266. // Tag-based info
  267. if (tags.hasOwnProperty("language")) {
  268. const languageTags = tags.language;
  269. const translatedIndex = languageTags.indexOf("translated");
  270. translated = (translatedIndex >= 0);
  271. if (translatedIndex !== 0) {
  272. language = toProperCase(languageTags[0]);
  273. }
  274. } else {
  275. language = "Japanese";
  276. translated = false;
  277. }
  278. }
  279.  
  280. function toEhDl(info) { // EH-DL
  281. var d = new Date(info.posted * 1000);
  282. var d2 = new Date();
  283. var output = info.title + "\r\n" + info.title_jpn + "\r\n" + document.URL + "\r\n\r\nCategory: " + info.category + "\r\nUploader: " + info.uploader + "\r\nPosted: " + d.getUTCFullYear().toString() + '-' + (d.getUTCMonth() + 1).toString().padStart(2, '0') + '-' + d.getUTCDate().toString().padStart(2, '0') + ' ' + d.getUTCHours().toString().padStart(2, '0') + ':' + d.getUTCMinutes().toString().padStart(2, '0') +
  284. "\r\nParent: " + (spl[3] == 'g' ? $('.gdt2 > a').text() : "None" ) + "\r\nVisible: " + (info.expunged ? 'No' : 'Yes') + "\r\nLanguage: " + language + "\r\nFile Size: " + getPrettyFileSize(info.filesize) + "\r\nLength: " + info.filecount + " pages\r\nFavorited: " + (spl[3] == 'g' ? $('#rating_count').text() : "Null" ) + "\r\nRating: " + info.rating + "\r\n\r\nTags:\r\n";
  285. for (const namespace in tags) {
  286. output += `> ${namespace}: `;
  287. for (var i = 0; i < tags[namespace].length; i++) {
  288. output += `${tags[namespace][i]}`;
  289. if (i < tags[namespace].length - 1) output += ", ";
  290. }
  291. output += "\r\n";
  292. }
  293. output += `\r\n\r\n\r\nDownloaded on ${d2.toUTCString()}`;
  294. return output;
  295. }
  296.  
  297. function toEze(info) { // EZE
  298. const date = new Date(toNumberOrDefault(info.posted * 1000, 0));
  299. return {
  300. gallery_info: {
  301. title: toStringOrDefault(info.title, ""),
  302. title_original: toStringOrDefault(info.title_jpn, ""),
  303. uploader: toStringOrDefault(info.uploader, ""),
  304.  
  305. category: toStringOrDefault(info.category, ""),
  306. tags: tagsToCommonJson(tags),
  307.  
  308. language: toStringOrDefault(language, ""),
  309. translated: !!translated,
  310.  
  311. upload_date: [
  312. date.getUTCFullYear(),
  313. date.getUTCMonth() + 1,
  314. date.getUTCDate(),
  315. date.getUTCHours(),
  316. date.getUTCMinutes(),
  317. date.getUTCSeconds()
  318. ],
  319.  
  320. source: {
  321. site: toStringOrDefault(document.location.host.substr(0, document.location.host.length - 4), ""),
  322. gid: (info.identifier !== null ? toNumberOrDefault(info.gid, 0) : 0),
  323. token: (info.identifier !== null ? toStringOrDefault(info.token, 0) : 0),
  324. parent_gallery: toParentOrDefault(),
  325. newer_version: []
  326. }
  327. }
  328. };
  329. }
  330.  
  331. function toGalleryDl(info) { // Gallery-DL
  332. const date = new Date(toNumberOrDefault(info.posted * 1000, 0));
  333. return {
  334. category: toStringOrDefault(document.location.host.substr(0, document.location.host.length - 4), ""),
  335. cost: null,
  336. count: info.filecount,
  337. date: toStringOrDefault(getTimestampDateString(date), ""),
  338. extension: null,
  339. filename: null,
  340. gallery_id: toNumberOrDefault(info.gid, 0),
  341. gallery_size: toNumberOrDefault(info.filesize, 0),
  342. gallery_token: toStringOrDefault(info.token, 0),
  343. height: null,
  344. image_token: null,
  345. lang: null,
  346. language: toStringOrDefault(language, ""),
  347. num: 1,
  348. parent: toParentOrDefault(),
  349. size: toNumberOrDefault(info.filesize, 0),
  350. subcategory: 'gallery',
  351. tags: info.tags,
  352. title: toStringOrDefault(info.title, ""),
  353. title_jp: toStringOrDefault(info.title_jpn, ""),
  354. uploader: toStringOrDefault(info.uploader, ""),
  355. visible: info.expunged ? 'No' : 'Yes',
  356. width: null
  357. };
  358. }
  359.  
  360. /**
  361. * Convert the star count of a specified element to a double
  362. * @param {Object} el - A specific element within the DOM, or a double
  363. * @param {Boolean} transpose - Whether we're converting background position to double, or double to background position
  364. */
  365. function getStarNumber(el, transpose) {
  366. var starCount = {5: '0px -1px', 4.5: '0px -21px', 4: '-16px -1px', 3.5: '-16px -21px', 3: '-32px -1px', 2.5: '-32px -21px', 2: '-48px -1px', 1.5: '-48px -21px', 1: '-64px -1px', 0.5: '-64px -21px'};
  367. if (!transpose) {
  368. var stars = $(el).find('.ir').css('background-position');
  369. return Object.keys(starCount).find(key => starCount[key] === stars);
  370. } else return starCount[(Math.round(el * 2) / 2).toFixed(1)]; // Ratings are given in x.xx numbers, but we need either whole integers, or half integers
  371. }
  372.  
  373. /** ------ Helper functions cannibalized from the dnsev's script ------ */
  374. function getTagAndNamespace(tag) {
  375. const pattern = /^(?:([^:]*):)?([\w\W]*)$/;
  376. const match = pattern.exec(tag);
  377. return (match !== null) ?
  378. ({ tag: match[2], namespace: match[1] || defaultNamespace }) :
  379. ({ tag: tag, namespace: defaultNamespace });
  380. }
  381.  
  382. function getJsonString(value) {
  383. if (typeof(value) === "string") { return value; }
  384. if (typeof(value) === "undefined" || value === null) { return value; }
  385. return `${value}`;
  386. }
  387.  
  388. function toProperCase(text) {
  389. return text.replace(/(^|\W)(\w)/g, (m0, m1, m2) => `${m1}${m2.toUpperCase()}`);
  390. }
  391.  
  392. function getPrettyFileSize(bytes) {
  393. const ii = fileSizeLabels.length - 1;
  394. let i = 0;
  395. while (i < ii && bytes >= 1024) {
  396. bytes /= 1024;
  397. ++i;
  398. }
  399. return `${bytes.toFixed(i === 0 ? 0 : 2)} ${fileSizeLabels[i]}`;
  400. }
  401.  
  402. function getTimestampDateString(timestamp) {
  403. const date = new Date(timestamp);
  404. const year = date.getUTCFullYear().toString();
  405. const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
  406. const day = date.getUTCDate().toString().padStart(2, "0");
  407. const hour = date.getUTCHours().toString().padStart(2, "0");
  408. const minute = date.getUTCMinutes().toString().padStart(2, "0");
  409. const seconds = date.getUTCSeconds().toString().padStart(2, "0");
  410. return `${year}-${month}-${day} ${hour}:${minute}:${seconds}`;
  411. }
  412.  
  413. function toStringOrDefault(value, defaultValue) {
  414. return typeof(value) === "string" ? value : defaultValue;
  415. }
  416.  
  417. function toNumberOrDefault(value, defaultValue) {
  418. return Number.isNaN(value) ? defaultValue : value;
  419. }
  420.  
  421. function toParentOrDefault() {
  422. if (dead) return null;
  423. if ($('.gdt2 > a').length) return {gid: $('.gdt2 > a').attr('href').split('/')[4], token: $('.gdt2 > a').attr('href').split('/')[5]};
  424. else return null
  425. }
  426.  
  427. function tagsToCommonJson(tags) {
  428. const result = {};
  429. for (const namespace in tags) {
  430. if (!Object.prototype.hasOwnProperty.call(tags, namespace)) { continue; }
  431. const tagList = tags[namespace];
  432. result[namespace] = [...tagList];
  433. }
  434. return result;
  435. }
  436. /** ------------ */
  437. })();