// ==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 21
// @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 */
'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.en
const 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 showInfoID
const 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 titleText
if ($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].innerText
if (titleText && titleText.trim()) {
return titleText.trim()
}
}
return ''
}
const getArtistName = function ($artistnodes) {
let artistText
if (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].innerText
if (artistText && artistText.trim()) {
return artistText.trim()
}
}
if ($artistnodes.find('.standalone-ellipsis-one-line').length > 0) {
artistText = $artistnodes.find('.standalone-ellipsis-one-line')[0].innerText
if (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].textContent
if (artistText && artistText.trim()) {
return artistText.trim()
}
} else {
if ($('.Root h1').length > 0) {
artistText = $('.Root h1')[0].textContent
if (artistText && artistText.trim()) {
return artistText.trim()
}
}
}
}
if (document.location.pathname.startsWith('/album/')) {
artistText = document.querySelector('.os-content h1').textContent
if (artistText && artistText.trim()) {
return artistText.trim()
}
}
if ($('.track-info__artists').length > 0) {
artistText = $('.track-info__artists')[0].innerText
if (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 design
artist = 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 page
if (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 page
artist = artistContent[0].textContent
}
const artistPageH1 = $('main>section[data-testid="artist-page"] .contentSpacing h1')
if (artistPageH1.length > 0) {
// Artist page
artist = artistPageH1[0].textContent
}
}
if (artist.length === 0 && document.location.pathname.startsWith('/track/')) {
// Single track page
artist = $('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 entry
let 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 clipboard
const s = entry.data('gmcopy')
if (typeof GM_setClipboard !== 'undefined') { // eslint-disable-line camelcase
GM_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 entry
entry.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)
}
}
const onContextMenu = function (ev) {
// Wait for the React context menu to open
const t = this
window.setTimeout(function () {
populateContextMenu.call(t, ev)
}, 200)
}
let lastNode = null
const searchForOpenContextMenu = function () {
const node = document.querySelector('[data-context-menu-open]')
if (node && node !== lastNode) {
lastNode = node
populateContextMenu.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)
}
})
})()
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.