Pixiv Download Helper

Add download button or link for the Pixiv original picture in view page

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           Pixiv Download Helper
// @name:zh        Pixiv 下载助手
// @name:ja        ダウンロードヘルパー
// @description    Add download button or link for the Pixiv original picture in view page
// @description:zh 为 Pixiv 图片阅览页增加下载原图按钮或链接
// @description:ja Pixiv作品ダウンロードヘルパー
// @namespace      https://github.com/Sg4Dylan/PixivDownloadHelper
// @icon           https://www.pixiv.net/favicon.ico
// @include        http://www.pixiv.net/*
// @include        https://www.pixiv.net/*
// @grant          GM_xmlhttpRequest
// @grant          GM_setValue
// @grant          GM_getValue
// @connect        i.pximg.net
// @connect        i1.pixiv.net
// @connect        i2.pixiv.net
// @connect        i3.pixiv.net
// @connect        i4.pixiv.net
// @connect        i5.pixiv.net
// @version        2018.04.22.0(just merge code & deprecated)
// ==/UserScript==

//Turn thumbnail titles into direct links (single images) or mode=manga links.  Some kinds of thumbnails aren't covered, and an isolated few (like #17099702) don't work.
var directTitles = false;

//Append direct links below images on mode=manga pages
var directManga = true;

//Force pixiv's 'book view' style for manga sequences to something like the normal view.  Clicking a page won't scroll the window to the next page.
var breakBookView = false;

//Replace the medium thumbnail on mode=medium pages with the full size.  The image will be relinked to the full size regardless of this setting.
var fullSizeMedium = true;

//Disable lazy loading images.  These appear on mode=manga pages, rankings, and the "Recommended" section of the bookmarks page.
var dontSayLazy = true;

//Text for Button & link
var mangaModeLang = [["Right click \"Save As\" to download, file name: "], ["名前をつけて保存、ファイル名:"], ["下载请使用右键“链接另存为”保存,文件名:"]];
var normalModeLangZero = [["Direct download", "Right click \"Save As\" to download"], ["直接ダウンロード", "名前をつけて保存"], ["直接下载", "使用右键链接另存为"]];
var saveFileNameFormat = [["<tr> <th> Pixiv Download Helper<br>setting<br> <a name=\"saveSetting\" class=\"btn_type01\">Save setting</a> </th> <td> <dl> <dt>format of saving file name</dt> <dd> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt0\" checked>[author name][illustrate name]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt1\">[author name][illustrate name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt2\">[author name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt3\">[illustrate name][pixiv id]</label> </dd> <dt>split mode</dt> <dd> <label><input type=\"radio\" name=\"pdh1\" value=\"spl0\" checked>[Name][Name]</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl1\">【Name】【...】</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl2\">Name - Name</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl3\">Name ~ Name</label> </dd> </dl> </td> </tr>","script setting saved"], ["<tr> <th> Pixivダウンロードヘルパー設定<br> <a name=\"saveSetting\" class=\"btn_type01\">設定を保存する</a> </th> <td> <dl> <dt>ファイル名を保存する形式</dt> <dd> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt0\" checked>[author name][illustrate name]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt1\">[author name][illustrate name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt2\">[author name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt3\">[illustrate name][pixiv id]</label> </dd> <dt>スプリットモード</dt> <dd> <label><input type=\"radio\" name=\"pdh1\" value=\"spl0\" checked>[Name][Name]</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl1\">【Name】【...】</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl2\">Name - Name</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl3\">Name ~ Name</label> </dd> </dl> </td> </tr>","スクリプト設定が保存されました"], ["<tr> <th> Pixiv 下载助手设置<br> <a name=\"saveSetting\" class=\"btn_type01\">保存插件设置</a> </th> <td> <dl> <dt>设置保存格式</dt> <dd> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt0\" checked>[author name][illustrate name]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt1\">[author name][illustrate name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt2\">[author name][pixiv id]</label><br> <label><input type=\"radio\" name=\"pdh0\" value=\"fmt3\">[illustrate name][pixiv id]</label> </dd> <dt>设置分割方式</dt> <dd> <label><input type=\"radio\" name=\"pdh1\" value=\"spl0\" checked>[Name][Name]</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl1\">【Name】【...】</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl2\">Name - Name</label><br> <label><input type=\"radio\" name=\"pdh1\" value=\"spl3\">Name ~ Name</label> </dd> </dl> </td> </tr>","插件设置保存成功"]];

//----------------------------------------------------------------//

var fullSizeWidth = "740px";

if( typeof(custom) != "undefined" )
    custom();

if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) {
      return typeof args[number] != 'undefined' ? args[number] : match;
    });
  };
}

if( location.search.indexOf("mode=manga_big") > 0 || location.search.indexOf("mode=big") > 0 )
{
    //Make the 'big'/'manga_big' image link to itself instead of closing the window
    console.log("Mode=manga_big");
    let image = document.getElementsByTagName("img")[0];
    if( image )
    {
        let link = document.createElement("a");
        link.href = image.src;
        link.appendChild( document.createElement("img") ).src = image.src;
        document.body.innerHTML = "";
        document.body.appendChild( link );
    }
}
else if( location.search.indexOf("mode=manga") > 0 )
{
    console.log("Mode=manga");
    let container = document.getElementsByClassName("full-size-container");
    if( directManga && container.length )
    {
        //Check the mode=manga_big page for the first page, since the sample extension is always "jpg".
        let req = new XMLHttpRequest();
        req.open( "GET", location.href.replace(/page=\d+&?/,'').replace('mode=manga','mode=manga_big&page=0'), true );
        req.onload = function()
        {
            console.log("Pixiv Download Helper Patch ver 0.1 by SgDylan.");
            console.log("Parsing download link...");
            let firstImage = req.responseXML.querySelector("img[src*='_p0.']").src;
            for( let i = 0; i < container.length; i++ )
            {
                console.log("Getting link...");
                var sourcePictureLink = firstImage.replace( "_p0.", "_p"+i+"." );
                var extName = "." + sourcePictureLink.split(".").pop(-1);
                authorName = document.getElementsByClassName("breadcrumbs")[0].children[1].children[0].innerHTML.split(">")[1];
                illustName = document.getElementsByClassName("breadcrumbs")[0].children[2].children[0].children[0].innerHTML;
                FileName = filenameConcater(authorName, illustName, i) + extName;
                console.log("File name: "+FileName);
                console.log("File Link: "+sourcePictureLink);
                console.log("Put download link");
                let link = document.createElement("a");
                link.textContent = multiLang(0, 0)+FileName;
                link.style.display = "block";
                link.href = sourcePictureLink;
                link.download = FileName;
                container[i].parentNode.appendChild( link );
            }
            console.log("All Ready !");
        };
        req.responseType = "document";
        req.send(null);
    }
    else if( breakBookView && document.head.innerHTML.indexOf("pixiv.context.images") > 0 )
    {
        //Book view (e.g. #54139174, #57045668)

        console.log("Mode=bookview");
        let mangaSection = document.createElement("section");
        mangaSection.className = "manga";

        let scripts = document.head.getElementsByTagName("script");
        let hits = 0;
        for( let i = 0; i < scripts.length; i++ )
        {
            let urls = scripts[i].innerHTML.match( /pixiv.context.images[^"]+"([^"]+)".*pixiv.context.originalImages[^"]+"([^"]+)"/ );
            if( urls )
            {
                let full = urls[2].replace( /\\\//g, "/");
                mangaSection.innerHTML += '<div class="item-container"><a href="'+full+'" class="full-size-container"><i class="_icon-20 _icon-full-size"></i></a><img style="width:auto;height:auto;max-width:1200px;max-height:1200px" src="'+full+'" class="image">'+( directManga ? '<a href="'+full+'" style="display:block">direct link</a>' : '' )+'</div>';
                hits++;
            }
        }

        if( hits > 0 )
        {
            let sheet = document.createElement("link");
            sheet.setAttribute("rel","stylesheet");
            sheet.setAttribute("href","http://source.pixiv.net/www/css/member_illust_manga.css");
            document.head.appendChild( sheet );
            document.getElementsByTagName("html")[0].className = "verticaltext no-textcombine no-ie";
            document.body.innerHTML = "";
            document.body.appendChild( mangaSection );
        }
    }
}
else if( window == window.top )//not inside iframe
{
    if( directTitles )
    {
        //Link dem titles.
        linkThumbTitles([document]);
        new MutationObserver( function(mutationSet)
        {
            mutationSet.forEach( function(mutation){ linkThumbTitles( mutation.addedNodes ); } );
        }).observe( document.body, { childList:true, subtree:true } );
    }

    let worksDisplay = document.getElementsByClassName("works_display")[0];
    if( worksDisplay )
    {
        let mainImage, fullsizeSrc = 0, mainLink = worksDisplay.querySelector("a[href*='mode=']");
        if( mainLink )
            mainLink.removeAttribute('target');//Make link open in same window

        let oClass = document.getElementsByClassName("original-image");
        let downloadButton = document.getElementsByClassName("bookmark-container")[0];
        if( oClass.length == 1 )//47235071
        {
            let worksDiv = worksDisplay.getElementsByTagName("div")[0];
            worksDisplay.removeChild( worksDiv );//Need to remove instead of hide to prevent double source search links in other script
            let link = worksDisplay.insertBefore( document.createElement("a"), worksDisplay.firstChild );
            mainImage = link.appendChild( fullSizeMedium ? document.createElement("img") : worksDiv.getElementsByTagName("img")[0] );
            fullsizeSrc = link.href = oClass[0].getAttribute("data-src");
            //Add button to page
            if( fullSizeMedium )
            {
                console.log("Pixiv Download Helper Patch ver 0.1 by SgDylan.");
                console.log("Parsing download link...");
                let dButton0 = downloadButton.insertBefore( document.createElement("a"), downloadButton.firstChild );
                let dButton1 = downloadButton.insertBefore( document.createElement("a"), downloadButton.firstChild );
                console.log("Getting link...");
                let sourcePictureLink = oClass[0].getAttribute("data-src");
                let extName = "." + sourcePictureLink.split(".").pop(-1);
                authorTagList = document.getElementsByClassName("user-name");
                authorName = "";
                for (let index=0; index<authorTagList.length; index++) {
                    if (authorTagList[index].tagName=="A" && authorTagList[index].classList.value=="user-name") {
                        authorName = authorTagList[index].innerHTML;
                    }
                }
                let FileName = filenameConcater(authorName, document.getElementsByClassName("title")[1].innerHTML) + extName;
                console.log("File name: "+FileName);
                console.log("File Link: "+sourcePictureLink);
                console.log("Prepare right click button");
                dButton0.className = "_bookmark-toggle-button add-bookmark";
                dButton0.innerHTML = "<span class=\"description\">"+multiLang(1, 1)+"</span>";
                dButton0.download = FileName;
                dButton0.href = sourcePictureLink;
                console.log("Prepare right click button - Done !");
                // Prepare direct click button
                let retry_count = 0;
                get_blob_obj();
                function get_blob_obj() {
                    console.log("Prepare direct click button");
                    console.log("Preparing download file...");
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: sourcePictureLink,
                        responseType: "blob",
                        timeout: 6000,
                        ontimeout: function() {
                            console.log("Timeout");
                            retry_count++;
                            if(retry_count<6) get_blob_obj();
                        },
                        onerror: function() {
                            console.log("Error");
                            retry_count++;
                            if(retry_count<6) get_blob_obj();
                        },
                        onload: function(response){
                            dButton1.className = "_bookmark-toggle-button add-bookmark";
                            dButton1.innerHTML = "<span class=\"description\">"+multiLang(1, 0)+"</span>";
                            dButton1.download = FileName;
                            dButton1.href = URL.createObjectURL(response.response);
                            console.log("Prepare direct click button - Done !");
                            console.log("All Ready !");
                        }
                    });
                }
            }
        }
        else if( mainLink && mainLink.href.indexOf("mode=big") > 0 && (mainImage = mainLink.getElementsByTagName("img")[0]) !== null )//17099702
        {
            //New thumbnails are always jpg, need to query mode=big page to get the right file extension.
            console.log("Mode=big");
            let req = new XMLHttpRequest();
            req.open( "GET", mainLink.href, true );
            req.onload = function()
            {
                mainLink.href = req.responseXML.getElementsByTagName("img")[0].src;
                if( fullSizeMedium ) {
                    mainImage.src = mainLink.href;
                }
            };
            req.responseType = "document";
            req.send(null);
        }

        if( mainImage && fullSizeMedium )
        {
            if( fullsizeSrc )
                mainImage.src = fullsizeSrc;
            mainImage.setAttribute("style", "max-width: "+fullSizeWidth+"; height: auto; width: auto;");
            worksDisplay.style.width = fullSizeWidth;
        }
    }
}

if( dontSayLazy && unlazyImage() && window == window.top )
{
    //Initial page has lazy images; listen for more images added later
    new MutationObserver( function(mutationSet)
    {
        mutationSet.forEach( function(mutation)
        {
            for( let i = 0; i < mutation.addedNodes; i++ )
                unlazyImage( mutation.addedNodes[i] );
        } );
    }).observe( document.body, { childList:true, subtree:true } );
}

//----------------------------------------------------------------//

if ( location.href.indexOf('setting_user.php') > 0 ) {
    // add some options
    appendSwitchChildren = document.getElementsByTagName('tr');
    appendSwitchParent = appendSwitchChildren[appendSwitchChildren.length-1].parentNode;
    switchOption = document.createElement('tr');
    switchOption.innerHTML = multiLang(2,0);
    appendSwitchParent.insertBefore(switchOption, appendSwitchParent.childNodes[appendSwitchChildren.length-1]);
    // bind button
    document.getElementsByName("saveSetting")[0].addEventListener('click', saveScriptSetting, false);
}

function saveScriptSetting() {
    for(let index=0; index<4; index++) {
        if(document.getElementsByName("pdh0")[index].checked) {
            GM_setValue("concatFmt", index);
            break;
        }
    }
    for(let index=0; index<4; index++) {
        if(document.getElementsByName("pdh1")[index].checked) {
            GM_setValue("splitFmt", index);
            break;
        }
    }
    alert(multiLang(2,1));
}

function filenameConcater(author_name, ill_name, index_num) {
    name_template = "";
    ill_id = "ID:" + getQueryString("illust_id");
    template_two = ["[{0}][{1}]","【{0}】【{1}】","{0} - {1}","{0} ~ {1}"];
    template_three = ["[{0}][{1}][{2}]","【{0}】【{1}】【{2}】","{0} - {1} - {2}","{0} ~ {1} ~ {2}"];
    template_index = ["[{0}]","【{0}】","- {0}","~ {0}"];
    if (GM_getValue("concatFmt", 0)==1) {
        name_template = template_three[GM_getValue("splitFmt", 0)].format(author_name, ill_name, ill_id);
        if (index_num) {
            name_template += template_index[GM_getValue("splitFmt", 0)].format(index_num);
        }
        return name_template;
    }
    switch(GM_getValue("splitFmt", 0)) {
        case 2:
            name_template = template_two[2].format(author_name, ill_id);
            break;
        case 3:
            name_template = template_two[3].format(ill_name, ill_id);
            break;
        default:
            name_template = template_two[0].format(author_name, ill_name);
    }
    if (index_num) {
        name_template += template_index[GM_getValue("splitFmt", 0)].format(index_num);
    }
    return name_template;
}

function getQueryString(name) {
    let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
    let r = window.location.search.substr(1).match(reg);
    if (r !== null) return unescape(r[2]); return null;
}

//----------------------------------------------------------------//

function multiLang(mode, position) {
    lang_mode = 0;
    localUsingLang = navigator.language;
    if(!localUsingLang) {
        localUsingLang = navigator.languages[0];
    }
    if(localUsingLang.indexOf("zh") !== -1) {
        lang_mode = 2;
    } else if(localUsingLang.indexOf("ja") !== -1) {
        lang_mode = 1;
    } else {
        lang_mode = 0;
    }
    switch(mode) {
        case 0:
            return mangaModeLang[lang_mode][position];
        case 1:
            return normalModeLangZero[lang_mode][position];
        case 2:
            return saveFileNameFormat[lang_mode][position];
        default:
            break;
    }
}

function unlazyImage(target)
{
    let images = ( target || document ).querySelectorAll("img[data-src]");
    for( let i = 0; i < images.length; i++ )
        images[i].src = images[i].getAttribute("data-src");
    return images.length;
}

function pushTitleLink(list, link)
{
    let matcher;
    if( link && link.href && (matcher = link.href.match(/illust_id=(\d+)/)) && matcher[1] > 0 )
        list.push({ "id": matcher[1], "link": link });
}

function linkThumbTitles(targets)
{
    let titleList = [];

    for( let i = 0; i < targets.length; i++ )
        if( targets[i] == document || targets[i].nodeType == Node.ELEMENT_NODE )
        {
            //search.php
            let foundTitle = targets[i].querySelectorAll("a[href*='mode=medium'][href*='illust_id='][title]");
            for( let j = 0; j < foundTitle.length; j++ )
                pushTitleLink( titleList, foundTitle[j] );

            //bookmark.php, member_illust.php, new_illust.php, member.php (uploads), mypage.php (new works)
            foundTitle = targets[i].querySelectorAll("a[href*='mode=medium'][href*='illust_id='] > .title");
            for( let j = 0; j < foundTitle.length; j++ )
                pushTitleLink( titleList, foundTitle[j].parentNode );

            //ranking.php
            foundTitle = targets[i].querySelectorAll(".ranking-item a.title[href*='mode=medium'][href*='illust_id=']");
            for( let j = 0; j < foundTitle.length; j++ )
                pushTitleLink( titleList, foundTitle[j] );

            //member_illust.php (what image was responding to)
            foundTitle = targets[i].querySelector(".worksImageresponseInfo a.response-out-work[href*='mode=medium'][href*='illust_id=']");
            if( foundTitle )
                pushTitleLink( titleList, foundTitle );

            //response.php, member_illust.php (before/after thumbnails), ?member.php (bookmarks)?
            let image = targets[i].querySelectorAll("li a[href*='mode=medium'][href*='illust_id='] img");
            for( let j = 0; j < image.length; j++ )
            {
                let page, title;
                for( page = image[j].parentNode; page.tagName != "A"; page = page.parentNode );

                //The prev/next thumbnails on mode=medium pages have text before/after the image.  Text also follows the image on image responses listings.
                if( !(title = page.getElementsByClassName("title")[0]) && (title = page.lastChild).nodeName != '#text' && (title = page.firstChild).nodeName != '#text' )
                    continue;//Can't find title element

                //Start title link at mode=medium and change later.
                let titleLink = document.createElement("a");
                titleLink.href = page.href;
                titleLink.style.color = "#333333";//Style used on some pages

                //Move the title out of the thumbnail link
                page.removeChild(title);
                titleLink.appendChild(title);
                page.parentNode.insertBefore( titleLink, page.nextSibling );

                pushTitleLink( titleList, titleLink );
            }
	}

    for( let i = 0; i < titleList.length; i++ )
        directLinkSingle( titleList[i] );
}

//Query an image's mode=medium page.
function directLinkSingle(title)
{
    let req = new XMLHttpRequest();
    req.open( "GET", location.protocol+"//www.pixiv.net/member_illust.php?mode=medium&illust_id="+title.id, true );
    req.onload = function()
    {
        let select = req.responseXML.getElementsByClassName("original-image");
        if( select.length == 1 )
            title.link.href = select[0].getAttribute("data-src");
        else if( (select = req.responseXML.querySelector(".works_display a[href*='mode=manga']")) !== null )
        {
            title.link.href = select.href;
            let page = req.responseXML.querySelectorAll("ul.meta li")[1].textContent.match(/(\d+)P$/);
            if( page )
                ( title.link.firstChild.nodeName == '#text' ? title.link : title.link.firstChild ).title += " ("+page[1]+" pages)";
        }
    };
    req.responseType = "document";
    req.send(null);
}