Adds an entry in the context menu that copies the selected song name and artist to the clipboard
// ==UserScript==// @name Spotify Web - Copy track info to clipboard// @name:es Spotify Web - Copiar info de la canción// @name:pt Spotify Web - Copiar info da canción// @name:it Spotify Web - Copia l'informazione sul brano// @name:fr Spotify Web - Copier les informations de titre// @name:zh-TW Spotify Web - 複製歌曲信息// @name:zh-CN Spotify Web - 复制歌曲信息// @name:zh Spotify Web - 复制歌曲信息// @name:ar Spotify Web - انسخ معلومات الأغنية// @name:iw Spotify Web - העתקת מידע השיר// @name:ru Spotify Web - Копировать данные трека// @name:id Spotify Web - Salin Informasi Lagu// @name:ms Spotify Web - Salin Maklumat Lagu// @name:de Spotify Web - Songinformation kopieren// @name:ja Spotify Web - 曲情報をコピー// @name:pl Spotify Web - Skopiuj informacje o utworze// @name:cs Spotify Web - Kopírovat informace o skladbě// @name:el Spotify Web - Αντιγραφή πληροφοριών τραγουδιού// @name:hu Spotify Web - Dal adat másolása// @name:tr Spotify Web - Şarkı Bilgilerini Kopyala// @name:th Spotify Web - คัดลอกข้อมูลเพลง// @name:vi Spotify Web - Sao chép Thông tin Bài hát// @name:sv Spotify Web - Kopiera sånginfoen// @name:nl Spotify Web - Info van nummer kopiëren// @description Adds an entry in the context menu that copies the selected song name and artist to the clipboard// @description:es Agrega una entrada en el menú contextual que copia el nombre de la canción y el artista seleccionados al portapapeles// @description:pt Adiciona uma entrada no menu de contexto que copia o nome da música selecionada e o artista para a área de transferência// @description:it Aggiunge una voce nel menu contestuale che copia il nome del brano e l'artista selezionati negli appunti// @description:fr Ajoute une entrée dans le menu contextuel qui copie le nom de la chanson et l'artiste sélectionnés dans le presse-papiers// @description:zh-TW 在上下文菜單中添加一個條目,該條目將選定的歌曲名稱和歌手複製到剪貼板// @description:zh-CN 在上下文菜单中添加一个条目,将选定的歌曲名称和歌手复制到剪贴板// @description:zh 在上下文菜单中添加一个条目,将选定的歌曲名称和歌手复制到剪贴板// @description:ar أضف إدخالاً في قائمة السياق ينسخ اسم الأغنية والفنان المحدد إلى الحافظة// @description:iw הוסף ערך בתפריט ההקשר שמעתיק ללוח הלוח את שם השיר והאמן שנבחרו// @description:ru Добавить пункт контекстного меню, копирующий имя выбранной песни и исполнителя в буфер обмена.// @description:id Tambahkan entri menu konteks yang menyalin nama lagu dan artis yang dipilih ke clipboard// @description:ms Tambahkan entri menu konteks yang menyalin nama lagu dan artis yang dipilih ke papan keratan// @description:de Fügt einen Eintrag im Kontextmenü hinzu, der den ausgewählten Songnamen und Interpreten in die Zwischenablage kopiert// @description:ja 選択した曲名とアーティストをクリップボードにコピーするエントリをコンテキストメニューに追加します// @description:pl Dodaje wpis w menu kontekstowym, który kopiuje wybrany tytuł utworu i wykonawcę do schowka// @description:cs Přidá položku do místní nabídky, která zkopíruje název vybrané skladby a umělce do schránky// @description:el Προσθέτει μια καταχώριση στο μενού περιβάλλοντος που αντιγράφει το επιλεγμένο όνομα τραγουδιού και τον καλλιτέχνη στο πρόχειρο// @description:hu Hozzáad egy bejegyzést a helyi menübe, amely átmásolja a kiválasztott dal nevét és előadót a vágólapra// @description:tr Bağlam menüsüne seçili şarkı adını ve sanatçıyı panoya kopyalayan bir giriş ekler// @description:th เพิ่มรายการในเมนูบริบทที่คัดลอกชื่อเพลงและศิลปินที่เลือกไปยังคลิปบอร์ด// @description:vi Thêm một mục vào menu ngữ cảnh để sao chép tên bài hát và nghệ sĩ đã chọn vào khay nhớ tạm// @description:sv Lägger till en post i snabbmenyn som kopierar det valda låtnamnet och artisten till Urklipp// @description:nl Voegt een item toe aan het contextmenu dat de geselecteerde songnaam en artiest naar het klembord kopieert// @namespace https://openuserjs.org/users/cuzi// @icon https://open.spotify.com/favicon.ico// @version 23// @license MIT// @copyright 2020, cuzi (https://openuserjs.org/users/cuzi)// @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js// @grant GM.setClipboard// @grant GM_setClipboard// @match https://open.spotify.com/*// @sandbox JavaScript// ==/UserScript==// ==OpenUserJS==// @author cuzi// ==/OpenUserJS==/* globals $, GM, GM_setClipboard *//* jshint asi: true, esversion: 8 */'use strict';(function () {const translations = {es: ['Copiar info de la canción', 'Copiado: %s'],pt: ['Copiar info da canción', 'Copiado: %s'],it: ['Copia l\'informazione', 'Copiato: %s'],fr: ['Copier les informations de titre', '%s copié'],'zh-HK': ['複製歌曲信息', '已復制: %s'],'zh-TW': ['複製歌曲信息', '已復制: %s'],zh: ['复制歌曲信息', '已複製: %s'],ar: ['انسخ معلومات الأغنية', '%s :تمّ نسخ'],iw: ['העתקת מידע השיר', '%s :הועתק'],ru: ['Копировать данные трека', 'Скопирована: %s'],id: ['Salin Informasi Lagu', 'Disalin: %s'],ms: ['Salin Maklumat Lagu', 'Disalin: %s'],de: ['Songinformation kopieren', '%s kopiert'],ja: ['曲情報をコピー', '%s をコピーしました'],pl: ['Skopiuj informacje o utworze', '%s skopiowano'],cs: ['Kopírovat informace o skladbě', '%s byl zkopírován'],el: ['Αντιγραφή πληροφοριών τραγουδιού', '%s αντιγράφηκε'],hu: ['Dal adat másolása', '%s másolva'],tr: ['Şarkı Bilgilerini Kopyala', '%s kopyalandı'],th: ['คัดลอกข้อมูลเพลง', '%s ไปที่คลิปบอร์ดแล้ว'],vi: ['Sao chép Thông tin Bài hát', '%s đã được sao chép'],sv: ['Kopiera sånginfoen', '%s kopierad'],nl: ['Info van nummer kopiëren', '%s gekopieerd'],en: ['Copy track info', 'Copied: %s']}let [menuString, copiedString] = translations.enconst htmlTag = document.querySelector('html[lang]')if (htmlTag && htmlTag.lang !== 'en' && htmlTag.lang in translations) {[menuString, copiedString] = translations[htmlTag.lang]} else {for (const lang in translations) {if (navigator.language.startsWith(lang)) {[menuString, copiedString] = translations[lang]break}}}let showInfoIDconst showInfo = function (str) {window.clearTimeout(showInfoID)if (!document.getElementById('copied_song_info_outer')) {document.head.appendChild(document.createElement('style')).innerHTML = `#copied_song_info_outer {margin: -32px calc(var(--panel-gap)*-1) 0;display: grid;grid-area: 1/1/now-playing-bar-start/-1;pointer-events: none;position: relative;z-index: 5;}#copied_song_info_inner {margin-bottom: 16px;place-self: end center;pointer-events: none;z-index: 100;}#copied_song_info_text {background: #2e77d0;border-radius: 8px;-webkit-box-shadow: 0 4px 12px 4px rgba(0,0,0,.5);box-shadow: 0 4px 12px 4px rgba(0,0,0,.5);color: #fff;display: inline-block;font-size: 16px;line-height: 20px;max-width: 450px;padding: 12px 36px;text-align: center;-webkit-transition: none .5s cubic-bezier(.3,0,.4,1);transition: none .5s cubic-bezier(.3,0,.4,1);transition-property: none;-webkit-transition-property: opacity;transition-property: opacity;}`const node = $('<div id="copied_song_info_outer"><div id="copied_song_info_inner"><div id="copied_song_info_text"></div></div></div>')if (document.querySelector('.Root footer')) {$('.Root footer').parent().after(node)} else {node.appendTo('.Root')}}const copiedSongInfoOuter = $('#copied_song_info_outer')const copiedSongInfoText = $('#copied_song_info_text')copiedSongInfoOuter.css('display', 'grid')copiedSongInfoText.css('opacity', 1)copiedSongInfoText.html(str.replace('\n', '<br>\n'))showInfoID = window.setTimeout(function () {copiedSongInfoText.css('opacity', 0)showInfoID = window.setTimeout(function () {copiedSongInfoOuter.css('display', 'none')}, 700)}, 4000)}const getSongTitle = function ($titlenodes) {let titleTextif ($titlenodes && $titlenodes.length > 0) {titleText = $titlenodes.text()if (titleText && titleText.trim()) {return titleText.trim()}}if ($('.track-info__name').length > 0) {titleText = $('.track-info__name')[0].innerTextif (titleText && titleText.trim()) {return titleText.trim()}}return ''}const getArtistName = function ($artistnodes) {let artistTextif (typeof $artistnodes === 'string') {return $artistnodes.trim()}if ($artistnodes) {const artistTextNodes = $artistnodes.not((i, e) => e.className)if (artistTextNodes.length === 1) {artistText = artistTextNodes.text()if (artistText && artistText.trim()) {return artistText.trim()}} else if (artistTextNodes.length > 1) {artistText = artistTextNodes.map((i, e) => e.textContent.trim()).get()artistText = artistText.join(', ')return artistText.trim()}// In playlist:if ($artistnodes.find('.ellipsis-one-line').length > 0) {artistText = $artistnodes.find('.ellipsis-one-line')[0].innerTextif (artistText && artistText.trim()) {return artistText.trim()}}if ($artistnodes.find('.standalone-ellipsis-one-line').length > 0) {artistText = $artistnodes.find('.standalone-ellipsis-one-line')[0].innerTextif (artistText && artistText.trim()) {return artistText.trim()}}// Something else, just accumulate all artist links: <a href="/artist/ARTISTID">Artistname</a>if ($artistnodes.find('a[href*="/artist/"]').length > 0) {return $.map($artistnodes.find('a[href*="/artist/"]'), (element) => $(element).text().trim()).join(', ')}}if (document.location.pathname.startsWith('/artist/')) {if ($('.content.artist>div h1').length > 0) {artistText = $('.content.artist>div h1')[0].textContentif (artistText && artistText.trim()) {return artistText.trim()}} else {if ($('.Root main .contentSpacing [data-testid="adaptiveEntityTitle"]').length > 0) {artistText = $('.Root main .contentSpacing [data-testid="adaptiveEntityTitle"]')[0].textContentif (artistText && artistText.trim()) {return artistText.trim()}}}}if (document.location.pathname.startsWith('/album/')) {artistText = document.querySelector('.os-content h1').textContentif (artistText && artistText.trim()) {return artistText.trim()}}if ($('.track-info__artists').length > 0) {artistText = $('.track-info__artists')[0].innerTextif (artistText && artistText.trim()) {return artistText.trim()}}return ''}const populateContextMenu = function (ev) {console.debug('populateContextMenu')let $this = $(this)let menu = $('.react-contextmenu--visible')if (!menu[0]) {menu = $('#context-menu-root')}if (!menu[0]) {menu = $('#context-menu')}let title = $this.find('.tracklist-name')if (title.length === 0) {title = $this.find('div[data-testid="tracklist-row"] .standalone-ellipsis-one-line')}if (title.length === 0) {title = $this.find('div[role="gridcell"] img').parent().find('.standalone-ellipsis-one-line')}if (title.length === 0 && $this.hasClass('now-playing')) {title = $this.find('.ellipsis-one-line>.ellipsis-one-line').eq(0)}let artist = $this.find('.artists-album span')if (artist.length === 0 && $this.hasClass('now-playing')) {artist = $this.find('.ellipsis-one-line>.ellipsis-one-line').eq(1)}if (artist.length === 0 && title.length === 0 && $this.find('[data-testid="nowplaying-track-link"]')) {title = $this.find('[data-testid="nowplaying-track-link"]')artist = $this.find('[data-testid="nowplaying-artist"]')}if (artist.length === 0) {if ($this.find('.second-line').length !== 0) {artist = $this.find('.second-line') // in playlist}if ($this.parents('.now-playing').length !== 0) {// Now playing bar$this = $($this.parents('.now-playing')[0])if ($this.find('.ellipsis-one-line a[href*="/artist/"]').length !== 0) {artist = $this.find('.ellipsis-one-line a[href*="/artist/"]')title = $this.find('a[data-testid="nowplaying-track-link"]')}}if ($this.parents('.Root footer').length !== 0) {// New: Now playing bar 2021-09$this = $($this.parents('.Root footer')[0])if ($this.find('.ellipsis-one-line a[href*="/artist/"],.standalone-ellipsis-one-line a[href*="/artist/"]').length !== 0) {artist = $this.find('.ellipsis-one-line a[href*="/artist/"],.standalone-ellipsis-one-line a[href*="/artist/"]')title = $this.find('.ellipsis-one-line a[href*="/album/"],.ellipsis-one-line a[href*="/track/"],.standalone-ellipsis-one-line a[href*="/album/"],.standalone-ellipsis-one-line a[href*="/track/"]')} else if ($this.find('[data-testid="context-item-info-artist"]').length !== 0) {artist = $this.find('a[data-testid="context-item-info-artist"][href*="/artist/"],[data-testid="context-item-info-artist"] a[href*="/artist/"]')title = $this.find('[data-testid="context-item-info-title"] a[href*="/album/"],[data-testid="context-item-info-title"] a[href*="/track/"]')} else if ($this.find('a[href*="/artist/"],a[href*="/album/"],a[href*="/track/"]').length > 1) {artist = $this.find('a[href*="/artist/"]')title = $this.find('a[href*="/album/"],a[href*="/track/"]')}}const artistGridCell = $this.find('*[role="gridcell"] a[href*="/artist/"]')if (artistGridCell.length > 0) {// New playlist designartist = artistGridCell.parent()title = $(artistGridCell.parent().parent().find('span')[0])if (artist.has(title)) {// title is child of artist, so it's the same node, the real title is somewhere else// This happens on album pageif (artist.parent().parent().find('div.standalone-ellipsis-one-line').length) {title = $(artist.parent().parent().find('div.standalone-ellipsis-one-line')[0])}}}const artistContent = $('.content.artist>div h1')if (artistContent.length > 0) {// Artist pageartist = artistContent[0].textContent}const artistPageH1 = $('main>section[data-testid="artist-page"] .contentSpacing h1')if (artistPageH1.length > 0) {// Artist pageartist = artistPageH1[0].textContent}}if (artist.length === 0 && document.location.pathname.startsWith('/track/')) {// Single track pageartist = $('section [data-testid="creator-link"][href*="/artist/"]')}if (title && artist && menu[0]) {const titleText = getSongTitle(title)const artistText = getArtistName(artist)if (!titleText || !artistText) {return}// Create context menu entrylet entry = menu.find('.gmcopytrackinfo')if (entry.length === 0 || !entry[0]) {const liButton = menu.find('li button')let li = $(liButton[0]).parent()if (liButton.length > 4) {li = $(liButton[4]).parent()}entry = $(`<li role="presentation"><button role="menuitem" tabindex="-1"><div style="filter: grayscale(100%);font-size: 1.2rem;padding: 0px;margin: 0px 0px 0px -0.5rem;">🍝</div><span as="span" dir="auto">${menuString}</span></button></li>`).appendTo(li.parent()).click(function (ev) {// Copy string to clipboardconst s = entry.data('gmcopy')if (typeof GM_setClipboard !== 'undefined') { // eslint-disable-line camelcaseGM_setClipboard(s)} else if (GM.setClipboard) {GM.setClipboard(s)} else {navigator.clipboard.writeText(s)}menu.parent().remove()showInfo(copiedString.replace('%s', s))})// Copy classes from an existing entryentry.attr('class', li.attr('class'))entry.addClass('gmcopytrackinfo')entry.find('button').attr('class', li.find('button').attr('class'))entry.find('button span').attr('class', li.find('button span').attr('class'))}entry.data('gmcopy', artistText + ' - ' + titleText)menu.css('margin-top', '-26px')}}const onContextMenu = function (ev) {// Wait for the React context menu to openconst t = thiswindow.setTimeout(function () {populateContextMenu.call(t, ev)}, 200)}let lastNode = nullconst searchForOpenContextMenu = function () {const node = document.querySelector('[data-context-menu-open]')if (node && node !== lastNode) {lastNode = nodepopulateContextMenu.call(node, null)}}const bindEvents = function () {// Remove all events and then reattach them$('*[data-testid="tracklist-row"],.now-playing,*[data-testid="now-playing-widget"]').unbind('.gmcopytrackinfo').bind('contextmenu.gmcopytrackinfo', onContextMenu)}window.setTimeout(bindEvents, 500)window.setInterval(bindEvents, 1000)let searchIv = window.setInterval(searchForOpenContextMenu, 50)document.addEventListener('visibilitychange', function () {clearInterval(searchIv)if (!document.hidden) {searchIv = window.setInterval(searchForOpenContextMenu, 50)}})document.addEventListener('focus', function () {clearInterval(searchIv)if (!document.hidden) {searchIv = window.setInterval(searchForOpenContextMenu, 50)}})})()