// ==UserScript== // @name Spotify Genius Lyrics // @description Shows lyrics from genius.com on the Spotify web player // @description:es Mostra la letra de genius.com de las canciones en el reproductor web de Spotify // @description:de Zeigt den Songtext von genius.com im Spotify-Webplayer an // @description:fr Présente les paroles de chansons de genius.com sur Spotify // @description:pl Pokazuje teksty piosenek z genius.com na Spotify // @description:pt Mostra letras de genius.com no Spotify // @description:it Mostra i testi delle canzoni di genius.com su Spotify // @description:ja スクリプトは、Spotify (スポティファイ)上の genius.com から歌詞を表示します // @namespace https://greasyfork.org/users/20068 // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @copyright 2020, cuzi (https://github.com/cvzi) // @supportURL https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues // @icon https://avatars.githubusercontent.com/u/251374?s=200&v=4 // @version 23.0.2 // @require https://greasyfork.org/scripts/406698-geniuslyrics/code/GeniusLyrics.js // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @grant GM.registerMenuCommand // @grant GM_openInTab // @connect genius.com // @include https://open.spotify.com/* // @include https://genius.com/songs/new // @sandbox JavaScript // @downloadURL none // ==/UserScript== /* Copyright (C) 2020 cuzi (cuzi@openmail.cc) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* global genius, geniusLyrics, unsafeWindow, GM, GM_openInTab, KeyboardEvent */ // eslint-disable-line no-unused-vars 'use strict' const scriptName = 'Spotify Genius Lyrics' let genius let resizeLeftContainer let resizeContainer let optionCurrentSize = 30.0 GM.getValue('optioncurrentsize', optionCurrentSize).then(function (value) { optionCurrentSize = value }) function alertUI (text, buttons = { OK: true }) { return new Promise(function (resolve) { const bg = document.body.appendChild(document.createElement('div')) bg.style.display = 'block' bg.style.position = 'fixed' bg.style.backgroundColor = 'rgba(0,0,0,0.5)' bg.style.top = '0' bg.style.left = '0' bg.style.width = '100%' bg.style.height = '100%' bg.style.zIndex = '999' const div = bg.appendChild(document.createElement('div')) div.style.display = 'block' div.style.position = 'fixed' div.style.backgroundColor = '#bbb' div.style.top = '50%' div.style.left = '50%' div.style.transform = 'translate(-50%, -50%)' div.style.padding = '20px' div.style.borderRadius = '10px' div.style.boxShadow = '0 0 10px 0 rgba(0,0,0,0.5)' div.style.zIndex = '1000' div.style.width = '400px' div.style.height = 'auto' div.style.textAlign = 'center' div.style.fontSize = '20px' div.style.lineHeight = '1.5' div.style.fontFamily = 'sans-serif' div.style.color = 'black' div.style.wordBreak = 'break-word' div.style.overflowWrap = 'break-word' div.style.whiteSpace = 'pre-wrap' div.style.overflow = 'auto' div.style.maxHeight = '80%' div.style.maxWidth = '80%' div.innerHTML = text const buttonDiv = div.appendChild(document.createElement('div')) buttonDiv.style.marginTop = '20px' Object.entries(buttons).forEach(function (pair) { const button = buttonDiv.appendChild(document.createElement('button')) button.style.margin = '0 10px' button.style.padding = '10px' button.style.borderRadius = '5px' button.style.border = 'none' button.style.backgroundColor = '#ddd' button.style.color = 'black' button.style.fontFamily = 'sans-serif' button.style.fontSize = '16px' button.style.cursor = 'pointer' button.innerHTML = pair[0] button.addEventListener('click', function () { bg.remove() resolve(pair[1]) }) }) }) } function confirmUI (text) { return alertUI(text, { OK: true, Cancel: false }) } function setFrameDimensions (container, iframe, bar) { iframe.style.width = container.clientWidth - 6 + 'px' try { iframe.style.height = (document.querySelector('.Root__nav-bar nav,nav.Root__nav-bar').clientHeight + document.querySelector('.Root__now-playing-bar').clientHeight - bar.clientHeight) + 'px' } catch (e) { console.error(e) iframe.style.height = document.documentElement.clientHeight + 'px' } } function onResize () { const iframe = document.getElementById('lyricsiframe') if (iframe) { iframe.style.width = document.getElementById('lyricscontainer').clientWidth - 1 + 'px' try { iframe.style.height = (document.querySelector('.Root__nav-bar nav,nav.Root__nav-bar').clientHeight + document.querySelector('.Root__now-playing-bar').clientHeight - document.querySelector('.lyricsnavbar').clientHeight) + 'px' } catch (e) { console.error(e) iframe.style.height = document.documentElement.clientHeight + 'px' } } } function initResize () { window.addEventListener('mousemove', onMouseMoveResize) window.addEventListener('mouseup', stopResize) window.removeEventListener('resize', onResize) } function onMouseMoveResize (e) { optionCurrentSize = 100 - (e.clientX / document.body.clientWidth * 100) resizeLeftContainer.style.width = (100 - optionCurrentSize) + '%' resizeContainer.style.width = optionCurrentSize + '%' } function stopResize () { window.removeEventListener('mousemove', onMouseMoveResize) window.removeEventListener('mouseup', stopResize) window.addEventListener('resize', onResize) onResize() GM.setValue('optioncurrentsize', optionCurrentSize) } function getCleanLyricsContainer () { document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove()) const topContainer = document.querySelector('.Root__top-container') if (!document.getElementById('lyricscontainer')) { topContainer.style.width = (100 - optionCurrentSize) + '%' topContainer.style.float = 'left' resizeContainer = document.createElement('div') resizeContainer.id = 'lyricscontainer' resizeContainer.style = 'min-height: 100%; width: ' + optionCurrentSize + '%; position: relative; z-index: 1; float:left;background-color: rgb(80, 80, 80);background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgb(18, 18, 18))' topContainer.parentNode.insertBefore(resizeContainer, topContainer.nextSibling) } else { resizeContainer = document.getElementById('lyricscontainer') resizeContainer.innerHTML = '' topContainer.parentNode.insertBefore(resizeContainer, topContainer.nextSibling) } resizeLeftContainer = topContainer resizeContainer.style.zIndex = 10 return document.getElementById('lyricscontainer') } async function onNoResults (songTitle, songArtistsArr) { const showSpotifyLyricsEnabled = await GM.getValue('show_spotify_lyrics', true) const submitSpotifyLyricsEnabled = await GM.getValue('submit_spotify_lyrics', true) const submitSpotifyLyricsIgnored = JSON.parse(await GM.getValue('submit_spotify_lyrics_ignore', '[]')) const key = songTitle + ' - ' + songArtistsArr.join(', ') if (submitSpotifyLyricsIgnored.indexOf(key) !== -1) { // User has previously clicked "Cancel" on the confirm dialog for this song console.debug('onNoResults() Key "' + key + '" is ignored') return } if (showSpotifyLyricsEnabled && document.querySelector('[data-testid="lyrics-button"]')) { // Open lyrics if they are not already open if (!document.querySelector('[data-testid="fullscreen-lyric"]')) { document.querySelector('[data-testid="lyrics-button"]').click() } // Wait one second for lyrics to open window.setTimeout(async function () { const lyrics = Array.from(document.querySelectorAll('[data-testid="fullscreen-lyric"]')).map(div => div.textContent).join('\n') if (submitSpotifyLyricsEnabled && lyrics && lyrics.trim()) { // Add this song to the ignored list so we don't ask again GM.getValue('submit_spotify_lyrics_ignore', '[]').then(async function (s) { const arr = JSON.parse(s) arr.push(key) await GM.setValue('submit_spotify_lyrics_ignore', JSON.stringify(arr)) }) // Ask user if they want to submit the lyrics if (await confirmUI(`Genius.com doesn't have the lyrics for this song but Spotify has the lyrics. Would you like to submit the lyrics from Spotify to Genius.com?\n(You need to be logged in your Genius.com account to do this)\n${songTitle} by ${songArtistsArr.join(', ')}`)) { submitLyricsToGenius(songTitle, songArtistsArr, lyrics) } else { // Once (globally) show the suggestion to disable this feature GM.getValue('suggest_to_disable_submit_spotify_lyrics', true).then(async function (suggestToDisable) { if (suggestToDisable) { alertUI('You can disable this suggestion in the options of the script.') GM.setValue('suggest_to_disable_submit_spotify_lyrics', false) } }) } } }, 1000) } } function submitLyricsToGenius (songTitle, songArtistsArr, lyrics) { GM.setValue('submitToGenius', JSON.stringify({ lyrics, songTitle, songArtistsArr })).then(function () { GM_openInTab('https://genius.com/songs/new') }) } async function fillGeniusForm () { const data = JSON.parse(await GM.getValue('submitToGenius', '{}')) await GM.setValue('submitToGenius', '{}') if ('lyrics' in data && 'songTitle' in data && 'songArtistsArr' in data) { document.getElementById('song_primary_artist').value = data.songArtistsArr.join(', ') document.getElementById('song_title').value = data.songTitle document.getElementById('song_lyrics').value = data.lyrics // Create keyup event on song name, to generate the warning about duplicates const evt = new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'e', char: 'e' }) document.getElementById('song_primary_artist').dispatchEvent(evt) document.getElementById('song_title').dispatchEvent(evt) } } function hideLyrics () { addLyricsButton() document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove()) if (document.getElementById('lyricscontainer')) { document.getElementById('lyricscontainer').parentNode.removeChild(document.getElementById('lyricscontainer')) const topContainer = document.querySelector('.Root__top-container') topContainer.style.width = '100%' topContainer.style.removeProperty('float') } } function listSongs (hits, container, query) { if (!container) { container = getCleanLyricsContainer() } container.style.backgroundColor = 'rgba(0,0,0,.8)' // Back to search button const backToSearchButton = document.createElement('a') backToSearchButton.href = '#' backToSearchButton.appendChild(document.createTextNode('Back to search')) backToSearchButton.addEventListener('click', function backToSearchButtonClick (ev) { ev.preventDefault() if (query) { showSearchField(query) } else if (genius.current.artists) { showSearchField(genius.current.artists + ' ' + genius.current.title) } else { showSearchField() } }) const separator = document.createElement('span') separator.setAttribute('class', 'second-line-separator') separator.setAttribute('style', 'padding:0px 3px') separator.appendChild(document.createTextNode('•')) // Hide button const hideButton = document.createElement('a') hideButton.href = '#' hideButton.appendChild(document.createTextNode('Hide')) hideButton.addEventListener('click', function hideButtonClick (ev) { ev.preventDefault() hideLyrics() }) // List search results const trackhtml = `
🅖
📄
$title
$artist 👁 $stats.pageviews $lyrics_state
` container.innerHTML = '
    ' container.insertBefore(hideButton, container.firstChild) container.insertBefore(separator, container.firstChild) container.insertBefore(backToSearchButton, container.firstChild) const ol = container.querySelector('ol.tracklist') const searchresultsLengths = hits.length const title = genius.current.title const artists = genius.current.artists const onclick = function onclick () { genius.f.rememberLyricsSelection(title, artists, this.dataset.hit) genius.f.showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths) } hits.forEach(function forEachHit (hit) { const li = ol.appendChild(document.createElement('li')) li.setAttribute('class', 'tracklist-row') li.setAttribute('role', 'button') li.innerHTML = trackhtml.replace(/\$title/g, hit.result.title_with_featured).replace(/\$artist/g, hit.result.primary_artist.name).replace(/\$lyrics_state/g, hit.result.lyrics_state).replace(/\$stats\.pageviews/g, 'pageviews' in hit.result.stats ? genius.f.metricPrefix(hit.result.stats.pageviews, 1) : ' - ') li.dataset.hit = JSON.stringify(hit) li.addEventListener('click', onclick) const geniushitname = li.querySelector('.geniushitname') if (geniushitname.clientWidth > (li.clientWidth - 30)) { geniushitname.style.width = (li.clientWidth - 30) + 'px' geniushitname.classList.add('runningtext') } }) } const songTitleQuery = 'a[data-testid="nowplaying-track-link"],.Root__now-playing-bar .ellipsis-one-line a[href^="/track/"],.Root__now-playing-bar .ellipsis-one-line a[href^="/album/"],.Root__now-playing-bar .standalone-ellipsis-one-line a[href^="/album/"],[data-testid="context-item-info-title"] a[href^="/album/"],[data-testid="context-item-info-title"] a[href^="/track/"]' function addLyrics (force, beLessSpecific) { let songTitle = document.querySelector(songTitleQuery).innerText songTitle = genius.f.cleanUpSongTitle(songTitle) let musicIsPlaying = false if (document.querySelector('.now-playing-bar .player-controls__buttons .control-button.control-button--circled')) { // Old design musicIsPlaying = document.querySelector('.now-playing-bar .player-controls__buttons .control-button.control-button--circled').className.toLowerCase().indexOf('pause') !== -1 } else if (document.querySelector('.Root__now-playing-bar .player-controls__buttons button')) { // New design 11-2020 document.querySelectorAll('.Root__now-playing-bar .player-controls__buttons button').forEach(function (button) { if (button.getAttribute('aria-label') === 'Pause' || button.innerHTML.indexOf('M3 2h3v12H3zM10 2h3v12h-3z') !== -1 || button.innerHTML.indexOf('M3 2h3v12H3zm7 0h3v12h-3z') !== -1 || button.innerHTML.indexOf('M2.7 1a.7.7 0 00-.7.7v12.6a.7.7 0') !== -1 ) { musicIsPlaying = true } }) } const songArtistsArr = [] document.querySelectorAll('.Root__now-playing-bar .ellipsis-one-line a[href^="/artist/"],.Root__now-playing-bar .standalone-ellipsis-one-line a[href^="/artist/"],a[data-testid="context-item-info-artist"][href^="/artist/"],[data-testid="context-item-info-artist"] a[href^="/artist/"]').forEach((e) => songArtistsArr.push(e.innerText)) genius.f.loadLyrics(force, beLessSpecific, songTitle, songArtistsArr, musicIsPlaying) } let lastPos = null function updateAutoScroll () { let pos = null try { const els = document.querySelectorAll('.Root__now-playing-bar [data-testid="playback-position"],.Root__now-playing-bar [data-testid="playback-duration"]') if (els.length !== 2) { throw new Error(`Expected 2 playback elements, found ${els.length}`) } const [current, total] = Array.from(els).map(e => e.textContent.trim()).map(s => s.split(':').reverse().map((d, i, a) => parseInt(d) * Math.pow(60, i)).reduce((a, c) => a + c, 0)) pos = current / total } catch (e) { // Could not parse current song position pos = null console.debug(`Could not parse song position: ${e}`) } if (pos != null && !Number.isNaN(pos) && lastPos !== pos) { genius.f.scrollLyrics(pos) lastPos = pos } } function showSearchField (query) { const b = getCleanLyricsContainer() const div = b.appendChild(document.createElement('div')) div.style = 'padding:5px' div.appendChild(document.createTextNode('Search genius.com: ')) // Hide button const hideButton = div.appendChild(document.createElement('a')) hideButton.href = '#' hideButton.style = 'float: right; padding-right: 10px;' hideButton.appendChild(document.createTextNode('Hide')) hideButton.addEventListener('click', function hideButtonClick (ev) { ev.preventDefault() hideLyrics() }) const br = div.appendChild(document.createElement('br')) br.style.clear = 'right' div.style.paddingRight = '15px' const input = div.appendChild(document.createElement('input')) input.style = 'width:92%;border:0;border-radius:500px;padding:8px 5px 8px 25px;text-overflow:ellipsis' input.placeholder = 'Search genius.com...' if (query) { input.value = query } else if (genius.current.artists) { input.value = genius.current.artists } input.addEventListener('change', function onSearchLyricsButtonClick () { this.style.color = 'black' if (input.value) { genius.f.searchByQuery(input.value, b) } }) input.addEventListener('keyup', function onSearchLyricsKeyUp (ev) { this.style.color = 'black' if (ev.keyCode === 13) { ev.preventDefault() if (input.value) { genius.f.searchByQuery(input.value, b) } } }) input.focus() const mag = div.appendChild(document.createElement('div')) mag.style.marginTop = '-27px' mag.style.marginLeft = '3px' mag.appendChild(document.createTextNode('🔎')) } function addLyricsButton () { if (document.getElementById('showlyricsbutton')) { return } const b = document.createElement('div') b.setAttribute('id', 'showlyricsbutton') b.setAttribute('style', 'position:absolute; top: 0px; right:0px; font-size:14px; color:#ffff64; cursor:pointer; z-index:3000;') b.setAttribute('title', 'Load lyrics from genius.com') b.appendChild(document.createTextNode('🅖')) b.addEventListener('click', function onShowLyricsButtonClick () { genius.option.autoShow = true // Temporarily enable showing lyrics automatically on song change window.clearInterval(genius.iv.main) genius.iv.main = window.setInterval(main, 2000) b.remove() addLyrics(true) }) document.body.appendChild(b) if (b.clientWidth < 10) { b.setAttribute('style', 'position:absolute; top: 0px; right:0px; font-size:14px; background-color:#0007; color:#ffff64; cursor:pointer; z-index:3000;border:1px solid #ffff64;border-radius: 100%;padding: 0px 5px;font-size: 10px;') b.innerHTML = 'G' } } function configShowSpotifyLyrics (div) { // Input: Show lyrics from Spotify if no lyrics found on genius.com const id = 'input945455' const input = div.appendChild(document.createElement('input')) input.type = 'checkbox' input.id = id GM.getValue('show_spotify_lyrics', true).then(function (v) { input.checked = v }) const label = div.appendChild(document.createElement('label')) label.setAttribute('for', id) label.appendChild(document.createTextNode('Open lyrics from Spotify if no lyrics found on genius.com')) const onChange = function onChangeListener () { GM.setValue('show_spotify_lyrics', input.checked) } input.addEventListener('change', onChange) } function configSubmitSpotifyLyrics (div) { // Input: Submit lyrics from Spotify to genius.com const id = 'input337565' const input = div.appendChild(document.createElement('input')) input.type = 'checkbox' input.id = id input.setAttribute('title', '...in case Spotify has lyrics that genius.com does not have') GM.getValue('submit_spotify_lyrics', true).then(function (v) { input.checked = v }) const label = div.appendChild(document.createElement('label')) label.setAttribute('for', id) label.appendChild(document.createTextNode('Suggest to submit lyrics from Spotify to genius.com')) label.setAttribute('title', '...in case Spotify has lyrics that genius.com does not have') const onChange = function onChangeListener () { GM.setValue('submit_spotify_lyrics', input.checked) } input.addEventListener('change', onChange) } function addCss () { document.head.appendChild(document.createElement('style')).innerHTML = ` .lyricsiframe { opacity:0.1; transition:opacity 2s; margin:0px; padding:0px; } .loadingspinnerholder { position:absolute; top:100px; left:100px; cursor:progress } .lyricsnavbar span,.lyricsnavbar a:link,.lyricsnavbar a:visited { color: rgb(179, 179, 179); text-decoration:none; transition:color 400ms; } .lyricsnavbar a:hover,.lyricsnavbar span:hover { color:white; text-decoration:none; } .geniushits li { cursor:pointer } .geniushits li:hover { background-color: #fff5; border-radius: 5px; } .geniushits li .geniushiticonout { display:inline-block } .geniushits li:hover .geniushiticonout { display:none } .geniushits li .geniushiticonover { display:none } .geniushits li:hover .geniushiticonover { display:inline-block } .geniushiticon { width:25px; height:2em; display:inline-block; } .geniushitname { display:inline-block; position: relative; overflow:hidden } .geniushitname .tracklist-name { font-size: 16px; font-weight: 400; color:white; } .geniushitname.runningtext .tracklist-name { display: inline-block; position: relative; animation: 3s linear 0s infinite alternate runtext; } .geniushits .second-line-separator { opacity: 0.7 } .geniushitname .geniusbadge { color: #121212; background-color: hsla(0,0%,100%,.6); border-radius: 2px; text-transform: uppercase; font-size: 9px; line-height: 10px; min-width: 16px; height: 16px; padding: 0 2px; margin: 0 3px; } @keyframes runtext { 0%, 25% { transform: translateX(0%); left: 0%; } 75%, 100% { transform: translateX(-100%); left: 100%; } } ` } function main () { if (document.querySelector('.Root__now-playing-bar .playback-bar') && document.querySelector(songTitleQuery)) { if (genius.option.autoShow) { addLyrics() } else { addLyricsButton() } } } if (document.location.hostname === 'genius.com') { // https://genius.com/songs/new fillGeniusForm() } else { window.setInterval(function removeAds () { // Remove "premium" button try { const button = document.querySelector('.Root__top-bar header>button') if (button && button.outerHTML.toLowerCase().indexOf('premium') !== -1) { button.style.display = 'none' } } catch (e) { console.warn(e) } // Remove "install app" button try { const button = document.querySelector('a[href*="/download"]') if (button) { button.parentNode.style.display = 'none' } } catch (e) { console.warn(e) } // Remove iframe "GET 3 MONTHS FREE" try { const iframe = document.querySelector('iframe[data-testid="inAppMessageIframe"]') if (iframe && iframe.contentDocument && iframe.contentDocument.body) { iframe.contentDocument.body.querySelectorAll('button').forEach(function (button) { if (button.parentNode.innerHTML.indexOf('Dismiss_action') !== -1) { button.click() } }) } } catch (e) { console.warn(e) } }, 3000) genius = geniusLyrics({ GM, scriptName, scriptIssuesURL: 'https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues', scriptIssuesTitle: 'Report problem: github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues', domain: 'https://open.spotify.com', emptyURL: 'https://open.spotify.com/robots.txt', main, addCss, listSongs, showSearchField, addLyrics, hideLyrics, getCleanLyricsContainer, setFrameDimensions, initResize, onResize, config: [configShowSpotifyLyrics, configSubmitSpotifyLyrics], toggleLyricsKey: { shiftKey: true, ctrlKey: false, altKey: false, key: 'L' }, onNoResults }) GM.registerMenuCommand(scriptName + ' - Show lyrics', () => addLyrics(true)) GM.registerMenuCommand(scriptName + ' - Options', () => genius.f.config()) window.setInterval(updateAutoScroll, 7000) }