What.CD: YAVAH

Yet Another Various Artists Helper

  1. // ==UserScript==
  2. // @name What.CD: YAVAH
  3. // @namespace hateradio)))
  4. // @author hateradio
  5. // @version 7.2
  6. // @description Yet Another Various Artists Helper
  7. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY0NDMyN0UzOUEwQzExRTA4REU5OUY2M0Q5RDZGNTQ1IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjY0NDMyN0U0OUEwQzExRTA4REU5OUY2M0Q5RDZGNTQ1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjQ0MzI3RTE5QTBDMTFFMDhERTk5RjYzRDlENkY1NDUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjQ0MzI3RTI5QTBDMTFFMDhERTk5RjYzRDlENkY1NDUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6q1rvzAAAEMElEQVR42uyaP0gcQRTG12CpYBkVUkWto22CYqtiq7FWrNVervdPK5r2oq2orSiptQkE1LRqkSooBNIk+xvyLe/m9la9XDzIvoFh9mZnZme+973vvRE7enp6kjKXF3/aD2n9VbJa4eAdKQPepu2nkhJgAAa8KrEHvHyRlLw4AA6AA+AAOAAOgAPgADgADoAD4AA4AA6AA+AAOAAOgAPgADgADoAD4AA4AA6AA+AAOAAOgAPQmjI7O5scHx8nq6urWd/o6Gjoo/JeZWFhIfTRtqIsLS3VfaNtDOju7s6ep6amcvsnJiZCe3Bw0JJvjoyMhPbi4iL3fedzHPzm5ia0XV1doR0cHEyGh4eTu7u7cHj1Awq/T09Pk9vb21wAsSjsoVxeXiaVSqVmLCzT+729vWztvPXapgGiIwe1DNDGT05Ocg+/tbWVjRGQy8vL2e+1tbWa9zMzM2EeQLcVAH2czVCxPn2Hh4ehv7e3N2MF/QImBo1x5+fnyfj4eLK4uBj6BwYGMvZo/tzcXBjDWMrV1VXDvXU+p+WhIwcBBOhpgRkbGwvPAiUusuz6+npGfw4Zv2e+1mUMoNC2FQBLP0SO37u7u+E39AQYDsBzHgAAhPUp9/f3ud/Qe+mN7WtE/7ZEAYlc3gHpB4SiIsorvMXhUqoPoGLF9fV1exnAoaT4tLK+rCMRtOK3v78f2unp6TAHGqMTCJ2dq7V4D4iTk5Oh2lKkAc8eBWIri9IcQKKF3wKKpS7hTu8pPK+srGRrbWxsZO+tK7FGEav4X+H3aVstaSb87tEMIAbjc/heXppbrVZr4rnSXJv+QmH1S6Di1Jh1rDbgCvTLJeJCf/yOeUVzmnIBCRcb1UE50Pz8fBaeRDUrTIxRgeYaY/vtHMKjzeP1LRsJVOQqsY9LKIt8/8kAIDYSLImM2LC5uZn5n01YFIpsri+/7uvrq0tyAEh3AAACbMZrTgxaf39/Xegr6v9rEZSwAACHZ0P0adMWnO3t7WzjsohYYGO01tOcmBGsf3Z2VgdaUZz/JwwQC6Cwwg1WRn1jS7JpDqqNDQ0N1TFA8VpzcDGxBssrgxPz7JzYvXBDaQtVgBbF/6bD4NHRUc1NzAoPWR4A7ezshD5Zzlpb11KyP/q5sDDHWl+XJcV4zYk1wDIrrzyGAU9OhCRiWMvGVytYsfpay2lTjOUmF98LOLwsayOIAKAyVtqiy48dQyR5KP43zQBtzvqdBKvoEmQB1Fzd3mxmGGdxjXL+Rv6ft7+WMkAfsP5lQ5ilsuK4rsDycaWtEjlZykYDXXdtHsK30RPWka7ENz0JpdyvpQxQLGbDsWDFOX5sbSuE2rRETmujB9b3bdEcGaARAx66NXoq3Gwq7H8WdwD+XwB+lPj8PwHgOK3fSnj4r2n9TB7wPa1v0ko0eF2Sw39J60fY/1uAAQDAo7S7+Gt3nAAAAABJRU5ErkJggg==
  8. // @include /https://redacted\.sh/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
  9. // @include /https://orpheus\.network/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
  10. // @include /https://notwhat\.cd/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
  11. // #updated 26 Nov 2024
  12. // #since 18 Jun 2010
  13. // ==/UserScript==
  14.  
  15. (() => {
  16.  
  17. if (!Element.prototype.matches)
  18. Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector
  19.  
  20. if (!Element.prototype.closest) {
  21. Element.prototype.closest = function (s) {
  22. let el = this
  23. if (!document.documentElement.contains(el)) return null
  24. do {
  25. if (el.matches(s)) return el
  26. el = el.parentElement || el.parentNode
  27. } while (el !== null && el.nodeType === 1)
  28. return null
  29. }
  30. }
  31.  
  32. const _ = {
  33. css: text => {
  34. if (!this.style) {
  35. this.style = document.createElement('style')
  36. this.style.type = 'text/css'
  37. document.body.appendChild(this.style)
  38. }
  39. this.style.appendChild(document.createTextNode(`${text}\n`))
  40. },
  41. js: func => {
  42. const script = document.createElement('script')
  43. script.type = 'application/javascript'
  44. script.textContent = `;(${func})();`
  45. document.body.appendChild(script)
  46. document.body.removeChild(script)
  47. },
  48. debounce: (func, wait) => {
  49. let timeout
  50. return function (...args) {
  51. const run = () => {
  52. timeout = null
  53. func.apply(this, args)
  54. }
  55.  
  56. clearTimeout(timeout)
  57. timeout = setTimeout(run, wait)
  58. }
  59. },
  60. on: (element, type, selector, listener) => {
  61. element.addEventListener(type, event => {
  62. const found = event.target.closest(selector)
  63. if (found) listener.call(found, event)
  64. }, false)
  65. }
  66. }
  67.  
  68. class YavaMenu {
  69.  
  70. constructor() {
  71. this.sibling = document.querySelector('.box_addartists, #artist_tr')
  72. this.setup()
  73. YavaMenu.check = document.getElementById('yavah_semi')
  74. }
  75.  
  76. get types() {
  77. return ['Main', 'Guest', 'Remixer', 'Composer', 'Conductor', 'DJ / Compiler', 'Producer']
  78. }
  79.  
  80. setup() {
  81. if (!this.sibling) return
  82.  
  83. const box = document.createElement('div')
  84. box.id = 'YAVAH'
  85. _.on(box, 'click', 'a', this.toggle)
  86.  
  87. this.boxSetup(box)
  88.  
  89. box.querySelector('a').click()
  90. this.box = box
  91. }
  92.  
  93. boxSetup(box) {
  94. box.className = 'box'
  95. box.innerHTML = `
  96. <div class="head">
  97. <strong><abbr title="Yet Another Various Artists Helper">YAVAH</abbr></strong>
  98. </div>
  99. <div style="padding: 3px 6px 6px">
  100. ${this.generateInputs()}
  101. </div>`
  102.  
  103. this.sibling.parentElement.insertBefore(box, this.sibling)
  104. }
  105.  
  106. toggle(e) {
  107. e.preventDefault()
  108. const tog = this.parentElement.nextElementSibling.classList.toggle('hidden')
  109. this.innerHTML = `<code>${tog ? '+' : '-'}</code> ${this.dataset.type}`
  110. }
  111.  
  112. generateInputs() {
  113. // let yavahtog = this.nextElementSibling.classList.toggle('hidden');
  114. // this.firstElementChild.innerHTML = '<code>' + (yavahtog ? '+' : '-') + '</code> ' + this.firstElementChild.dataset.type
  115. const boxes = this.types.map(type => {
  116. return `<p><a href="#" data-type="${type}"><code>+</code> ${type}</a></p><textarea class="noWhutBB yavahtext hidden"></textarea>`
  117. }).join('')
  118.  
  119. return `
  120. <p>Enter artists, one per line.</p>
  121. <p>
  122. <input type="checkbox" id="yavah_semi"> <label for="yavah_semi">Split semi-colons</label>
  123. </p>
  124. ${boxes}
  125. <p>Review the changes below.</p>`
  126. }
  127.  
  128. addEvent(cb) {
  129. return this.box && !this.box.addEventListener('input', _.debounce(cb, 250), false)
  130. }
  131.  
  132. }
  133.  
  134. class YavaMenuTr extends YavaMenu {
  135. boxSetup(box) {
  136. box.innerHTML = this.generateInputs()
  137.  
  138. const tr = document.createElement('tr')
  139. tr.innerHTML = '<td class="label">YAVAH</td><td></td>'
  140. tr.lastElementChild.appendChild(box)
  141.  
  142. this.sibling.parentElement.insertBefore(tr, this.sibling)
  143. }
  144. }
  145.  
  146. class Yavah {
  147.  
  148. constructor() {
  149. this.selector = 'input[name="aliasname[]"]:last-of-type, input[name="artists[]"]:last-of-type'
  150. this.add = document.querySelector('.box_addartists a, #artistfields a.brackets')
  151. this.inputs = document.querySelector('#AddArtists, #artistfields')
  152.  
  153. if (!this.inputs)
  154. return
  155.  
  156. _.css('#YAVAH p a { display:block } .yavahtext{width: 90%; height: 6em}')
  157.  
  158. this.stored = this.inputs.innerHTML
  159. this.event = this.event.bind(this)
  160. }
  161.  
  162. regex() {
  163. if (YavaMenu.check.checked)
  164. return /[^\r\n;]+/g
  165. return /[^\r\n]+/g
  166. }
  167.  
  168. /**
  169. *
  170. * @param {HTMLTextAreaElement} textarea
  171. * @param {number} index Index of HTMLOptionElement within HTMLSelectElement to set (Main, Guest, Composer, etc.)
  172. */
  173. fill(textarea, index) {
  174. const lines = textarea.value.match(this.REGEX) || []
  175. const unique = new Set(lines.map(_ => _.trim()))
  176. unique.delete('')
  177.  
  178. unique.forEach(name => {
  179. this.inputs.querySelector(this.selector).value = name
  180. this.inputs.querySelector('select:last-of-type').value = index + 1
  181. this.add.click()
  182. })
  183. }
  184.  
  185. event() {
  186. // Reset the artist box
  187. this.inputs.innerHTML = this.stored
  188. _.js(() => window.ArtistFieldCount = -1000)
  189.  
  190. const textareas = document.querySelectorAll('#YAVAH textarea')
  191. this.REGEX = this.regex()
  192. Array.from(textareas).forEach(this.fill, this)
  193. }
  194.  
  195. static main() {
  196. const yava = new Yavah()
  197. const menu = /(?:requests\.php|upload\.php)/.test(document.location.pathname) ? new YavaMenuTr : new YavaMenu
  198. menu.addEvent(yava.event)
  199.  
  200. if (document.location.hash === '#yavah-test') {
  201. new YavaTest(yava)
  202. }
  203. }
  204.  
  205. }
  206.  
  207. class YavaTest {
  208. constructor(yavah) {
  209. this.y = yavah
  210.  
  211. this.indices = '1-1-1-2-2-2-3-3-3-4-4-4-5-5-5-6-6-6-7-7-7-1'
  212. this.values = 'A1;A1.1-A2-A3-B1;B1.1-B2-B3-C1;C1.1-C2-C3-D1;D1.1-D2-D3-E1;E1.1-E2-E3-F1;F1.1-F2-F3-G1;G1.1-G2-G3-'
  213.  
  214. this.indicesSemi = '1-1-1-1-2-2-2-2-3-3-3-3-4-4-4-4-5-5-5-5-6-6-6-6-7-7-7-7-1'
  215. this.valuesSemi = this.values.replaceAll(';', '-')
  216.  
  217. this.testInit()
  218. this.testRegular()
  219. this.testSemi()
  220. }
  221.  
  222. testInit() {
  223. console.log('YAVAH TEST!')
  224.  
  225. // asume if no inputs, then there is nothing to do
  226. if (!this.y.inputs)
  227. return
  228.  
  229. // fill data
  230. const textareas = Array.from(document.querySelectorAll('.yavahtext'))
  231. textareas.forEach((t, i) => {
  232. const ch = String.fromCharCode(65 + i);
  233. t.value = `${ch}1;${ch}1.1\n${ch}2\n${ch}3`
  234. })
  235.  
  236. // go for it!
  237. this.y.event()
  238. }
  239.  
  240. selects(indices) {
  241. // assert dropdowns
  242. console.group('YAVAH DROPDOWN TEST')
  243.  
  244. const selects = document.querySelectorAll('#AddArtists select, #artistfields select')
  245.  
  246. if (selects.length === indices.split('-').length)
  247. console.debug('[Passed] Valid number of dropdowns')
  248. else
  249. console.warn('Invalid number of dropdowns')
  250.  
  251. if (Array.from(selects).map(_ => _.value).join('-') === indices)
  252. console.debug('[Passed] Valid values for dropdowns')
  253. else
  254. console.warn('Invalid values for dropdowns')
  255.  
  256. console.groupEnd()
  257. }
  258.  
  259. inputs(values) {
  260. // assert inputs
  261. console.group('YAVAH INPUT TEST')
  262.  
  263. const inputs = document.querySelectorAll('input[name="aliasname[]"], input[name="artists[]"]')
  264.  
  265. if (inputs.length === values.split('-').length)
  266. console.debug('[Passed] Valid number of inputs')
  267. else
  268. console.warn('Invalid number of inputs')
  269.  
  270. if (Array.from(inputs).map(_ => _.value).join('-') === values)
  271. console.debug('[Passed] Valid values for inputs')
  272. else
  273. console.warn('Invalid values for inputs')
  274. console.groupEnd()
  275. }
  276.  
  277. testRegular() {
  278. console.info('Starting Regular Test')
  279. this.selects(this.indices)
  280. this.inputs(this.values)
  281. }
  282.  
  283. testSemi() {
  284. console.info('Starting Semicolon Test')
  285. YavaMenu.check.checked = true
  286. this.y.event()
  287. this.selects(this.indicesSemi)
  288. this.inputs(this.valuesSemi)
  289. }
  290.  
  291. }
  292.  
  293. Yavah.main()
  294.  
  295. })()