VoiceLinks

Makes RJ codes more useful.(8-bit RJCode supported.)

2024-07-08 일자. 최신 버전을 확인하세요.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name        VoiceLinks
// @namespace   Sanya
// @description Makes RJ codes more useful.(8-bit RJCode supported.)
// @include     *://*/*
// @version     2.7.0
// @grant       GM.xmlHttpRequest
// @grant       GM_xmlhttpRequest
// @run-at      document-start
// @homepage    https://greasyfork.org/zh-CN/scripts/456775
// ==/UserScript==

(function () {
    'use strict';
    const RJ_REGEX = new RegExp("(R[JE][0-9]{8})|(R[JE][0-9]{6})|([VB]J[0-9]{8})|([VB]J[0-9]{6})", "gi");
    const RJ_REGEX_NEW = new RegExp("R[JE][0-9]{8}", "gi");
    const VBJ_REGEX = new RegExp("([VB]J[0-9]{6})|([VB]J[0-9]{8})", "gi");
    const VOICELINK_CLASS = 'voicelink';
    const RJCODE_ATTRIBUTE = 'rjcode';
    const css = `
      .voicepopup {
          min-width: 600px !important;
          z-index: 2147483646 !important;
          max-width: 80% !important;
          position: fixed !important;
          line-height: 1.4em;
          font-size:1.1em!important;
          margin-bottom: 10px;
          box-shadow: 0 0 .125em 0 rgba(0,0,0,.5);
          border-radius: 0.5em;
          background-color:#8080C0;
          color:#F6F6F6;
          text-align: left;
          padding: 10px;
          pointer-events: none;
      }
      
      .voicepopup-maniax{
          background-color:#8080C0;
      }
      
      .voicepopup-girls{
          background-color:#B33761;
      }

      .voicepopup img {
          width: 270px;
          height: auto;
          margin: 3px 15px 3px 3px;
          max-width: fit-content;
      }
      
      .voicepopup a {
          text-decoration: none;
          color: pink;
      }
      
      .voicepopup .age-18{
          color: hsl(300deg 76% 77%);
      }
      
      .voicepopup .age-all{
          color: hsl(157deg 82% 52%);
      }

      .voice-title {
          font-size: 1.4em;
          font-weight: bold;
          text-align: center;
          margin: 5px 10px 0 0;
          display: block;
      }

      .rjcode {
          text-align: center;
          font-size: 1.2em;
          font-style: italic;
          opacity: 0.3;
      }

      .error {
          height: 210px;
          line-height: 210px;
          text-align: center;
      }

      .discord-dark {
          background-color: #36393f;
          color: #dcddde;
          font-size: 0.9375rem;
      }
      
      .${VOICELINK_CLASS}_work_title:hover #${VOICELINK_CLASS}_copy_btn {
          opacity: 1;
      }
      
      #${VOICELINK_CLASS}_copy_btn {
          background: transparent;
          border-color: transparent;
          cursor: pointer;
          transition: all 0.3s;
          opacity: 0;
          font-size: 0.75em;
          user-select: none;
          position: absolute;
      }
      
      #${VOICELINK_CLASS}_copy_btn:hover {
          scale: 1.2;
      }
      
      #${VOICELINK_CLASS}_copy_btn:active {
          scale: 1.1;
      }
  `

    /**
     * Work promise cache
     * @type {{info:{}, api:{}}}
     */
    const work_promise = {};

    function getAdditionalPopupClasses() {
        const hostname = document.location.hostname;
        switch (hostname) {
            case "boards.4chan.org": return "post reply";
            case "discordapp.com": return "discord-dark";
            default: return null;
        }
    }

    /**
     * Convert to valid file name.
     * @param {String} original
     */
    function convertToValidFileName(original){
        const charMap = {
            "/": "/",
            "\\": "\",
            ":": ":",
            "*": "*",
            "?": "?",
            "\"": """,
            "<": "<",
            ">": ">",
            "|": "|"
        }

        let fileName = original;
        for (let key in charMap){
            fileName = fileName.replaceAll(key, charMap[key]);
        }
        return fileName;
    }

    function setUserSelectTitle(){
        // Make title selectable
        const hostname = document.location.hostname;
        if(!hostname.endsWith("dlsite.com")){
            return;
        }

        const title = document.getElementById("work_name");
        if(!title){
            return;
        }

        const button = document.createElement("button");
        const titleStr = title.innerText;
        button.id = `${VOICELINK_CLASS}_copy_btn`;
        button.innerHTML = "📃";
        button.addEventListener("mouseenter", function(){
            button.innerHTML = "📃 复制为有效文件名";
        });
        button.addEventListener("mouseleave", function(){
            button.innerHTML = "📃";
        });
        button.addEventListener("click", function(){
            const fileName = convertToValidFileName(titleStr);
            const promise = navigator.clipboard.writeText(fileName);
            promise.then(() => {
                button.innerHTML = "✔ 复制成功";
            });
            promise.catch(e => {
                window.prompt("复制失败,请手动复制", fileName);
                button.innerHTML = "📃";
            });
        });

        title.style.userSelect = "text";
        title.classList.add(`${VOICELINK_CLASS}_work_title`);
        title.appendChild(button);
    }

    function getXmlHttpRequest() {
        return (typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : GM_xmlhttpRequest);
    }

    const Parser = {
        walkNodes: function (elem) {
            const rjNodeTreeWalker = document.createTreeWalker(
                elem,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: function (node) {
                        if (node.parentElement.classList.contains(VOICELINK_CLASS))
                            return NodeFilter.FILTER_ACCEPT;
                        if (node.nodeValue.match(RJ_REGEX_NEW))
                            return NodeFilter.FILTER_ACCEPT;
                        if (node.nodeValue.match(RJ_REGEX))
                            return NodeFilter.FILTER_ACCEPT;
                        if (node.nodeValue.match(VBJ_REGEX))
                            return NodeFilter.FILTER_ACCEPT;
                    }
                },
                false,
            );
            while (rjNodeTreeWalker.nextNode()) {
                const node = rjNodeTreeWalker.currentNode;

                //Ignore Element which let user input (textarea), input can be ignored because it's not a text node.
                if(node.parentElement.nodeName === "TEXTAREA"){
                    continue;
                }

                if (node.parentElement.classList.contains(VOICELINK_CLASS))
                    Parser.rebindEvents(node.parentElement);
                else
                    Parser.linkify(node);
            }
        },

        wrapRJCode: function (rjCode) {
            let e;
            e = document.createElement("a");
            e.classList = VOICELINK_CLASS;
            e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode.toUpperCase()}.html`
            e.innerHTML = rjCode;
            e.target = "_blank";
            e.rel = "noreferrer";
            e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase());
            e.addEventListener("mouseover", Popup.over);
            e.addEventListener("mouseout", Popup.out);
            e.addEventListener("mousemove", Popup.move);
            return e;
        },

        linkify: function (textNode) {
            const nodeOriginalText = textNode.nodeValue;
            const matches = [];

            let match;
            while (match = RJ_REGEX.exec(nodeOriginalText)) {
                matches.push({
                    index: match.index,
                    value: match[0],
                });
            }

            // Keep text in text node until first RJ code
            textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index);

            // Insert rest of text while linkifying RJ codes
            let prevNode = null;
            for (let i = 0; i < matches.length; ++i) {
                // Insert linkified RJ code
                let code = matches[i].value
                const rjLinkNode = Parser.wrapRJCode(code);
                textNode.parentNode.insertBefore(
                    rjLinkNode,
                    prevNode ? prevNode.nextSibling : textNode.nextSibling,
                );

                // Insert text after if there is any
                let upper;
                if (i === matches.length - 1)
                    upper = undefined;
                else
                    upper = matches[i + 1].index;
                let substring;
                if (substring = nodeOriginalText.substring(matches[i].index + matches[i].value.length, upper)) {
                    const subtextNode = document.createTextNode(substring);
                    textNode.parentNode.insertBefore(
                        subtextNode,
                        rjLinkNode.nextElementSibling,
                    );
                    prevNode = subtextNode;
                }
                else {
                    prevNode = rjLinkNode;
                }
            }
        },

        rebindEvents: function (elem) {
            if (elem.nodeName === "A") {
                elem.addEventListener("mouseover", Popup.over);
                elem.addEventListener("mouseout", Popup.out);
                elem.addEventListener("mousemove", Popup.move);
            }
            else {
                const voicelinks = elem.querySelectorAll("." + VOICELINK_CLASS);
                for (let i = 0, j = voicelinks.length; i < j; i++) {
                    const voicelink = voicelinks[i];
                    voicelink.addEventListener("mouseover", Popup.over);
                    voicelink.addEventListener("mouseout", Popup.out);
                    voicelink.addEventListener("mousemove", Popup.move);
                }
            }
        },

    }

    const Popup = {
        makePopup: function (e, rjCode) {
            const popup = document.createElement("div");
            popup.className = "voicepopup voicepopup-maniax " + (getAdditionalPopupClasses() || '');
            popup.id = "voice-" + rjCode;
            popup.style = "display: flex";
            document.body.appendChild(popup);

            let workFound = true;
            WorkPromise.getFound(rjCode).then(found => {
                if(!found) {
                    popup.innerHTML = "Work Not Found.";
                    workFound = false;
                }
            })

            WorkPromise.getGirls(rjCode).then(isGirls => {
                if(isGirls) popup.className += (" voicepopup-girls")
            });

            const imgContainer = document.createElement("div")
            const img = document.createElement("img");
            imgContainer.appendChild(img);
            WorkPromise.getImgLink(rjCode).then(link => {
                img.src = link;
            });

            /*let html = `
                      <div>
                          <div class='voice-title'>${workInfo.title}</div>
                          <div class='rjcode'>[${workInfo.rj}]</div>
                          <br />
                          Circle: <a>${workInfo.circle}</a>
                          <br />
                  `;*/

            const infoContainer = document.createElement("div");

            const titleElement = document.createElement("div");
            titleElement.classList.add("voice-title");
            titleElement.innerText = "Loading...";
            WorkPromise.getWorkTitle(rjCode).then(title => titleElement.innerText = title)
                .catch(_ => titleElement.innerHTML = "");
            infoContainer.appendChild(titleElement);

            const rjCodeElement = document.createElement("div");
            rjCodeElement.classList.add("rjcode");
            rjCodeElement.innerText = `[${rjCode}]`;
            infoContainer.appendChild(rjCodeElement);

            const flagElement = document.createElement("div");
            flagElement.style.marginTop = "20px"
            flagElement.innerHTML = "";
            WorkPromise.getWorkPromise(rjCode).api.then(data => {
                if(data.is_special){
                    //特典作品
                    flagElement.innerHTML = `<span style="color: gold; font-size: 15px; align-self: center; font-weight: bold">[BONUS]</span>`;
                }
                else if(!data.is_sale && !data.is_coming_soon) {
                    flagElement.innerHTML = `<span style="color: darkred; font-size: 15px; align-self: center">(No longer for Sale)</span>`;
                }
            })
                .catch(_ => flagElement.innerHTML = "");
            infoContainer.appendChild(flagElement);


            const circleElement = document.createElement("div");
            circleElement.innerHTML = "Circle: Loading...";
            WorkPromise.getCircle(rjCode).then(circle => circleElement.innerHTML = `Circle: <a>${circle}</a>`)
                .catch(_ => circleElement.innerHTML = "");
            infoContainer.appendChild(circleElement);

            const debugElement = document.createElement("div");
            debugElement.innerHTML = "";
            WorkPromise.getDebug(rjCode).then(debug => debugElement.innerHTML = debug)
                .catch(_ => debugElement.innerHTML = "");
            infoContainer.appendChild(debugElement);

            const translatorElement = document.createElement("div");
            translatorElement.innerHTML = "";
            WorkPromise.getTranslatorName(rjCode).then(name => translatorElement.innerHTML = `Translator: <a>${name}</a>`)
                .catch(_ => translatorElement.innerHTML = "");
            infoContainer.appendChild(translatorElement);

            /*if (workInfo.date)
                html += `Release: <a>${workInfo.date}</a> <br />`;*/

            const releaseElement = document.createElement("div");
            releaseElement.innerHTML = "Release: Loading...";
            WorkPromise.getReleaseDate(rjCode).then(date => releaseElement.innerHTML = `Release: <a>${date}</a>`)
                .catch(_ => releaseElement.innerHTML = "");
            infoContainer.appendChild(releaseElement);

            /*if (workInfo.update)
                html += `Update: <a>${workInfo.update}</a> <br />`;*/

            const updateElement = document.createElement("div");
            updateElement.innerHTML = "";
            WorkPromise.getUpdateDate(rjCode).then(date => updateElement.innerHTML = `Update: <a>${date}</a>`)
                .catch(_ => updateElement.innerHTML = "");
            infoContainer.appendChild(updateElement);

            /*let ratingClass = "age-all";
            if (workInfo.rating.includes("18")) {
                ratingClass = "age-18";
            }
            html += `Age rating: <a class="${ratingClass}">${workInfo.rating}</a><br />`*/

            const ageElement = document.createElement("div");
            ageElement.innerHTML = "Age rating: Loading...";
            WorkPromise.getAgeRating(rjCode).then(rating => {
                let ratingClass = "age-all";
                if(rating.includes("18")){
                    ratingClass = "age-18";
                }
                ageElement.innerHTML = `Age rating: <a class="${ratingClass}">${rating}</a>`;
            }).catch(_ => ageElement.innerHTML = "");
            infoContainer.appendChild(ageElement);

            /*if (workInfo.cv)
                html += `CV: <a>${workInfo.cv}</a> <br />`;*/

            const cvElement = document.createElement("div");
            cvElement.innerHTML = "CV: Loading...";
            WorkPromise.getCV(rjCode).then(cv => cvElement.innerHTML = `CV: <a>${cv}</a>`)
                .catch(_ => cvElement.innerHTML = "");
            infoContainer.appendChild(cvElement);

            /*if (workInfo.tags) {
                html += `Tags: <a>`
                workInfo.tags.forEach(tag => {
                    html += tag + "\u3000";
                });
                html += "</a><br />";
            }*/

            const tagsElement = document.createElement("div");
            tagsElement.innerHTML = "Tags: Loading...";
            WorkPromise.getTags(rjCode).then(tags => {
                let tagsHtml = "Tags: <a>";
                tags.forEach(tag => {
                    tagsHtml += tag + "\u3000";
                });
                tagsHtml += "</a>";
                tagsElement.innerHTML = tagsHtml;
            }).catch(_ => tagsElement.innerHTML = "");
            infoContainer.appendChild(tagsElement);

            /*if (workInfo.filesize)
                html += `File size: ${workInfo.filesize}<br />`;*/

            const filesizeElement = document.createElement("div");
            filesizeElement.innerHTML = "File size: Loading...";
            WorkPromise.getFileSize(rjCode).then(filesize => filesizeElement.innerHTML = `File size: ${filesize}`)
                .catch(_ => filesizeElement.innerHTML = "");
            infoContainer.appendChild(filesizeElement);

            /*html += "</div>"
            popup.innerHTML = html;*/

            infoContainer.style.paddingBottom = "3px";
            popup.appendChild(infoContainer);
            popup.insertBefore(imgContainer, popup.childNodes[0]);

            /*if (workInfo === null)
                popup.innerHTML = "<div class='error'>Work not found.</span>";
            else {

            }*/

            Popup.move(e);
        },

        over: function (e) {
            const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE);
            const popup = document.querySelector("div#voice-" + rjCode);
            if (popup) {
                const style = popup.getAttribute("style").replace("none", "flex");
                popup.setAttribute("style", style);
            }
            else {
                Popup.makePopup(e, rjCode);
            }
        },

        out: function (e) {
            const rjCode = e.target.getAttribute("rjcode");
            const popup = document.querySelector("div#voice-" + rjCode);
            if (popup) {

                const style = popup.getAttribute("style").replace("flex", "none");;
                popup.setAttribute("style", style);
            }
        },

        move: function (e) {
            const rjCode = e.target.getAttribute("rjcode");
            const popup = document.querySelector("div#voice-" + rjCode);
            if (popup) {
                if (popup.offsetWidth + e.clientX + 10 < window.innerWidth - 10) {
                    popup.style.left = (e.clientX + 10) + "px";
                }
                else {
                    popup.style.left = (window.innerWidth - popup.offsetWidth - 10) + "px";
                }

                if (popup.offsetHeight + e.clientY + 50 > window.innerHeight) {
                    popup.style.top = (e.clientY - popup.offsetHeight - 8) + "px";
                }
                else {
                    popup.style.top = (e.clientY + 20) + "px";
                }
            }
        },
    }

    const WorkPromise = {
        /**
         * 标题、社团、发行日期、更新日期、年龄指定
         * CV、标签、文件大小、封面地址
         */

        checkNotNull: function (obj){
            if(!obj) throw new Error();
            return obj;
        },

        getWorkPromise: function (rjCode){
            if(work_promise[rjCode]){
                return work_promise[rjCode];
            }
            work_promise[rjCode] = DLsite.getWorkRequestPromise(rjCode);
            return work_promise[rjCode];
        },

        getFound: async function(rjCode){
            try{
                const data = await this.getWorkPromise(rjCode).api;
                return data !== null;
            }catch (e){
                //说明是网络问题,删除缓存并返回true
                delete work_promise[rjCode];
                return true;
            }
        },

        getGirls: async function(rjCode){
            const data = await this.getWorkPromise(rjCode).api;
            return data.is_girls
        },

        getDebug: async function(rjCode){
            return "";
            /*const work = this.getWorkPromise(rjCode);
            const api = await work.api;
            const info = await work.info;
            const circle = work.circle;

            return `is_sale: ${api.is_sale} <br/>
                    is_free: ${api.is_free} <br/>
                    is_oly: ${api.is_oly} <br/>
                    is_led: ${api.is_led} <br/>`;*/

        },

        getImgLink: async function(rjCode){
            try{
                const apiData = await this.getWorkPromise(rjCode).api;
                if(apiData.img_url) return "https://" + apiData.img_url;
                this.checkNotNull(apiData.img_url);
            }catch (e) {
                const info = await this.getWorkPromise(rjCode).info;
                this.checkNotNull(info.img);
                return info.img;
            }
        },

        getWorkTitle: async function(rjCode){
            const apiData = await this.getWorkPromise(rjCode).api;
            this.checkNotNull(apiData.title);
            return apiData.title;
        },

        getAgeRating: async function(rjCode){
            const info = await this.getWorkPromise(rjCode).info;
            this.checkNotNull(info.rating);
            return info.rating;
        },

        getCircle: async function(rjCode){
            let work = this.getWorkPromise(rjCode);
            let info = await work.info;

            if(!info){
                //页面解析失败,可能作品无法显示,此时直接获取RG信息
                const circleInfo = await work.circle;
                this.checkNotNull(circleInfo);
                this.checkNotNull(circleInfo.name);
                return circleInfo.name;
            }

            if(info.circleId && info.circleId !== "RG60289"){
                this.checkNotNull(info.circle);
                return info.circle;
            }

            //匹配原版社团名,而不是大家翻
            const api = await work.api;
            this.checkNotNull(api.original_rj);
            work = this.getWorkPromise(api.original_rj)
            info = await work.info;
            this.checkNotNull(info.circle);
            return info.circle;
        },

        getTranslatorName: async function(rjCode){
            const api = await this.getWorkPromise(rjCode).api;
            this.checkNotNull(api.maker_name);
            return api.maker_name;
        },

        getReleaseDate: async function(rjCode){
            const info = await this.getWorkPromise(rjCode).info;
            if(info && info.date) return info.date;

            //从api中查找发售时间
            const api = await this.getWorkPromise(rjCode).api;
            this.checkNotNull(api.regist_date)
            return api.regist_date;
        },

        getUpdateDate: async function(rjCode) {
            const info = await this.getWorkPromise(rjCode).info;
            this.checkNotNull(info["update"]);
            return info["update"];
        },

        getCV: async function(rjCode){
            const info = await this.getWorkPromise(rjCode).info;
            this.checkNotNull(info.cv);
            return info.cv;
        },

        getTags: async function(rjCode) {
            const info = await this.getWorkPromise(rjCode).info;
            this.checkNotNull(info.tags);
            return info.tags;
        },

        getFileSize: async function(rjCode) {
            let info = await this.getWorkPromise(rjCode).info;
            if(info.filesize) return info.filesize;

            let api = await this.getWorkPromise(rjCode).api;
            const original_rj = api.original_rj;
            info = await this.getWorkPromise(original_rj).info;
            this.checkNotNull(info.filesize);
            return info.filesize;
        }
    }

    const DLsite = {
        parseWorkDOM: function (dom, rj) {
            // workInfo: {
            //     rj: any;
            //     img: string;
            //     title: any;
            //     circle: any;
            //     date: any;
            //     rating: any;
            //     tags: any[];
            //     cv: any;
            //     filesize: any;
            //     dateAnnounce: any;
            // }
            const workInfo = {};
            workInfo.rj = rj;

            let rj_group;
            if (rj.slice((rj.length === 10 ? 7 : 5)) === "000")
                rj_group = rj;
            else {
                rj_group = (parseInt(rj.slice(2, (rj.length === 10 ? 7 : 5))) + 1).toString() + "000";
                if(rj_group.length < rj.length - 2){
                    let zero = Math.pow(10, rj.length - rj_group.length - 2).toString().slice(1)
                    rj_group = zero + rj_group
                }
                rj_group = "RJ" + rj_group; //("000000" + rj_group).substring(rj_group.length);
            }

            workInfo.img = "https://img.dlsite.jp/modpub/images2/work/doujin/" + rj_group + "/" + rj + "_img_main.jpg";

            let metaList = dom.getElementsByTagName("meta")
            for (let i = 0; i < metaList.length; i++){
                let meta = metaList[i];
                if(meta.getAttribute("property") === 'og:image'){
                    workInfo.img = meta.content;
                    break;
                }
            }

            workInfo.title = dom.getElementById("work_name").innerText;
            workInfo.circle = dom.querySelector("span.maker_name").innerText;
            workInfo.circleId = dom.querySelector("#work_maker a").href;
            workInfo.circleId = workInfo.circleId.substring(workInfo.circleId.lastIndexOf("/") + 1, workInfo.circleId.lastIndexOf(".")).trim();

            const table_outline = dom.querySelector("table#work_outline");
            for (var i = 0, ii = table_outline.rows.length; i < ii; i++) {
                const row = table_outline.rows[i];
                const row_header = row.cells[0].innerText;
                const row_data = row.cells[1];
                switch (true) {
                    case (row_header.includes("販売日")||row_header.includes("贩卖日")||row_header.includes("Release date")||row_header.includes("販賣日")||row_header.includes("판매일")):
                        workInfo.date = row_data.innerText;
                        break;
                    case (row_header.includes("更新情報")||row_header.includes("更新信息")||row_header.includes("Update information")||row_header.includes("更新資訊")||row_header.includes("갱신 정보")):
                        workInfo.update = row_data.firstChild.data;
                        break;
                    case (row_header.includes("年齢指定")||row_header.includes("年龄指定")||row_header.includes("Age")||row_header.includes("年齡指定")||row_header.includes("연령 지정")):
                        workInfo.rating = row_data.innerText;
                        break;
                    case (row_header.includes("ジャンル")||row_header.includes("分类")||row_header.includes("Genre")||row_header.includes("分類")||row_header.includes("장르")):
                        const tag_nodes = row_data.querySelectorAll("a");
                        workInfo.tags = [...tag_nodes].map(a => { return a.innerText });
                        break;
                    case (row_header.includes("声優")||row_header.includes("声优")||row_header.includes("Voice Actor")||row_header.includes("聲優")||row_header.includes("성우")):
                        workInfo.cv = row_data.innerText;
                        break;
                    case (row_header.includes("ファイル容量")||row_header.includes("文件容量")||row_header.includes("File size")||row_header.includes("檔案容量")||row_header.includes("파일 용량")):
                        workInfo.filesize = row_data.innerText.trim();
                        break;
                    default:
                        break;
                }
            }

            const work_date_ana = dom.querySelector("strong.work_date_ana");
            if (work_date_ana) {
                workInfo.dateAnnounce = work_date_ana.innerText;
                workInfo.img = "https://img.dlsite.jp/modpub/images2/ana/doujin/" + rj_group + "/" + rj + "_ana_img_main.jpg"
            }

            return workInfo;
        },

        request: function (rjCode, callback) {
            const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
            getXmlHttpRequest()({
                method: "GET",
                url,
                headers: {
                    "Accept": "text/xml",
                    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
                },
                onload: function (resp) {
                    if (resp.readyState === 4 && resp.status === 200) {
                        const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
                        const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                        callback(workInfo);
                    }
                    else if (resp.readyState === 4 && resp.status === 404)
                        DLsite.requestAnnounce(rjCode, callback);
                },
            });
        },

        requestAnnounce: function (rjCode, callback) {
            const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
            getXmlHttpRequest()({
                method: "GET",
                url,
                headers: {
                    "Accept": "text/xml",
                    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
                },
                onload: function (resp) {
                    if (resp.readyState === 4 && resp.status === 200) {
                        const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
                        const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                        callback(workInfo);
                    }
                    else if (resp.readyState === 4 && resp.status === 404)
                        callback(null);
                },
            });
        },

        // Get language code for DLSite API
        getLangCode: function (lang) {
            if(!lang) return "ja-JP";

            switch (lang.toUpperCase()) {
                case "JPN":
                    return "ja-JP";
                case "ENG":
                    return "en-US";
                case "KO_KR":
                    return "ko-KR";
                case "CHI_HANS":
                    return "zh-CN";
                case "CHI_HANT":
                    return "zh-TW";
                default:
                    return "ja-JP"
            }
        },

        parseApiData: function (rjCode, data){
            if(!data) data = {};
            const translation_info = data.translation_info ? data.translation_info : {};
            let apiData = {
                title: data.work_name,
                img_url: data.work_image ? data.work_image.substring(2) : null,
                original_rj: translation_info.original_workno ? translation_info.original_workno : rjCode,
                maker_name: data.maker_name,
                maker_id: data.maker_id,
                regist_date: data.regist_date,
                is_sale: data.is_sale,
                is_free: data.is_free,
                is_oly: data.is_oly,
                is_led: data.is_led,
                is_special: !data.is_sale && data.is_free && data.is_oly && data.wishlist_count === false,
                is_girls: data.site_id === "girls"
            }

            if(data.regist_date){
                let reg_date = data.regist_date.replace(/-/g, '/');
                let releaseDate = new Date(reg_date);
                apiData.regist_timestamp = releaseDate.getTime();
                apiData.regist_date = `${releaseDate.getFullYear()} / ${releaseDate.getMonth() + 1} / ${releaseDate.getDate()}`;
                if(apiData.regist_timestamp > Date.now()){
                    apiData.is_coming_soon = true;

                    //计算倒计时天数
                    let date_reg = new Date(releaseDate.getFullYear(), releaseDate.getMonth(), releaseDate.getDate());
                    let date_today = new Date(Date.now());
                    date_today = new Date(date_today.getFullYear(), date_today.getMonth(), date_today.getDate());
                    let days = (date_reg.getTime() - date_today.getTime()) / (1000 * 60 * 60 * 24);

                    apiData.regist_date += `<span style="color:#ffeb3b; font-size: 16px; font-style: italic; margin-left: 16px">(Coming in ${days} day${(days > 1 ? "s" : "")})</span>`;
                }
            }
            return apiData;
        },

        getHttp: function (url, onload, onerror){
            return getXmlHttpRequest()({
                method: "GET",
                url,
                headers: {
                    "Accept": "text/xml",
                    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)"
                },
                onload: onload,
                onerror: onerror
            });
        },

        getAnnouncePromise: function (rjCode) {
            const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
            return new Promise(
                (resolve, reject) => {
                    this.getHttp(url, resp => {
                        if (resp.readyState === 4 && resp.status === 200) {
                            const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
                            const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                            resolve(workInfo);
                        }
                        else if (resp.readyState === 4 && resp.status === 404) {
                            resolve(null);
                        }
                    }, () => reject(null))
                }
            )
        },

        getHtmlPromise: function (rjCode) {
            const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
            return new Promise(
                (resolve, reject) => {
                    this.getHttp(url, resp => {
                        if (resp.readyState === 4 && resp.status === 200) {
                            const dom = new DOMParser().parseFromString(resp.responseText, "text/html");
                            const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                            resolve(workInfo);
                        }
                        else if (resp.readyState === 4 && resp.status === 404) {
                            resolve(this.getAnnouncePromise(rjCode))
                        }
                    }, () => reject(null))
                }
            )
        },

        getApiPromise: function (rjCode){
            const url = `https://www.dlsite.com/maniax/product/info/ajax?product_id=${rjCode}&cdn_cache_min=1`
            const p1 = new Promise(
                (resolve, reject) => {
                    this.getHttp(url, resp => {
                        if (resp.readyState === 4 && resp.status === 200) {
                            const data = JSON.parse(resp.responseText);
                            if(Array.prototype.isPrototypeOf(data)){
                                resolve(data)
                            }
                            resolve(data[rjCode]);
                        }
                        else if (resp.readyState === 4 && resp.status === 404) {
                            reject(null);
                        }
                    })
                }
            )

            return p1.then(data => {
                if(Array.prototype.isPrototypeOf(data)){
                    return data;
                }

                const translation_info = data.translation_info ? data.translation_info : {};
                const lang = this.getLangCode(translation_info.lang);
                let next_rj = rjCode;
                let translator_name = undefined;
                if(translation_info.is_child) {
                    //找到父级RJ信息,因为子级信息不全面
                    next_rj = translation_info.parent_workno;

                    //子级可以先获取翻译者的信息
                    translator_name = data.maker_name;
                }

                const url = `https://www.dlsite.com/maniax/product/info/ajax?product_id=${next_rj}&cdn_cache_min=1&locale=${lang}`;
                return new Promise(
                    (resolve, reject) => {
                        this.getHttp(url,
                            resp => {
                                if (resp.readyState === 4 && resp.status === 200) {
                                    const data = JSON.parse(resp.responseText);
                                    data[next_rj].maker_name = translator_name;
                                    resolve(data[next_rj]);
                                }
                                else if (resp.readyState === 4 && resp.status === 404) {
                                    reject(null);
                                }
                            })
                    }
                )
            }).then(data => {
                if(Array.prototype.isPrototypeOf(data)){
                    return null;
                }
                return this.parseApiData(rjCode, data)
            });
        },

        getCirclePromise: function (rjCode, apiPromise){
            return apiPromise.then(data => {
                if(!data.maker_id) return null;
                const maker_id = data.maker_id;
                const url = `https://media.ci-en.jp/dlsite/lookup/${maker_id}.json`;
                return new Promise(
                    (resolve, reject) => {
                        this.getHttp(url, resp => {
                            if (resp.readyState === 4 && resp.status === 200) {
                                const data = JSON.parse(resp.responseText);
                                data[0] = data[0] ? data[0] : {};
                                data[0].maker_id = maker_id;
                                resolve(data[0]);
                            }
                            else if (resp.readyState === 4 && resp.status === 404) {
                                reject(null);
                            }
                        })
                    }
                )
            }).then(data => {
                data = data ? data : {}
                return {
                    maker_id: data.maker_id,
                    id: data.id,
                    name: data.name,
                    rating: data.rating,
                }
            });
        },

        getWorkRequestPromise: function (rjCode) {
            let infoPromise = this.getHtmlPromise(rjCode);
            let apiPromise = this.getApiPromise(rjCode);
            let circlePromise = this.getCirclePromise(rjCode, apiPromise);
            return {
                info: infoPromise,
                api: apiPromise,
                circle: circlePromise
            }
        }
    }


    document.addEventListener("DOMContentLoaded", function () {
        const style = document.createElement("style");
        style.innerHTML = css;
        document.head.appendChild(style);

        Parser.walkNodes(document.body);

        const observer = new MutationObserver(function (m) {
            for (let i = 0; i < m.length; ++i) {
                let addedNodes = m[i].addedNodes;

                for (let j = 0; j < addedNodes.length; ++j) {
                    Parser.walkNodes(addedNodes[j]);
                }
            }
        });

        document.addEventListener("securitypolicyviolation", function (e) {
            if (e.blockedURI.includes("img.dlsite.jp")) {
                const img = document.querySelector(`img[src="${e.blockedURI}"]`);
                img.remove();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true })

        // Make title selectable
        setUserSelectTitle();
    });
})();