Tumblr-image-sorter-get

Format file name & save path for current image by its tags

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name		Tumblr-image-sorter-get
// @description	Format file name & save path for current image by its tags
// @version	    1.3.1.0
// @author		Seedmanc
// @namespace	https://github.com/Seedmanc/Tumblr-image-sorter

// @include		http*://*.amazonaws.com/data.tumblr.com/* 
// @include		http*://*.media.tumblr.com/*
//these sites were used by animage.tumblr.com to host original images
// @include		http://scenario.myweb.hinet.net/*										
// @include		http*://mywareroom.files.wordpress.com/*
// @include		http://e.blog.xuite.net/* 
// @include		http://voice.x.fc2.com/*

// @grant 		none 
// @require 	https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js
// @require 	https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js
// @require 	https://greasyfork.org/scripts/11847-swfstore/code/SwfStore.js?version=77621 
// @require 	https://greasyfork.org/scripts/11848-downloadify-clip/code/Downloadify%20+%20Clip.js?version=68937
// @run-at 		document-start
// @noframes
// ==/UserScript==
													
// ==Settings=====================================================

	var root=			'E:\\#-A\\!Seiyuu\\';								//Main collection folder
																			//Make sure to use double backslashes instead of single ones everywhere	
	var ms=				'!';												//Metasymbol, denotes folders for categories instead of names, must be their first character
	
	var folders=		{													//Folder and names matching database
		"	!!group	"	:	"		!!group	",								// used both for tag translation and providing the list of existing folders
		"	!!solo	"	:	"		!!solo	",								// trailing whitespaces are voluntary in both keys and values,
		"	!!unsorted"	:	"		!!unsorted	", 							// first three key names are not to be changed, but folder names can be anything
		"	原由実		"	:	"	 !iM@S\\Hara Yumi",					      // subfolders for categories instead of names must have the metasymbol as first symbol
		"	今井麻美	"	:	"	!iM@S\\Imai Asami	",
		"	沼倉愛美	"	:	"	!iM@S\\Numakura Manami",
		"	けいおん!	"	:	"	!K-On	",								 //Category folders can have their own tag, which, if present, will affect the folder choice
		"	日笠陽子	"	:	"	!K-On\\Hikasa Yoko	",					 // for solo and group images
		"	寿美菜子	"	:	"	!K-On\\Kotobuki Minako",
		"	竹達彩奈	"	:	"	!K-On\\Taketatsu Ayana",
		"	豊崎愛生	"	:	"	!K-On\\Toyosaki Aki	",
		"	クリスマス	"	:	"	 !Kurisumasu	",
		"	Lisp	"		:	"	!Lisp	",								//Roman tags can be used as well
		"	阿澄佳奈	"	:	"	!Lisp\\Asumi Kana	",
		"	酒井香奈子	"	:	"	!Lovedoll\\Sakai Kanako",
		"	らき☆すた	"	:	"	!Lucky Star	",
		"	遠藤綾		"	:	"	 !Lucky Star\\Endo Aya	",
		"	福原香織	"	:	"	!Lucky Star\\Fukuhara Kaori",
		"	長谷川静香	"	:	"	!Lucky Star\\Hasegawa Shizuka",
		"	加藤英美里	"	:	"	!Lucky Star\\Kato Emiri	",
		"	今野宏美	"	:	"	!Lucky Star\\Konno Hiromi	", 
		"	井上麻里奈	"	:	"	!Minami-ke\\Inoue Marina	",
		"	佐藤利奈	"	:	"	!Minami-ke\\Sato Rina	",
		"	Petit Milady	":	"	!Petit Milady	", 
		"	悠木碧		"	:	"	 !Petit Milady\\Yuuki Aoi	",
		"	ロウきゅーぶ! "	:	"	!Ro-Kyu-Bu	",
		"	Kalafina 	"	:	"	!Singer\\Kalafina	",
		"	LiSA		"	:	"	!Singer\\LiSA	",
		"	May'n		"	:	"	!Singer\\May'n	", 
		"	茅原実里	"	:	"	!SOS-dan\\Chihara Minori",
		"	後藤邑子	"	:	"	!SOS-dan\\Goto Yuko	",
		"	平野綾		"	:	"	 !SOS-dan\\Hirano Aya	", 
		"	スフィア	"	:	"	 !Sphere	", 
		"	やまとなでしこ "	:	"	!Yamato Nadeshiko	",
		"	堀江由衣	"	:	"	!Yamato Nadeshiko\\Horie Yui",
		"	田村ゆかり	"	:	"	!Yamato Nadeshiko\\Tamura Yukari",
		"	雨宮天	"		:	" 	Amamiya Sora	",
		"	千葉紗子	"	:	"	Chiba Saeko	",
		"	渕上舞		"	:	"	 Fuchigami Mai	",
		"	藤田咲		"	:	"	 Fujita Saki	",
		"	後藤沙緒里	"	:	"	Goto Saori	",
		"	花澤香菜	"	:	"	Hanazawa Kana	",
		"	早見沙織	"	:	"	Hayami Saori	",
		"	井口裕香	"	:	"	Iguchi Yuka	",
		"	井上喜久子	"	:	"	Inoue Kikuko	",
		"	伊藤かな恵	"	:	"	Ito Kanae	",
		"	伊藤静		"	:	"	 Ito Shizuka	",
		"	門脇舞以	"	:	"	Kadowaki Mai	",
		"	金元寿子	"	:	"	Kanemoto Hisako	",
		"	茅野愛衣	"	:	"	Kayano Ai	",
		"	喜多村英梨	"	:	"	Kitamura Eri	",
		"	小林ゆう	"	:	"	 Kobayashi Yuu	",
		"	小清水亜美	"	:	"	Koshimizu Ami	",
		"	釘宮理恵	"	:	"	Kugimiya Rie	",
		"	宮崎羽衣	"	:	"	Miyazaki Ui	",
		"	水樹奈々	"	:	"	Mizuki Nana	",
		"	桃井はるこ	"	:	"	Momoi Haruko	",
		"	中原麻衣	"	:	"	Nakahara Mai	",
		"	中島愛		"	:	"	 Nakajima Megumi	",
		"	名塚佳織	"	:	"	Nazuka Kaori	",
		"	野川さくら	"	:	"	 Nogawa Sakura	",
		"	野中藍		"	:	"	 Nonaka Ai	",
		"	能登麻美子	"	:	"	Noto Mamiko	",
		"	折笠富美子	"	:	"	Orikasa Fumiko	",
		"	朴璐美		"	:	"	 Paku Romi	",
		"	榊原ゆい	"	:	"	Sakakibara Yui	",
		"	坂本真綾	"	:	"	Sakamoto Maaya	",
		"	佐倉綾音	"	:	"	Sakura Ayane	",
		"	沢城みゆき	"	:	"	Sawashiro Miyuki	",
		"	椎名へきる	"	:	"	Shiina Hekiru	",
		"	清水愛		"	:	"	 Shimizu Ai	",
		"	下田麻美	"	:	"	Shimoda Asami	",
		"	新谷良子	"	:	"	Shintani Ryoko	",
		"	白石涼子	"	:	"	Shiraishi Ryoko	",
		"	田中理恵	"	:	"	Tanaka Rie	",
		"	丹下桜		"	:	"	 Tange Sakura	",
		"	東山奈央	"	:	"	Toyama Nao	",
		"	植田佳奈	"	:	"	Ueda Kana	",
		"	上坂すみれ	"	:	"	Uesaka Sumire	",
		"	ゆかな		"	:	"	 Yukana	"
	};

	var ignore=			"歌手, seiyuu, 声優";									//These tags will not count towards any category and won't be included into filename	

	var allowUnicode=	false;												//Whether to allow unicode characters in manual translation input, not tested
	
	var useFolderNames=	true;												//In addition to tags listed in keys of the folders object, recognize also folder names themselves
																			// this way you won't have to provide both roman and kanji spellings for names as separate tags

	var debug=			false;												//Initial debug state, affects creation of flashDBs. Value saved in the DB overrides it after DB init.
	
	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;																//Makes sure databases are accessible from console for debugging
   names=null ;
   meta=null ; 		
var title;
var filename;															
var folder = ''; 
var DBrec='';																//Raw DB record, stringified object with fields for saved flag and tag list
var N=M=T=false;															//Flags indicating readiness of plugins loaded simultaneously
var exclrgxp=/%|\/|:|\||>|<|\?|"|\*/g;										//Pattern of characters not to be used in filepaths
var	downloadifySwf=	'//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.swf';			
																			//Flash button URL

var style={																	//In an object so you can fold it in any decent editor. If only you had that in chrome.
	s:"		 							\
	div#output {						\
		position: absolute;				\
		left: 0;		top: 0;			\
		width: 100px;	height: 30px;	\
	}									\
	div#down {							\
		left: 1px;						\
		position: fixed;				\
		z-index: 98;					\
	}									\
	table#port {						\
		top: 30px;						\
		left: 1px;						\
		position: fixed;				\
		background-color: 				\
			rgba(192,192,192,0.85);		\
		border-bottom: 1px solid black;	\
		z-index: 97;					\
		width: 100px;					\
		border-collapse: collapse;		\
	}									\
	table#translations {				\
		position: absolute;				\
		background-color:				\
			rgba(255,255,255,0.8);		\
		top: 48px;						\
		overflow: scroll;				\
		font-size: 90%;					\
		margin-left: -1px;				\
		width: 103px;					\
		table-layout: fixed;			\
	}									\
	td.settings {						\
		border-left:  1px solid black;	\
		border-right: 1px solid black;	\
	}									\
	a.settings {						\
		text-decoration: none;			\
	}									\
	table, tr {							\
		text-align: center;				\
	}									\
	td#ex {								\
		padding: 0;						\
	}									\
	input.txt {							\
		width: 95%;						\
	}									\
	td.cell, td.radio{					\
		border: 1px solid black;		\
		overflow:	hidden;				\
	}									\
	table.cell {						\
		background-color: 				\
			rgba(255,255,255,0.75);		\
		width: 100%; 					\
		border-collapse: collapse;		\
	}									\
	a {									\
		font-family: Arial;				\
		font-size:   small;				\
	}									\
	th {								\
		border: 0;						\
		color:black;		\
	}									\
	input#submit {						\
		width:  98%;					\
		height: 29px;					\
	}									\
"};																			//This certainly needs optimisation
		
var out=$('<div id="output"><div id="down"></div></div>');					//Main layer that holds the GUI 
var tb =$('<table id="translations">');										//Table for entering manual translation of unknown tags

var	tagcell='<table class="cell"><tr>														\
		<td class="radio"><input type="radio" class="category"  value="name"/></td>			\
		<td class="radio"><input type="radio" class="category"  value="meta"/></td>			\
	</tr><tr>																				\
		<td colspan="2"><a href="#" title="Click to ignore this tag for now" class="ignr">';
																			//Each cell has the following in it:
																			//	two radiobuttons to choose a category for the tag - name or meta
																			//	the tag itself, either in roman or in kanji
																			//		the tag is also a link, clicking which removes the tag from results until refresh
																			//	if the tag is in kanji, cell has a text field to input translation manually
																			// 		if there are also roman tags, they are used as options for quick input into the text field
																			//	if the tag is in roman and consists of two words, cell has a button enabled to swap their order
																			//		otherwise the button is disabled
var tfoot=$('<tfoot><tr><td>														\
	<input type="submit" id="submit" value="submit">								\
</td></tr></tfoot>');														//At the bottom of the table there is the "submit" button that applies changes
var thead=$('<thead><tr><td			>												\
	<table class="cell" style="font-width:95%; font-size:small;">					\
		<tr class="cell"><th class="cell">name</th><th class="cell">meta</th></tr>	\
	</table>																		\
</td></tr></thead>');

tb.append(thead).append(tfoot).hide();

	
port=document.createElement('table');										//Subtable for settings and im/export of tag databases
	row= port.insertRow(0);
	cell=row.insertCell(0);
	cell.setAttribute('class','settings');
	cell.innerHTML=' <a href="##" onclick=toggleSettings() class="settings">- settings -</a> ';	
	row0=port.insertRow(1);
	row0.insertCell(0).innerHTML='<input type="checkbox" id="debug"/> debug';	
	row1=port.insertRow(2);
	row1.insertCell(0).innerHTML=' <a href="###" onclick=ex() id="aex" class="exim">export db</a>';
	row2=port.insertRow(3);	
	row2.insertCell(0).id='ex';
	row3=port.insertRow(4);
	row3.insertCell(0).innerHTML=' <a href="####" onclick=im() id="aim" class="exim">import db</a> ';	
	row4=port.insertRow(5);
	row4.insertCell(0).id='im';
	port.id='port';

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 in debug mode
   if (msg.search('this.swf')!=-1) 
	 return true;															//Except for irrelevant errors
   document.title+='✗';
   if (debug)
   	 alert("Error: " + msg + "\nurl: " + url + "\nline: " + line + extra);
   var suppressErrorAlert = true;
   return suppressErrorAlert;
};

var xhr = new XMLHttpRequest();												//Redownloads opened image as blob 
	xhr.responseType="blob";												// so that it would be possible to get it via downloadify button
	xhr.onreadystatechange = function() {									// supposedly the image is being taken from cache so it shouldn't cause any slowdown
		if (this.readyState == 4 && this.status == 200) {
			var blob=this.response;
			var reader = new window.FileReader();
			reader.readAsDataURL(blob); 
			reader.onloadend = function() {
				base64data = reader.result;                
				base64data=base64data.replace(/data\:image\/\w+\;base64\,/,"");
				dl(base64data);												//Call the button creation function
		}
	} else if ((this.status!=200)&&(this.status!=0)) {
		if (this.status==404) {
			document.title='Error '+this.status;
			throw new Error('404');
		};
		throw new Error('Error getting image: '+this.status);
	};							
};

function expandFolders(){													 //Complement DB with tags produced from folders names
	var t,rx,x;
	for (var key in folders) {													 
		if (folders.hasOwnProperty(key)&&(['!group','!solo','!unsorted'].indexOf(key)==-1)) { 
			t=folders[key];		
			rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');			
			x=getFileName(t).toLowerCase().replace(rx,'');
			folders[x]=t;
		};
	};													 
}; 	
  
rootrgxp=/^([a-z]:){1}(\\[^<>:"/\\|?*]+)+\\$/gi;
try {
	if (!(rootrgxp.test(root))) 
		throw new Error('Illegal characters in root folder path: "'+root+'"');
	ms=ms[0];																//It's a symbol, not a string, after all
	if ((exclrgxp.test(ms))||(/\\|\s/.test(ms)))  
		throw new Error ('Illegal character as metasymbol: "'+ms+'"');
} catch (err) {
	if (!debug)
		alert(err.name+': '+err.message);									 
	throw err;
};				

function checkMatch(obj,fix){												//Remove trailing whitespace in object keys and values & check correctness of user input 
	fix=fix||false;
  try {																		//make sure that folder names have no illegal characters
	for (var key in obj) {													//Convert keys to lower case for better matching
		if (obj.hasOwnProperty(key)) { 
			t=obj[key].trim().replace(/^\\|\\$/g, '').trim();			
			delete obj[key];
			k=key.trim().toLowerCase();
			obj[k]=t;
			if (exclrgxp.test(obj[k]))  									//Can't continue until the problem is fixed
				if (!fix)
					throw new Error('Illegal characters in folder name entry: "'+obj[k]+'" for name "'+k+'"')
				else
					obj[k]=t.replace(exclrgxp, '-');
		};
	};
  } catch (err) {
	if (!debug)
		alert(err.name+': '+err.message);									//Gotta always notify the user 
	throw err;
  };														//TODO: even more checks here
}; 															

function toggleSettings(){													//Show drop-down menu with settings
	$('table#port td').not('.settings').toggle();
	$('table#translations').css('top',($('table#port').height()+30)+'px');
	sign=$('a.settings').eq(0);
	if (sign.text().search(/\+/,'-')!=-1) {
		sign.text(sign.text().replace(/\+/gi,'-'));
		$('td.settings').css('border-bottom','');
	}
	else {
		sign.text(sign.text().replace(/\-/gi,'+'));
		$('td.settings').css('border-bottom','1px solid black');
	}
};

function debugSwitch(checkbox){												//Toggling debug mode requires page reload
	debug = checkbox.checked;
	tagsDB.set(':debug:',debug );
	location.reload();
};

onDOMcontentLoaded();
function onDOMcontentLoaded(){ 												//Load plugins and databases

	checkMatch(folders);													//Run checks on user-input content and format it
	if (useFolderNames)
		expandFolders();
	ignore=$.map(ignore.split(','), function(v,i){
		return v.trim().toLowerCase();
	});
	
	href=document.location.href;
	if (href.indexOf('tumblr')==-1) 										//If not on tumblr
		if (!(/(jpe?g|bmp|png|gif)/gi).test(href.split('.').pop()))			// check if this is actually an image link
			return;
	$('img').wrap("<center></center>");
	$('body').append(out);

	names = new SwfStore({													//Auxiliary database for names that don't have folders
		namespace: "names",
		swf_url: storeUrl,  
		onready: function(){
			document.title+=(debug)?' NM ':'';
			N=true;
			mutex();
		},
		onerror: function() {
			document.title+=' ✗ names failed to load';}
	});

	meta = new SwfStore({													//Auxiliary DB for meta tags such as franchise name or costume/accessories
		namespace: "meta",
		swf_url: storeUrl,   
		onready: function(){	
			M=true;
			mutex();
		},
		onerror: function() {
			document.title+=' ✗ meta failed to load';}
	});
																			
	tagsDB = new SwfStore({													//Loading main tag database, holds pairs "filename	{s:is_saved?1:0,t:'tag1,tag2,...,tagN'}"
		namespace: "animage",
		swf_url: storeUrl,   
		onready: function(){ 
			document.title+=(debug)?' T ':'';
			debug =(tagsDB.get(':debug:')=='true');							//Override initial debug state with the one stored in DB
			tagsDB.config.debug=debug;
			getTags();
		},
		debug: debug,
		onerror: function() {
			document.title='tagsdb error';
			throw new Error('tagsDB failed to load');
		}
	});										//TODO: delay aux DBs loading until & if they're actually needed? 
};

function getTags(retry){													//Manages tags acquisition for current image file name from db
	DBrec=JSON.parse(tagsDB.get(getFileName(document.location.href)));			// first attempt at getting taglist for current filename is done upon the beginning of image load
	if ((DBrec!=null) || (debug)) {											// if tags are found report readiness
		T=true;																// or if we're in debug mode, proceed anyway
		mutex();		
	} else 
		if ((retry) || (document.readyState=='complete'))					//Otherwise if we ran out of attempts or it's too late 
			return															// stop execution
		else {
			retry=true;														// but if not schedule the second attempt at retrieving tags to image load end
			window.addEventListener('load',function(){ getTags(true);},false);
		};										
};										//TODO: make getTags actually return  the value to main() to get rid of the global var

function mutex(){															//Check readiness of plugins and databases when they're loading simultaneously 
	if (N && M && T) {														// when everything is loaded, proceed further
		N=M=T=false;
		main();
	};
};
	
function main(){ 															//Launch tag processing and handle afterwork
	$("<style>"+style.s+"</style>" ).appendTo( "head" );					//assign functions to events and whatnot
	$('div#output').append(port);	
	toggleSettings();	
	$('input#debug').prop('checked',debug);	
	$('a#aim')[0].onclick=im; 
 	$('a#aex')[0].onclick=ex; 
	$('a.settings')[0].onclick=toggleSettings;
	$('input#debug')[0].onclick=function(){debugSwitch(this);};

	if (debug) 
		$("div[id^='SwfStore_animage_']").css('top','0').css('left','101px').css("position",'absolute').css('opacity','0.7');
											//TODO: make the code above run regardless of found DB record
	$('div#output').append(tb);
	analyzeTags();
	$('input#submit')[0].onclick=submit; 
	$('input.txt').on('change',selected);

	xhr.open("get", document.location.href, true); 							//Reget the image to attach it to downloadify button
	xhr.send();
	
	$(window).load(function(){document.title=title;});
};

function isANSI(s) {														//Some tags might be already in roman and do not require translation
	is=true;
	s=s.split('');
	$.each(s,function(i,v){
		is=is&&(/[\u0000-\u00ff]/.test(v));});
    return is;
};

function analyzeTags() {   													//This is where the tag matching magic occurs
	filename=getFileName(document.location.href, true);
 	if (!DBrec) return;														// if there are any tags, that is
	folder='';

    if (debug)
		document.title=JSON.stringify(DBrec,null,' ')+' '					//Show raw DB record 
	else
		document.title='';												
	
	tags=DBrec.t.split(',');
 
	fldrs=[];
	nms=[];
	mt=[];
	ansi={}
	rest=[];
	
	tags=$.map(tags,function(v,i){											//Some formatting is applied to the taglist before processing

		v=v.replace(/’/g,"\'").replace(/"/g,"''");					
		v=v.replace(/\\/g, '-');									
		v=v.replace(/(ou$)|(ou )/gim,'o ').trim();							//Eliminate variations in writing 'ō' as o/ou at the end of the name in favor of 'o'
																			// I dunno if it should be done in the middle of the name as well		
		sp=v.split(' ');	
		if (sp.length>1) 
			$.each(tags, function(ii,vv){
				if (ii==i) return true;
				if (sp.join('')==vv) 
					return v=false;											//Some bloggers put kanji tags both with and without spaces, remove duplicates with spaces
				}
			); 
		
		if (!v) 
			return null;
																
		if ((ignore.indexOf(v)!=-1)||(ignore.indexOf(v.split(' ').reverse().join(' '))!=-1))
			return null														//Remove ignored tags so that they don't affect the tag amount
		else return v;
	});		
																			//1st sorting stage, no prior knowledge about found categories
	$.each(tags, function(i,v){ 											//Divide tags for the image into 5 categories
		if (folders.hasOwnProperty(v)) 										//	the "has folder" category
			fldrs.push(folders[v])
		else if (names.get(v)) 												//	the "no folder name tag" category
			nms.push(names.get(v))
		else if (meta.get(v))												//	the "no folder meta tag" category,
			mt.push(meta.get(v))											// which doesn't count towards final folder decision, but simply adds to filename
		else if (isANSI(v)) {											
			if (tags.length==1)												//If the tag is already in roman and has no folder it might be either name or meta
				nms.push(v)													//if it's the only tag it is most likely the name
			else {															//	otherwise put it into the "ansi" category that does not require translation
				splt=v.split(' ');
				if (splt.length==2)	{										//Some bloggers put tags for both name reading orders (name<->surname),
					rvrs=splt.reverse().join(' ');
					if (names.get(rvrs)) {									// thus creating duplicating tags
						nms.push(names.get(rvrs))							// try to find database entry for reversed order first,
						return true;									
					}
					else if (ansi.hasOwnProperty(rvrs))									// then check for duplicates		
						return true;
				}
				ansi[v]=true;											
			};
		}				 
		else 
			rest.push(v);													//	finally the "untranslated" category
	});
																			//2nd sorting stage, now we know how many tags of each category there are
																			//It's time to filter the "ansi" category further
	$.each(fldrs.concat(nms.concat(mt)), function(i,v){						//Some bloggers put both kanji and translated names into tags
		rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');
		x=getFileName(v).toLowerCase().replace(rx,'');
		y=x.split(' ').reverse().join(' ');									// check if we already have a name translated to avoid duplicates
		delete ansi[x];														//I have to again check for both orders even though I deleted one of them before,
		delete ansi[y];														// but at the time of deletion there was no way to know yet which one would match the kanji tag
	});																		//This also gets rid of reverse duplicates between recognized tags and ansi
	fldrs=mkUniq(fldrs);	
	nms=$(nms).not(fldrs).get();											//subtract fldrs from nms if they happen to have repeating elements
	fldrs2=[];			
	
	fldrs=$.grep(fldrs,function(v,i){										//A trick to process folders for meta tags, having subfolders for names inside
		fmeta=getFileName(v);
		if ((fmeta.indexOf(ms)==0)) {										// such folders must have the metasymbol as the first character
			fldrs2.push(fmeta);
			if (fldrs.concat(nms).length==1)								//In the rare case when there are no name tags at all we put the image to meta folder
				folder+=v+'\\'												// no need to put meta tag into filename this way, since the image will be in the same folder
			else
				mt.push(fmeta.replace(ms,''));	 							//usually it needs to be done though
			return false;													//exclude processed meta tags from folder category
			}
		else
			return true;													//return all the non-meta folder tags
		}
	);
	if (fldrs2.length==1) {													//Make sure only one folder meta tag exists
		folders['!!solo']=fldrs2[0];										//replace solo folder with metatag folder, so the image can go there if needed,
		folders['!!group']=fldrs2[0];										// same for group folder (see 3rd sorting stage)
	};		
	
	fldrs2=$.map(fldrs,function(vl,ix){
		return getFileName(vl);												//Extract names from folder paths
	});		
	
	mt=mt.concat(Object.keys(ansi));										//Roman tags have to go somewhere until assigned a category manually	
	filename=(mkUniq(fldrs2.concat(nms)).concat(['']).concat(mkUniq(mt)).join(',').replace(/\s/g,'_').replace(/\,/g,' ')+' '+filename).trim();																	
																			//Format the filename in a booru-compatible way, replacing spaces with underscores,
																			// first come the names alphabetically sorted, then the meta sorted separately 
																			// and lastly the original filename;		
																			// any existing commas will be replaced with spaces as well	
																			//this way the images are ready to be uploaded to boorus using the mass booru uploader script
																		
	unsorted=(rest.length>0)||(Object.keys(ansi).length>0);					//Unsorted flag is set if there are tags outside of 3 main categories  
	
																			//Final, 3rd sorting stage, assign a folder to the image based on found tags and categories
	nms=mkUniq(nms);
	if (unsorted)  {														//If there are any untranslated tags, make a table with text fields to provide manual translation
		var fn=rest.reduce(function (fn, v){
			return fn+' '+'['+v.replace(/\s/g,'_')+']';						// such tags are enclosed in [ ]  in filename for better searchability on disk
		},'');	
		buildTable(ansi, rest);
		folder=folders["!!unsorted"]+'\\';   								//Mark image as going to "unsorted" folder if it still has untranslated tags
		filename=fn+' '+filename;
		document.title+='? ';												//no match ;_;
	} else											//TODO: option to disable unsorted category if translations are not required by user
	 if ((fldrs.length==1)&&(nms.length==0)){								//Otherwise if there's only one tag and it's a folder tag, assign the image right there
		folder=fldrs[0]+'\\';
		filename=filename.split(' ');
		filename.shift();													//Remove the folder name from file name since the image goes into that folder anyway
		filename=filename.join(' ').trim();
		document.title+='✓ '; 												//100% match, yay
	} else
	 if ((fldrs.length==0)&&(nms.length==1)){								//If there's only one name tag without a folder for it, goes into default "solo" folder
		folder=folders['!!solo']+'\\'; 										// unless we had a !meta folder tag earlier, then the solo folder 
																			// would have been replaced with the appropriate !meta folder
	} else 
	 if (nms.length+fldrs.length>1)											//Otherwise if there are several name tags, folder or not, move to the default "group" folder
		folder=folders['!!group']+'\\';										// same as the above applies for meta
	filename=filename.replace(exclrgxp, '-').trim();						//Make sure there are no forbidden characters in the resulting name 
	document.title+=' \\'+folder+filename;
	folder=(root+folder).replace(/\\\\/g,'\\');								//If no name or folder tags were found, folder will be set to root directory
	
	if (DBrec.s=='1') document.title='♥ '+document.title;					//Indicate if the image has been marked as saved before
	title=document.title; 
};

function buildTable(ansi, rest) {											//Create table of untranslated tags for manual translation input
	tb.show(); 
	options='';
	tbd=tb[0].appendChild(document.createElement('tbody'));
	$.each(ansi, function(i,v){												//First process the unassigned roman tags
		row1=tbd.insertRow(0);
		cell1=row1.insertCell(0);  
		cell1.id=i;
		swp='<input type="button" value="swap"  id="swap" />'
		cell1.innerHTML=tagcell+i+'</a><br>'+swp+'</td></tr></table>'; 
		if (i.split(' ').length!=2)											//For roman tags consisting of 2 words enable button for swapping their order
			$(cell1).find('input#swap').attr('disabled','disabled');		// script can't know which name/surname order is correct so the choice is left to user
		$(cell1).attr('class','cell ansi');
		$(cell1).find('input[type="radio"]').attr('name',i);			
		options='<option value="'+i+'"></option>'+options;					//Populate the drop-down selection lists with these tags
		$(cell1).find('input#swap').on('click',function(){swap(this);});
	});																		// so they can be used for translating kanji tags if possible
 
	$.each(rest, function(i,v){												//Now come the untranslated kanji tags
		row1=tbd.insertRow(0);
		cell1=row1.insertCell(0); 
		cell1.id=v;
		cell1.innerHTML=tagcell+v+'</a><br><input list="translation" size=10 class="txt"/>\
			<datalist id="translation">'+options+'</datalist></td></tr></table>'; 
		$(cell1).attr('class','cell kanji');
		$(cell1).find('input[type="radio"]').attr('name',v);				//In case the blogger provided both roman tag and kanji tag for names,
	}); 																	// the user can simply select one of roman tags for every kanji tag as translation
																			// to avoid typing them in manually. Ain't that cool?		
 	$.each($('a.ignr'),function(i,v){v.onclick=function(){ignor3(this);};}); 
};

function ignor3(anc){														//Remove clicked tag from results for current session (until page reload)
	ignore.push(anc.textContent);											// this way you don't have to fill in the "ignore" list, 
																			// while still being able to control which tags will be counted
	tdc=$(anc).parent().parent().parent().parent().parent().parent();		//a long way up from tag link to tag cell table					
	tdc.attr('hidden','hidden');
	tdc.attr('ignore','ignore');	

	$.each($('datalist').find('option'), function(i,v){						//Hide these tags from the drop-down lists of translations too
		if (v.value==anc.textContent)										 
			v.parentNode.removeChild(v);								 
		}																 
	);
};

function swap(txt){															//Swap roman tags consisting of 2 words
																		
	data=$('datalist');														// these are most likely the names so they can have different writing orders
	set=[];
	theTag=$(txt).prev().prev()[0];
	$.each(data.find('option'), function(i,v){
		if (v.value==theTag.textContent)
			set.push(v);													//Collect all options from drop-down lists containing the tag to be swapped
		}
	);
	swapped=theTag.textContent.split(' ').reverse().join(' ');

	theTag.textContent=swapped;
	tdc=$(txt).parent().parent().parent().parent().parent();				//Change ids of tag cells as well
	tdc.prop('swap',!tdc.prop('swap'));										//mark node as swapped
	$.each(set,function(i,v){
		v.value=swapped;													//apply changes to the quick selection lists too
		}
	);
};

function selected(e){														//Hide the corresponding roman tag from results when it has been selected 
	$(e.target).css('background-color','');
	ansi=$('td.ansi');														// as a translation for kanji tag
	kanji=$('td.kanji').find('input.txt');									//that's not a filename, fyi
	knj={};
	$.each(kanji,function(i,v){
		knj[v.value]=true;
		$.each(ansi,function(ix,vl){ 										//Have to show a previously hidden tag if another was selected
			if (vl.textContent.trim()==v.value.trim())
				$(vl).parent().attr('hidden','hidden');
		});
	});
	$.each(ansi,function(ix,vl){
		if ((!knj.hasOwnProperty(vl.textContent.trim()))&&(!$(vl).parent().attr('ignore')))
			$(vl).parent().removeAttr('hidden');
	});
	var test={tag:e.target.value};
	checkMatch(test, true);
	if (test.tag!=e.target.value) {
		$(e.target).css('background-color','#ffff00');
		e.target.value=test.tag;
	}
}

function mkUniq(arr){														//Sorts an array and ensures uniqueness of its elements
	to={};
	$.each(arr, function(i,v){
		to[v.toLowerCase()]=true;});
	arr2=Object.keys(to);
	return arr2.sort();														//I thought key names are already sorted in an object but for some reason they're not
};

function getFileName(fullName, full){										//Source URL processing for filename
	full=full || false;
	fullName=fullName.replace(/(#|\?).*$/gim,'');							//first remove url parameters
	if (fullName.indexOf('xuite')!=-1) {									//This blog 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
	}
	else if ((fullName.indexOf('amazonaws')!=-1)&&(!full))  				//Older tumblr images are weirdly linked via some encrypted redirect to amazon services,
		fullName=fullName.substring(0,fullName.lastIndexOf('_')-2);			// where links only have a part of the filename without a few last symbols and extension,
																			// have to match it here as well, but we need full filename for downloadify, thus the param
	if ((fullName.indexOf('tumblr_')!=-1)&&!full) 
		fullName=fullName.replace(/(tumblr_)|(_\d{2}\d{0,2})(?=\.)/gim,'');
	fullName=fullName.replace(/\\/g,'/');									//Function is used both for URLs and folder paths which have opposite slashes
	return fullName.split('/').pop();
};

function dl(base64data){													//Make downloadify button with base64 encoded image file as parameter
																			// which will both cause save file dialog with custom filename and copy save path to clipboard
	Downloadify.create( 'down'  ,{
		filename: function(){ return filename;}, 							//is this called "stateless"?
		data: base64data, 
		dataType: 'base64',
		downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.png',
		onError: function(){ throw new Error('Downloadify error');},
		onComplete: onCmplt,
		swf:  downloadifySwf,
		width: 100,
		height: 30,
		transparent: true,
		append: true,
		textcopy: function(){ if (DBrec) {return folder+filename;} else return '';}	
	});																		//If no database record is found, don't change the clipboard
};

function onCmplt(){															//Mark image as saved in the tag database
	if (DBrec)	{															// it is used to mark saved images on tumblr pages
		DBrec.s='1';							
		tagsDB.set(getFileName(document.location.href), JSON.stringify(DBrec));
		document.title='♥ '+document.title;									//Actually I wanted to put a diskette symbol there,
	};																		// but because chromse sucks it does not support extended unicode in title
}

function submit(){															//Collects entered translations for missing tags
	tgs=$('td.cell');														//saves them to databases and relaunches tag analysis with new data
	$('input.category').parent().parent().css("background-color","");
	missing=false;
	$.each(tgs,function(i,v){
		if ($(v).parent().attr('ignore')) {
			ignore.push(v.id);												//Mark hidden tags as ignored
			return true;
		};
		if ($(v).parent().attr('hidden'))
			return true;
		tg=$(v).find('input.txt');
		if (tg.length)
			tg=tg[0].value.trim();											//found translation tag
		else {
			tg=v.textContent.trim(); 											//found roman tag
			if ($(v).prop('swap')) {
				t=DBrec.t.replace(tg.split(' ').reverse().join(' '),tg);
				DBrec.t=t;													//Apply swap changes to the current taglist
			};
		}											//TODO: add checks for existing entries in another DB?
		cat=$(v).find('input.category');
		if (tg.length){
			if (!isANSI(tg)&&!allowUnicode) {
				$(v).find('input.txt').css("background-color","#ffb080");
				missing=true;												//Indicate unicode characters in user input
			} 
			else if (cat[0].checked) 										//name category was selected for this tag
				names.set(v.textContent.trim().toLowerCase(),tg)		
			else if (cat[1].checked)										//meta category was selected
				meta.set(v.textContent.trim().toLowerCase(), tg)
			else { 															//no category was selected, indicate missing input
				$(cat[0].parentNode.parentNode ).css("background-color","#ff8080");
				missing=true;
			}
		}
		else {
			$(v).find('input.txt').css("background-color","#ff8080");
			missing=true;													//no translation was provided, indicate missing input
			return true;
			}
		}
	);						
	tbd=$('#translations > tbody')[0];
	if (!missing){
		tbd.parentNode.removeChild(tbd);
		tb.hide();
		analyzeTags();
	};
};

function ex(){																//Export auxiliary tag databases as a text file
	Downloadify.create('ex' ,{
		filename: 'names&meta tags DB.txt', 
		data: function(){
			xport={names:names.getAll(), meta:meta.getAll()};
			return JSON.stringify(xport, null, '\t');
		},
		dataType:'string',
		downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify2.png',
		onError: function(){  throw new Error('Downloadify2 error');},
		swf:  downloadifySwf,
		width: 100,
		height: 30,
		transparent: true,
		append: false,
		textcopy: ''	
	});	
	$('a.exim')[0].removeAttribute('onclick');
	$('a#aex')[0].textContent=''; 
};	

function im(){																//Import auxiliary tag databases as text file
	$('#im').append('<input type="file" id="files" style="width:97px;" accept="text/plain"/>'); 
	$('input#files')[0].onchange=handleFileSelect; 
	$('a.exim')[1].removeAttribute('onclick');	
	$('a#aim')[0].textContent=''; 
};

function handleFileSelect(evt) {											//Fill in databases with data from imported file
    var file = evt.target.files[0]; 
	
	$('input#files')[0].value='';
	if (file.type!='text/plain') {
		alert('Wrong filetype: must be text');
		return false;
	};
	var reader = new FileReader();
	reader.onloadend = function(e) {
		clear=confirm('Would you like to clear existing databases before importing?');
	  try {	
		o=JSON.parse(e.target.result);
	  } catch(err){
		alert('Error: '+err.message);
		return false;
	  };
	    if (o.meta) {
			checkMatch(o.meta);
			if (clear) 
				meta.clearAll();
			$.each(o.meta, function(i,v){
				meta.set(i,v);});
		}
		else
			alert('No meta DB found');
	    if (o.names) {
			checkMatch(o.names);
			if (clear) 
				names.clearAll();
			$.each(o.names, function(i,v){
				names.set(i,v);});
		}
		else
			alert('No names DB found');				
	};
    reader.readAsText(file);
};
//TODO: add save button activation via keyboard
//TODO: improve the button: open assigned folder directly, use modern dialog
//TODO: ^ try to set last used directory in flash save dialog so as to avoid clipboard usage
//TODO: add fallback to the tumblr hosted image if link url fails (requires storing post id and blog name)
//TODO: add checks for common mistakes in unicode names like 実/美 & 奈/菜
//TODO: option to disable unsorted category if translations are not required by user