Endless Pixiv Pages

By Mango Last update May 19, 2012 — Installed 7,890 times.

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.