Sleazy Fork is available in English.

General Favoriates Adder

General Favoriates Adder for pixiv.net or other websites

// ==UserScript==
// @name         General Favoriates Adder
// @namespace    https://greasyfork.org/zh-CN/scripts/424020-general-favoriates-adder
// @version      0.21.4.27
// @description  General Favoriates Adder for pixiv.net or other websites
// @author       MangoPomelo
// @include      /^https?://safebooru\.org/index\.php.*id=.*$/
// @include      /^https?://www\.pixiv\.net/artworks/.*$/
// @include      /^https?://hitomi\.la/(doujinshi|gamecg|cg|manga)/.*?\.html$/
// @include      /^https?://nozomi\.la/post/.*?\.html$/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    if (window != window.top){return undefined;} // iframe is not allowed

    let MODE = "PRODUCTION"; // "TUNNING" if want to tune the threshold and create new pattern, else use "PRODUCTION"
    let TEMPLATE = "{author}\t{URL}\t{character}\t{full_color}"; // placeholders must corresponding to the subjects in CONFIG
    let BUTTON = `
        <div class="btn-circle-extract">
            <svg id="extract" viewBox="0 0 1024 1024" width="24px" height="24px">
                <path d="M160.41 522.24l90.47-90.47 45.21 45.26-90.42 90.42a192 192 0 0 0 271.36 271.82l90.47-90.47 45.21 45.21-90.47 90.47a256 256 0 0 1-361.83-362.24z m678.86-45.21l-90.47 90.42 45.21 45.26 90.47-90.47a256 256 0 0 0-362.24-361.83l-90.47 90.47 45.26 45.21 90.42-90.42a192 192 0 0 1 271.82 271.36z" fill="#ffffff"></path><path d="M341.35 642.97a42.7 42.7 0 1 0 60.16 60.62l302.08-302.08a42.65 42.65 0 1 0-60.62-60.16L341.35 642.97z" fill="#ffffff"></path>
            </svg>
            <svg id="check" width="21px" height="15px" viewBox="13 17 21 15">
                <polyline points="32.5 18.5 20 31 14.5 25.5"></polyline>
            </svg>
        </div>
    `;
    let [MAIN_H, MAIN_S, MAIN_L] = [197, 85, 63]; // use HSL, not RGB
    let STYLE = `
        <style>
            /* Based on: https://dribbble.com/shots/4525196-Jelly-Download */
            .btn-circle-extract {
                position: fixed;
                bottom: 28px;
                left: 20px;
                height: 48px;
                width: 48px;
                margin: auto;
                border-radius: 100%;
                background: #FFF;
                opacity: 0.97;
                cursor: pointer;
                overflow: hidden;
                box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.16), 0px 1px 2px hsla(${MAIN_H}deg, ${MAIN_S}%, ${MAIN_L}%, 0.5);
                transition: all 0.5s ease;
                transition: bottom 1s ease;
                z-index: 999;
            }

            .btn-circle-extract::before {
                content: "";
                position: absolute;
                top: 2px;
                left: 2px;
                right: 2px;
                bottom: 2px;
                border-radius: 50%;
                background: radial-gradient(circle, hsl(${MAIN_H}deg ,${MAIN_S * .75}%, ${MAIN_L}%) 0%, hsl(${MAIN_H}deg, ${MAIN_S}%, ${MAIN_L}%) 60%, hsl(${MAIN_H-5}deg, ${MAIN_S * .5}%, ${MAIN_L-5}%) 63%, hsl(0deg, 0%, 100%) 65%);
            }

            .btn-circle-extract svg {
                stroke-width: 2;
                stroke-linecap: round;
                stroke-linejoin: round;
                fill: none;
            }

            .btn-circle-extract svg#extract {
                position: absolute;
                top: 12px;
                left: 12px;
            }

            .btn-circle-extract svg#check {
                position: absolute;
                top: 17px;
                left: 13px;
                stroke: white;
                transform: scale(0);
            }

            .btn-circle-extract.done {
                animation: rubberBand 0.8s;
            }

            .btn-circle-extract.done:after {
                transform: translateX(50%);
                transition: transform 0.4s ease;
                transition-delay: 0.7s;
            }

            .btn-circle-extract.done #extract {
                transform: scale(0);
                transition: all 0.2s ease;
            }

            .btn-circle-extract.done #check {
                transform: scale(1);
                transition: all 0.2s ease;
                transition-delay: 0.2s;
            }

            @keyframes rubberBand {
                from {
                    transform: scale(1, 1, 1);
                }

                30% {
                    transform: scale3d(1.15, 0.75, 1);
                }

                40% {
                    transform: scale3d(0.75, 1.15, 1);
                }

                50% {
                    transform: scale3d(1.1, 0.85, 1);
                }

                65% {
                    transform: scale3d(0.95, 1.05, 1);
                }

                75% {
                    transform: scale3d(1.05, 0.95, 1);
                }

                to {
                    transform: scale3d(1, 1, 1);
                }
            }
        </style>
    `;
    let CALLBACK = () => {
        document.querySelector(".btn-circle-extract").onclick = function() {
            document.querySelector(".btn-circle-extract").classList.add("done");
        };
    };
    let CONFIG = {
        "author": {
            "type": "is",
            "resultMap": res => res? res.replace(/@.+/, ""): "",
            "evaluations": [
                "#root > div:nth-child(2) > div.sc-1nr368f-0.kCKAFN > div > div.sc-1nr368f-3.iHKGIi > aside > section.sc-171jvz-1.sc-171jvz-3.sc-10r3j8-0.f30yhg-3.dfhJPe > h2 > div > div > a > div:nth-child(1)", // pixiv.net
                "#tag-sidebar > li.tag-type-artist.tag > a", // safebooru.org
                "body > div.container > div.content > div.gallery > h2", "body > div.container > div.content > div > div > table > tbody > tr:nth-child(1) > td:nth-child(2) > ul > li > a", // hitomi.la
                "body > div > div.sidebar > ul > li > a.artist", // nozomi.la
            ]
        },
        "URL": {
            "type": "is",
            "resultMap": x => window.location.href.replace(location.hash, "")
        },
        "character": {
            "type": "is",
            "ranker": {
                "threshold": 0,
                "patterns": [
                    {"reg": /^.*_.*$/, "weight": 5},
                    {"reg": /^.* .*$/, "weight": 3},
                    {"reg": /^.*\(.*\)$/, "weight": 15},
                    {"reg": /^.*・.*$/, "weight": 15},
                    {"reg": /^[ァ-ヴー]{4}$/u, "weight": 1},
                    {"reg": /^[ァ-ヴー]{2,3}$/u, "weight": 5},
                    {"reg": /^[ァ-ヴー]+$/u, "weight": 2},
                    {"reg": /^[一-龠ァ-ヴー]{2,5}$/u, "weight": 3},
                    {"reg": /^[一-龠]{3,4}$/u, "weight": 6},
                    {"reg": /^[一-龠]{1,3}[ァ-ヴーぁ-ゔ]{2,3}$/u, "weight": 5},
                ]
            },
            "evaluations": [
                "#root > div:nth-child(2) > div.sc-1nr368f-0.kCKAFN > div > div.sc-1nr368f-3.iHKGIi > main > section > div.sc-171jvz-0.ketmXG > div > figcaption > div.sc-1u8nu73-13.KzfRK > div > footer > ul > li > span > span:nth-child(1)", // pixiv.net
                "#tag-sidebar > li.tag-type-character.tag > a", // safebooru.org
                "body > div > div.content > div > div > table > tbody > tr:nth-child(5) > td:nth-child(2) > ul > li", // hitomi.la
                "body > div > div.sidebar > ul > li > a.character", // nozomi.la
            ]
        },
        "full_color": {
            "type": "has",
            "featureTags": ["Full Color"],
            "resultMap": res => (res || /.*(cg|post|artworks).*/.test(window.location.href))? "True": "False",
            "evaluations": [
                "#root > div:nth-child(2) > div.sc-1nr368f-0.kCKAFN > div > div.sc-1nr368f-3.iHKGIi > main > section > div.sc-171jvz-0.ketmXG > div > figcaption > div.sc-1u8nu73-13.KzfRK > div > footer > ul > li > span > span:nth-child(1)", // pixiv.net
                "#tag-sidebar > li.tag-type-general > a", // safebooru.org
                "body > div > div.content > div > div > table > tbody > tr > td:nth-child(2) > ul > li", // hitomi.la
                "body > div > div.sidebar > ul > li > a.general", // nozomi.la
            ]
        }
    };

    // Codes below

    class HasVerifier {
        constructor(subject) {
            this.dataLoaded = false;
            if (subject !== undefined) {
                // if subject exists
                this.setData(subject);
            }
        }
        setData(subject) {
            this.subject = subject;
            this.type = CONFIG[subject].type.toLowerCase();
            this.featureTags = CONFIG[subject].featureTags;
            this.resultMap = CONFIG[subject].resultMap? CONFIG[subject].resultMap: res => res? "true": "false";
            this.evaluations = CONFIG[subject].evaluations;

            if (this.type != 'has') {
                throw Error("<Class HasVerifier>: Only the type 'has' is accepted in HasVerifier, do you mean to use <Class IsVerifier>?")
            }

            this.dataLoaded = true;
            return this;
        }
        verify() {

            if (this.dataLoaded == false) {
                throw Error("<Class HasVerifier>: The configuration hasn't been loaded, use .setData(subj) to load the data")
            }

            let result = false;
            for (let selector of this.evaluations) {
                const candidates = document.querySelectorAll(selector);
                const tags =  [...candidates].map(elem => elem.innerText);
                const tagsToCheck = this.featureTags;
                for (let tag of tags) {
                    for (let tagToCheck of tagsToCheck) {
                        if (tag.includes(tagToCheck)) {
                            result = true;
                            return this.resultMap(result);
                        }
                    }
                }
            }
            return this.resultMap(result);
        }
    }

    class IsVerifier {
        constructor(subject) {
            this.dataLoaded = false;
            if (subject !== undefined) {
                // if subject exists
                this.setData(subject);
            }
        }
        setData(subject) {
            // TODO: use destruction
            this.subject = subject;
            this.type = CONFIG[subject].type.toLowerCase();
            this.resultMap = CONFIG[subject].resultMap? CONFIG[subject].resultMap: res => res? res: "";
            this.evaluations = CONFIG[subject].evaluations? CONFIG[subject].evaluations: [];
            this.patterns = CONFIG[subject].ranker? CONFIG[subject].ranker.patterns? CONFIG[subject].ranker.patterns: [{"reg": /.*/, "weight": 1}]: [{"reg": /.*/, "weight": 1}];
            this.threshold = CONFIG[subject].ranker? CONFIG[subject].ranker.threshold? CONFIG[subject].ranker.threshold: 0: 0;

            if (this.type != 'is') {
                throw Error("<Class IsVerifier>: Only the type 'is' is accepted in IsVerifier, do you mean to use <Class HasVerifier>?")
            }

            this.dataLoaded = true;
            return this;
        }
        verify() {

            if (this.dataLoaded == false) {
                throw Error("<Class IsVerifier>: The configuration hasn't been loaded, use .setData(subj) to load the data")
            }

            let globalCandidates = [];
            for (let selector of this.evaluations) {
                const candidates = document.querySelectorAll(selector);
                const tags = [...candidates].map(elem => elem.innerText);
                globalCandidates = [...globalCandidates, ...tags];
            }

            let highestScore = -1;
            let correspondingCandidate = "";
            for (let candidate of globalCandidates) {
                if (MODE == "TUNNING") {
                    console.log(`${candidate}:`);
                }
                let score = 0;
                for (let pattern of this.patterns) {
                    let reg = pattern.reg;
                    if (reg.test(candidate)) {
                        score += pattern.weight;
                        if (MODE == "TUNNING") {
                            console.log(`  ${reg} +${pattern.weight}`);
                        }
                    }
                }
                if (score > highestScore) {
                    highestScore = score;
                    correspondingCandidate = candidate;
                }
                if (MODE == "TUNNING") {
                    console.log(`  (${score}/${this.threshold})`);
                }
            }
            return this.resultMap(highestScore >= this.threshold? correspondingCandidate: null);
        }
    }

    class Verifier {
        constructor(subject) {
            this.innerHasVerifier = new HasVerifier();
            this.innerIsVerifier = new IsVerifier();
            this.currentVerifier = null;

            this.dataLoaded = false;
            if (subject !== undefined) {
                // if subject exists
                this.setData(subject);
            }
        }
        setData(subject) {
            const type = CONFIG[subject].type.toLowerCase();
            if (type == 'is') {
                this.currentVerifier = this.innerIsVerifier;
            } else if (type == 'has') {
                this.currentVerifier = this.innerHasVerifier;
            } else {
                throw Error('<Class Verifier>: Type must be either "is" or "has"');
            }
            this.dataLoaded = true;
            this.currentVerifier.setData(subject);
            return this;
        }
        verify() {

            if (this.dataLoaded == false) {
                throw Error("<Class Verifier>: The configuration hasn't been loaded, use .setData(subj) to load the data")
            }

            return this.currentVerifier.verify();
        }
    }

    class Formatter {
        fill(pairs) {
            let newString = `${TEMPLATE}`;
            for (let subject in pairs) {
                let key = subject;
                let value = pairs[subject];
                newString = newString.replace("{" + key + "}", value);
            }
            return newString;
        }
    }

    const copyToClipboard = str => {
        const el = document.createElement('textarea');
        el.value = str;
        el.setAttribute('readonly', '');
        el.style.position = 'absolute';
        el.style.left = '-9999px';
        document.body.appendChild(el);
        el.select();
        document.execCommand('copy');
        document.body.removeChild(el);
    };

    const extract = () => {
        let elem = document.querySelector(".btn-circle-extract");
        let v = new Verifier();
        let f = new Formatter();
        let pairs = {};
        for (let subject in CONFIG) {
            let value = v.setData(subject).verify();
            pairs[subject] = value;
        }
        let result = f.fill(pairs);
        copyToClipboard(result);

        setTimeout(()=>{
            elem.style.bottom = "-200px";
        }, 3000);
        setTimeout(()=>{
            elem.remove();
        }, 5000);
    };

    (()=>{
        document.querySelector('head').insertAdjacentHTML('afterbegin', STYLE);
        document.querySelector("body").insertAdjacentHTML('afterbegin', BUTTON);
        document.querySelector(".btn-circle-extract").addEventListener("click", extract);
        if (CALLBACK !== undefined) {
            CALLBACK();
        }
    })();

})();