MusicBrainz: Highlight identical barcodes and toggle merge checkboxes

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

当前为 2025-05-23 提交的版本,查看 最新版本

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