Chosic Copy Button

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或关注我们的公众号极客氢云获取最新地址