AO3: [Wrangling] Search my canonicals for illegal characters

automatically runs a fandom-specific tag search over all your assigned fandoms, to find any canonicals with 'illegal' characters such as curly quotes or Chinese pipes

  1. // ==UserScript==
  2. // @name AO3: [Wrangling] Search my canonicals for illegal characters
  3. // @namespace https://greasyfork.org/en/users/906106-escctrl
  4. // @version 3.0
  5. // @description automatically runs a fandom-specific tag search over all your assigned fandoms, to find any canonicals with 'illegal' characters such as curly quotes or Chinese pipes
  6. // @author escctrl
  7. // @match https://archiveofourown.org/tags/search
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const COPY_SEARCH_STRING = false;
  16.  
  17. const DEBUG = false;
  18.  
  19. var found = 0; // counting found results
  20.  
  21. var node_ul = document.getElementById('wranglerbuttons') || document.querySelector('#main ul.navigation.actions');
  22. const node_li = document.createElement('li');
  23.  
  24. node_li.id = 'checkillegal';
  25. node_li.className = 'reindex';
  26. node_li.innerHTML = "<a href='#'>Check Illegal Characters</a>";
  27. if (COPY_SEARCH_STRING) node_li.addEventListener("click", copySearchString);
  28. else node_li.addEventListener("click", startBackgroundCheck);
  29. node_ul.appendChild(node_li);
  30.  
  31. function copySearchString() {
  32. document.getElementById('tag_search_name').value = "*’* OR *‘* OR *“* OR *”* OR *|* OR *–* OR *—* OR *―* OR *(* OR *)* OR *\\'\\'*";
  33. var tagtype = document.getElementById('tag_search_status');
  34. if (tagtype !== null) tagtype.value = 'T'; // if the Smaller Tag Search script is enabled
  35. else document.getElementById('tag_search_canonical_t').checked = true; // for the plain New Tag Search form
  36. }
  37.  
  38. function startBackgroundCheck() {
  39. // who am I logged in as?
  40. const user = document.querySelector('#greeting ul.user.navigation li.dropdown>a.dropdown-toggle[href*="/users/"]').href.match(/\/([A-Za-z0-9_]+)\/?$/i)[1];
  41. if (DEBUG) console.log("Illegal Characters: Logged in as user "+user);
  42.  
  43. window.ao3jail = false;
  44.  
  45. // get a list of all my wrangled fandoms
  46. pageload('https://archiveofourown.org/tag_wranglers/'+user, 'fandoms');
  47. }
  48.  
  49. function retrieveFandomList(me) {
  50. // find all the assigned fandoms in the table (URL and name)
  51. const fandomlist = me.querySelectorAll('table tbody tr th a');
  52.  
  53. // add a tracker for how many open XHR there will be
  54. window.openXHR = fandomlist.length;
  55. if (DEBUG) console.log("Illegal Characters: There are "+window.openXHR+" fandoms assigned, to be checked");
  56. document.getElementById('checkillegal').firstChild.innerText = "Checking " + window.openXHR + " fandoms";
  57.  
  58. fandomlist.forEach( (f, i) => {
  59. var url = 'https://archiveofourown.org/tags/search?tag_search%5Bname%5D=*%E2%80%99*+OR+*%E2%80%98*+OR+*%E2%80%9C*+OR+*%E2%80%9D*+OR+*%EF%BD%9C*+OR+*%E2%80%93*+OR+*%E2%80%94*+OR+*%E2%80%95*+OR+*%EF%BC%88*+OR+*%EF%BC%89*+OR+*%5C%27%5C%27*&tag_search%5Btype%5D=&tag_search%5Bcanonical%5D=T&tag_search%5Bsort_column%5D=name&tag_search%5Bsort_direction%5D=asc&commit=Search+Tags&tag_search%5Bfandoms%5D=';
  60. url += encodeURIComponent(f.innerText);
  61.  
  62. if (DEBUG) console.log("Illegal Characters: Fandom "+f.innerText+" will be checked in "+3000*i+" second");
  63. if (DEBUG) url = 'https://archiveofourown.org/tags/search?tag_search%5Bname%5D=*POV*&tag_search%5Btype%5D=&tag_search%5Bcanonical%5D=T&tag_search%5Bsort_column%5D=name&tag_search%5Bsort_direction%5D=asc&commit=Search+Tags&tag_search%5Bfandoms%5D='+ encodeURIComponent(f.innerText);
  64.  
  65. // collect the search results for specifically those fandoms
  66. setTimeout(function() {
  67. pageload(url, 'search');
  68. }, 3000*i);
  69. });
  70. }
  71.  
  72. function retrieveSearchResults(me) {
  73. window.openXHR--;
  74. // avoids updating the button by a slower (working) response after a page load was already jailed
  75. if (!window.ao3jail) document.getElementById('checkillegal').firstChild.innerText = "Checking " + window.openXHR + " fandoms";
  76.  
  77. const results = me.querySelectorAll('ol.tag.index li span');
  78. const fandom = me.querySelector('#tag_search_fandoms').value;
  79.  
  80. if (DEBUG) console.log("Illegal Characters: Fandom "+fandom+" has "+results.length+" tags with illegal characters");
  81. if (DEBUG) console.log("Illegal Characters: There are "+window.openXHR+" fandoms remaining to be checked");
  82.  
  83. // adds the found nodelist to the object
  84. var printtext = "";
  85. if (results.length > 0) {
  86. printtext = '<h4 class="heading">'+fandom+'</h4><ol class="tag index group">';
  87. found = found + results.length;
  88. for (let n of results.values()) { printtext += '<li>'+n.outerHTML+'</li>'; }
  89. printtext += '</ol>';
  90.  
  91. const node_parent = document.querySelector('#main');
  92. node_parent.innerHTML += printtext; // appending at the bottom of the list
  93. }
  94.  
  95. if (window.openXHR == 0) {
  96. if (DEBUG) console.log("Illegal Characters: Last fandom was checked, tallying up the result tags");
  97. // this was the last open XHR so we can print the total number found!
  98. printTotals();
  99. }
  100. }
  101.  
  102. function printTotals() {
  103. const heading = document.createElement('h3');
  104. heading.className = "heading"
  105. heading.innerText = found+' Found';
  106. const node_parent = document.querySelector('#new_tag_search');
  107. node_parent.insertAdjacentElement('afterend', heading);
  108. if (DEBUG) console.log("Illegal Characters: Script completed.");
  109. document.getElementById('checkillegal').firstChild.innerText = "Check for illegal characters finished";
  110. }
  111.  
  112. function saySorry(me) {
  113. // this does not count down the checked fandoms so button can never show as "finished" when there were errors
  114.  
  115. // user output only if this is the fist page that failed
  116. if (window.ao3jail == false) {
  117. // check if we received a retry-after value in the response
  118. if (DEBUG) console.log("Illegal Characters: 429 retry-after " + me.getResponseHeader('Retry-After'));
  119. var timeout = parseInt(me.getResponseHeader('Retry-After') || 0);
  120. timeout = (timeout > 0) ? ' You can try again in '+ Math.ceil(timeout / 60) +' minutes.' : '';
  121. document.querySelector('#new_tag_search').outerHTML += "<h3 class='heading'>Sorry, the script stopped because it ran into the "+me.statusText+" error."+ timeout +"</h3>";
  122. document.getElementById('checkillegal').firstChild.innerText = "Check stopped: error";
  123. }
  124. window.ao3jail = true;
  125. // console output for every page that didn't load properly
  126. console.log("Illegal Characters: The background page load ran into an issue: "+me.status+" "+me.statusText);
  127. }
  128.  
  129. function pageload(url, what) {
  130. if (DEBUG) console.log("Illegal Characters: Loading page "+url);
  131. if (window.ao3jail) {
  132. if (DEBUG) console.log("Illegal Characters: This page load was skipped due to previously encountered errors.");
  133. return false;
  134. }
  135. const xhr = new XMLHttpRequest();
  136. xhr.onload = () => {
  137. if (xhr.status==200 && xhr.response != "") {
  138. if (what == 'fandoms') retrieveFandomList(xhr.response);
  139. else retrieveSearchResults(xhr.response);
  140. }
  141. else saySorry(xhr);
  142. };
  143. xhr.onerror = () => { saySorry(xhr); };
  144. xhr.open("GET", url);
  145. xhr.responseType = "document";
  146. xhr.send();
  147. }
  148.  
  149. })();