您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add copy button on chosic genre finder page
// ==UserScript== // @name Chosic Copy Button // @namespace https://gf.qytechs.cn/users/1470715 // @author cattishly6060 // @version 1.4 // @description Add copy button on chosic genre finder page // @match https://www.chosic.com/music-genre-finder/* // @match https://www.chosic.com/artist/* // @icon https://www.google.com/s2/favicons?sz=64&domain=chosic.com // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; /** * Main function */ /** * @param {string} textToCopy * @param {string} btnText * @returns {HTMLAnchorElement} */ function createCopyBtn(textToCopy = "", btnText = "Copy") { // Create the button/link element const copyButton = document.createElement('a'); copyButton.textContent = btnText; copyButton.href = '#'; // Makes it look like a link copyButton.style.cssText = ` display: inline-block; margin: 5px; padding: 2px 5px; background: #0078d7; color: white; border-radius: 2px; text-decoration: none; cursor: pointer; font-size: 12px; font-weight: bold; `; // Add click event handler copyButton.addEventListener('click', function(e) { e.preventDefault(); // Prevent default anchor behavior // Copy text to clipboard navigator.clipboard.writeText(textToCopy) .then(() => { // Visual feedback const originalText = copyButton.textContent; copyButton.textContent = 'Copied!'; setTimeout(() => { copyButton.textContent = originalText; }, 2000); }) .catch(err => { console.error('Failed to copy text: ', err); }); }); return copyButton; } /** * @param {string} uri * @param {string} btnText * @returns {HTMLAnchorElement} */ function createOpenBtn(uri = "", btnText = "Open") { // Create the button/link element const openButton = document.createElement('a'); openButton.textContent = btnText; openButton.href = uri; openButton.target = '_blank'; openButton.style.cssText = ` display: inline-block; margin: 5px; padding: 2px 5px; background: #0078d7; color: white; border-radius: 2px; text-decoration: none; cursor: pointer; font-size: 12px; font-weight: bold; `; return openButton; } /** * @param {string} textToCopy * @param {string} uri * @param {string} btnText * @returns {HTMLDivElement} */ function createGroupBtn(textToCopy = "", uri = "", btnText = "Open") { const div = document.createElement('div'); div.style.display = 'inline-block'; // Create the button/link element const openButton = document.createElement('a'); openButton.textContent = btnText; openButton.href = uri; openButton.target = '_blank'; openButton.style.cssText = ` display: inline-block; margin: 5px 0 5px 5px; padding: 2px 5px; background: #0078d7; color: white; border-radius: 2px 0 0 2px; text-decoration: none; cursor: pointer; font-size: 12px; font-weight: bold; `; // Create copy button const copyButton = document.createElement('a'); copyButton.href = '#'; copyButton.style.cssText = ` display: inline-block; margin: 5px 5px 5px 0; padding: 2px 10px; color: white; border-radius: 0 2px 2px 0; text-decoration: none; cursor: pointer; font-size: 12px; font-weight: bold; `; copyButton.style.background = '#005ca3'; // SVG style change: copyButton.innerHTML = `<svg width="12" height="12" viewBox="0 0 448 512" fill="currentColor" style="display: inline-block; vertical-align: middle;"><path d="M208 0L332.1 0c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9L448 336c0 26.5-21.5 48-48 48l-192 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48zM48 128l80 0 0 64-64 0 0 256 192 0 0-32 64 0 0 48c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 176c0-26.5 21.5-48 48-48z"/></svg>`; // Add click event handler copyButton.addEventListener('click', function(e) { e.preventDefault(); // Prevent default anchor behavior // Copy text to clipboard navigator.clipboard.writeText(textToCopy) .then(() => { // Visual feedback - change both icon and text const originalHTML = copyButton.innerHTML; copyButton.innerHTML = `<svg width="12" height="12" viewBox="0 0 448 512" fill="currentColor" style="display: inline-block; vertical-align: middle;"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>`; setTimeout(() => { copyButton.innerHTML = originalHTML; }, 2000); }) .catch(err => { console.error('Failed to copy text: ', err); }); }); div.append(openButton, copyButton); return div; } /** * Variables */ /** @type {HTMLElement[]} */ let toRemoveElement = []; /** * Main Handler (Listener/Observer) */ function main() { if (toRemoveElement?.length) { for (const e of toRemoveElement) { e?.remove(); } toRemoveElement = []; } /** @type {?MutationObserver} */ const observerSp = waitForElement('#spotify-tags .pl-tags > a', (_) => { /** @type {HTMLElement[]} */ const spGenreList = [...document.querySelectorAll("#spotify-tags .pl-tags > a")]; /** @type {string[]} */ const spotifyGenres = spGenreList.map(e => e.innerText.trim().toLowerCase()); /** @type {{uri: string, genre: string}[]} */ const spotifyTags = spGenreList.map(e => ({uri: e.href, genre: e.innerText.trim().toLowerCase()})); // remove native genre tags spGenreList.forEach(e => e.remove()) console.log({spotifyGenres, spotifyTags}); // todo log if (spotifyGenres?.length) { const copyBtn = createCopyBtn(spotifyGenres.join(', '), "Copy"); document.querySelector("#spotify-tags .section-header")?.appendChild(copyBtn); } if (spotifyTags?.length) { const div = document.createElement('div'); div.style.cssText = `text-align: center;`; for (const spTag of spotifyTags) { const groupBtn = createGroupBtn(spTag.genre, spTag.uri, spTag.genre); div.appendChild(groupBtn); } const isRelatedGenre = Boolean(document.querySelector("#spotify-tags span.related-artists-genres-extra")); if (isRelatedGenre) { document.querySelector("#spotify-tags div.pl-tags") ?.insertAdjacentElement('afterend', div); } else { document.querySelector("#spotify-tags .section-header") ?.insertAdjacentElement('afterend', div); } } }, {timeoutDelay: 5_000}); /** @type {?MutationObserver} */ const observerWk = waitForElement('#wiki-genres.pl-tags > a', (_) => { /** @type {HTMLElement[]} */ const wikiGenreList = [...document.querySelectorAll("#wiki-genres.pl-tags > a")]; /** @type {string[]} */ const wikiGenres = wikiGenreList.map(e => e.innerText.trim().toLowerCase()); /** @type {{uri: string, genre: string}[]} */ const wikiTags = wikiGenreList.map(e => ({uri: e.href, genre: e.innerText.trim().toLowerCase()})); // remove native wiki genre list wikiGenreList.forEach(e => e.remove()); console.log({wikiGenres, wikiTags}); // todo log if (wikiGenres?.length) { const copyBtn = createCopyBtn(wikiGenres.join(', '), "Copy"); toRemoveElement.push(copyBtn); document.querySelector("#wiki-genres.pl-tags") ?.parentNode ?.querySelector(".section-header") ?.appendChild(copyBtn); } if (wikiTags?.length) { const div = document.createElement('div'); div.style.cssText = `text-align: center;`; for (const spTag of wikiTags) { const groupBtn = createGroupBtn(spTag.genre, spTag.uri, spTag.genre); div.appendChild(groupBtn); } toRemoveElement.push(div); document.querySelector("#wiki-genres.pl-tags") ?.parentNode ?.querySelector(".section-header") ?.insertAdjacentElement('afterend', div); } }, {timeoutDelay: 5_000}); // player copy button const observerTrackItem = waitForElement("#song-player div.track-list-item", (_) => { const trackItem = document.querySelector("#song-player div.track-list-item"); if (trackItem) { const titleAnchor = trackItem.querySelector(".track-list-item-info-text a"); const authorAnchor = trackItem.querySelector(".track-list-item-info-genres a"); if (titleAnchor && authorAnchor) { /** @type {?string} */ const title = titleAnchor.innerText; /** @type {?string} */ const author = authorAnchor.innerText; /** @type {?string} */ const cArtistUri = authorAnchor.getAttribute("data-link"); /** @type {?string} */ const cListGeneUri = titleAnchor.getAttribute("data-link"); /** @type {?string} */ const spArtistId = cArtistUri?.split('/')?.[5]; /** @type {?string} */ const spTrackId = cListGeneUri?.split('track=')?.[1]; /** @type {string} */ const spArtistUri = `https://open.spotify.com/artist/${spArtistId}`; /** @type {string} */ const spTrackUri = `https://open.spotify.com/track/${spTrackId}`; const div = document.createElement("div"); div.style.cssText = "position: relative; width: 100%;"; trackItem.insertBefore(div, trackItem.firstChild); /** @type {HTMLDivElement[]} */ const divs = []; if (title && author) { const _div = document.createElement("div"); _div.style.cssText = "position: absolute; right: 0; top: 0;"; const copyTitle = createCopyBtn(title, "Copy Title"); const copyAuthor = createCopyBtn(author, "Copy Author"); const copyAuthorTitle = createCopyBtn(`${title} - ${author}`, "Copy Title + Author"); _div.append(copyTitle, copyAuthor, copyAuthorTitle); divs.push(_div); } if (spTrackId && spArtistId) { const _div = document.createElement("div"); _div.style.cssText = "position: absolute; right: 0; top: 40px;"; const copyTrackId = createCopyBtn(spTrackId, "Copy Track ID"); const copyArtistId = createCopyBtn(spArtistId, "Copy Artist ID"); _div.append(copyArtistId, copyTrackId); divs.push(_div); const __div = document.createElement("div"); __div.style.cssText = "position: absolute; right: 0; top: 80px;"; const groupArtist = createGroupBtn(spArtistUri, spArtistUri, "Open Artist"); const groupTrack = createGroupBtn(spTrackUri, spTrackUri, "Open Track"); __div.append(groupArtist, groupTrack); divs.push(__div); } div.append(...divs); console.log({ title, author, cArtistUri, cListGeneUri, spArtistId, spTrackId, spArtistUri, spTrackUri, }, {depth: null, colors: true}); // todo log } } }, {timeoutDelay: 5_000}); if (window.location.pathname.startsWith('/artist/')) { /** @type {HTMLAnchorElement[]} */ const genreElements = [...(document.querySelectorAll("#artist-genres a") || [])]; /** @type {string[]} */ const genres = genreElements .map(e => e.innerText.trim().toLowerCase()); genreElements.forEach(e => e.remove()); if (genres?.length) { const div = document.createElement('div'); for (const g of genres) { const genreUri = `https://www.chosic.com/genre-chart/${toSlug(g)}`; const groupBtn = createGroupBtn(g, genreUri, g); div.appendChild(groupBtn); } document.querySelector("#artist-genres") ?.insertAdjacentElement('afterend', div); const copyBtn = createCopyBtn(genres.join(', '), "Copy Genres"); document.querySelector("#artist-genres") ?.appendChild(copyBtn); } } } // initiate listener for first page load main(); // listen on data changed const loading = document.querySelector("#primary .loading-result"); if (loading) { const styleObserver = observeInlineStyleChanges(loading, (oldStyle, newStyle) => { console.log('|| Inline styles changed:'); console.log('|| Old style:', oldStyle); console.log('|| New style:', newStyle); if (newStyle?.includes("display: none;")) { console.log('|| Starting main..'); main(); } }); } /** * ********************** * Utility * ********************** */ /** * @param {string} selector * @param {function(HTMLElement): void} callback * @param {{rootElement: ?HTMLElement, timeoutDelay: ?number}} [options={}] * @returns {?MutationObserver} */ function waitForElement(selector, callback, options = {}) { // Default to document.body if no root element is specified const rootElement = options.rootElement || document.body; delete options.rootElement; // Remove our custom option before passing to MutationObserver /** @type {?number} */ const timeoutDelay = options?.timeoutDelay; delete options?.timeoutDelay; // First try immediately (element might already exist) const element = document.querySelector(selector); if (element) { callback(element); return; } /** @type {?Timeout} */ let removeObserverTimeout; const observer = new MutationObserver((mutations, obs) => { // Only query when nodes are added for (const mutation of mutations) { if (mutation.addedNodes.length) { const el = document.querySelector(selector); if (el) { clearTimeout(removeObserverTimeout); obs.disconnect(); callback(el); return; } } } }); if (timeoutDelay) { removeObserverTimeout = setTimeout(() => { observer.disconnect(); }, timeoutDelay); } observer.observe(rootElement, { childList: true, subtree: true, ...options }); // Return the observer so caller can disconnect if needed return observer; } /** * Observes inline style changes on a specific DOM element * @param {HTMLElement} targetElement - The element to observe * @param {Function} callback - Function to call when inline styles change * @returns {?MutationObserver} The observer instance */ function observeInlineStyleChanges(targetElement, callback) { if (!targetElement) { return; } // Options for the observer (what to observe) const config = { attributes: true, // Watch for attribute changes attributeFilter: ['style'], // Only watch the style attribute attributeOldValue: true, // Record the old value before mutation }; // Create an observer instance const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { // Get the old and new style values const oldValue = mutation.oldValue; const newValue = targetElement.getAttribute('style'); // Execute callback with both values callback(oldValue, newValue); } }); }); // Start observing the target element observer.observe(targetElement, config); return observer; } /** * @param {string} str * @returns {string} */ function toSlug(str) { return str .toLowerCase() .replace(/\s+/g, '-') .replace(/[^\w\-]+/g, '') .replace(/\-\-+/g, '-') .replace(/^-+/, '') .replace(/-+$/, ''); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址