// ==UserScript==
// @name Tumblr-image-sorter-post
// @description Store tags for images and indicate saved state
// @version 1.3.1
// @author Seedmanc
// @namespace https://github.com/Seedmanc/Tumblr-image-sorter
// @include http://*.tumblr.com/post/*
// @include http://*.tumblr.com/page/*
// @include http://*.tumblr.com/tagged/*
// @include http://*.tumblr.com/
// @include http://*.tumblr.com/image/*
// @include http://*.tumblr.com/search/*
// @include http*://www.tumblr.com/dashboard*
// @include http*://www.tumblr.com/tagged/*
//you can turn off the script for certain blogs by putting '// @exclude http://name.tumlbr.com/*' at a new line
// @exclude http*://*.media.tumblr.com/*
// @grant none
// @run-at document-start
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js
// @require https://greasyfork.org/scripts/11847-swfstore/code/SwfStore.js?version=77621
// @noframes
// ==/UserScript==
// ==Settings=====================================================
var highlightColor= 'black'; //Because chrome sucks it does not support outline-color invert which ensured visibility
// you'll have to specify a color to mark saved images and hope it won't blend with bg
var fixMiddleClick= true; //Because chrome sucks, it launches left onClick events for middle click as well,
// images open in the photoset viewer instead of a new tab as required for the script
// this option will 'fix' this by removing the view in photoset feature altogether
// alternatively you'll have to right-click open in a new tab instead
var enableOnDashboard= true; //Will try to collect post info from dashboard posts too
// might be slow and/or glitchy so made optional
var linkify= true; //Make every image (even inline images in non-photo posts) to be processed
// and linked to either itself, it's larger version or its reverse image search
// might break themes like PixelUnion Fluid
var debug= false; //Initial debug value, gets changed to settings value after DB creation
var storeUrl= '//dl.dropboxusercontent.com/u/74005421/js%20requisites/storage.swf';
//Flash databases are bound to the URL, must be same as in the other script
// ==/Settings====================================================
tagsDB=null;
var J=T=false;
var blogName=document.location.host;
var isImage=(document.location.href.indexOf('/image/')!=-1);
var isPost=(document.location.href.indexOf('/post/')!=-1);
var isDash=(blogName.indexOf('www.')==0);
var asked=false;
var posts=jQuery([]);
var progress=[];
window.onerror = function(msg, url, line, col, error) { //General error handler
var extra = !col ? '' : '\ncolumn: ' + col;
extra += !error ? '' : '\nerror: ' + error; //Shows '✗' for errors in title and also alerts a message if debug is on
if (msg.search('Script error')!=-1)
return true; // except for irrelevant errors
document.title+='✗';
if (debug)
alert("Error: " + msg + "\nurl: " + url + "\nline: " + line + extra)
else
throw error;
var suppressErrorAlert = true;
return suppressErrorAlert;
};
document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);
function getFname(fullName){ //Extract filename from image URL and format it
fullName=fullName||'';
fullName=fullName.replace(/(\?).*$/gim,''); //First remove url parameters
if (fullName.indexOf('tumblr_')!=-1)
fullName=fullName.replace(/(tumblr_)|(_\d{2}\d{0,2})(?=\.)/gim,'') //Prefix and postfix of tumblr image names can be omitted without info loss
else if (fullName.indexOf('xuite')!=-1) { //this hosting names their images as "(digit).jpg" causing filename collisions
i=fullName.lastIndexOf('/');
fullName=fullName.substr(0,i)+'-'+fullName.substr(i+1); //add parent catalog name to the filename to ensure uniqueness
};
return fullName.split('/').pop();
};
function getID(lnk){ //Extract numerical post ID from self-link
if (lnk.search(/[^0-9]/g)==-1)
return lnk; //Sometimes the argument is the ID itself that needs checking
Result=lnk.substring(lnk.indexOf('/post/')+7+lnk.indexOf('image/')); //one of those will be -1, another the actual offset
Result=Result.replace(/(#).*$/gim,''); //remove url postfix
i=Result.lastIndexOf('/');
if (i!=-1)
Result=Result.substring(0,i);
if ((Result=='')||(Result.search(/[^0-9]/g)!=-1))
throw new Error('IDentification error: '+Result)
else
return Result;
};
function identifyPost(i){ //Find the ID of post in question and request info via API for it
var post=posts.eq(i);
if (isDash) {
slflnk=post.find('a.post_permalink')[0];
blogName=slflnk.hostname; //On dashboard every post might have a different author
id=getID(slflnk.href);
} else if (isPost)
id=getID(document.location.href) //Even simpler on the post page
else { // but it gets tricky in the wild
id='';
h=post.find("a[href*='"+blogName+"/post/']"); //Several attempts to find selflink
h=(h.length)?h:post.next().find("a[href*='"+blogName+"/post/']"); //workaround for Optica and seigaku themes that don't have selflinks within post elements
h=(h.length)?h:post.find("a[href*='"+blogName+"/image/']"); //IDs can be found both in links to post and to image page
if (h.length)
id=getID(h[h.length-1].href);
if (id == '') { //If no link was found, try to find ID in attributes of nodes
phtst=post.find("div[id^='photoset']"); // photosets have IDs inside, well, id attributes starting with photoset_
pht=post.attr('id'); // single photos might have ID inside same attribute
if (phtst.length)
id=phtst.attr('id').split('_')[1]
else if (pht)
id=getID(pht)
else {
throw new Error('IDs not found');
};
};
};
jQuery.ajax({ //get info about current post via tumblr API based on the ID
type:'GET',
url: "//api.tumblr.com/v2/blog/"+blogName+"/posts/photo",
dataType:'jsonp',
data: {
api_key : "fuiKNFp9vQFvjLNvx4sUwti4Yb5yGutBN4Xh10LXZhhRKjWlV4",
id: id
}
}).done(function(result) {process({r:result, i:i});}) //Have to return two values at once, data and pointer to post on page
.fail(function(jqXHR, textStatus, errorThrown) {throw new Error(textStatus+' in post #'+i) ;}) ;
};
function loadAndExecute(url, callback){ //Load specified js library and launch a function after that
var scriptNode = document.createElement ("script");
scriptNode.addEventListener("load", callback);
scriptNode.onerror=function(){
throw new Error("Can't load "+url);
};
scriptNode.src = url;
document.head.appendChild(scriptNode);
};
function main(){ //Search for post IDs on page and call API to get info about them
if (debug)
jQuery("div[id^='SwfStore_animage_']").css({'top':'0','left':'0',"position":'absolute','opacity':'0.8'});
else //Bring the flash window in or out of the view depending on the debug mode
jQuery("div[id^='SwfStore_animage_']").css({'top':'-2000px', 'left':'-2000px', "position":'absolute'});
if (isDash)
posts=jQuery('ol.posts').find('div.post').not('.new_post') //Getting posts on dashboard is straightforward with its constant design,
else { // but outside of it are all kinds of faulty designs, so we have to experiment
posts=jQuery('article.entry > div.post').not('.n').parent(); //Some really stupid plain theme has to be checked before everything
posts=(posts.length)?posts:jQuery('.post').not('#description'); //General way to obtain posts that are inside containers with class='post'
if (isImage)
if (tagsDB.get(getFname(jQuery('img#content-image')[0].src)))
document.location.href=jQuery('img#content-image')[0].src //Proceed directly to the image if it already has a DB record with tags
else
posts=$('<div><a href="'+document.location.href+'" >a</a></div>'); //Make it work also on image pages, since we can get post id from url
posts=posts.length?posts:jQuery('.column').eq(2).find('.bottompanel').parent();
//for "Catching elephant" theme
posts=posts.length?posts:jQuery('[id="post"]'); //for "Cinereoism" that uses IDs instead of Classes /0
posts=posts.length?posts:jQuery('[id="designline"]'); //the Minimalist, not tested though and saved indication probably won't work
posts=posts.length?posts:jQuery("article[class^='photo']"); //alva theme for ge
posts=posts.length?posts:jQuery('[id="posts"]'); //tincture pls why are you doing this
posts=posts.length?posts:jQuery("div.posts").not('#allposts'); //some redux theme, beats me
posts=posts.length?posts:jQuery("article[class^='post-photo']"); //no idea what theme, uccm uses it
posts=posts.length?posts:jQuery('div[id="entry"]'); //seigaku by sakurane, dem ids again
if (posts.length==0){
document.title+=' [No posts found]'; //Give up
return;
};
};
if (!isImage) {
hc=posts.find('.hc.nest');
if (hc.length) {
hc.css('position','relative'); //Fix 'broken' themes with image links being under a large div
posts=hc.parent(); // because sharing is caring
};
};
posts.each(identifyPost);
};
function mkUniq(arr){ //Sorts an array and ensures uniqueness of its elements
to={};
jQuery.each(arr, function(i,v){
to[v.toLowerCase()]=true;});
arr2=Object.keys(to);
return arr2;
};
function mutex(){ //Check readiness of libraries being loaded simultaneously
if (J&&T){
J=T=false;
main(); //when everything is loaded, proceed further
}
};
function onDOMContentLoaded(){ //Load plugins
if (isDash && !enableOnDashboard) //don't run on dashboard unless enabled
return;
if (jQuery.fn.jquery.split('.')[1]<5) { //@require doesn't load jQuery if it's already present on the site
loadAndExecute('//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js', function(){
$.noConflict(); // but existing version might be older than required (1.5)
J=true;
mutex(); // force load the newer jQuery if that's the case
});
}
else
J=true;
try{
loadTagsDB('animage'); //This is some weird shit
} catch(err) { //Apparently launching the script at document-body sometimes can be too early
delete tagsDB; //so flashDB fails to attach itself to the page for the lack of body (thanks chrome)
jQuery("object[id^='SwfStore_animage_']").remove(); //schedule a second attempt to window.load to be sure
delete window.SwfStore['animage']; //get rid of failed remains
jQuery(window).load(function(){
loadTagsDB('animage');
});
};
};
function loadTagsDB(nmspc){ //Main tag database, holds pairs "filename {s:is_saved?1:0,t:'tag1,tag2,...,tagN'}"
tagsDB = new SwfStore({
namespace: nmspc,
swf_url: storeUrl,
debug: debug,
onready: function(){
debug=(tagsDB.get(':debug:')=='true'); //Update initial debug value with the one saved in DB
tagsDB.config.debug=debug;
T=true;
mutex();
},
onerror: function() {
document.title="✗ tagsDB error";
throw new Error('tagsDB failed to load');
}
});
};
function process(postData) { //Process information obtained from API by post ID
post=posts.eq(postData.i); //pointer to post on page
res=postData.r; //API response
var link_url='';
var inlimg=[];
var photos=0;
var img=jQuery([]);
var bar='';
if (res.meta.status!='200') { //I don't even know if this is reachable
throw new Error('API error: '+res.meta.msg);
return;
};
var isPhoto=res.response.posts[0].type=='photo';
if (linkify) { //Find inline images
inlimg=post.find('img[src*="tumblr_inline_"]');
inlimg=jQuery.grep(inlimg, function(vl,ix) {
if (vl.src.search(/(_\d{2}\d{0,2})(?=\.)/gim)!=-1) {
href=vl.src.replace(/(_\d{2}\d{0,2})(?=\.)/gim,'_1280'); //If there is an HD version, link it
if (vl.src.split('.').pop()=='gif')
href=vl.src; //except for gifs
r=true;
bar='('+inlimg.length+')';
}
else {
href='http://www.google.com/searchbyimage?sbisrc=cr_1_0_0&image_url='+escape(vl.src);
r=false; // otherwise link to google reverse image search
};
a='<a href="'+href+'" style=""></a>';
i=jQuery(vl);
x=i.parent().is('a')?i:i.parent().parent().is('a')?i.parent():i; //I dunno either
if ((x.parent().is('a'))||(i.width()<128)) // basically either direct parent or grandparent of the image can be a link already
return false; // in which case we need to skip processing to avoid problems
// as well as if the image was in fact a button and not a part of the post
i.wrap(a);
if (typeof pxuDemoURL !== 'undefined' && pxuDemoURL=="fluid-theme.pixelunion.net")
i.parent().css('position','relative'); //Fix for PixelUnion Fluid which otherwise gets rekt if you insert a link
return r;
});
};
if (!isPhoto) { //Early termination if there are no images at all
if ((!linkify)||(inlimg.length==0)) { // or if processing is disabled
progressBar(' ',postData.i);
return;
};
} else {
photos=res.response.posts[0].photos.length; //Find whether this is a single photo post or a photoset
if (photos>1) {
img=post.find('iframe.photoset').contents();
img=img.length?img:post.find('figure.photoset');
if (img.length==0) //Some photosets are in iframes, some aren't
img=post.find("div[id^='photoset'] img")
else
img=img.find('img');
img=img.not('img[src*="tumblr_inline_"]');
} else {
link_url+=res.response.posts[0].link_url; //For a single photo post, link url might have the highest-quality image version,
ext=link_url.split('.').pop(); // unaffected by tumblr compression
r=/(jpe*g|bmp|png|gif)/gi; // check if this is actually an image link
link_url=(r.test(ext))?link_url:'';
img=post.find('img[src*="tumblr_"]').not('img[src*="tumblr_inline_"]'); //Find image in the post to linkify it
if (img.length && linkify) {
p=img.parent().wrap('<p/>'); //Parent might be either the link itself or contain it as a child,
lnk=p.parent().find('a[href*="/image/"]'); // depends on particular theme
lnk=(lnk.length)?lnk:p.parent().find('a[href*="'+res.response.posts[0].link_url+'"]');
lnk=(lnk.length)?lnk:p.parent().find('a[href*="'+res.response.posts[0].photos[0].original_size.url+'"]');
if ((lnk.length) && (lnk[0].href))
lnk[0].href=link_url?link_url:res.response.posts[0].photos[0].original_size.url
else if (typeof pxuDemoURL == 'undefined')
img.wrap('<a href="'+res.response.posts[0].photos[0].original_size.url+'"></a>');
p.unwrap(); //^ this might break themes like Fluid by PU
};
};
bar=String.fromCharCode(10111+photos); //Piece of progressbar, (№) for amount of photos in a post
// empty space for non-photo posts, ✗ for errors
};
img=jQuery(img.toArray().concat(inlimg)); //Make sure the resulting list of images is in order
tags=res.response.posts[0].tags; //get tags associated with the post
DBrec={s:0, t:tags.toString().toLowerCase()}; //create an object for database record
for (j=0; j<photos+inlimg.length; j++) {
if (j<photos) //First come the images in photo posts if exist
url=(link_url)?link_url:res.response.posts[0].photos[j].original_size.url
else // then the inline ones
url=img.eq(j).parent().attr('href');
tst=tagsDB.get(getFname(url)); //Check if there's already a record in database for this image
if (tags.length) {
if (tst) { // if there is we need to merge existing tags with newfound ones
oldtags=JSON.parse(tst).t.split(',');
newtags=mkUniq(oldtags.concat(tags));
DBrec.t=newtags.toString().toLowerCase();
DBrec.s=parseInt(JSON.parse(tst).s);
} else
DBrec.s=0;
tagsDB.set(getFname(url), JSON.stringify(DBrec));
if (tagsDB.get(getFname(url))!=JSON.stringify(DBrec)) //Immediately check whether the write was successful
if (!debug && !asked) // if not and no debug mode enabled, prompt to enable it
if (confirm('Failed writing to DB. Flashcookies size limit might have been hit.\n Would you like to enable debug mode to get a possibility to fix that? (Will reload the page)')) {
tagsDB.set(':debug:','true');
location.reload();
asked=true; //avoid asking multiple times for every post
} else
throw new Error('Failed to write to DB')
else if (!asked){ // if already in debug, try to bring the flash window into view
alert('Failed writing to DB. Flashcookies size limit might have been hit. If you see a flash dialog window at the top-left corner, try raising the limit.');
window.scrollTo(0, 0);
asked=true;
};
};
if ((tst)&&(JSON.parse(tst).s=='1')&&(!isImage)) //Otherwise if there is a record and it says the image has been saved
img.eq(j).css('outline','3px solid '+highlightColor).css('outline-offset','-3px');
//Add a border of highlight color around the image to indicate that
if ((photos>1)&&(j<photos)) {
y=img.eq(j).parent();
y=y.is('a')?y:y.parent().find('a').eq(0); //Look for a link either directly above the image or around it
if (y.is('a'))
removeEvents(y[0]); //get rid of that annoying photoview feature
};
};
if (isImage) //Redirect to actual image from image page after we got the ID
document.location.href=jQuery('img#content-image')[0].src;
progressBar((tags.length)?bar:'-', postData.i); //dash indicates no found tags for the post
};
function progressBar(bar, i){ //Outputs a piece of progress bar at a correct place in title
progress[i]=bar;
document.title='▶['+progress.join('');
if (progress.length==posts.length)
document.title+=']■';
};
function removeEvents(node){ //Remove event listeners such as onclick, because in chrome they mess with middlebutton new tab opening
if (fixMiddleClick) {
jQuery(node).attr('target','_blank');
elClone = node.cloneNode(true);
node.parentNode.replaceChild(elClone, node);
};
};
//TODO: add support for custom domains
//TODO: add tags retrieval from reblog source if no tags were found here
//TODO: output FlashDB messages to flash window instead of console on debug.
//TODO: implement some kind of feedback from flash to script about space request success
//TODO: check if the actual width of an image to be linked is within limits of the _ postfix, because tumblr lies
//TODO: store post ID and blog name for images? Will make it possible to have a backlink from image page
//TODO: only call API if no DB record was found for images in the current post (requires ^)