您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a button to copy the current song URL, works even on YouTube Music homepage
// ==UserScript== // @name YouTube Music URL Link Copy Button // @namespace http://tampermonkey.net/ // @version 1.2 // @description Adds a button to copy the current song URL, works even on YouTube Music homepage // @author YourName // @match https://music.youtube.com/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // Add custom styles const style = document.createElement('style'); style.textContent = ` .yt-direct-copy-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; cursor: pointer; color: #909090; background: none; border: none; margin: 0 4px; position: relative; padding: 0; } .yt-direct-copy-btn:hover { color: #FFFFFF; } .yt-direct-copy-btn svg { width: 20px; height: 20px; } .yt-copy-tooltip { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background-color: rgba(28, 28, 28, 0.9); color: white; padding: 5px 8px; border-radius: 2px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; z-index: 9999; } .yt-direct-copy-btn:hover .yt-copy-tooltip { opacity: 1; } .yt-copy-tooltip.show { opacity: 1; } `; document.head.appendChild(style); // Function to create the copy button function createCopyButton() { const button = document.createElement('button'); button.className = 'yt-direct-copy-btn'; button.type = 'button'; button.setAttribute('aria-label', 'Copy song link'); button.innerHTML = ` <svg viewBox="0 0 24 24" fill="currentColor"> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path> </svg> <div class="yt-copy-tooltip">Copy song link</div> `; // Add click handler button.addEventListener('click', async function(e) { e.stopPropagation(); e.preventDefault(); // Get the current song information const songData = await getCurrentSongData(); if (songData && songData.videoId) { // Construct a proper YouTube Music song URL const songUrl = `https://music.youtube.com/watch?v=${songData.videoId}`; // Copy to clipboard try { await navigator.clipboard.writeText(songUrl); showCopiedTooltip(button); } catch (err) { // Fallback for browsers without clipboard API const textArea = document.createElement('textarea'); textArea.value = songUrl; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showCopiedTooltip(button); } } else { // If we couldn't get the song data, try clicking the share button as fallback tryUsingShareButton(); } return false; }); return button; } // Function to get current song data async function getCurrentSongData() { // Try several methods to find the current song's video ID // Method 1: Check URL if we're on a watch page if (window.location.pathname === '/watch') { const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get('v'); if (videoId) { return { videoId: videoId }; } } // Method 2: Look for the player info directly try { // Try to find the video ID in player data const playerBar = document.querySelector('ytmusic-player-bar'); if (playerBar) { // Try to get the data from the player state const playerApi = document.querySelector('#movie_player'); if (playerApi && playerApi.getVideoData) { const videoData = playerApi.getVideoData(); if (videoData && videoData.video_id) { return { videoId: videoData.video_id }; } } // Try to get from the thumbnail const thumbnail = playerBar.querySelector('img.ytmusic-player-bar'); if (thumbnail && thumbnail.src) { // YouTube Music thumbnail URLs contain the video ID const match = thumbnail.src.match(/\/vi\/([a-zA-Z0-9_-]{11})\/|\/hqdefault\.([a-zA-Z0-9_-]{11})_/); if (match && (match[1] || match[2])) { return { videoId: match[1] || match[2] }; } } } } catch (error) { console.error('Error getting song data from player:', error); } // Method 3: Look for video ID in player attributes try { const ytMusicPlayer = document.querySelector('ytmusic-player'); if (ytMusicPlayer) { const videoIdAttribute = ytMusicPlayer.getAttribute('video-id'); if (videoIdAttribute) { return { videoId: videoIdAttribute }; } } } catch (error) { console.error('Error getting video ID from player attributes:', error); } // Method 4: Try to find it in the player bar queue button data try { const queueButton = document.querySelector('.ytmusic-player-bar .left-controls .middle-controls tp-yt-paper-icon-button'); if (queueButton) { const dataAttr = queueButton.getAttribute('data-video-id') || queueButton.getAttribute('aria-label-of') || queueButton.getAttribute('data-song-id'); if (dataAttr && dataAttr.length === 11) { return { videoId: dataAttr }; } } } catch (error) { console.error('Error getting data from queue button:', error); } // Method 5: Last resort - try to use the share button const shareButton = await findShareButton(); if (shareButton) { try { // Click the share button to open the share dialog shareButton.click(); // Wait for the share dialog to open await new Promise(resolve => setTimeout(resolve, 300)); // Try to get the URL from the share dialog const shareInput = document.querySelector('#share-url'); if (shareInput && shareInput.value) { const url = shareInput.value; const urlObj = new URL(url); const videoId = urlObj.searchParams.get('v'); // Close the dialog const closeButton = document.querySelector('[aria-label="Close"]'); if (closeButton) closeButton.click(); if (videoId) { return { videoId: videoId }; } } // Close the dialog if we're still here const closeButton = document.querySelector('[aria-label="Close"]'); if (closeButton) closeButton.click(); } catch (error) { console.error('Error using share button:', error); // Close any open dialog const closeButton = document.querySelector('[aria-label="Close"]'); if (closeButton) closeButton.click(); } } return null; } // Function to find share button async function findShareButton() { // Wait for menu button const menuButton = await waitForElement('ytmusic-player-bar .right-controls-buttons tp-yt-paper-icon-button[aria-label="More actions"], ytmusic-player-bar .menu-button'); if (menuButton) { // Click menu button to open menu menuButton.click(); // Wait for menu to open await new Promise(resolve => setTimeout(resolve, 200)); // Find share option const shareOption = Array.from(document.querySelectorAll('ytmusic-menu-renderer tp-yt-paper-listbox yt-formatted-string')).find(el => el.textContent.trim() === 'Share'); // Close menu if we didn't find share option if (!shareOption) { document.body.click(); // Click away to close menu return null; } return shareOption; } return null; } // Function to try using the share button as fallback async function tryUsingShareButton() { try { // Find and click more actions button const menuButton = document.querySelector('ytmusic-player-bar .right-controls-buttons tp-yt-paper-icon-button[aria-label="More actions"], ytmusic-player-bar .menu-button'); if (!menuButton) return; menuButton.click(); await new Promise(resolve => setTimeout(resolve, 200)); // Find and click share option const shareOption = Array.from(document.querySelectorAll('ytmusic-menu-renderer tp-yt-paper-listbox yt-formatted-string')).find(el => el.textContent.trim() === 'Share'); if (!shareOption) { document.body.click(); // Close menu return; } shareOption.click(); await new Promise(resolve => setTimeout(resolve, 200)); // Find and click copy button const copyButton = document.querySelector('#copy-button button'); if (copyButton) { copyButton.click(); } // Close dialog after short delay setTimeout(() => { const closeButton = document.querySelector('[aria-label="Close"]'); if (closeButton) closeButton.click(); }, 500); } catch (error) { console.error('Error using share fallback:', error); } } // Function to wait for an element to appear function waitForElement(selector, timeout = 3000) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(document.querySelector(selector)); }, timeout); }); } // Function to show "Copied!" tooltip function showCopiedTooltip(button) { const tooltip = button.querySelector('.yt-copy-tooltip'); const originalText = tooltip.textContent; tooltip.textContent = 'Copied!'; tooltip.classList.add('show'); setTimeout(() => { tooltip.classList.remove('show'); setTimeout(() => { tooltip.textContent = originalText; }, 300); }, 2000); } // Function to inject the button function injectCopyButton() { // Try to find the right-controls-buttons that contains volume, etc. const rightControls = document.querySelector('.right-controls-buttons'); // Skip if button already exists or controls not found if (!rightControls || document.querySelector('.yt-direct-copy-btn')) { return; } // Create and insert the button const copyButton = createCopyButton(); // Find a good insertion point - typically before the More button const moreButton = rightControls.querySelector('tp-yt-paper-icon-button[aria-label="More actions"]'); if (moreButton) { // Insert before more button rightControls.insertBefore(copyButton, moreButton); } else { // Fallback - insert at beginning of controls rightControls.insertBefore(copyButton, rightControls.firstChild); } } // Initial injection attempts setTimeout(injectCopyButton, 3000); setTimeout(injectCopyButton, 6000); // Set up observer to watch for player changes function setupObserver() { const observer = new MutationObserver(() => { if (!document.querySelector('.yt-direct-copy-btn')) { injectCopyButton(); } }); const playerBar = document.querySelector('ytmusic-player-bar'); if (playerBar) { observer.observe(playerBar, { childList: true, subtree: true }); } } // Setup observer after a delay setTimeout(setupObserver, 4000); // Listen for navigation events document.addEventListener('yt-navigate-finish', () => { setTimeout(injectCopyButton, 1500); }); // Continuous injection attempts at intervals const intervalId = setInterval(() => { if (document.querySelector('.yt-direct-copy-btn')) { // Once button is found, reduce frequency of checks clearInterval(intervalId); // Keep a less frequent check running setInterval(injectCopyButton, 10000); } else { injectCopyButton(); } }, 3000); })();