Add download button or link for the Pixiv original picture in view page
// ==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); }