ExResurrect

Resurrect E/Ex gallery listings

À partir de 2020-09-13. Voir la dernière version.

// ==UserScript==
// @name         ExResurrect
// @namespace    https://sleazyfork.org/en/users/285675-hauffen
// @version      1.11
// @description  Resurrect E/Ex gallery listings
// @author       Hauffen
// @runat        document-start
// @require      https://code.jquery.com/jquery-3.3.1.min.js
// @include      /https?:\/\/(e-|ex)hentai\.org\/.*/
// ==/UserScript==

(function() {
    let $ = window.jQuery;
    var spl = document.URL.split('/');
    const category = {doujinshi: 'ct2', manga: 'ct3', artistcg: 'ct4', gamecg: 'ct5', western: 'cta', nonh: 'ct9', imageset: 'ct6', cosplay: 'ct7', asianporn: 'ct8', misc: 'ct1'};
    const fileSizeLabels = [ "B", "KB", "MB", "GB" ];
    const defaultNamespace = "misc";

    function gotonext () {
        // Override the default function to prevent redirect
    }

    addJS_Node (gotonext); // Inject the override function before doing anything to avoid the timeout redirect

    function addJS_Node(text, s_URL, funcToRun, runOnLoad) {
        var scriptNode = document.createElement('script');
        if (runOnLoad) {
            scriptNode.addEventListener("load", runOnLoad, false);
        }
        scriptNode.type = "text/javascript";
        if (text) scriptNode.textContent = text;
        if (s_URL) scriptNode.src = s_URL;
        if (funcToRun) scriptNode.textContent = '(' + funcToRun.toString() + ')()';

        var targ = document.getElementsByTagName('head')[0] || document.body || document.documentElement;
        targ.appendChild(scriptNode);
    }
    // There's a better critera for this probably
    if (!$('.d').length) return; // Quit out of the function if the gallery is available
    $('.d').remove(); // Leave us with an entirely blank page to build up
    generateRequest();

    /**
     * Convert the star count of a specified element to a double
	 * @param {Object} el - A specific element within the DOM, or a double
     * @param {Boolean} transpose - Whether we're converting background position to double, or double to background position
	 */
    function getStarNumber(el, transpose) {
		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'};
		if (!transpose) {
			var stars = $(el).find('.ir').css('background-position');
			return Object.keys(starCount).find(key => starCount[key] === stars);
		} 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
    }

    /** ------ Helper functions cannibalized from the dnsev's script ------ */
    function getTagAndNamespace(tag) {
	const pattern = /^(?:([^:]*):)?([\w\W]*)$/;
	const match = pattern.exec(tag);
	return (match !== null) ?
		({ tag: match[2], namespace: match[1] || defaultNamespace }) :
		({ tag: tag, namespace: defaultNamespace });
    }

    function getJsonString(value) {
	    if (typeof(value) === "string") { return value; }
	    if (typeof(value) === "undefined" || value === null) { return value; }
	    return `${value}`;
    }

    function toProperCase(text) {
	    return text.replace(/(^|\W)(\w)/g, (m0, m1, m2) => `${m1}${m2.toUpperCase()}`);
    }

    function getPrettyFileSize(bytes) {
	    const ii = fileSizeLabels.length - 1;
	    let i = 0;
	    while (i < ii && bytes >= 1024) {
		    bytes /= 1024;
		    ++i;
	    }
	    return `${bytes.toFixed(i === 0 ? 0 : 2)} ${fileSizeLabels[i]}`;
    }
    /** ------------ */

    /**
     * Generate the JSON request for the E-H API
     */
    function generateRequest() {
        var reqList = []; // We use an array for our gidlist, since the API can handle up to 25 galleries per request
        reqList[0] = [spl[4], spl[5]];
        var request = {"method": "gdata", "gidlist": reqList, "namespace": 1};

        var req = new XMLHttpRequest();
        req.onreadystatechange = e => {
            if (req.readyState == 4) {
				if (req.status == 200) {
					var apirsp = JSON.parse(req.responseText);
					for (var i = 0; i < apirsp.gmetadata.length; i++) generateListing(apirsp.gmetadata[i]);
				} else {
					console.error();
				}
			}
        }
        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
        req.send(JSON.stringify(request));
    }

    function generateListing(glisting) {
        var d = new Date(glisting.posted * 1000);
        var language, translated;
        document.title = glisting.title;
        /** ------ Code blocks cannibalized from dnsev's code ------ */
        const tags = {};
	    if (Array.isArray(glisting.tags)) {
		    for (const jsonTag of glisting.tags) {
			    const stringTag = getJsonString(jsonTag);
			    if (stringTag === null) { continue; }

			    const {tag, namespace} = getTagAndNamespace(stringTag);

			    let namespaceTags;
			    if (tags.hasOwnProperty(namespace)) {
				    namespaceTags = tags[namespace];
			    } else {
				    namespaceTags = [];
				    tags[namespace] = namespaceTags;
			    }

			    namespaceTags.push(tag);
		    }
	    }

	    // Tag-based info
	    if (tags.hasOwnProperty("language")) {
		    const languageTags = tags.language;
		    const translatedIndex = languageTags.indexOf("translated");
		    translated = (translatedIndex >= 0);
		    if (translatedIndex !== 0) {
			    language = toProperCase(languageTags[0]);
		    }
	    } else {
		    language = "Japanese";
		    translated = false;
	    }
        /** ------------ */
        // There's a better way to do this, but I suck
        var listing = $(`
        <div id="nb" class="nose1">
            <div><a href="` + document.location.origin + `">Front<span class="nbw1"> Page</span></a></div>
            <div><a href="` + document.location.origin + `/watched">Watched</a></div>
            <div><a href="` + document.location.origin + `/popular">Popular</a></div>
            <div><a href="` + document.location.origin + `/torrents.php">Torrents</a></div>
            <div><a href="` + document.location.origin + `/favorites.php">Fav<span class="nbw1">orite</span>s</a></div>
            <div><a href="` + document.location.origin + `/uconfig.php">Settings</a></div>
            <div><a href="` + document.location.origin + `/upload/manage.php"><span class="nbw2">My </span>Uploads</a></div>
            <div><a href="` + document.location.origin + `/mytags">My Tags</a></div>
        </div>
        <div class="gm">
            <div id="gleft">
                <div id="gd1">
                    <div style="width:250px; height:354px; background:transparent url(` + glisting.thumb.substring(0, glisting.thumb.length-5) + `250.jpg) 0 0 no-repeat"></div>
                </div>
            </div>
            <div id="gd2">
                <h1 id="gn">` + glisting.title + `</h1>
                <h1 id="gj">` + glisting.title_jpn + `</h1>
            </div>
            <div id="gmid">
                <div id="gd3">
                    <div id="gdc">
                        <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>
                    </div>
                    <div id="gdn">
                        <a href="` + document.location.origin + '/uploader/' + glisting.uploader + '">' + glisting.uploader + `</a>
                    </div>
                    <div id="gdd">
                        <table>
                            <tbody>
                                <tr><td class="gdt1">Posted:</td><td class="gdt2">` + d.getFullYear().toString() + '-' + (d.getMonth() + 1).toString().padStart(2, '0') + '-' + d.getDate().toString().padStart(2, '0') + ' ' + d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0') + `</td></tr>
                                <tr><td class="gdt1">Visible:</td><td class="gdt2">` + (glisting.expunged ? 'No' : 'Yes') + `</td></tr>
                                <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>
                                <tr><td class="gdt1">File Size:</td><td class="gdt2">` + getPrettyFileSize(glisting.filesize) + `</td></tr>
                                <tr><td class="gdt1">Length:</td><td class="gdt2">` + glisting.filecount + ` pages</td></tr>
                            </tbody>
                        </table>
                    </div>
                    <div id="gdr">
                        <table>
                            <tbody>
                                <tr>
                                    <td class="grt1">Rating:</td>
                                    <td class="grt2">
                                        <div id="rating_image" class="ir" style="background-position:` + getStarNumber(glisting.rating, true) + `"></div>
                                    </td>
                                </tr>
                                <tr><td id="rating_label" colspan="2">Average: ` + glisting.rating + `</td></tr>
                            </tbody>
                        </table>
                    </div>
                </div>
                <div id="gd4">
                    <div id="taglist">
                    </div>
                </div>
                <div id="gd5">
                    <p class="g3 gsp">
                        <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
                        <a href="">Gallery Unavailable</a>
                    </p>
                    <p class="g2 gsp">
                        <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
                        <a id="dl_eze" href="#">EZE JSON</a>
                    </p>
                    <p class="g2">
                        <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
                        <a id="dl_ehdl">E-HDL JSON</a>
                    </p>
                    <p class="g2">
                        <img src="` + (window.location.hostname.indexOf("exhentai") >= 0 ? "https://exhentai.org/img/mr.gif" : "https://ehgt.org/g/mr.gif") + `" />
                        <a id="dl_gdl">Gallery-DL JSON</a>
                    </p>
                </div>
                <div class="c"></div>
            </div>
            <div class="c"></div>
        </div>
        `);
        $('body').append(listing);
        // Generate taglist table
        var taglist = "<table><tbody>";
        for (const namespace in tags) {
            taglist += `<tr><td class="tc">${namespace}:</td><td>`;
            for (var i = 0; i < tags[namespace].length; i++) {
                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>`;
            }
            taglist += "</td></tr>";
        }
        taglist += "</tbody></table>";
        $('#taglist').append(taglist);
        $('#dl_eze').on('click', function() {
            var json = JSON.stringify(toCommonJson(glisting, tags, language, translated), null, "  "),
                blob = new Blob([json], {type: "octet/stream"}),
                url = window.URL.createObjectURL(blob);
            this.href = url;
            this.target = '_blank';
            this.download = 'info.json';
        });
        $('#dl_ehdl').on('click', function() {
            var blob = new Blob([buildEhdl(glisting, tags, language)], {type: "text/plain"}),
                url = window.URL.createObjectURL(blob);
            this.href = url;
            this.target = '_blank';
            this.download = 'info.json';
        });
        $('#dl_gdl').on('click', function() {
            var json = JSON.stringify(toCommonGalleryInfoJsonG(glisting, language), null, "  "),
                blob = new Blob([json], {type: "octet/stream"}),
                url = window.URL.createObjectURL(blob);
            this.href = url;
            this.target = '_blank';
            this.download = 'info.json';
        });
    }

    function buildEhdl(info, tags, language) {
        var d = new Date(info.posted * 1000);
        var d2 = new Date();
        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.getFullYear().toString() + '-' + (d.getMonth() + 1).toString().padStart(2, '0') + '-' + d.getDate().toString().padStart(2, '0') + ' ' + d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0') +
            "\r\nParent: Null\r\nVisible: " + (info.expunged ? 'No' : 'Yes') + "\r\nLanguage: " + language + "\r\nFile Size: " + getPrettyFileSize(info.filesize) + "\r\nLength: " + info.filecount + " pages\r\nFavorited: Null\r\nRating: " + info.rating + "\r\n\r\nTags:\r\n";
        for (const namespace in tags) {
            output += `> ${namespace}: `;
            for (var i = 0; i < tags[namespace].length; i++) {
                output += `${tags[namespace][i]}`;
                if (i < tags[namespace].length - 1) output += ", ";
            }
            output += "\r\n";
        }
        output += `\r\n\r\n\r\nDownloaded on ${d2.toUTCString()}`;
        return output;
    }

    /** ------ Functions cannabilized from dnsev's code ------ */
    function toCommonGalleryInfoJson(info, tags, language, translated) {
	    const date = new Date(toNumberOrDefault(info.posted * 1000, 0));
	    return {
		    title: toStringOrDefault(info.title, ""),
		    title_original: toStringOrDefault(info.title_jpn, ""),
            uploader: toStringOrDefault(info.uploader, ""),

		    category: toStringOrDefault(info.category, ""),
		    tags: tagsToCommonJson(tags),

		    language: toStringOrDefault(language, ""),
		    translated: !!translated,

		    upload_date: [
			    date.getUTCFullYear(),
			    date.getUTCMonth() + 1,
			    date.getUTCDate(),
			    date.getUTCHours(),
			    date.getUTCMinutes(),
			    date.getUTCSeconds()
		    ],

		    source: {
			    site: toStringOrDefault(document.location.host.substr(0, document.location.host.length - 4), ""),
			    gid: (info.identifier !== null ? toNumberOrDefault(info.gid, 0) : 0),
			    token: (info.identifier !== null ? toStringOrDefault(info.token, 0) : 0),
                parent_gallery: null,
                newer_version: []
		    }
	    };
    }

    function toCommonGalleryInfoJsonG(info, language) {
	    const date = new Date(toNumberOrDefault(info.posted * 1000, 0));
	    return {
            category: toStringOrDefault(document.location.host.substr(0, document.location.host.length - 4), ""),
            cost: null,
            count: info.filecount,
		    date: toStringOrDefault(getTimestampDateString(date), ""),
            extension: null,
            filename: null,
		    gallery_id: toNumberOrDefault(info.gid, 0),
            gallery_size: toNumberOrDefault(info.filesize, 0),
            gallery_token: toStringOrDefault(info.token, 0),
            height: null,
            image_token: null,
            lang: null,
            language: toStringOrDefault(language, ""),
            num: 1,
            parent: null,
            size: toNumberOrDefault(info.filesize, 0),
		    subcategory: 'gallery',
		    tags: info.tags,
            title: toStringOrDefault(info.title, ""),
		    title_jp: toStringOrDefault(info.title_jpn, ""),
            uploader: toStringOrDefault(info.uploader, ""),
		    visible: info.expunged ? 'No' : 'Yes',
            width: null
	    };
    }

    function getTimestampDateString(timestamp) {
	    const date = new Date(timestamp);
	    const year = date.getFullYear().toString();
	    const month = (date.getMonth() + 1).toString().padStart(2, "0");
	    const day = date.getDate().toString().padStart(2, "0");
	    const hour = date.getHours().toString().padStart(2, "0");
	    const minute = date.getMinutes().toString().padStart(2, "0");
	    return `${year}-${month}-${day} ${hour}:${minute}:00`;
    }

    function toStringOrDefault(value, defaultValue) {
	    return typeof(value) === "string" ? value : defaultValue;
    }

    function toNumberOrDefault(value, defaultValue) {
	    return Number.isNaN(value) ? defaultValue : value;
    }

    function tagsToCommonJson(tags) {
	    const result = {};
		    for (const namespace in tags) {
			    if (!Object.prototype.hasOwnProperty.call(tags, namespace)) { continue; }
			    const tagList = tags[namespace];
			    result[namespace] = [...tagList];
		    }
	    return result;
    }

    function toCommonJson(info, tags, language, translated) {
	    return {
		    gallery_info: toCommonGalleryInfoJson(info, tags, language, translated)
	    };
    }
})();