NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Spotify Enhancer (Copy Track Info, ID & Link) // @description Integrates copy button with options in Spotify Web Player for easy access to track information, IDs, and links. // @icon https://raw.githubusercontent.com/exyezed/spotify-enhancer/refs/heads/main/extras/spotify-enhancer.png // @version 1.2 // @author exyezed // @namespace https://github.com/exyezed/spotify-enhancer/ // @supportURL https://github.com/exyezed/spotify-enhancer/issues // @license MIT // @match https://open.spotify.com/* // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; const createSVG = (path, viewBox = "0 0 384 512", width = "16", height = "16", style = "cursor: pointer; margin-left: 8px; fill: #b3b3b3; vertical-align: middle") => { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", viewBox); svg.setAttribute("width", width); svg.setAttribute("height", height); svg.setAttribute("style", style); const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path"); pathElement.setAttribute("d", path); svg.appendChild(pathElement); return svg; }; const copyIcon = createSVG("M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM72 272a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm104-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zM72 368a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm88 0c0-8.8 7.2-16 16-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16z"); const successIcon = createSVG("M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z", "0 0 512 512", "16", "16", "cursor: pointer; margin-left: 8px; fill: #1ed760; vertical-align: middle"); const errorIcon = createSVG("M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z", "0 0 512 512", "16", "16", "cursor: pointer; margin-left: 8px; fill: #f3727f; vertical-align: middle"); const trackIdIcon = createSVG("M48 256C48 141.1 141.1 48 256 48c63.1 0 119.6 28.1 157.8 72.5c8.6 10.1 23.8 11.2 33.8 2.6s11.2-23.8 2.6-33.8C403.3 34.6 333.7 0 256 0C114.6 0 0 114.6 0 256l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40zm458.5-52.9c-2.7-13-15.5-21.3-28.4-18.5s-21.3 15.5-18.5 28.4c2.9 13.9 4.5 28.3 4.5 43.1l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40c0-18.1-1.9-35.8-5.5-52.9zM256 80c-19 0-37.4 3-54.5 8.6c-15.2 5-18.7 23.7-8.3 35.9c7.1 8.3 18.8 10.8 29.4 7.9c10.6-2.9 21.8-4.4 33.4-4.4c70.7 0 128 57.3 128 128l0 24.9c0 25.2-1.5 50.3-4.4 75.3c-1.7 14.6 9.4 27.8 24.2 27.8c11.8 0 21.9-8.6 23.3-20.3c3.3-27.4 5-55 5-82.7l0-24.9c0-97.2-78.8-176-176-176zM150.7 148.7c-9.1-10.6-25.3-11.4-33.9-.4C93.7 178 80 215.4 80 256l0 24.9c0 24.2-2.6 48.4-7.8 71.9C68.8 368.4 80.1 384 96.1 384c10.5 0 19.9-7 22.2-17.3c6.4-28.1 9.7-56.8 9.7-85.8l0-24.9c0-27.2 8.5-52.4 22.9-73.1c7.2-10.4 8-24.6-.2-34.2zM256 160c-53 0-96 43-96 96l0 24.9c0 35.9-4.6 71.5-13.8 106.1c-3.8 14.3 6.7 29 21.5 29c9.5 0 17.9-6.2 20.4-15.4c10.5-39 15.9-79.2 15.9-119.7l0-24.9c0-28.7 23.3-52 52-52s52 23.3 52 52l0 24.9c0 36.3-3.5 72.4-10.4 107.9c-2.7 13.9 7.7 27.2 21.8 27.2c10.2 0 19-7 21-17c7.7-38.8 11.6-78.3 11.6-118.1l0-24.9c0-53-43-96-96-96zm24 96c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 24.9c0 59.9-11 119.3-32.5 175.2l-5.9 15.3c-4.8 12.4 1.4 26.3 13.8 31s26.3-1.4 31-13.8l5.9-15.3C267.9 411.9 280 346.7 280 280.9l0-24.9z", "0 0 512 512", "16", "16", "margin-right: 8px; fill: #b3b3b3; vertical-align: middle"); const trackLinkIcon = createSVG("M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z", "0 0 640 512", "16", "16", "margin-right: 8px; fill: #b3b3b3; vertical-align: middle"); const enabledIcon = createSVG("M384 128c70.7 0 128 57.3 128 128s-57.3 128-128 128H192c-70.7 0-128-57.3-128-128s57.3-128 128-128H384zM576 256c0-106-86-192-192-192H192C86 64 0 150 0 256S86 448 192 448H384c106 0 192-86 192-192zM384 352c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96s43 96 96 96z", "0 0 576 512", "16", "16", "margin-right: 8px; fill: currentColor; vertical-align: middle"); const disabledIcon = createSVG("M192 128c-70.7 0-128 57.3-128 128s57.3 128 128 128H384c70.7 0 128-57.3 128-128s-57.3-128-128-128H192zM0 256C0 150 86 64 192 64H384c106 0 192 86 192 192s-86 192-192 192H192C86 448 0 362 0 256zm192 96c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96s43 96 96 96z", "0 0 576 512", "16", "16", "margin-right: 8px; fill: currentColor; vertical-align: middle"); const titleIcon = createSVG("M498.7 6c8.3 6 13.3 15.7 13.3 26l0 64c0 13.8-8.8 26-21.9 30.4L416 151.1 416 432c0 44.2-50.1 80-112 80s-112-35.8-112-80s50.1-80 112-80c17.2 0 33.5 2.8 48 7.7L352 128l0-64c0-13.8 8.8-26 21.9-30.4l96-32C479.6-1.6 490.4 0 498.7 6zM32 64l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96S14.3 64 32 64zm0 128l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 256c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 128l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z", "0 0 512 512", "16", "16", "margin-right: 8px; fill: #b3b3b3; vertical-align: middle"); const artistIcon = createSVG("M224 0a128 128 0 1 1 0 256A128 128 0 1 1 224 0zM178.3 304l91.4 0c36.3 0 70.1 10.9 98.3 29.5l0 51.6c-18 2.5-34.8 9.1-48.5 19.4c-17.6 13.2-31.5 34-31.5 59.5c0 19.1 7.7 35.4 18.9 48L29.7 512C13.3 512 0 498.7 0 482.3C0 383.8 79.8 304 178.3 304zM630 164.5c6.3 4.5 10 11.8 10 19.5l0 48 0 160c0 1.2-.1 2.4-.3 3.6c.2 1.5 .3 2.9 .3 4.4c0 26.5-28.7 48-64 48s-64-21.5-64-48s28.7-48 64-48c5.5 0 10.9 .5 16 1.5l0-88.2-144 48L448 464c0 26.5-28.7 48-64 48s-64-21.5-64-48s28.7-48 64-48c5.5 0 10.9 .5 16 1.5L400 296l0-48c0-10.3 6.6-19.5 16.4-22.8l192-64c7.3-2.4 15.4-1.2 21.6 3.3z", "0 0 640 512", "16", "16", "margin-right: 8px; fill: #b3b3b3; vertical-align: middle"); const defaultSettings = { copyType: 'trackId', isEnabled: true }; let settings = GM_getValue('spotifyCopySettings', defaultSettings); function removeCopyButtons() { const copyButtons = document.querySelectorAll('.copy-track-info-btn'); copyButtons.forEach(button => button.remove()); } function createMenuSeparator() { const separator = document.createElement('div'); separator.style.height = '1px'; separator.style.backgroundColor = '#404040'; separator.style.margin = '8px 0'; return separator; } function getTrackId(row) { const trackLink = row.querySelector('a[href^="/track/"]'); if (trackLink) { const trackId = trackLink.getAttribute('href').split('/').pop(); return { id: trackId, link: `https://open.spotify.com/track/${trackId}` }; } return null; } function getTrackInfo(row) { let title, artist; if (window.location.href.startsWith("https://open.spotify.com/artist/")) { const titleElement = row.querySelector('div[data-testid="tracklist-row"] div[role="gridcell"]:nth-child(2)'); const artistElement = document.querySelector('span[data-testid="entityTitle"] h1'); title = titleElement ? titleElement.textContent.trim() : 'Title Not Found'; artist = artistElement ? artistElement.textContent.trim() : 'Artist Not Found'; } else { const titleElement = row.querySelector('div[data-encore-id="text"].standalone-ellipsis-one-line'); const artistElement = row.querySelector('span.encore-text-body-small[data-encore-id="text"]'); title = titleElement ? titleElement.childNodes[0].textContent.trim() : 'Title Not Found'; const artists = artistElement ? Array.from(artistElement.querySelectorAll('a')).map(el => el.textContent.trim()) : []; artist = artists.length > 0 ? artists.join(', ') : 'Artist Not Found'; } return { title, artist, titleFirst: `${title} - ${artist}`, artistFirst: `${artist} - ${title}` }; } function addCopyButton() { if (!settings.isEnabled) return; const trackRows = document.querySelectorAll('[data-testid="tracklist-row"]'); trackRows.forEach(row => { if (row.querySelector('.copy-track-info-btn')) return; const copyBtn = document.createElement('span'); copyBtn.className = 'copy-track-info-btn'; copyBtn.style.display = 'inline-flex'; copyBtn.style.alignItems = 'center'; copyBtn.appendChild(copyIcon.cloneNode(true)); copyBtn.onclick = function(e) { e.preventDefault(); e.stopPropagation(); const trackInfo = getTrackInfo(row); let textToCopy; let isSuccess = true; switch (settings.copyType) { case 'trackId': const trackIdInfo = getTrackId(row); if (trackIdInfo && trackIdInfo.id) { textToCopy = trackIdInfo.id; } else { textToCopy = 'Track ID Not Found'; isSuccess = false; } break; case 'trackLink': const trackLinkInfo = getTrackId(row); if (trackLinkInfo && trackLinkInfo.link) { textToCopy = trackLinkInfo.link; } else { textToCopy = 'Track Link Not Found'; isSuccess = false; } break; case 'titleFirst': textToCopy = trackInfo.titleFirst; break; case 'artistFirst': textToCopy = trackInfo.artistFirst; break; default: textToCopy = 'Invalid copy type'; isSuccess = false; } navigator.clipboard.writeText(textToCopy).then(() => { this.replaceChild(isSuccess ? successIcon.cloneNode(true) : errorIcon.cloneNode(true), this.firstChild); setTimeout(() => { this.replaceChild(copyIcon.cloneNode(true), this.firstChild); }, 250); }).catch(() => { this.replaceChild(errorIcon.cloneNode(true), this.firstChild); setTimeout(() => { this.replaceChild(copyIcon.cloneNode(true), this.firstChild); }, 250); }); }; const compactContainer = row.querySelector('div[class="ft6dUifK4i03829TBAqC"]'); const listContainer = row.querySelector('div[class="_iQpvk1c9OgRAc8KRTlH"]'); if (compactContainer) { const explicitSpan = compactContainer.querySelector('.Ps9zgW56WZaBVLo1n3cg'); if (explicitSpan) { const parentSpan = explicitSpan.closest('span[data-encore-id="text"]'); parentSpan.after(copyBtn); } else { compactContainer.appendChild(copyBtn); } } else if (listContainer) { const textContainer = listContainer.querySelector('[data-encore-id="text"]'); if (textContainer) { textContainer.style.display = 'flex'; textContainer.style.alignItems = 'center'; textContainer.appendChild(copyBtn); } else { listContainer.appendChild(copyBtn); } } }); } function createOptionsButton() { const actionBar = document.querySelector('.eSg4ntPU2KQLfpLGXAww[data-testid="action-bar-row"]'); if (!actionBar || actionBar.querySelector('.spotify-copy-options')) return; const optionsBtn = document.createElement('button'); optionsBtn.className = 'Button-sc-1dqy6lx-0 dbhFGF spotify-copy-options'; optionsBtn.appendChild(copyIcon.cloneNode(true)); const optionsText = document.createElement('span'); optionsText.textContent = 'OPTIONS'; optionsText.style.marginLeft = '8px'; optionsBtn.appendChild(optionsText); const menu = document.createElement('div'); menu.className = 'spotify-copy-menu'; menu.style.display = 'none'; menu.style.position = 'absolute'; menu.style.backgroundColor = '#282828'; menu.style.padding = '8px'; menu.style.borderRadius = '4px'; menu.style.zIndex = '1000'; menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; const toggleOption = createMenuItem(settings.isEnabled ? 'Enabled' : 'Disabled', settings.isEnabled ? enabledIcon : disabledIcon, 'toggle'); const separator = createMenuSeparator(); const titleFirstOption = createMenuItem('Track Info (Title - Artist)', titleIcon, 'titleFirst'); const artistFirstOption = createMenuItem('Track Info (Artist - Title)', artistIcon, 'artistFirst'); const trackIdOption = createMenuItem('Track ID', trackIdIcon, 'trackId'); const trackLinkOption = createMenuItem('Track Link', trackLinkIcon, 'trackLink'); menu.appendChild(toggleOption); menu.appendChild(separator); menu.appendChild(titleFirstOption); menu.appendChild(artistFirstOption); menu.appendChild(trackIdOption); menu.appendChild(trackLinkOption); optionsBtn.addEventListener('click', (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; const rect = optionsBtn.getBoundingClientRect(); menu.style.top = `${rect.bottom + 5}px`; menu.style.left = `${rect.left}px`; }); document.addEventListener('click', () => { menu.style.display = 'none'; }); const moreButton = actionBar.querySelector('[data-testid="more-button"]'); if (moreButton) { moreButton.after(optionsBtn); } document.body.appendChild(menu); } function createMenuItem(text, icon, value) { const item = document.createElement('div'); item.className = 'spotify-copy-menu-item'; item.style.padding = '8px'; item.style.cursor = 'pointer'; item.style.display = 'flex'; item.style.alignItems = 'center'; if (value === 'toggle') { item.style.color = settings.isEnabled ? '#1ed760' : '#f3727f'; } else { item.style.color = settings.copyType === value ? '#1ed760' : '#ffffff'; } item.appendChild(icon.cloneNode(true)); const textSpan = document.createElement('span'); textSpan.textContent = text; item.appendChild(textSpan); item.addEventListener('mouseover', () => { item.style.backgroundColor = '#333333'; }); item.addEventListener('mouseout', () => { item.style.backgroundColor = 'transparent'; }); item.addEventListener('click', () => { if (value === 'toggle') { settings.isEnabled = !settings.isEnabled; item.style.color = settings.isEnabled ? '#1ed760' : '#f3727f'; item.replaceChild(settings.isEnabled ? enabledIcon.cloneNode(true) : disabledIcon.cloneNode(true), item.firstChild); textSpan.textContent = settings.isEnabled ? 'Enabled' : 'Disabled'; if (!settings.isEnabled) { removeCopyButtons(); } else { addCopyButton(); } } else { settings.copyType = value; document.querySelectorAll('.spotify-copy-menu-item').forEach(menuItem => { if (!menuItem.textContent.includes('Enabled') && !menuItem.textContent.includes('Disabled')) { menuItem.style.color = menuItem.textContent.includes(text) ? '#1ed760' : '#ffffff'; } }); } GM_setValue('spotifyCopySettings', settings); }); return item; } function initialize() { createOptionsButton(); addCopyButton(); } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { initialize(); } } }); observer.observe(document.body, { childList: true, subtree: true }); initialize(); console.log('Spotify Enhancer (Copy Track Info, ID & Link) is running'); })();