🏠 Home 

Tumblr-image-sorter-get

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


Install this script?
  1. // ==UserScript==
  2. // @name Tumblr-image-sorter-get
  3. // @description Format file name & save path for current image by its tags
  4. // @version 1.3.1.0
  5. // @author Seedmanc
  6. // @namespace https://github.com/Seedmanc/Tumblr-image-sorter
  7. // @include http*://*.amazonaws.com/data.tumblr.com/*
  8. // @include http*://*.media.tumblr.com/*
  9. //these sites were used by animage.tumblr.com to host original images
  10. // @include http://scenario.myweb.hinet.net/*
  11. // @include http*://mywareroom.files.wordpress.com/*
  12. // @include http://e.blog.xuite.net/*
  13. // @include http://voice.x.fc2.com/*
  14. // @grant none
  15. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js
  16. // @require https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js
  17. // @require https://greasyfork.org/scripts/11847-swfstore/code/SwfStore.js?version=77621
  18. // @require https://greasyfork.org/scripts/11848-downloadify-clip/code/Downloadify%20+%20Clip.js?version=68937
  19. // @run-at document-start
  20. // @noframes
  21. // ==/UserScript==
  22. // ==Settings=====================================================
  23. var root= 'E:\\#-A\\!Seiyuu\\'; //Main collection folder
  24. //Make sure to use double backslashes instead of single ones everywhere
  25. var ms= '!'; //Metasymbol, denotes folders for categories instead of names, must be their first character
  26. var folders= { //Folder and names matching database
  27. " !!group " : " !!group ", // used both for tag translation and providing the list of existing folders
  28. " !!solo " : " !!solo ", // trailing whitespaces are voluntary in both keys and values,
  29. " !!unsorted" : " !!unsorted ", // first three key names are not to be changed, but folder names can be anything
  30. " 原由実 " : " !iM@S\\Hara Yumi", // subfolders for categories instead of names must have the metasymbol as first symbol
  31. " 今井麻美 " : " !iM@S\\Imai Asami ",
  32. " 沼倉#美 " : " !iM@S\\Numakura Manami",
  33. " けいおん! " : " !K-On ", //Category folders can have their own tag, which, if present, will affect the folder choice
  34. " 日笠陽子 " : " !K-On\\Hikasa Yoko ", // for solo and group images
  35. " 寿美菜子 " : " !K-On\\Kotobuki Minako",
  36. " 竹達彩奈 " : " !K-On\\Taketatsu Ayana",
  37. " 豊崎#生 " : " !K-On\\Toyosaki Aki ",
  38. " クリスマス " : " !Kurisumasu ",
  39. " Lisp " : " !Lisp ", //Roman tags can be used as well
  40. " 阿澄佳奈 " : " !Lisp\\Asumi Kana ",
  41. " 酒井香奈子 " : " !Lovedoll\\Sakai Kanako",
  42. " らき☆すた " : " !Lucky Star ",
  43. " 遠藤綾 " : " !Lucky Star\\Endo Aya ",
  44. " 福原香織 " : " !Lucky Star\\Fukuhara Kaori",
  45. " 長谷川静香 " : " !Lucky Star\\Hasegawa Shizuka",
  46. " 加藤英美里 " : " !Lucky Star\\Kato Emiri ",
  47. " 今野宏美 " : " !Lucky Star\\Konno Hiromi ",
  48. " 井上麻里奈 " : " !Minami-ke\\Inoue Marina ",
  49. " 佐藤利奈 " : " !Minami-ke\\Sato Rina ",
  50. " Petit Milady ": " !Petit Milady ",
  51. " 悠木碧 " : " !Petit Milady\\Yuuki Aoi ",
  52. " ロウきゅーぶ! " : " !Ro-Kyu-Bu ",
  53. " Kalafina " : " !Singer\\Kalafina ",
  54. " LiSA " : " !Singer\\LiSA ",
  55. " May'n " : " !Singer\\May'n ",
  56. " 茅原実里 " : " !SOS-dan\\Chihara Minori",
  57. " 後藤邑子 " : " !SOS-dan\\Goto Yuko ",
  58. " 平野綾 " : " !SOS-dan\\Hirano Aya ",
  59. " スフィア " : " !Sphere ",
  60. " やまとなでしこ " : " !Yamato Nadeshiko ",
  61. " 堀江由衣 " : " !Yamato Nadeshiko\\Horie Yui",
  62. " 田村ゆかり " : " !Yamato Nadeshiko\\Tamura Yukari",
  63. " 雨宮天 " : " Amamiya Sora ",
  64. " 千葉紗子 " : " Chiba Saeko ",
  65. " 渕上舞 " : " Fuchigami Mai ",
  66. " 藤田咲 " : " Fujita Saki ",
  67. " 後藤沙緒里 " : " Goto Saori ",
  68. " 花澤香菜 " : " Hanazawa Kana ",
  69. " 早見沙織 " : " Hayami Saori ",
  70. " 井口裕香 " : " Iguchi Yuka ",
  71. " 井上喜久子 " : " Inoue Kikuko ",
  72. " 伊藤かな恵 " : " Ito Kanae ",
  73. " 伊藤静 " : " Ito Shizuka ",
  74. " 門脇舞以 " : " Kadowaki Mai ",
  75. " 金元寿子 " : " Kanemoto Hisako ",
  76. " 茅野#衣 " : " Kayano Ai ",
  77. " 喜多村英梨 " : " Kitamura Eri ",
  78. " 小林ゆう " : " Kobayashi Yuu ",
  79. " 小清水亜美 " : " Koshimizu Ami ",
  80. " 釘宮理恵 " : " Kugimiya Rie ",
  81. " 宮崎羽衣 " : " Miyazaki Ui ",
  82. " 水樹奈々 " : " Mizuki Nana ",
  83. " 桃井はるこ " : " Momoi Haruko ",
  84. " 中原麻衣 " : " Nakahara Mai ",
  85. " 中島# " : " Nakajima Megumi ",
  86. " 名塚佳織 " : " Nazuka Kaori ",
  87. " 野川さくら " : " Nogawa Sakura ",
  88. " 野中藍 " : " Nonaka Ai ",
  89. " 能登麻美子 " : " Noto Mamiko ",
  90. " 折笠富美子 " : " Orikasa Fumiko ",
  91. " 朴璐美 " : " Paku Romi ",
  92. " 榊原ゆい " : " Sakakibara Yui ",
  93. " 坂本真綾 " : " Sakamoto Maaya ",
  94. " 佐倉綾音 " : " Sakura Ayane ",
  95. " 沢城みゆき " : " Sawashiro Miyuki ",
  96. " 椎名へきる " : " Shiina Hekiru ",
  97. " 清水# " : " Shimizu Ai ",
  98. " 下田麻美 " : " Shimoda Asami ",
  99. " 新谷良子 " : " Shintani Ryoko ",
  100. " 白石涼子 " : " Shiraishi Ryoko ",
  101. " 田中理恵 " : " Tanaka Rie ",
  102. " 丹下桜 " : " Tange Sakura ",
  103. " 東山奈央 " : " Toyama Nao ",
  104. " 植田佳奈 " : " Ueda Kana ",
  105. " 上坂すみれ " : " Uesaka Sumire ",
  106. " ゆかな " : " Yukana "
  107. };
  108. var ignore= "歌手, seiyuu, 声優"; //These tags will not count towards any category and won't be included into filename
  109. var allowUnicode= false; //Whether to allow unicode characters in manual translation input, not tested
  110. var useFolderNames= true; //In addition to tags listed in keys of the folders object, recognize also folder names themselves
  111. // this way you won't have to provide both roman and kanji spellings for names as separate tags
  112. var debug= false; //Initial debug state, affects creation of flashDBs. Value saved in the DB overrides it after DB init.
  113. var storeUrl= '//dl.dropboxusercontent.com/u/74005421/js%20requisites/storage.swf';
  114. //Flash databases are bound to the URL, must be same as in the other script
  115. // ==/Settings=========================================================
  116. tagsDB=null; //Makes sure databases are accessible from console for debugging
  117. names=null ;
  118. meta=null ;
  119. var title;
  120. var filename;
  121. var folder = '';
  122. var DBrec=''; //Raw DB record, stringified object with fields for saved flag and tag list
  123. var N=M=T=false; //Flags indicating readiness of plugins loaded simultaneously
  124. var exclrgxp=/%|\/|:|\||>|<|\?|"|\*/g; //Pattern of characters not to be used in filepaths
  125. var downloadifySwf= '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.swf';
  126. //Flash button URL
  127. var style={ //In an object so you can fold it in any decent editor. If only you had that in chrome.
  128. s:" \
  129. div#output { \
  130. position: absolute; \
  131. left: 0; top: 0; \
  132. width: 100px; height: 30px; \
  133. } \
  134. div#down { \
  135. left: 1px; \
  136. position: fixed; \
  137. z-index: 98; \
  138. } \
  139. table#port { \
  140. top: 30px; \
  141. left: 1px; \
  142. position: fixed; \
  143. background-color: \
  144. rgba(192,192,192,0.85); \
  145. border-bottom: 1px solid black; \
  146. z-index: 97; \
  147. width: 100px; \
  148. border-collapse: collapse; \
  149. } \
  150. table#translations { \
  151. position: absolute; \
  152. background-color: \
  153. rgba(255,255,255,0.8); \
  154. top: 48px; \
  155. overflow: scroll; \
  156. font-size: 90%; \
  157. margin-left: -1px; \
  158. width: 103px; \
  159. table-layout: fixed; \
  160. } \
  161. td.settings { \
  162. border-left: 1px solid black; \
  163. border-right: 1px solid black; \
  164. } \
  165. a.settings { \
  166. text-decoration: none; \
  167. } \
  168. table, tr { \
  169. text-align: center; \
  170. } \
  171. td#ex { \
  172. padding: 0; \
  173. } \
  174. input.txt { \
  175. width: 95%; \
  176. } \
  177. td.cell, td.radio{ \
  178. border: 1px solid black; \
  179. overflow: hidden; \
  180. } \
  181. table.cell { \
  182. background-color: \
  183. rgba(255,255,255,0.75); \
  184. width: 100%; \
  185. border-collapse: collapse; \
  186. } \
  187. a { \
  188. font-family: Arial; \
  189. font-size: small; \
  190. } \
  191. th { \
  192. border: 0; \
  193. color:black; \
  194. } \
  195. input#submit { \
  196. width: 98%; \
  197. height: 29px; \
  198. } \
  199. "}; //This certainly needs optimisation
  200. var out=$('<div id="output"><div id="down"></div></div>'); //Main layer that holds the GUI
  201. var tb =$('<table id="translations">'); //Table for entering manual translation of unknown tags
  202. var tagcell='<table class="cell"><tr> \
  203. <td class="radio"><input type="radio" class="category" value="name"/></td> \
  204. <td class="radio"><input type="radio" class="category" value="meta"/></td> \
  205. </tr><tr> \
  206. <td colspan="2"><a href="#" title="Click to ignore this tag for now" class="ignr">';
  207. //Each cell has the following in it:
  208. // two radiobuttons to choose a category for the tag - name or meta
  209. // the tag itself, either in roman or in kanji
  210. // the tag is also a link, clicking which removes the tag from r###lts until refresh
  211. // if the tag is in kanji, cell has a text field to input translation manually
  212. // if there are also roman tags, they are used as options for quick input into the text field
  213. // if the tag is in roman and consists of two words, cell has a button enabled to swap their order
  214. // otherwise the button is disabled
  215. var tfoot=$('<tfoot><tr><td> \
  216. <input type="submit" id="submit" value="submit"> \
  217. </td></tr></tfoot>'); //At the bottom of the table there is the "submit" button that applies changes
  218. var thead=$('<thead><tr><td > \
  219. <table class="cell" style="font-width:95%; font-size:small;"> \
  220. <tr class="cell"><th class="cell">name</th><th class="cell">meta</th></tr> \
  221. </table> \
  222. </td></tr></thead>');
  223. tb.append(thead).append(tfoot).hide();
  224. port=document.createElement('table'); //Subtable for settings and im/export of tag databases
  225. row= port.insertRow(0);
  226. cell=row.insertCell(0);
  227. cell.setAttribute('class','settings');
  228. cell.innerHTML=' <a href="##" onclick=toggleSettings() class="settings">- settings -</a> ';
  229. row0=port.insertRow(1);
  230. row0.insertCell(0).innerHTML='<input type="checkbox" id="debug"/> debug';
  231. row1=port.insertRow(2);
  232. row1.insertCell(0).innerHTML=' <a href="###" onclick=ex() id="aex" class="exim">export db</a>';
  233. row2=port.insertRow(3);
  234. row2.insertCell(0).id='ex';
  235. row3=port.insertRow(4);
  236. row3.insertCell(0).innerHTML=' <a href="####" onclick=im() id="aim" class="exim">import db</a> ';
  237. row4=port.insertRow(5);
  238. row4.insertCell(0).id='im';
  239. port.id='port';
  240. window.onerror = function(msg, url, line, col, error) { //General error handler
  241. var extra = !col ? '' : '\ncolumn: ' + col;
  242. extra += !error ? '' : '\nerror: ' + error; //Shows '✗' for errors in title and also alerts a message if in debug mode
  243. if (msg.search('this.swf')!=-1)
  244. return true; //Except for irrelevant errors
  245. document.title+='✗';
  246. if (debug)
  247. alert("Error: " + msg + "\nurl: " + url + "\nline: " + line + extra);
  248. var suppressErrorAlert = true;
  249. return suppressErrorAlert;
  250. };
  251. var xhr = new XMLHttpRequest(); //Redownloads opened image as blob
  252. xhr.responseType="blob"; // so that it would be possible to get it via downloadify button
  253. xhr.onreadystatechange = function() { // supposedly the image is being taken from cache so it shouldn't cause any slowdown
  254. if (this.readyState == 4 && this.status == 200) {
  255. var blob=this.response;
  256. var reader = new window.FileReader();
  257. reader.readAsDataURL(blob);
  258. reader.onloadend = function() {
  259. base64data = reader.r###lt;
  260. base64data=base64data.replace(/data\:image\/\w+\;base64\,/,"");
  261. dl(base64data); //Call the button creation function
  262. }
  263. } else if ((this.status!=200)&&(this.status!=0)) {
  264. if (this.status==404) {
  265. document.title='Error '+this.status;
  266. throw new Error('404');
  267. };
  268. throw new Error('Error getting image: '+this.status);
  269. };
  270. };
  271. function expandFolders(){ //Complement DB with tags produced from folders names
  272. var t,rx,x;
  273. for (var key in folders) {
  274. if (folders.hasOwnProperty(key)&&(['!group','!solo','!unsorted'].indexOf(key)==-1)) {
  275. t=folders[key];
  276. rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');
  277. x=getFileName(t).toLowerCase().replace(rx,'');
  278. folders[x]=t;
  279. };
  280. };
  281. };
  282. rootrgxp=/^([a-z]:){1}(\\[^<>:"/\\|?*]+)+\\$/gi;
  283. try {
  284. if (!(rootrgxp.test(root)))
  285. throw new Error('Illegal characters in root folder path: "'+root+'"');
  286. ms=ms[0]; //It's a symbol, not a string, after all
  287. if ((exclrgxp.test(ms))||(/\\|\s/.test(ms)))
  288. throw new Error ('Illegal character as metasymbol: "'+ms+'"');
  289. } catch (err) {
  290. if (!debug)
  291. alert(err.name+': '+err.message);
  292. throw err;
  293. };
  294. function checkMatch(obj,fix){ //Remove trailing whitespace in object keys and values & check correctness of user input
  295. fix=fix||false;
  296. try { //make sure that folder names have no illegal characters
  297. for (var key in obj) { //Convert keys to lower case for better matching
  298. if (obj.hasOwnProperty(key)) {
  299. t=obj[key].trim().replace(/^\\|\\$/g, '').trim();
  300. delete obj[key];
  301. k=key.trim().toLowerCase();
  302. obj[k]=t;
  303. if (exclrgxp.test(obj[k])) //Can't continue until the problem is fixed
  304. if (!fix)
  305. throw new Error('Illegal characters in folder name entry: "'+obj[k]+'" for name "'+k+'"')
  306. else
  307. obj[k]=t.replace(exclrgxp, '-');
  308. };
  309. };
  310. } catch (err) {
  311. if (!debug)
  312. alert(err.name+': '+err.message); //Gotta always notify the user
  313. throw err;
  314. }; //TODO: even more checks here
  315. };
  316. function toggleSettings(){ //Show drop-down menu with settings
  317. $('table#port td').not('.settings').toggle();
  318. $('table#translations').css('top',($('table#port').height()+30)+'px');
  319. sign=$('a.settings').eq(0);
  320. if (sign.text().search(/\+/,'-')!=-1) {
  321. sign.text(sign.text().replace(/\+/gi,'-'));
  322. $('td.settings').css('border-bottom','');
  323. }
  324. else {
  325. sign.text(sign.text().replace(/\-/gi,'+'));
  326. $('td.settings').css('border-bottom','1px solid black');
  327. }
  328. };
  329. function debugSwitch(checkbox){ //Toggling debug mode requires page reload
  330. debug = checkbox.checked;
  331. tagsDB.set(':debug:',debug );
  332. location.reload();
  333. };
  334. onDOMcontentLoaded();
  335. function onDOMcontentLoaded(){ //Load plugins and databases
  336. checkMatch(folders); //Run checks on user-input content and format it
  337. if (useFolderNames)
  338. expandFolders();
  339. ignore=$.map(ignore.split(','), function(v,i){
  340. return v.trim().toLowerCase();
  341. });
  342. href=document.location.href;
  343. if (href.indexOf('tumblr')==-1) //If not on tumblr
  344. if (!(/(jpe?g|bmp|png|gif)/gi).test(href.split('.').pop())) // check if this is actually an image link
  345. return;
  346. $('img').wrap("<center></center>");
  347. $('body').append(out);
  348. names = new SwfStore({ //Auxiliary database for names that don't have folders
  349. namespace: "names",
  350. swf_url: storeUrl,
  351. onready: function(){
  352. document.title+=(debug)?' NM ':'';
  353. N=true;
  354. mutex();
  355. },
  356. onerror: function() {
  357. document.title+=' ✗ names failed to load';}
  358. });
  359. meta = new SwfStore({ //Auxiliary DB for meta tags such as franchise name or costume/accessories
  360. namespace: "meta",
  361. swf_url: storeUrl,
  362. onready: function(){
  363. M=true;
  364. mutex();
  365. },
  366. onerror: function() {
  367. document.title+=' ✗ meta failed to load';}
  368. });
  369. tagsDB = new SwfStore({ //Loading main tag database, holds pairs "filename {s:is_saved?1:0,t:'tag1,tag2,...,tagN'}"
  370. namespace: "animage",
  371. swf_url: storeUrl,
  372. onready: function(){
  373. document.title+=(debug)?' T ':'';
  374. debug =(tagsDB.get(':debug:')=='true'); //Override initial debug state with the one stored in DB
  375. tagsDB.config.debug=debug;
  376. getTags();
  377. },
  378. debug: debug,
  379. onerror: function() {
  380. document.title='tagsdb error';
  381. throw new Error('tagsDB failed to load');
  382. }
  383. }); //TODO: delay aux DBs loading until & if they're actually needed?
  384. };
  385. function getTags(retry){ //Manages tags acquisition for current image file name from db
  386. 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
  387. if ((DBrec!=null) || (debug)) { // if tags are found report readiness
  388. T=true; // or if we're in debug mode, proceed anyway
  389. mutex();
  390. } else
  391. if ((retry) || (document.readyState=='complete')) //Otherwise if we ran out of attempts or it's too late
  392. return // stop execution
  393. else {
  394. retry=true; // but if not schedule the second attempt at retrieving tags to image load end
  395. window.addEventListener('load',function(){ getTags(true);},false);
  396. };
  397. }; //TODO: make getTags actually return the value to main() to get rid of the global var
  398. function mutex(){ //Check readiness of plugins and databases when they're loading simultaneously
  399. if (N && M && T) { // when everything is loaded, proceed further
  400. N=M=T=false;
  401. main();
  402. };
  403. };
  404. function main(){ //Launch tag processing and handle afterwork
  405. $("<style>"+style.s+"</style>" ).appendTo( "head" ); //assign functions to events and whatnot
  406. $('div#output').append(port);
  407. toggleSettings();
  408. $('input#debug').prop('checked',debug);
  409. $('a#aim')[0].onclick=im;
  410. $('a#aex')[0].onclick=ex;
  411. $('a.settings')[0].onclick=toggleSettings;
  412. $('input#debug')[0].onclick=function(){debugSwitch(this);};
  413. if (debug)
  414. $("div[id^='SwfStore_animage_']").css('top','0').css('left','101px').css("position",'absolute').css('opacity','0.7');
  415. //TODO: make the code above run regardless of found DB record
  416. $('div#output').append(tb);
  417. analyzeTags();
  418. $('input#submit')[0].onclick=submit;
  419. $('input.txt').on('change',selected);
  420. xhr.open("get", document.location.href, true); //Reget the image to attach it to downloadify button
  421. xhr.send();
  422. $(window).load(function(){document.title=title;});
  423. };
  424. function isANSI(s) { //Some tags might be already in roman and do not require translation
  425. is=true;
  426. s=s.split('');
  427. $.each(s,function(i,v){
  428. is=is&&(/[\u0000-\u00ff]/.test(v));});
  429. return is;
  430. };
  431. function analyzeTags() { //This is where the tag matching magic occurs
  432. filename=getFileName(document.location.href, true);
  433. if (!DBrec) return; // if there are any tags, that is
  434. folder='';
  435. if (debug)
  436. document.title=JSON.stringify(DBrec,null,' ')+' ' //Show raw DB record
  437. else
  438. document.title='';
  439. tags=DBrec.t.split(',');
  440. fldrs=[];
  441. nms=[];
  442. mt=[];
  443. ansi={}
  444. rest=[];
  445. tags=$.map(tags,function(v,i){ //Some formatting is applied to the taglist before processing
  446. v=v.replace(/’/g,"\'").replace(/"/g,"''");
  447. v=v.replace(/\\/g, '-');
  448. 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'
  449. // I dunno if it should be done in the middle of the name as well
  450. sp=v.split(' ');
  451. if (sp.length>1)
  452. $.each(tags, function(ii,vv){
  453. if (ii==i) return true;
  454. if (sp.join('')==vv)
  455. return v=false; //Some bloggers put kanji tags both with and without spaces, remove duplicates with spaces
  456. }
  457. );
  458. if (!v)
  459. return null;
  460. if ((ignore.indexOf(v)!=-1)||(ignore.indexOf(v.split(' ').reverse().join(' '))!=-1))
  461. return null //Remove ignored tags so that they don't affect the tag amount
  462. else return v;
  463. });
  464. //1st sorting stage, no prior knowledge about found categories
  465. $.each(tags, function(i,v){ //Divide tags for the image into 5 categories
  466. if (folders.hasOwnProperty(v)) // the "has folder" category
  467. fldrs.push(folders[v])
  468. else if (names.get(v)) // the "no folder name tag" category
  469. nms.push(names.get(v))
  470. else if (meta.get(v)) // the "no folder meta tag" category,
  471. mt.push(meta.get(v)) // which doesn't count towards final folder decision, but simply adds to filename
  472. else if (isANSI(v)) {
  473. if (tags.length==1) //If the tag is already in roman and has no folder it might be either name or meta
  474. nms.push(v) //if it's the only tag it is most likely the name
  475. else { // otherwise put it into the "ansi" category that does not require translation
  476. splt=v.split(' ');
  477. if (splt.length==2) { //Some bloggers put tags for both name reading orders (name<->surname),
  478. rvrs=splt.reverse().join(' ');
  479. if (names.get(rvrs)) { // thus creating duplicating tags
  480. nms.push(names.get(rvrs)) // try to find database entry for reversed order first,
  481. return true;
  482. }
  483. else if (ansi.hasOwnProperty(rvrs)) // then check for duplicates
  484. return true;
  485. }
  486. ansi[v]=true;
  487. };
  488. }
  489. else
  490. rest.push(v); // finally the "untranslated" category
  491. });
  492. //2nd sorting stage, now we know how many tags of each category there are
  493. //It's time to filter the "ansi" category further
  494. $.each(fldrs.concat(nms.concat(mt)), function(i,v){ //Some bloggers put both kanji and translated names into tags
  495. rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');
  496. x=getFileName(v).toLowerCase().replace(rx,'');
  497. y=x.split(' ').reverse().join(' '); // check if we already have a name translated to avoid duplicates
  498. delete ansi[x]; //I have to again check for both orders even though I deleted one of them before,
  499. delete ansi[y]; // but at the time of deletion there was no way to know yet which one would match the kanji tag
  500. }); //This also gets rid of reverse duplicates between recognized tags and ansi
  501. fldrs=mkUniq(fldrs);
  502. nms=$(nms).not(fldrs).get(); //subtract fldrs from nms if they happen to have repeating elements
  503. fldrs2=[];
  504. fldrs=$.grep(fldrs,function(v,i){ //A trick to process folders for meta tags, having subfolders for names inside
  505. fmeta=getFileName(v);
  506. if ((fmeta.indexOf(ms)==0)) { // such folders must have the metasymbol as the first character
  507. fldrs2.push(fmeta);
  508. 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
  509. folder+=v+'\\' // no need to put meta tag into filename this way, since the image will be in the same folder
  510. else
  511. mt.push(fmeta.replace(ms,'')); //usually it needs to be done though
  512. return false; //exclude processed meta tags from folder category
  513. }
  514. else
  515. return true; //return all the non-meta folder tags
  516. }
  517. );
  518. if (fldrs2.length==1) { //Make sure only one folder meta tag exists
  519. folders['!!solo']=fldrs2[0]; //replace solo folder with metatag folder, so the image can go there if needed,
  520. folders['!!group']=fldrs2[0]; // same for group folder (see 3rd sorting stage)
  521. };
  522. fldrs2=$.map(fldrs,function(vl,ix){
  523. return getFileName(vl); //Extract names from folder paths
  524. });
  525. mt=mt.concat(Object.keys(ansi)); //Roman tags have to go somewhere until assigned a category manually
  526. filename=(mkUniq(fldrs2.concat(nms)).concat(['']).concat(mkUniq(mt)).join(',').replace(/\s/g,'_').replace(/\,/g,' ')+' '+filename).trim();
  527. //Format the filename in a booru-compatible way, replacing spaces with underscores,
  528. // first come the names alphabetically sorted, then the meta sorted separately
  529. // and lastly the original filename;
  530. // any existing commas will be replaced with spaces as well
  531. //this way the images are ready to be uploaded to boorus using the mass booru uploader script
  532. unsorted=(rest.length>0)||(Object.keys(ansi).length>0); //Unsorted flag is set if there are tags outside of 3 main categories
  533. //Final, 3rd sorting stage, assign a folder to the image based on found tags and categories
  534. nms=mkUniq(nms);
  535. if (unsorted) { //If there are any untranslated tags, make a table with text fields to provide manual translation
  536. var fn=rest.reduce(function (fn, v){
  537. return fn+' '+'['+v.replace(/\s/g,'_')+']'; // such tags are enclosed in [ ] in filename for better searchability on disk
  538. },'');
  539. buildTable(ansi, rest);
  540. folder=folders["!!unsorted"]+'\\'; //Mark image as going to "unsorted" folder if it still has untranslated tags
  541. filename=fn+' '+filename;
  542. document.title+='? '; //no match ;_;
  543. } else //TODO: option to disable unsorted category if translations are not required by user
  544. 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
  545. folder=fldrs[0]+'\\';
  546. filename=filename.split(' ');
  547. filename.shift(); //Remove the folder name from file name since the image goes into that folder anyway
  548. filename=filename.join(' ').trim();
  549. document.title+='✓ '; //100% match, yay
  550. } else
  551. if ((fldrs.length==0)&&(nms.length==1)){ //If there's only one name tag without a folder for it, goes into default "solo" folder
  552. folder=folders['!!solo']+'\\'; // unless we had a !meta folder tag earlier, then the solo folder
  553. // would have been replaced with the appropriate !meta folder
  554. } else
  555. if (nms.length+fldrs.length>1) //Otherwise if there are several name tags, folder or not, move to the default "group" folder
  556. folder=folders['!!group']+'\\'; // same as the above applies for meta
  557. filename=filename.replace(exclrgxp, '-').trim(); //Make sure there are no forbidden characters in the r###lting name
  558. document.title+=' \\'+folder+filename;
  559. folder=(root+folder).replace(/\\\\/g,'\\'); //If no name or folder tags were found, folder will be set to root directory
  560. if (DBrec.s=='1') document.title='♥ '+document.title; //Indicate if the image has been marked as saved before
  561. title=document.title;
  562. };
  563. function buildTable(ansi, rest) { //Create table of untranslated tags for manual translation input
  564. tb.show();
  565. options='';
  566. tbd=tb[0].appendChild(document.createElement('tbody'));
  567. $.each(ansi, function(i,v){ //First process the unassigned roman tags
  568. row1=tbd.insertRow(0);
  569. cell1=row1.insertCell(0);
  570. cell1.id=i;
  571. swp='<input type="button" value="swap" id="swap" />'
  572. cell1.innerHTML=tagcell+i+'</a><br>'+swp+'</td></tr></table>';
  573. if (i.split(' ').length!=2) //For roman tags consisting of 2 words enable button for swapping their order
  574. $(cell1).find('input#swap').attr('disabled','disabled'); // script can't know which name/surname order is correct so the choice is left to user
  575. $(cell1).attr('class','cell ansi');
  576. $(cell1).find('input[type="radio"]').attr('name',i);
  577. options='<option value="'+i+'"></option>'+options; //Populate the drop-down selection lists with these tags
  578. $(cell1).find('input#swap').on('click',function(){swap(this);});
  579. }); // so they can be used for translating kanji tags if possible
  580. $.each(rest, function(i,v){ //Now come the untranslated kanji tags
  581. row1=tbd.insertRow(0);
  582. cell1=row1.insertCell(0);
  583. cell1.id=v;
  584. cell1.innerHTML=tagcell+v+'</a><br><input list="translation" size=10 class="txt"/>\
  585. <datalist id="translation">'+options+'</datalist></td></tr></table>';
  586. $(cell1).attr('class','cell kanji');
  587. $(cell1).find('input[type="radio"]').attr('name',v); //In case the blogger provided both roman tag and kanji tag for names,
  588. }); // the user can simply select one of roman tags for every kanji tag as translation
  589. // to avoid typing them in manually. Ain't that cool?
  590. $.each($('a.ignr'),function(i,v){v.onclick=function(){ignor3(this);};});
  591. };
  592. function ignor3(anc){ //Remove clicked tag from r###lts for current session (until page reload)
  593. ignore.push(anc.textContent); // this way you don't have to fill in the "ignore" list,
  594. // while still being able to control which tags will be counted
  595. tdc=$(anc).parent().parent().parent().parent().parent().parent(); //a long way up from tag link to tag cell table
  596. tdc.attr('hidden','hidden');
  597. tdc.attr('ignore','ignore');
  598. $.each($('datalist').find('option'), function(i,v){ //Hide these tags from the drop-down lists of translations too
  599. if (v.value==anc.textContent)
  600. v.parentNode.removeChild(v);
  601. }
  602. );
  603. };
  604. function swap(txt){ //Swap roman tags consisting of 2 words
  605. data=$('datalist'); // these are most likely the names so they can have different writing orders
  606. set=[];
  607. theTag=$(txt).prev().prev()[0];
  608. $.each(data.find('option'), function(i,v){
  609. if (v.value==theTag.textContent)
  610. set.push(v); //Collect all options from drop-down lists containing the tag to be swapped
  611. }
  612. );
  613. swapped=theTag.textContent.split(' ').reverse().join(' ');
  614. theTag.textContent=swapped;
  615. tdc=$(txt).parent().parent().parent().parent().parent(); //Change ids of tag cells as well
  616. tdc.prop('swap',!tdc.prop('swap')); //mark node as swapped
  617. $.each(set,function(i,v){
  618. v.value=swapped; //apply changes to the quick selection lists too
  619. }
  620. );
  621. };
  622. function selected(e){ //Hide the corresponding roman tag from r###lts when it has been selected
  623. $(e.target).css('background-color','');
  624. ansi=$('td.ansi'); // as a translation for kanji tag
  625. kanji=$('td.kanji').find('input.txt'); //that's not a filename, fyi
  626. knj={};
  627. $.each(kanji,function(i,v){
  628. knj[v.value]=true;
  629. $.each(ansi,function(ix,vl){ //Have to show a previously hidden tag if another was selected
  630. if (vl.textContent.trim()==v.value.trim())
  631. $(vl).parent().attr('hidden','hidden');
  632. });
  633. });
  634. $.each(ansi,function(ix,vl){
  635. if ((!knj.hasOwnProperty(vl.textContent.trim()))&&(!$(vl).parent().attr('ignore')))
  636. $(vl).parent().removeAttr('hidden');
  637. });
  638. var test={tag:e.target.value};
  639. checkMatch(test, true);
  640. if (test.tag!=e.target.value) {
  641. $(e.target).css('background-color','#ffff00');
  642. e.target.value=test.tag;
  643. }
  644. }
  645. function mkUniq(arr){ //Sorts an array and ensures uniqueness of its elements
  646. to={};
  647. $.each(arr, function(i,v){
  648. to[v.toLowerCase()]=true;});
  649. arr2=Object.keys(to);
  650. return arr2.sort(); //I thought key names are already sorted in an object but for some reason they're not
  651. };
  652. function getFileName(fullName, full){ //Source URL processing for filename
  653. full=full || false;
  654. fullName=fullName.replace(/(#|\?).*$/gim,''); //first remove url parameters
  655. if (fullName.indexOf('xuite')!=-1) { //This blog names their images as "(digit).jpg" causing filename collisions
  656. i=fullName.lastIndexOf('/');
  657. fullName=fullName.substr(0,i)+'-'+fullName.substr(i+1); // add parent catalog name to the filename to ensure uniqueness
  658. }
  659. else if ((fullName.indexOf('amazonaws')!=-1)&&(!full)) //Older tumblr images are weirdly linked via some encrypted redirect to amazon services,
  660. fullName=fullName.substring(0,fullName.lastIndexOf('_')-2); // where links only have a part of the filename without a few last symbols and extension,
  661. // have to match it here as well, but we need full filename for downloadify, thus the param
  662. if ((fullName.indexOf('tumblr_')!=-1)&&!full)
  663. fullName=fullName.replace(/(tumblr_)|(_\d{2}\d{0,2})(?=\.)/gim,'');
  664. fullName=fullName.replace(/\\/g,'/'); //Function is used both for URLs and folder paths which have opposite slashes
  665. return fullName.split('/').pop();
  666. };
  667. function dl(base64data){ //Make downloadify button with base64 encoded image file as parameter
  668. // which will both cause save file dialog with custom filename and copy save path to clipboard
  669. Downloadify.create( 'down' ,{
  670. filename: function(){ return filename;}, //is this called "stateless"?
  671. data: base64data,
  672. dataType: 'base64',
  673. downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.png',
  674. onError: function(){ throw new Error('Downloadify error');},
  675. onComplete: onCmplt,
  676. swf: downloadifySwf,
  677. width: 100,
  678. height: 30,
  679. transparent: true,
  680. append: true,
  681. textcopy: function(){ if (DBrec) {return folder+filename;} else return '';}
  682. }); //If no database record is found, don't change the clipboard
  683. };
  684. function onCmplt(){ //Mark image as saved in the tag database
  685. if (DBrec) { // it is used to mark saved images on tumblr pages
  686. DBrec.s='1';
  687. tagsDB.set(getFileName(document.location.href), JSON.stringify(DBrec));
  688. document.title='♥ '+document.title; //Actually I wanted to put a diskette symbol there,
  689. }; // but because chromse sucks it does not support extended unicode in title
  690. }
  691. function submit(){ //Collects entered translations for missing tags
  692. tgs=$('td.cell'); //saves them to databases and relaunches tag analysis with new data
  693. $('input.category').parent().parent().css("background-color","");
  694. missing=false;
  695. $.each(tgs,function(i,v){
  696. if ($(v).parent().attr('ignore')) {
  697. ignore.push(v.id); //Mark hidden tags as ignored
  698. return true;
  699. };
  700. if ($(v).parent().attr('hidden'))
  701. return true;
  702. tg=$(v).find('input.txt');
  703. if (tg.length)
  704. tg=tg[0].value.trim(); //found translation tag
  705. else {
  706. tg=v.textContent.trim(); //found roman tag
  707. if ($(v).prop('swap')) {
  708. t=DBrec.t.replace(tg.split(' ').reverse().join(' '),tg);
  709. DBrec.t=t; //Apply swap changes to the current taglist
  710. };
  711. } //TODO: add checks for existing entries in another DB?
  712. cat=$(v).find('input.category');
  713. if (tg.length){
  714. if (!isANSI(tg)&&!allowUnicode) {
  715. $(v).find('input.txt').css("background-color","#ffb080");
  716. missing=true; //Indicate unicode characters in user input
  717. }
  718. else if (cat[0].checked) //name category was selected for this tag
  719. names.set(v.textContent.trim().toLowerCase(),tg)
  720. else if (cat[1].checked) //meta category was selected
  721. meta.set(v.textContent.trim().toLowerCase(), tg)
  722. else { //no category was selected, indicate missing input
  723. $(cat[0].parentNode.parentNode ).css("background-color","#ff8080");
  724. missing=true;
  725. }
  726. }
  727. else {
  728. $(v).find('input.txt').css("background-color","#ff8080");
  729. missing=true; //no translation was provided, indicate missing input
  730. return true;
  731. }
  732. }
  733. );
  734. tbd=$('#translations > tbody')[0];
  735. if (!missing){
  736. tbd.parentNode.removeChild(tbd);
  737. tb.hide();
  738. analyzeTags();
  739. };
  740. };
  741. function ex(){ //Export auxiliary tag databases as a text file
  742. Downloadify.create('ex' ,{
  743. filename: 'names&meta tags DB.txt',
  744. data: function(){
  745. xport={names:names.getAll(), meta:meta.getAll()};
  746. return JSON.stringify(xport, null, '\t');
  747. },
  748. dataType:'string',
  749. downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify2.png',
  750. onError: function(){ throw new Error('Downloadify2 error');},
  751. swf: downloadifySwf,
  752. width: 100,
  753. height: 30,
  754. transparent: true,
  755. append: false,
  756. textcopy: ''
  757. });
  758. $('a.exim')[0].removeAttribute('onclick');
  759. $('a#aex')[0].textContent='';
  760. };
  761. function im(){ //Import auxiliary tag databases as text file
  762. $('#im').append('<input type="file" id="files" style="width:97px;" accept="text/plain"/>');
  763. $('input#files')[0].onchange=handleFileSelect;
  764. $('a.exim')[1].removeAttribute('onclick');
  765. $('a#aim')[0].textContent='';
  766. };
  767. function handleFileSelect(evt) { //Fill in databases with data from imported file
  768. var file = evt.target.files[0];
  769. $('input#files')[0].value='';
  770. if (file.type!='text/plain') {
  771. alert('Wrong filetype: must be text');
  772. return false;
  773. };
  774. var reader = new FileReader();
  775. reader.onloadend = function(e) {
  776. clear=confirm('Would you like to clear existing databases before importing?');
  777. try {
  778. o=JSON.parse(e.target.r###lt);
  779. } catch(err){
  780. alert('Error: '+err.message);
  781. return false;
  782. };
  783. if (o.meta) {
  784. checkMatch(o.meta);
  785. if (clear)
  786. meta.clearAll();
  787. $.each(o.meta, function(i,v){
  788. meta.set(i,v);});
  789. }
  790. else
  791. alert('No meta DB found');
  792. if (o.names) {
  793. checkMatch(o.names);
  794. if (clear)
  795. names.clearAll();
  796. $.each(o.names, function(i,v){
  797. names.set(i,v);});
  798. }
  799. else
  800. alert('No names DB found');
  801. };
  802. reader.readAsText(file);
  803. };
  804. //TODO: add save button activation via keyboard
  805. //TODO: improve the button: open assigned folder directly, use modern dialog
  806. //TODO: ^ try to set last used directory in flash save dialog so as to avoid clipboard usage
  807. //TODO: add fallback to the tumblr hosted image if link url fails (requires storing post id and blog name)
  808. //TODO: add checks for common mistakes in unicode names like 実/美 & 奈/菜
  809. //TODO: option to disable unsorted category if translations are not required by user