Tumblr-image-sorter-get

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

当前为 2015-08-19 提交的版本,查看 最新版本

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