您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copies the tracklist in MusicBrainz track parser compatible format to clipboard when clicking on track header
// ==UserScript== // @name Harmony: Copy tracklist to clipboard // @namespace https://musicbrainz.org/user/chaban // @description Copies the tracklist in MusicBrainz track parser compatible format to clipboard when clicking on track header // @tag ai-created // @version 2.1.2 // @author chaban // @license MIT // @match *://harmony.pulsewidth.org.uk/release* // @exclude *://harmony.pulsewidth.org.uk/release/actions* // @grant none // @icon https://harmony.pulsewidth.org.uk/harmony-logo.svg // ==/UserScript== (function() { 'use strict'; const TOOLTIP_DISPLAY_DURATION = 2000; /** * Displays a temporary tooltip message near specific coordinates. * @param {number} clientX The X coordinate of the cursor. * @param {number} clientY The Y coordinate of the cursor. * @param {string} message The message to display. * @param {string} type 'success' or 'error' to determine styling. */ function showTooltipAtCursor(clientX, clientY, message, type) { let tooltip = document.createElement('div'); tooltip.textContent = message; tooltip.style.cssText = ` position: fixed; background-color: ${type === 'success' ? '#4CAF50' : '#f44336'}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 10000; opacity: 0; transition: opacity 0.3s ease-in-out; pointer-events: none; white-space: nowrap; `; document.body.appendChild(tooltip); tooltip.style.left = `${clientX - (tooltip.offsetWidth / 2)}px`; tooltip.style.top = `${clientY - tooltip.offsetHeight - 10}px`; if (parseFloat(tooltip.style.left) < 5) { tooltip.style.left = '5px'; } if (parseFloat(tooltip.style.left) + tooltip.offsetWidth > window.innerWidth - 5) { tooltip.style.left = `${window.innerWidth - tooltip.offsetWidth - 5}px`; } if (parseFloat(tooltip.style.top) < 5) { tooltip.style.top = `${clientY + 10}px`; } setTimeout(() => { tooltip.style.opacity = '1'; }, 10); setTimeout(() => { tooltip.style.opacity = '0'; tooltip.addEventListener('transitionend', () => tooltip.remove()); }, TOOLTIP_DISPLAY_DURATION); } /** * Helper function to get clean text content by removing specified selectors. * This function is now defined once in the outer scope. * @param {HTMLElement} element The HTML element to extract text from. * @param {string[]} selectorsToRemove An array of CSS selectors for elements to remove. * @returns {string} The cleaned text content. */ const getCleanText = (element, selectorsToRemove) => { if (!element) return ''; const tempDiv = document.createElement('div'); tempDiv.innerHTML = element.innerHTML; Array.from(tempDiv.querySelectorAll(selectorsToRemove.join(','))).forEach(el => el.remove()); return tempDiv.textContent.trim(); }; /** * Extracts tracklist data from a given table and formats it for MusicBrainz parser. * @param {HTMLTableElement} table The tracklist table element. * @param {MouseEvent} event The click event for tooltip positioning. * @returns {string[]|null} An array of formatted tracklist strings, or null if an error occurs. */ function processTracklistTable(table, event) { const headerRow = table.querySelector('thead tr'); if (!headerRow) { showTooltipAtCursor(event.clientX, event.clientY, 'Table headers not found!', 'error'); return null; } const columnHeaderMap = { trackNum: 'Track', title: 'Title', artist: 'Artists', length: 'Length' }; const columnIndices = {}; const headers = Array.from(headerRow.querySelectorAll('th')); for (const [key, headerText] of Object.entries(columnHeaderMap)) { const foundIndex = headers.findIndex(th => th.textContent.trim() === headerText); if (foundIndex !== -1) { columnIndices[key] = foundIndex; } else { showTooltipAtCursor(event.clientX, event.clientY, `Column "${headerText}" not found in this table!`, 'error'); return null; } } const tracklistLines = []; const rows = Array.from(table.querySelectorAll('tbody tr')); for (const row of rows) { const trackNumberCell = row.children[columnIndices.trackNum]; const titleCell = row.children[columnIndices.title]; const artistCell = row.children[columnIndices.artist]; const lengthCell = row.children[columnIndices.length]; const trackNumber = trackNumberCell ? trackNumberCell.textContent.trim() : ''; const trackTitle = getCleanText(titleCell, ['ul.alt-values']); let trackArtist = ''; const artistCreditSpan = artistCell ? artistCell.querySelector('.artist-credit') : null; if (artistCreditSpan) { trackArtist = getCleanText(artistCreditSpan, ['ul.alt-values']); } const fullLength = getCleanText(lengthCell, ['ul.alt-values']); const trackLength = fullLength.split('.')[0]; let formattedLine = `${trackNumber}. ${trackTitle}`; if (trackArtist) { formattedLine += ` - ${trackArtist}`; } formattedLine += ` (${trackLength})`; tracklistLines.push(formattedLine); } return tracklistLines; } /** * Main function to extract and copy tracklist data. * @param {MouseEvent} event The click event that triggered this function. * @returns {void} */ async function extractAndCopyTracklist(event) { const clickedTrackHeader = event.target; const table = clickedTrackHeader.closest('table.tracklist'); if (!table) { showTooltipAtCursor(event.clientX, event.clientY, 'Tracklist table not found!', 'error'); return; } const tracklistLines = processTracklistTable(table, event); if (tracklistLines && tracklistLines.length > 0) { const clipboardContent = tracklistLines.join('\n'); try { await navigator.clipboard.writeText(clipboardContent); console.log('Tracklist copied to clipboard successfully!', clipboardContent); showTooltipAtCursor(event.clientX, event.clientY, 'Tracklist copied!', 'success'); clickedTrackHeader.style.color = 'green'; setTimeout(() => clickedTrackHeader.style.color = 'blue', TOOLTIP_DISPLAY_DURATION); } catch (err) { console.error('Failed to copy tracklist to clipboard:', err); showTooltipAtCursor(event.clientX, event.clientY, 'Failed to copy!', 'error'); clickedTrackHeader.style.color = 'red'; setTimeout(() => clickedTrackHeader.style.color = 'blue', TOOLTIP_DISPLAY_DURATION); } } else { showTooltipAtCursor(event.clientX, event.clientY, 'No tracks found in this table!', 'error'); } } const initializeScript = () => { const allTrackHeaders = document.querySelectorAll('table.tracklist th'); allTrackHeaders.forEach(header => { if (header.textContent.trim() === 'Track') { header.addEventListener('click', extractAndCopyTracklist); header.style.cursor = 'pointer'; header.style.textDecoration = 'underline'; header.style.color = 'blue'; header.title = 'Click to copy this tracklist'; } }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } })();