MusicBrainz: Highlight identical barcodes and toggle merge checkboxes

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

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

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