There are 48 previous versions of this script.
// ==UserScript==
// @name Endless Pixiv Pages
// @namespace http://userscripts.org/scripts/show/57567
// @include http://www.pixiv.net/*
// @description Makes Pixiv searches "bottomless", removes bookmark links, and adds links to Danbooru and the IQDB image search.
// @updated 2012-05-19
// ==/UserScript==
//Minimum height remaining to scroll before loading the next page.
var scrollBuffer = 500;
//Options to add links below each thumb; set to false to disable
var addIQDB = true;//IQDB image search (Danbooru)
var addSourceSearch = false;//Danbooru post search (looks for matching source)
//Default minimum number of favorites for a thumb to be displayed; only affects certain pages.
var minFavs = 0;
//Source search options; login info is required
var danbooruLogin = "";//e.g. "UserName"
var danbooruPassHash = "";//e.g. "ab3718d912849aff02482dbadf00d00ab391283a"
var styleSourceFound = "color:green; font-weight: bold;";
var styleSourceMissing = "color:red;";
var sourceTimeout = 20;//seconds to wait before retrying query
var maxAttempts = 20;//# of times to try a query before completely giving up on source searches
var refreshDays = 2;//Number of days to wait between cache clearings
//////////////////////////////////////////////////////////////////////////////////////
//////////// Don't mess with the below unless you really know your stuff. ////////////
//////////////////////////////////////////////////////////////////////////////////////
//Stop script if this page is inside an iframe.
if( window != window.top ) return;
//Remove Amazon ads from mode=medium pages
var ads = document.evaluate("//div/div[@class='ads_amazon_outer']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if( ads )
ads.parentNode.parentNode.removeChild(ads.parentNode);
if( typeof(GM_getValue) == "undefined" || !GM_getValue('a', 'b') )
{
GM_getValue = function(name,defV){ var value = localStorage.getItem("endless_pixiv."+name); return( value ? value : defV ); };
GM_setValue = function(name,value) { localStorage.setItem("endless_pixiv."+name, value ); }
}
if( typeof(custom) != "undefined" )
custom();
//Source search requires GM_xmlhttpRequest()
addSourceSearch = ( addSourceSearch && typeof(GM_xmlhttpRequest) != "undefined" );
if( !addSourceSearch )
( typeof(GM_deleteValue) != "undefined" ? GM_deleteValue : localStorage.removeItem )("illustCache");
//Startup variables.
var nextPage = null, timeout, pending = false, firstList = true, anyBookmarks = false;
var illustList = JSON.parse( GM_getValue("illustCache","{}") ), artistList = {}, thumbList = [], sourceTimer;
//Manga images have to be handled specially
if( location.search.indexOf("mode=manga") >= 0 && document.getElementById("image") )
{
if( addSourceSearch )
{
var divList = document.getElementById("image").getElementsByTagName("div");
for( var i = 0; i < divList.length; i++ )
{
divList[i].appendChild( document.createElement("br") );
thumbList.push({ link: divList[i].appendChild( document.createElement("a") ), source: divList[i].innerHTML.replace(/(.*unshift\(('|")|('|").*)/g,'').replace(/_(p\d+\.)/,'_*$1') });
}
sourceSearch();
}
return;
}
//Add ability to set minFavs inside Search Options
var addSearch = document.getElementById("word-and");
if( addSearch )
{
anyBookmarks = true;
//Load "minFavs" setting
if( GM_getValue("minFavs") )
minFavs = parseInt( GM_getValue("minFavs") );
//Set option
addSearch = addSearch.parentNode.parentNode;
var favTr = document.createElement("tr");
favTr.appendChild( document.createElement("th") ).textContent = "Minimum favorites";
favInput = favTr.appendChild( document.createElement("td") ).appendChild( document.createElement("input") );
favInput.type = "text";
favInput.value = ""+minFavs;
favInput.addEventListener("input", function()
{
if( /^ *\d+ *$/.test(this.value) )
{
GM_setValue("minFavs", this.value.replace(/ +/g,''));
minFavs = parseInt( this.value );
}
}, true);
addSearch.parentNode.insertBefore( favTr, addSearch );
}
//Fix hidden thumbs and add links if necessary
if( addIQDB || addSourceSearch )
{
processThumbs();
if( location.href.indexOf("mode=medium") > 0 )
processThumbs(null,true);
window.addEventListener( "DOMNodeInserted", function(e) { setTimeout( function() { processThumbs(e) }, 1 ) }, true );
}
//Stop script if there are no thumbnails.
if( (mainTable = getMainTable()) == null ) return;
var bottomDoc = getBottomPager();
if( bottomDoc == null ) return;
bottomDoc.parentNode.removeChild(bottomDoc);
mainTable.parentNode.parentNode.appendChild(bottomDoc);
//Remove that "showcase" box on /search.php, which this script breaks anyway.
while( mainTable.parentNode.firstChild != mainTable )
mainTable.parentNode.removeChild( mainTable.parentNode.firstChild );
//Stop script if there are no more pages.
if( (nextPage = getNextPage()) == null ) return;
//Adjust buffer height
scrollBuffer += window.innerHeight;
//Prevent page from being cut off after ~35 added pages
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = '#pixiv { overflow:visible; ! important }';
document.getElementsByTagName('head')[0].appendChild(style);
//Watch scrolling
window.addEventListener("scroll", testScrollPosition, false);
testScrollPosition();
//====================================== Functions ======================================
function processThumbs(e,separateQuery)
{
var thumbSearch = null, thisFirstList = firstList || e || separateQuery;
if( e && e.target.tagName != "LI" && e.target.tagName != "UL" && e.target.tagName != "DIV" )
return;
if( separateQuery )
{
firstList = true;
thumbSearch = document.evaluate("//div[@class = 'works_display']/a/img[not(@endless)]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
else
{
if( thisFirstList )
thumbSearch = document.evaluate("descendant-or-self::li/a[contains(@href,'mode=medium') or contains(@href,'/novel/show.php')]/img[not(@endless)] | //div/a[contains(@href,'mode=medium') or contains(@href,'/novel/show.php')]/img[not(@endless)]", (e ? e.target : document), null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if( !thumbSearch || thumbSearch.snapshotLength == 0 )
{
if( !e ) firstList = false;
thisFirstList = false;
thumbSearch = document.evaluate("descendant-or-self::li[@class='image']/a/p/img[not(@endless)]", (e ? e.target : document), null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
}
var sourceSearchDone = ( addSourceSearch && thumbList.length == 0 );
for( var i = 0; i < thumbSearch.snapshotLength; i++ )
{
var thumbImg = thumbSearch.snapshotItem(i);
var thumbPage = (thisFirstList ? thumbImg.parentNode : thumbImg.parentNode.parentNode);
var thumbDiv = thumbPage.parentNode;
var bookmarkCount = 0, bookmarkLink = document.evaluate( ".//a[contains(@href,'bookmark_detail.php')]", thumbDiv, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue;
//Mark thumb so it gets skipped when the next page loads.
thumbImg.setAttribute("endless","done");
//Skip generic restricted thumbs
if( thumbImg.src.indexOf("http://source.pixiv.net/") == 0 )
continue;
if( bookmarkLink )
bookmarkCount = parseInt( bookmarkLink.getAttribute("data-tooltip").replace(/(,|[^\d].*)/,'') );
else
{
if( thumbDiv.getElementsByTagName("ul").length == 0 && thumbDiv.getElementsByTagName("p").length == 0 )
thumbDiv.appendChild( document.createElement("br") );
bookmarkLink = thumbDiv.appendChild( document.createElement("a") );
bookmarkLink.setAttribute("class", "bookmark-count");
}
if( anyBookmarks && bookmarkCount < minFavs )
{
thumbDiv.parentNode.removeChild(thumbDiv);
continue;
}
if( addIQDB )
{
bookmarkLink.href = "http://danbooru.iqdb.org/?url="+thumbImg.src+"&fullimage="+thumbPage.href;
bookmarkLink.innerHTML = "(IQDB)";
}
if( addSourceSearch )
thumbList.push({ link: bookmarkLink.parentNode.appendChild( document.createElement("a") ), source: thumbImg.src.replace(/_(s|m|100)\..*/,'') });
}
if( e )
e.target.style.visibility = "visible";
if( sourceSearchDone )
sourceSearch();
}
//Function to extract illustID from pixiv URL
function indexFromURL(url, isMangaPage)
{
url = "x"+url.replace(/.*pixiv.net\/img\/[^\/]+\//,'');
if( !isMangaPage )
return url.replace( /(\.|_).*/,'');
return url.replace(/\?.*/,'').replace(/_(big_|)(p\d+\.)/,'_*$2');
}//url.replace(/(.*pixiv.net\/img\/[^\/]+\/|(\.|_).*)/g,''); }
function sourceSearch(thumbIndex, attempt)
{
//thumbList[index] = { link, source }
//artistList[name] = { fullSearchLen }
//illustList[illustID] = [] list of post IDs
if( sourceTimer != undefined )
clearTimeout(sourceTimer);
//Login info is required.
if( danbooruPassHash.length == 0 || danbooruLogin.length == 0 )
return;
if( attempt == undefined || thumbIndex == undefined )
{
//First attempt
attempt = 0;
//Check cached results for all remaining thumbs
for( thumbIndex = 0; thumbIndex < thumbList.length; thumbIndex++ )
sourceSearch( thumbIndex, -1 );
//Find the first thumb that is currently visible
for( thumbIndex = 0; thumbIndex < thumbList.length - 1; thumbIndex++ )
{
var offset = 0;
for( var elem = thumbList[thumbIndex].link; elem; elem = elem.offsetParent )
offset += elem.offsetTop;
if( offset >= ( document.body.scrollTop || document.documentElement.scrollTop ) )
break;
}
}
//Processed all existing thumbs
if( thumbList.length == 0 )
{
//Reload or refresh cache depending on how much time has passed
if( !illustList.lastReset || new Date().getTime() - refreshDays*24*3600*1000 > illustList.lastReset )
GM_setValue( "illustCache", JSON.stringify( { lastReset: new Date().getTime() } ) );
return;
}
//If this thumb has already been processed successfully, stop.
if( thumbList[thumbIndex].link.textContent.length > 0 &&
!/attempt/.test( thumbList[thumbIndex].link.textContent ) )
return;
//Keep a count of how many times we've tried to run this search
if( attempt >= maxAttempts )
{
thumbList[thumbIndex].link.textContent = " (failed after "+attempt+" attempts)";
return;
}
else if( attempt >= 0 )
{
thumbList[thumbIndex].link.textContent = " (attempt "+(++attempt)+")";
sourceTimer = setTimeout( (function(a,b){ return function(){ sourceSearch(a,b); }; })(thumbIndex,attempt), sourceTimeout*1000 );
}
// ...pixiv.net/img/artistName/... (source metatag doesn't work with capital letters)
var artistName = thumbList[thumbIndex].source.replace(/(.*\/img\/|\/.*)/g,'').replace(/[A-Z]+/g,'*');
var searchURL = thumbList[thumbIndex].source.replace(/[A-Z]+/g,'*').replace(/\?\d+$/,'')+"*";
var isMangaPage = ( thumbList[thumbIndex].source.indexOf("*") > 0 );
var wantedIndex = indexFromURL( thumbList[thumbIndex].source, isMangaPage );
if( illustList[wantedIndex] && illustList[wantedIndex].length > 0 )
{
if( illustList[wantedIndex].length == 1 )
{
//Found one post
thumbList[thumbIndex].link.textContent = " post #"+illustList[wantedIndex][0];
thumbList[thumbIndex].link.href = "http://danbooru.donmai.us/post/show/"+illustList[wantedIndex][0];
thumbList[thumbIndex].link.setAttribute("style",styleSourceFound);
}
else
{
//Found multiple posts
thumbList[thumbIndex].link.textContent = " ("+illustList[wantedIndex].length+" sources)";
thumbList[thumbIndex].link.href = "http://danbooru.donmai.us/post/index?tags=status:any+source:"+searchURL;
thumbList[thumbIndex].link.setAttribute("style",styleSourceFound);
}
thumbList.splice( thumbIndex, 1 );
if( attempt > 0 )
return sourceSearch();
}
else if( !isMangaPage && ( !artistList[artistName] || artistList[artistName].fullSearchLen < 0 ) )
{
//First time searching for this artist; try to find everything by this artist
artistList[artistName] = { fullSearchLen: -1 };
searchURL = "*.pixiv.net/img/"+artistName+"/*";
}
else if( (!isMangaPage && artistList[artistName].fullSearchLen < 100) ||
( illustList[wantedIndex] && illustList[wantedIndex].length == 0 ) )
{
//Either the full search didn't return the maximum amount or we did a specific search and still didn't find it.
thumbList[thumbIndex].link.textContent = " (no sources)";
thumbList[thumbIndex].link.setAttribute("style",styleSourceMissing);
thumbList.splice( thumbIndex, 1 );
if( attempt > 0 ) return sourceSearch();
}
if( attempt > 0 ) GM_xmlhttpRequest(
{
method: "GET",
url: 'http://danbooru.donmai.us/post/index.json?limit=100&tags=status:any+source:'+searchURL+"&login="+danbooruLogin+"&password_hash="+danbooruPassHash,
onload: function(responseDetails)
{
if( responseDetails.responseText.indexOf("<title>Downbooru</title>") > 0 )
{
//Danbooru is down; don't do any more queries.
thumbList[thumbIndex].link.textContent = " (Downbooru)";
thumbList[thumbIndex].link.setAttribute("style","color:blue; font-weight: bold;");
thumbList[thumbIndex].link.href = 'http://twitter.com/#!/search/danbooru%20from:teruyo%20within:15mi?q=danbooru+from%3Ateruyo+within%3A15mi';
return;
}
if( /^ *$/.test(responseDetails.responseText) )
return sourceSearch(thumbIndex, attempt);//Error, retry
var result, index;
try { result = JSON.parse(responseDetails.responseText); }
catch(err) { return sourceSearch(thumbIndex, attempt); /*Error, retry*/ }
var illustCache = JSON.parse( GM_getValue("illustCache","{}") );
//Remember the post IDs associated with each illustID
for( var i = 0; i < result.length; i++ )
{
index = indexFromURL(result[i].source, isMangaPage);
if( !illustList[index] )
illustList[index] = [ result[i].id ];
else if( illustList[index].indexOf( result[i].id ) < 0 )
illustList[index].push( result[i].id );
//Cache non-manga results
if( /\/\d+\./.test( result[i].source ) )
illustCache[index] = [ result[i].id ];
}
GM_setValue( "illustCache", JSON.stringify(illustCache) );
//If we're doing a full search, record total results to compare to 100 later
if( !isMangaPage && artistList[artistName].fullSearchLen < 0 )
artistList[artistName].fullSearchLen = result.length;
//If we're doing a specific search, note that no posts have that illust ID
else if( !illustList[wantedIndex] )
illustList[wantedIndex] = [];
return sourceSearch(thumbIndex, attempt);
},
onerror: function(x) { sourceSearch(thumbIndex, attempt); },
onabort: function(x) { sourceSearch(thumbIndex, attempt); }
});
}
function getMainTable(html)
{
if( !html )
{
var temp = document.evaluate(".//node()[contains(@class,'linkStyleWorks') or @id='search-result']/descendant-or-self::ul[not(contains(@class,'count-list'))]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
return !temp.snapshotLength ? null : temp.snapshotItem(temp.snapshotLength-1);
}
html = html.match( /.* (class="[^"]*linkStyleWorks"|id="search-result")([\s\S]+)/ );
if( !html || html.length < 3 )
return null;
//Remove /search.php?tag= showcase, which this script breaks anyway
html = html[2].replace(/[\s\S]*search_showcase.php/,'');
//Find start and stop of main list, including any sublists
var start = stop = html.indexOf("<ul");
while( html.indexOf( '<ul class="count-list">', stop ) >= 0 )
stop = html.indexOf("</ul>", stop) + 1;
return html.substring( start, html.indexOf( "</ul>", stop ) + 5 );
}
function getBottomPager(html)
{
if( !html )
{
var temp = document.evaluate(".//node()[@class='pages' or @class='pager']", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
return temp.snapshotItem(temp.snapshotLength-1);
}
var pager = html.match( / class="page.">(<(ul|ol).{1,3000}<\/(ul|ol)>)/ );
if( pager && pager.length > 1 )
return pager[1].replace(/<\/(ul|ol)>.+/,"</$1>");
return "x";
}
function getNextPage(html)
{
var page = (html || document.body.innerHTML).match( /class="page.".*<li[^>]*><a href="([^"]+p=\d+)"[^>]* rel="next"/ );
if( !page || page.length < 2 )
return null;
var temp = document.createElement("div");
temp.innerHTML = '<a href="'+page[1]+'"></a>';
return temp.firstChild.href;
}
function testScrollPosition()
{
if( !pending && window.pageYOffset + scrollBuffer > bottomDoc.offsetTop )
setTimeout( fetchNext, 0 );
}
function fetchNext()
{
var bottomPage, newTable, nextElem, pageLink;
pending = true;
var xhr = new XMLHttpRequest();
xhr.open( "GET", nextPage, true);
xhr.ontimeout = fetchNext;
xhr.onerror = fetchNext;
xhr.onload = function()
{
//Add page link
pageLink = mainTable.parentNode.appendChild( document.createElement("div") );
pageLink.setAttribute("style","font-size:large; text-align:left;");
pageLink.setAttribute("class","clear");
pageLink.innerHTML = '<hr style="clear: both;"><a href="'+nextPage+'">Page '+nextPage.replace(/.*p=([0-9]+)$/,'$1')+'</a>';
//Refresh the visible bottom paginator.
bottomDoc.innerHTML = getBottomPager(xhr.responseText);
//Add new content
var stuff = document.createElement("div");
stuff.style.visibility = "hidden";
stuff.innerHTML = getMainTable(xhr.responseText);
mainTable.parentNode.appendChild( stuff );
nextPage = getNextPage(xhr.responseText);
if( nextPage )
{
pending = false;
testScrollPosition();
}
else
{
pageLink = mainTable.parentNode.appendChild( document.createElement("div") );
pageLink.setAttribute("class","clear");
testScrollPosition = function() { }
}
};
xhr.send();
}
function updateCheck()
{
var scriptNum = 57567;
//Only check for update if using Greasemonkey and no check has been made in the last day.
if( typeof(GM_xmlhttpRequest) != "undefined" &&
parseInt( GM_getValue('last_check', 0) ) + 24*3600*1000 < new Date().getTime() )
{
GM_setValue( 'last_check', ''+new Date().getTime() );
GM_xmlhttpRequest(
{
method: 'GET',
url: 'http://userscripts.org/scripts/source/'+scriptNum+'.meta.js?'+new Date().getTime(),
headers: { 'Cache-Control': 'no-cache' },
onload: function(response)
{
var localVersion = parseInt( GM_getValue( 'local_version', 0 ) );
var remoteVersion = parseInt( /@uso:version\s*([0-9]+?)\s*$/m.exec(response.responseText)[1] );
if( !localVersion || remoteVersion <= localVersion )
GM_setValue( 'local_version', remoteVersion );
else if( confirm( 'There is an update available for the Greasemonkey script "'+/@name\s*(.*?)\s*$/m.exec(response.responseText)[1]+'".\nWould you like to go to the install page now?' ) )
{
GM_openInTab( 'http://userscripts.org/scripts/show/'+scriptNum );
GM_setValue( 'local_version', remoteVersion );
}
}
});
}
}
//So as I pray, Unlimited Pixiv Works.