MusicBrainz: Highlight identical barcodes and toggle merge checkboxes

Highlights sets of identical barcodes and toggles checkboxes for merging on click

  1. // ==UserScript==
  2. // @name MusicBrainz: Highlight identical barcodes and toggle merge checkboxes
  3. // @namespace https://musicbrainz.org/user/chaban
  4. // @version 1.2
  5. // @tag ai-created
  6. // @description Highlights sets of identical barcodes and toggles checkboxes for merging on click
  7. // @author chaban
  8. // @license MIT
  9. // @match *://*.musicbrainz.org/*/*/releases*
  10. // @match *://*.musicbrainz.org/release-group/*
  11. // @match *://*.musicbrainz.org/label/*
  12. // @match *://*.musicbrainz.org/*/*/*edits
  13. // @match *://*.musicbrainz.org/edit/*
  14. // @match *://*.musicbrainz.org/user/*/edits*
  15. // @match *://*.musicbrainz.org/search/edits*
  16. // @icon https://musicbrainz.org/static/images/favicons/android-chrome-512x512.png
  17. // @grant none
  18. // @run-at document-idle
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. const identifierToColor = {};
  25. const identifierToCheckboxes = {};
  26.  
  27. function getRandomColor() {
  28. const letters = '89ABCDEF';
  29. let color = '#';
  30. for (let i = 0; i < 6; i++) {
  31. color += letters[Math.floor(Math.random() * letters.length)];
  32. }
  33. return color;
  34. }
  35.  
  36. function removeLeadingZeros(barcode) {
  37. return barcode.replace(/^0+/, '');
  38. }
  39.  
  40. /**
  41. * Toggles the checkboxes for the entire group associated with the clicked barcode cell.
  42. * @param {Event} event The click event.
  43. */
  44. function toggleMergeCheckbox(event) {
  45. const clickedBarcodeCell = event.currentTarget;
  46. const clickedIdentifier = clickedBarcodeCell.dataset.barcodeIdentifier;
  47.  
  48. if (!clickedIdentifier || !identifierToCheckboxes[clickedIdentifier]) {
  49. return;
  50. }
  51.  
  52. const currentGroupCheckboxes = identifierToCheckboxes[clickedIdentifier];
  53. const shouldCheck = !currentGroupCheckboxes.some(cb => cb.checked);
  54.  
  55. const allCheckboxesOnPage = document.querySelectorAll('input[name="add-to-merge"][type="checkbox"]');
  56.  
  57. allCheckboxesOnPage.forEach(checkbox => {
  58. const row = checkbox.closest('tr');
  59. if (!row) return;
  60.  
  61. const barcodeCellInThisRow = row.querySelector('.barcode-cell[data-barcode-identifier]');
  62. if (barcodeCellInThisRow && barcodeCellInThisRow.dataset.barcodeIdentifier === clickedIdentifier) {
  63. checkbox.checked = shouldCheck;
  64. } else {
  65. checkbox.checked = false;
  66. }
  67. });
  68. }
  69.  
  70. /**
  71. * Processes a given table element to find and highlight identical barcodes.
  72. * @param {HTMLElement} table The table element to process.
  73. */
  74. function processTable(table) {
  75. const barcodeCellsInTable = {};
  76. let barcodeColumnIndex = -1;
  77. let formatColumnIndex = -1;
  78.  
  79. let headerRow = table.querySelector('thead tr');
  80. if (!headerRow) {
  81. headerRow = table.querySelector('tr:has(th)');
  82. }
  83. if (!headerRow) {
  84. headerRow = table.querySelector('tbody tr');
  85. }
  86.  
  87. if (headerRow) {
  88. const headerCells = Array.from(headerRow.children);
  89. headerCells.forEach((th, index) => {
  90. const headerText = th.textContent.trim();
  91. if (headerText === 'Barcode') {
  92. barcodeColumnIndex = index;
  93. }
  94. if (headerText === 'Format') {
  95. formatColumnIndex = index;
  96. }
  97. });
  98. }
  99.  
  100. const dataRows = table.querySelectorAll('tbody tr, tr:not(:has(th)):not(:first-child)');
  101.  
  102. dataRows.forEach(row => {
  103. let barcodeCell = null;
  104. let formatCell = null;
  105.  
  106. if (barcodeColumnIndex !== -1 && row.children[barcodeColumnIndex]) {
  107. barcodeCell = row.children[barcodeColumnIndex];
  108. }
  109. if (formatColumnIndex !== -1 && row.children[formatColumnIndex]) {
  110. formatCell = row.children[formatColumnIndex];
  111. }
  112.  
  113. if (!barcodeCell || barcodeCell.tagName === 'TH') {
  114. const potentialBarcodeCell = row.querySelector('.barcode-cell');
  115. if (potentialBarcodeCell && potentialBarcodeCell.tagName === 'TD') {
  116. barcodeCell = potentialBarcodeCell;
  117. }
  118. }
  119.  
  120. if (barcodeCell && barcodeCell.tagName === 'TD') {
  121. const barcode = barcodeCell.textContent.trim();
  122. const format = formatCell ? formatCell.textContent.trim() : '';
  123.  
  124. const mergeCheckbox = row.querySelector('input[name="add-to-merge"][type="checkbox"]');
  125.  
  126. if (barcode !== '[none]' && barcode !== '') {
  127. const normalizedBarcode = removeLeadingZeros(barcode);
  128. const identifier = `${normalizedBarcode}-${format}`;
  129.  
  130. barcodeCell.dataset.barcodeIdentifier = identifier;
  131.  
  132. if (!barcodeCellsInTable[identifier]) {
  133. barcodeCellsInTable[identifier] = [];
  134. }
  135. barcodeCellsInTable[identifier].push(barcodeCell);
  136.  
  137. if (mergeCheckbox) {
  138. if (!identifierToCheckboxes[identifier]) {
  139. identifierToCheckboxes[identifier] = [];
  140. }
  141. identifierToCheckboxes[identifier].push(mergeCheckbox);
  142. }
  143. }
  144. }
  145. });
  146.  
  147. for (const identifier in barcodeCellsInTable) {
  148. if (barcodeCellsInTable[identifier].length > 1) {
  149. let color = identifierToColor[identifier];
  150. if (!color) {
  151. color = getRandomColor();
  152. identifierToColor[identifier] = color;
  153. }
  154. barcodeCellsInTable[identifier].forEach(cell => {
  155. cell.style.backgroundColor = color;
  156. cell.style.fontWeight = 'bold';
  157. cell.style.padding = '2px 4px';
  158. cell.style.borderRadius = '3px';
  159.  
  160. if (identifierToCheckboxes[identifier] && identifierToCheckboxes[identifier].length > 0) {
  161. cell.style.cursor = 'pointer';
  162. cell.addEventListener('click', toggleMergeCheckbox);
  163. } else {
  164. cell.style.cursor = 'auto';
  165. cell.removeEventListener('click', toggleMergeCheckbox);
  166. }
  167. });
  168. }
  169. }
  170. }
  171.  
  172. function highlightBarcodesOnPage() {
  173. document.querySelectorAll('.mergeable-table, table.merge-releases').forEach(table => {
  174. processTable(table);
  175. });
  176. }
  177. highlightBarcodesOnPage();
  178. })();