VGMdb Metadata copy

Adds copy buttons to the VGMdb album pages to easily copy metadata.

  1. // ==UserScript==
  2. // @name VGMdb Metadata copy
  3. // @namespace https://vgmdb.net/
  4. // @version 1.8
  5. // @description Adds copy buttons to the VGMdb album pages to easily copy metadata.
  6. // @author kahpaibe
  7. // @match https://vgmdb.net/album/*
  8. // @grant none
  9. // @run-at document-end
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const PREFIX = '';
  17. const SUFFIX = '';
  18.  
  19. // Helper function to style buttons consistently
  20. const styleButton = (button) => {
  21. button.title = 'Copy this text to clipboard';
  22. button.style.marginLeft = '8px';
  23. button.style.padding = '1px 6px';
  24. button.style.fontSize = '0.75em';
  25. button.style.color = '#CEFFFF';
  26. button.style.background = 'transparent';
  27. button.style.border = '1px solid #CEFFFF';
  28. button.style.cursor = 'pointer';
  29. button.style.transition = 'background 0.3s, color 0.3s';
  30. button.style.verticalAlign = 'middle';
  31. };
  32.  
  33. // Helper function to create and append a "COPY" button next to a field
  34. const addCopyButton = (innerText, labelText, selector, tooltipText, fieldValue) => {
  35. const rows = document.querySelectorAll('#album_infobit_large tr');
  36.  
  37. rows.forEach(row => {
  38. const labelCell = row.querySelector('td span.label b');
  39. if (labelCell && labelCell.textContent.trim() === labelText) {
  40. const valueCell = row.cells[1];
  41. let field = fieldValue || valueCell.textContent.trim();
  42. if (selector) {
  43. const link = valueCell.querySelector(selector);
  44. if (link) {
  45. field = link.textContent.trim();
  46. }
  47. }
  48.  
  49. const button = document.createElement('span');
  50. button.innerText = innerText; // Set innerText here
  51. button.title = tooltipText;
  52. styleButton(button);
  53.  
  54. button.onclick = () => {
  55. navigator.clipboard.writeText(field).then(() => {
  56. const original = button.innerText;
  57. button.innerText = '✔ COPIED';
  58. setTimeout(() => button.innerText = original, 1500);
  59. });
  60. };
  61.  
  62. labelCell.parentElement.appendChild(button);
  63. }
  64. });
  65. };
  66.  
  67. // Function to handle Release Date buttons
  68. const addReleaseDateButtons = () => {
  69. const rows = document.querySelectorAll('#album_infobit_large tr');
  70. const releaseDateRow = Array.from(rows).find(row => {
  71. const labelCell = row.querySelector('td span.label b');
  72. return labelCell && labelCell.textContent.trim() === 'Release Date';
  73. });
  74.  
  75. if (!releaseDateRow) return;
  76.  
  77. const valueCell = releaseDateRow.cells[1];
  78. const dateLink = valueCell.querySelector('a[title^="View albums released on"]');
  79. const eventLink = valueCell.querySelector('a.link_event');
  80.  
  81. let formattedDate = '';
  82. if (dateLink) {
  83. const releaseDateStr = dateLink.textContent.trim();
  84. const dateObj = new Date(releaseDateStr);
  85. formattedDate = `${dateObj.getFullYear()}.${('0' + (dateObj.getMonth() + 1)).slice(-2)}.${('0' + dateObj.getDate()).slice(-2)}`;
  86. addCopyButton('⎘', 'Release Date', null, 'Copy Release Date to clipboard', formattedDate);
  87. }
  88.  
  89. let event = '';
  90. if (eventLink) {
  91. event = eventLink.textContent.trim();
  92. addCopyButton('⎘', 'Release Date', null, 'Copy Event to clipboard', event);
  93. }
  94.  
  95. if (dateLink) {
  96. const combined = event ? `[${formattedDate}][${event}]` : `[${formattedDate}]`;
  97. addCopyButton('⎘', 'Release Date', null, 'Copy formatted release date to clipboard', combined);
  98. }
  99. };
  100.  
  101. const addTitleCopyButtons = () => {
  102. const visibleTitles = document.querySelectorAll('.albumtitle');
  103.  
  104. visibleTitles.forEach(span => {
  105. const computedStyle = window.getComputedStyle(span);
  106. if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
  107. return;
  108. }
  109.  
  110. // Skip titles inside <a> tags (like in the album thumb list)
  111. if (span.closest('a')) {
  112. return;
  113. }
  114.  
  115. const fragments = [];
  116. let currentText = '';
  117.  
  118. // Break apart the nodes and <br> inside this span
  119. span.childNodes.forEach(node => {
  120. if (node.nodeName === 'BR') {
  121. if (currentText.trim()) fragments.push(currentText.trim());
  122. fragments.push('<br>');
  123. currentText = '';
  124. } else if (node.nodeType === Node.TEXT_NODE) {
  125. currentText += node.textContent;
  126. } else {
  127. currentText += node.outerHTML || node.textContent;
  128. }
  129. });
  130.  
  131. if (currentText.trim()) {
  132. fragments.push(currentText.trim());
  133. }
  134.  
  135. // Clear the span and re-inject each line with its own copy button
  136. span.innerHTML = '';
  137.  
  138. fragments.forEach(fragment => {
  139. if (fragment === '<br>') {
  140. span.appendChild(document.createElement('br'));
  141. return;
  142. }
  143.  
  144. const lineSpan = document.createElement('span');
  145. lineSpan.textContent = fragment;
  146.  
  147. const button = document.createElement('button');
  148. styleButton(button);
  149. button.innerText = '⎘'; // Ensure button has text
  150. button.title = 'Copy this title to clipboard';
  151.  
  152. button.addEventListener('click', () => {
  153. navigator.clipboard.writeText(fragment).then(() => {
  154. const original = button.innerText;
  155. button.innerText = '✔ COPIED!';
  156. setTimeout(() => button.innerText = original, 1000);
  157. });
  158. });
  159.  
  160. span.appendChild(lineSpan);
  161. span.appendChild(button);
  162.  
  163. // Add a second button for copying formatted string
  164. const formattedStringButton = document.createElement('button');
  165. styleButton(formattedStringButton);
  166. formattedStringButton.innerText = '⎘'; // Same button style
  167. formattedStringButton.title = 'Copy formatted album info to clipboard';
  168.  
  169. // Fetch Publisher and Catalog Number dynamically within the context
  170. const publisherCell = Array.from(document.querySelectorAll('td span.label b')).find(b => b.textContent.trim() === 'Publisher');
  171. const publisher = publisherCell ? publisherCell.closest('tr').querySelector('td:nth-child(2)').textContent.trim() : '';
  172.  
  173. const catalogNumberCell = Array.from(document.querySelectorAll('td span.label b')).find(b => b.textContent.trim() === 'Catalog Number');
  174. const catalogNumber = catalogNumberCell ? catalogNumberCell.closest('tr').querySelector('td:nth-child(2)').textContent.trim() : '';
  175.  
  176.  
  177. // Re-fetch Release Date and Event here to avoid assumptions
  178. const rows = document.querySelectorAll('#album_infobit_large tr');
  179. let formattedDate = '';
  180. let event = '';
  181.  
  182. rows.forEach(row => {
  183. const labelCell = row.querySelector('td span.label b');
  184. if (labelCell && labelCell.textContent.trim() === 'Release Date') {
  185. const valueCell = row.cells[1];
  186. const dateLink = valueCell.querySelector('a[title^="View albums released on"]');
  187. const eventLink = valueCell.querySelector('a.link_event');
  188.  
  189. if (dateLink) {
  190. const releaseDateStr = dateLink.textContent.trim();
  191. const dateObj = new Date(releaseDateStr);
  192. formattedDate = `${dateObj.getFullYear()}.${('0' + (dateObj.getMonth() + 1)).slice(-2)}.${('0' + dateObj.getDate()).slice(-2)}`;
  193. }
  194.  
  195. if (eventLink) {
  196. event = eventLink.textContent.trim();
  197. }
  198. }
  199. });
  200.  
  201. // Construct the formatted string
  202. let formattedString = PREFIX;
  203. formattedString += `[${formattedDate}]`;
  204. if (event) {
  205. formattedString += `[${event}]`;
  206. }
  207. if (publisher) {
  208. formattedString += ` ${publisher} -`;
  209. }
  210. formattedString += ` ${fragment}`;
  211. if (catalogNumber) {
  212. formattedString += ` {${catalogNumber}}`;
  213. } else {
  214. formattedString += ' {nocat#}';
  215. }
  216. formattedString += SUFFIX;
  217.  
  218.  
  219. formattedStringButton.addEventListener('click', () => {
  220. navigator.clipboard.writeText(formattedString).then(() => {
  221. const original = formattedStringButton.innerText;
  222. formattedStringButton.innerText = '✔ COPIED!';
  223. setTimeout(() => formattedStringButton.innerText = original, 1000);
  224. });
  225. });
  226.  
  227. // Append button to the DOM (you can adjust where this button goes)
  228. document.body.appendChild(formattedStringButton);
  229.  
  230. span.appendChild(formattedStringButton);
  231.  
  232. });
  233. });
  234. };
  235.  
  236.  
  237. // Main function to initialize the copy buttons
  238. const initializeCopyButtons = () => {
  239. // Add metadata buttons
  240. addCopyButton('⎘', 'Catalog Number', null, 'Copy Catalog Number to clipboard');
  241. addCopyButton('⎘', 'Publisher', '.productname', 'Copy Publisher to clipboard');
  242. addReleaseDateButtons(); // Add the buttons for Release Date, Event, and Date+Event
  243.  
  244. // Add inline copy buttons for album titles and secondary names
  245. addTitleCopyButtons();
  246. };
  247.  
  248. // Initialize the script
  249. initializeCopyButtons();
  250.  
  251. })();