Spotify Genius Lyrics

Shows lyrics from genius.com on the Spotify web player

სკრიპტის ინსტალაცია?
ავტორის შემოთავაზებული სკრიპტი

შეიძლება მოგეწონოს Youtube Music Genius Lyrics.

სკრიპტის ინსტალაცია
  1. // ==UserScript==
  2. // @name Spotify Genius Lyrics
  3. // @description Shows lyrics from genius.com on the Spotify web player
  4. // @description:es Mostra la letra de genius.com de las canciones en el reproductor web de Spotify
  5. // @description:de Zeigt den Songtext von genius.com im Spotify-Webplayer an
  6. // @description:fr Présente les paroles de chansons de genius.com sur Spotify
  7. // @description:pl Pokazuje teksty piosenek z genius.com na Spotify
  8. // @description:pt Mostra letras de genius.com no Spotify
  9. // @description:it Mostra i testi delle canzoni di genius.com su Spotify
  10. // @description:ja スクリプトは、Spotify (スポティファイ)上の genius.com から歌詞を表示します
  11. // @namespace https://greasyfork.org/users/20068
  12. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  13. // @copyright 2020, cuzi (https://github.com/cvzi)
  14. // @supportURL https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues
  15. // @icon https://avatars.githubusercontent.com/u/251374?s=200&v=4
  16. // @version 23.6.11
  17. // @require https://greasyfork.org/scripts/406698-geniuslyrics/code/GeniusLyrics.js
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js
  19. // @grant GM.xmlHttpRequest
  20. // @grant GM.setValue
  21. // @grant GM.getValue
  22. // @grant GM.registerMenuCommand
  23. // @grant GM_openInTab
  24. // @connect genius.com
  25. // @match https://open.spotify.com/*
  26. // @match https://genius.com/songs/new
  27. // @sandbox JavaScript
  28. // ==/UserScript==
  29.  
  30. /*
  31. Copyright (C) 2020 cuzi (cuzi@openmail.cc)
  32.  
  33. This program is free software: you can redistribute it and/or modify
  34. it under the terms of the GNU General Public License as published by
  35. the Free Software Foundation, either version 3 of the License, or
  36. (at your option) any later version.
  37.  
  38. This program is distributed in the hope that it will be useful,
  39. but WITHOUT ANY WARRANTY; without even the implied warranty of
  40. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  41. GNU General Public License for more details.
  42.  
  43. You should have received a copy of the GNU General Public License
  44. along with this program. If not, see <https://www.gnu.org/licenses/>.
  45. */
  46.  
  47. /* global genius, geniusLyrics, unsafeWindow, GM, GM_openInTab, KeyboardEvent */ // eslint-disable-line no-unused-vars
  48. /* jshint asi: true, esversion: 8 */
  49.  
  50. 'use strict'
  51.  
  52. const scriptName = 'Spotify Genius Lyrics'
  53. let genius
  54. let resizeLeftContainer
  55. let resizeContainer
  56. let optionCurrentSize = 30.0
  57. GM.getValue('optioncurrentsize', optionCurrentSize).then(function (value) {
  58. optionCurrentSize = value
  59. })
  60.  
  61. function setFrameDimensions (container, iframe, bar) {
  62. iframe.style.width = container.clientWidth - 6 + 'px'
  63. iframe.style.height = document.documentElement.clientHeight - bar.clientHeight - 15 + 'px'
  64. }
  65.  
  66. function onResize () {
  67. const iframe = document.getElementById('lyricsiframe')
  68. if (iframe) {
  69. setFrameDimensions(document.getElementById('lyricscontainer'), document.getElementById('lyricsiframe'), document.querySelector('.lyricsnavbar'))
  70. }
  71. }
  72. function initResize () {
  73. window.addEventListener('mousemove', onMouseMoveResize)
  74. window.addEventListener('mouseup', stopResize)
  75. window.removeEventListener('resize', onResize)
  76. }
  77. function onMouseMoveResize (e) {
  78. optionCurrentSize = 100 - (e.clientX / document.body.clientWidth * 100)
  79. resizeLeftContainer.style.width = (100 - optionCurrentSize) + '%'
  80. resizeContainer.style.width = optionCurrentSize + '%'
  81. }
  82. function stopResize () {
  83. window.removeEventListener('mousemove', onMouseMoveResize)
  84. window.removeEventListener('mouseup', stopResize)
  85. window.addEventListener('resize', onResize)
  86. onResize()
  87. GM.setValue('optioncurrentsize', optionCurrentSize)
  88. }
  89. function getCleanLyricsContainer () {
  90. document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove())
  91.  
  92. const topContainer = document.querySelector('div.Root')
  93. if (!document.getElementById('lyricscontainer')) {
  94. topContainer.style.width = (100 - optionCurrentSize) + '%'
  95. topContainer.style.float = 'left'
  96. if (topContainer.style.getPropertyValue('--panel-gap')) {
  97. topContainer.style.marginRight = '-' + topContainer.style.getPropertyValue('--panel-gap')
  98. }
  99. resizeContainer = document.createElement('div')
  100. resizeContainer.id = 'lyricscontainer'
  101. resizeContainer.style = 'min-height: 100%; width: ' + optionCurrentSize + '%; position: relative; z-index: 1; float:left;background:black'
  102. topContainer.parentNode.insertBefore(resizeContainer, topContainer.nextSibling)
  103. } else {
  104. resizeContainer = document.getElementById('lyricscontainer')
  105. resizeContainer.innerHTML = ''
  106. topContainer.parentNode.insertBefore(resizeContainer, topContainer.nextSibling)
  107. }
  108. resizeLeftContainer = topContainer
  109. resizeContainer.style.zIndex = 10
  110.  
  111. return document.getElementById('lyricscontainer')
  112. }
  113.  
  114. function onNewSongPlaying () {
  115. genius.f.closeModalUIs()
  116. }
  117.  
  118. async function onNoResults (songTitle, songArtistsArr) {
  119. const showSpotifyLyricsEnabled = await GM.getValue('show_spotify_lyrics', true)
  120. const submitSpotifyLyricsIgnored = JSON.parse(await GM.getValue('submit_spotify_lyrics_ignore', '[]'))
  121.  
  122. const key = songTitle + ' - ' + songArtistsArr.join(', ')
  123. if (submitSpotifyLyricsIgnored.indexOf(key) !== -1) {
  124. // User has previously clicked "Cancel" on the confirm dialog for this song
  125. console.debug('onNoResults() Key "' + key + '" is ignored')
  126. return
  127. }
  128.  
  129. if (showSpotifyLyricsEnabled && document.querySelector('[data-testid="lyrics-button"]')) {
  130. openAndAskToSubmitSpotifyLyrics(songTitle, songArtistsArr, false)
  131. }
  132. }
  133.  
  134. async function openAndAskToSubmitSpotifyLyrics (songTitle, songArtistsArr, forceSubmit = false) {
  135. const submitSpotifyLyricsEnabled = forceSubmit || (await GM.getValue('submit_spotify_lyrics', true))
  136. const key = songTitle + ' - ' + songArtistsArr.join(', ')
  137.  
  138. // Open lyrics if they are not already open
  139. if (!document.querySelector('[data-testid="fullscreen-lyric"]')) {
  140. document.querySelector('[data-testid="lyrics-button"]').click()
  141. }
  142. // Wait one second for lyrics to open
  143. window.setTimeout(async function () {
  144. const lyrics = Array.from(document.querySelectorAll('[data-testid="fullscreen-lyric"]')).map(div => div.textContent).join('\n')
  145.  
  146. // Close lyrics again, if there are no lyrics
  147. if (document.querySelectorAll('[data-testid="fullscreen-lyric"]').length === 0) {
  148. console.debug('Closing lyrics-view, because Spotify has no lyrics either.')
  149. document.querySelector('[data-testid="lyrics-button"]').click()
  150. return
  151. }
  152.  
  153. // Check if the lyrics are behind a premium modal overlay
  154. for (let p = document.querySelector('[data-testid="fullscreen-lyric"]'); p && p.parentElement; p = p.parentElement) {
  155. if (p.tagName === 'MAIN') {
  156. if (p.querySelector('button span')) {
  157. console.debug('Lyrics are behind paywall, abort submit to genius.')
  158. improveLyricsPaywall()
  159. return
  160. }
  161. break
  162. }
  163. }
  164.  
  165. if (submitSpotifyLyricsEnabled && lyrics && lyrics.trim()) {
  166. // Add this song to the ignored list so we don't ask again
  167. GM.getValue('submit_spotify_lyrics_ignore', '[]').then(async function (s) {
  168. const arr = JSON.parse(s)
  169. arr.push(key)
  170. await GM.setValue('submit_spotify_lyrics_ignore', JSON.stringify(arr))
  171. })
  172. // Ask user if they want to submit the lyrics
  173. genius.f.closeModalUIs()
  174. if (forceSubmit || (await genius.f.modalConfirm(`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 a Genius.com account to do this)\n${songTitle} by ${songArtistsArr.join(', ')}`))) {
  175. submitLyricsToGenius(songTitle, songArtistsArr, lyrics)
  176. } else {
  177. // Once (globally) show the suggestion to disable this feature
  178. GM.getValue('suggest_to_disable_submit_spotify_lyrics', true).then(async function (suggestToDisable) {
  179. if (suggestToDisable) {
  180. genius.f.modalAlert('You can disable this suggestion in the options of the script.')
  181. GM.setValue('suggest_to_disable_submit_spotify_lyrics', false)
  182. }
  183. })
  184. }
  185. }
  186. }, 1000)
  187. }
  188.  
  189. function improveLyricsPaywall () {
  190. if (!document.querySelector('[data-testid="fullscreen-lyric"]')) {
  191. return
  192. }
  193. let main
  194. for (let p = document.querySelector('[data-testid="fullscreen-lyric"]'); p && p.parentElement; p = p.parentElement) {
  195. if (p.tagName === 'MAIN') {
  196. if (p.querySelector('button span')) {
  197. main = p
  198. break
  199. } else {
  200. return
  201. }
  202. }
  203. }
  204. const modal = main.querySelector('button span').parentNode.parentNode.parentNode
  205. modal.style.width = '50%'
  206. modal.style.height = '30%'
  207. modal.style.top = 'auto'
  208. modal.style.bottom = 0
  209. modal.style.left = 'auto'
  210. modal.style.right = 0
  211. const lyricsHolder = document.querySelector('[data-testid="fullscreen-lyric"]').parentNode
  212. const style = window.getComputedStyle(document.querySelector('[data-testid="fullscreen-lyric"]').firstElementChild, null)
  213. lyricsHolder.className = ''
  214. lyricsHolder.style.fontSize = style.fontSize
  215. lyricsHolder.style.fontWeight = style.fontWeight
  216. lyricsHolder.style.color = style.color
  217. }
  218.  
  219. function submitLyricsFromMenu () {
  220. genius.f.closeModalUIs()
  221.  
  222. const [ret, songTitle, songArtistsArr] = getSongTitleAndArtist()
  223. if (ret < 0) return
  224.  
  225. if (songTitle && document.querySelector('[data-testid="lyrics-button"]')) {
  226. openAndAskToSubmitSpotifyLyrics(songTitle, songArtistsArr, true)
  227. } else {
  228. genius.f.modalAlert('Spotify lyrics are not available for this song.')
  229. }
  230. }
  231.  
  232. function submitLyricsToGenius (songTitle, songArtistsArr, lyrics) {
  233. GM.setValue('submitToGenius', JSON.stringify({
  234. lyrics,
  235. songTitle,
  236. songArtistsArr
  237. })).then(function () {
  238. GM_openInTab('https://genius.com/songs/new', { active: true })
  239. })
  240. }
  241.  
  242. async function fillGeniusForm () {
  243. const data = JSON.parse(await GM.getValue('submitToGenius', '{}'))
  244. await GM.setValue('submitToGenius', '{}')
  245. if ('lyrics' in data && 'songTitle' in data && 'songArtistsArr' in data) {
  246. document.getElementById('song_primary_artists__name').value = data.songArtistsArr.join(', ')
  247. document.getElementById('song_title').value = data.songTitle
  248. document.getElementById('song_lyrics').value = data.lyrics
  249.  
  250. // Create keyup event on song name, to generate the warning about duplicates
  251. const evt = new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'e', char: 'e' })
  252. document.getElementById('song_primary_artists__name').dispatchEvent(evt)
  253. document.getElementById('song_title').dispatchEvent(evt)
  254. }
  255. }
  256.  
  257. function hideLyrics () {
  258. addLyricsButton()
  259. document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove())
  260. if (document.getElementById('lyricscontainer')) {
  261. document.getElementById('lyricscontainer').parentNode.removeChild(document.getElementById('lyricscontainer'))
  262. const topContainer = document.querySelector('div.Root')
  263. topContainer.style.width = '100%'
  264. topContainer.style.removeProperty('float')
  265. }
  266. }
  267.  
  268. function listSongs (hits, container, query) {
  269. if (!container) {
  270. container = getCleanLyricsContainer()
  271. }
  272. container.style.backgroundColor = 'rgba(0,0,0,.8)'
  273.  
  274. // Back to search button
  275. const backToSearchButton = document.createElement('a')
  276. backToSearchButton.href = '#'
  277. backToSearchButton.appendChild(document.createTextNode('Back to search'))
  278. backToSearchButton.addEventListener('click', function backToSearchButtonClick (ev) {
  279. ev.preventDefault()
  280. if (query) {
  281. showSearchField(query)
  282. } else if (genius.current.compoundTitle) {
  283. showSearchField(genius.current.compoundTitle.replace('\t', ' '))
  284. } else if (genius.current.artists && genius.current.title) {
  285. showSearchField(genius.current.artists + ' ' + genius.current.title)
  286. } else if (genius.current.artists) {
  287. showSearchField(genius.current.artists)
  288. } else {
  289. showSearchField()
  290. }
  291. })
  292.  
  293. const separator = document.createElement('span')
  294. separator.setAttribute('class', 'second-line-separator')
  295. separator.setAttribute('style', 'padding:0px 10px')
  296.  
  297. separator.appendChild(document.createTextNode('•'))
  298.  
  299. // Hide button
  300. const hideButton = document.createElement('a')
  301. hideButton.href = '#'
  302. hideButton.appendChild(document.createTextNode('Hide'))
  303. hideButton.addEventListener('click', function hideButtonClick (ev) {
  304. ev.preventDefault()
  305. hideLyrics()
  306. })
  307.  
  308. // List search results
  309. const trackhtml = `
  310. <div class="geniushiticon">
  311. <div class="geniushiticonout">
  312. <span style="color:silver;font-size:2.0em">🅖</span>
  313. </div>
  314. <div class="geniushiticonover">
  315. <span style="opacity:0.7;font-size:1.5em">📄</span>
  316. </div>
  317. </div>
  318. <div class="geniushitname">
  319. <div class="track-name-wrapper tracklist-top-align">
  320. <div class="tracklist-name ellipsis-one-line" dir="auto">$title</div>
  321. <div class="second-line">
  322. <span class="ellipsis-one-line" dir="auto">$artist</span>
  323. <span class="second-line-separator" aria-label="in album">•</span>
  324. <span class="ellipsis-one-line" dir="auto">👁 <span style="font-size:0.8em">$stats.pageviews</span></span>
  325. <span class="second-line-separator" aria-label="in album">•</span>
  326. <span class="geniusbadge">$lyrics_state</span>
  327. </div>
  328. </div>
  329. </div>`
  330. container.innerHTML = '<section class="tracklist-container"><ol class="tracklist geniushits" style="width:99%"></ol></section>'
  331.  
  332. container.insertBefore(hideButton, container.firstChild)
  333. container.insertBefore(separator, container.firstChild)
  334. container.insertBefore(backToSearchButton, container.firstChild)
  335.  
  336. const ol = container.querySelector('ol.tracklist')
  337. const searchresultsLengths = hits.length
  338. const compoundTitle = genius.current.compoundTitle
  339. const onclick = function onclick () {
  340. genius.f.rememberLyricsSelection(compoundTitle, null, this.dataset.hit)
  341. genius.f.showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths)
  342. }
  343. hits.forEach(function forEachHit (hit) {
  344. const li = ol.appendChild(document.createElement('li'))
  345. li.setAttribute('class', 'tracklist-row')
  346. li.setAttribute('role', 'button')
  347. 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) : ' - ')
  348. li.dataset.hit = JSON.stringify(hit)
  349.  
  350. li.addEventListener('click', onclick)
  351.  
  352. const geniushitname = li.querySelector('.geniushitname')
  353. if (geniushitname.clientWidth > (li.clientWidth - 30)) {
  354. geniushitname.style.width = (li.clientWidth - 30) + 'px'
  355. geniushitname.classList.add('runningtext')
  356. }
  357. })
  358. if (hits.length === 0) {
  359. const li = ol.appendChild(document.createElement('li'))
  360. li.style.fontSize = 'larger'
  361. li.innerHTML = 'No results found'
  362. }
  363. }
  364.  
  365. const songTitleQuery = 'a[data-testid="nowplaying-track-link"],.Root footer .ellipsis-one-line a[href*="/track/"],.Root footer .ellipsis-one-line a[href*="/album/"],.Root footer .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/"]'
  366. const songArtistsQuery = '.Root footer .ellipsis-one-line a[href*="/artist/"],.Root footer .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/"]'
  367.  
  368. function getSongTitleAndArtist () {
  369. const nowPlayingFooter = document.querySelector('[data-testid="now-playing-widget"]')
  370. const songTitleDOM = nowPlayingFooter ? HTMLElement.prototype.querySelector.call(nowPlayingFooter, songTitleQuery) : document.querySelector(songTitleQuery) // eslint-disable-line no-undef
  371. if (!songTitleDOM) {
  372. console.warn('The song title element is not found.')
  373. return [-1]
  374. }
  375. const songTitle = genius.f.cleanUpSongTitle(songTitleDOM.textContent)
  376. if (!songTitle) {
  377. console.warn('The song title is empty.')
  378. return [-2]
  379. }
  380. const songArtistsArr = []
  381. const ArtistLinks = nowPlayingFooter ? HTMLElement.prototype.querySelectorAll.call(nowPlayingFooter, songArtistsQuery) : document.querySelectorAll(songArtistsQuery) // eslint-disable-line no-undef
  382. for (const e of ArtistLinks) {
  383. songArtistsArr.push(e.textContent)
  384. }
  385. return [0, songTitle, songArtistsArr]
  386. }
  387.  
  388. function addLyrics (force, beLessSpecific) {
  389. let musicIsPlaying = false
  390. const buttons = document.querySelectorAll('.Root footer button[data-testid="control-button-playpause"]')
  391. if (buttons.length) {
  392. buttons.forEach(function (button) {
  393. if (button.getAttribute('aria-label') === 'Pause' ||
  394. button.innerHTML.indexOf('M3 2h3v12H3zM10 2h3v12h-3z') !== -1 ||
  395. button.innerHTML.indexOf('M3 2h3v12H3zm7 0h3v12h-3z') !== -1 ||
  396. button.innerHTML.indexOf('M2.7 1a.7.7 0 00-.7.7v12.6a.7.7 0') !== -1 ||
  397. button.innerHTML.indexOf('M2.7 1a.7.7 0 0 0-.7.7v12.6a') !== -1
  398. ) {
  399. musicIsPlaying = true
  400. }
  401. })
  402. }
  403. const [ret, songTitle, songArtistsArr] = getSongTitleAndArtist()
  404. if (ret < 0) return
  405. genius.f.loadLyrics(force, beLessSpecific, songTitle, songArtistsArr, musicIsPlaying)
  406. }
  407.  
  408. let lastPos = null
  409. function updateAutoScroll () {
  410. let pos = null
  411. try {
  412. const els = document.querySelectorAll('[data-testid="player-controls"] [data-testid="playback-position"],[data-testid="player-controls"] [data-testid="playback-duration"]')
  413. if (els.length !== 2) {
  414. throw new Error(`Expected 2 playback elements, found ${els.length}`)
  415. }
  416. const [current, remaining] = Array.from(els).map(e => e.textContent.trim().replace('-', '')).map(s => s.split(':').reverse().map((d, i, a) => parseInt(d) * Math.pow(60, i)).reduce((a, c) => a + c, 0))
  417. pos = current / (current + remaining)
  418. } catch (e) {
  419. // Could not parse current song position
  420. pos = null
  421. }
  422. if (pos != null && !Number.isNaN(pos) && lastPos !== pos) {
  423. genius.f.scrollLyrics(pos)
  424. lastPos = pos
  425. }
  426. }
  427.  
  428. function startSearch (query, container) {
  429. genius.f.searchByQuery(query, container, (res) => {
  430. if (res && res.status === 200) {
  431. listSongs(res.hits, container, query)
  432. } else {
  433. const div = container.appendChild(document.createElement('div'))
  434. div.classList.add('geniushit')
  435. div.innerHTML = `Error:<pre>${JSON.stringify(res, null, 2)}</pre>`
  436. }
  437. })
  438. }
  439.  
  440. function showSearchField (query) {
  441. const b = getCleanLyricsContainer()
  442. const div = b.appendChild(document.createElement('div'))
  443. div.style = 'padding:5px'
  444. div.appendChild(document.createTextNode('Search genius.com: '))
  445.  
  446. // Hide button
  447. const hideButton = div.appendChild(document.createElement('a'))
  448. hideButton.href = '#'
  449. hideButton.style = 'float: right; padding-right: 10px;'
  450. hideButton.appendChild(document.createTextNode('Hide'))
  451. hideButton.addEventListener('click', function hideButtonClick (ev) {
  452. ev.preventDefault()
  453. hideLyrics()
  454. })
  455.  
  456. const br = div.appendChild(document.createElement('br'))
  457. br.style.clear = 'right'
  458.  
  459. div.style.paddingRight = '15px'
  460. const input = div.appendChild(document.createElement('input'))
  461. input.style = 'width:92%;border:0;border-radius:500px;padding:8px 5px 8px 25px;text-overflow:ellipsis'
  462. input.placeholder = 'Search genius.com...'
  463. if (query) {
  464. input.value = query
  465. } else if (genius.current.compoundTitle) {
  466. input.value = genius.current.compoundTitle.replace('\t', ' ')
  467. } else if (genius.current.artists && genius.current.title) {
  468. input.value = genius.current.artists + ' ' + genius.current.title
  469. } else if (genius.current.artists) {
  470. input.value = genius.current.artists
  471. }
  472. input.addEventListener('focus', function onSearchLyricsButtonFocus () {
  473. this.style.color = 'black'
  474. })
  475. input.addEventListener('change', function onSearchLyricsButtonClick () {
  476. this.style.color = 'black'
  477. if (input.value) {
  478. startSearch(input.value, b)
  479. }
  480. })
  481. input.addEventListener('keyup', function onSearchLyricsKeyUp (ev) {
  482. this.style.color = 'black'
  483. if (ev.code === 'Enter' || ev.code === 'NumpadEnter') {
  484. ev.preventDefault()
  485. if (input.value) {
  486. startSearch(input.value, b)
  487. }
  488. }
  489. })
  490. input.focus()
  491. const mag = div.appendChild(document.createElement('div'))
  492. mag.style.marginTop = '-27px'
  493. mag.style.marginLeft = '3px'
  494. mag.appendChild(document.createTextNode('🔎'))
  495. }
  496.  
  497. function addLyricsButton () {
  498. if (document.getElementById('showlyricsbutton')) {
  499. return
  500. }
  501. const b = document.createElement('div')
  502. b.setAttribute('id', 'showlyricsbutton')
  503. b.setAttribute('style', 'position:absolute; top: 0px; right:0px; font-size:14px; color:#ffff64; cursor:pointer; z-index:3000;')
  504. b.setAttribute('title', 'Load lyrics from genius.com')
  505. b.appendChild(document.createTextNode('🅖'))
  506. b.addEventListener('click', function onShowLyricsButtonClick () {
  507. genius.option.autoShow = true // Temporarily enable showing lyrics automatically on song change
  508. window.clearInterval(genius.iv.main)
  509. genius.iv.main = window.setInterval(main, 2000)
  510. b.remove()
  511. addLyrics(true)
  512. })
  513. document.body.appendChild(b)
  514. if (b.clientWidth < 10) {
  515. 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;')
  516. b.innerHTML = 'G'
  517. }
  518. }
  519.  
  520. function configShowSpotifyLyrics (div) {
  521. // Input: Show lyrics from Spotify if no lyrics found on genius.com
  522. const id = 'input945455'
  523.  
  524. const input = div.appendChild(document.createElement('input'))
  525. input.type = 'checkbox'
  526. input.id = id
  527. GM.getValue('show_spotify_lyrics', true).then(function (v) {
  528. input.checked = v
  529. })
  530.  
  531. const label = div.appendChild(document.createElement('label'))
  532. label.setAttribute('for', id)
  533. label.appendChild(document.createTextNode('Open lyrics from Spotify if no lyrics found on genius.com'))
  534.  
  535. const onChange = function onChangeListener () {
  536. GM.setValue('show_spotify_lyrics', input.checked)
  537. }
  538. input.addEventListener('change', onChange)
  539. }
  540.  
  541. function configSubmitSpotifyLyrics (div) {
  542. // Input: Submit lyrics from Spotify to genius.com
  543. const id = 'input337565'
  544.  
  545. const input = div.appendChild(document.createElement('input'))
  546. input.type = 'checkbox'
  547. input.id = id
  548. input.setAttribute('title', '...in case Spotify has lyrics that genius.com does not have')
  549. GM.getValue('submit_spotify_lyrics', true).then(function (v) {
  550. input.checked = v
  551. })
  552.  
  553. const label = div.appendChild(document.createElement('label'))
  554. label.setAttribute('for', id)
  555. label.appendChild(document.createTextNode('Suggest to submit lyrics from Spotify to genius.com'))
  556. label.setAttribute('title', '...in case Spotify has lyrics that genius.com does not have')
  557.  
  558. const onChange = function onChangeListener () {
  559. GM.setValue('submit_spotify_lyrics', input.checked)
  560. }
  561. input.addEventListener('change', onChange)
  562. }
  563.  
  564. function configHideSpotifySuggestions (div) {
  565. // Input: Hide suggestions and hints from Spotify about new features
  566. const id = 'input875687'
  567.  
  568. const input = div.appendChild(document.createElement('input'))
  569. input.type = 'checkbox'
  570. input.id = id
  571. input.setAttribute('title', 'Hide suggestions and hints from Spotify about new features')
  572. GM.getValue('hide_spotify_suggestions', true).then(function (v) {
  573. input.checked = v
  574. })
  575.  
  576. const label = div.appendChild(document.createElement('label'))
  577. label.setAttribute('for', id)
  578. label.appendChild(document.createTextNode('Hide suggestions and hints from Spotify about new features'))
  579.  
  580. const onChange = function onChangeListener () {
  581. GM.setValue('hide_spotify_suggestions', input.checked)
  582. }
  583. input.addEventListener('change', onChange)
  584. }
  585.  
  586. function configHideSpotifyNowPlayingView (div) {
  587. // Input: Hide "Now Playing View"
  588. const id = 'input12567826'
  589.  
  590. const input = div.appendChild(document.createElement('input'))
  591. input.type = 'checkbox'
  592. input.id = id
  593. input.setAttribute('title', 'Hide Spotify\'s "Now Playing View"')
  594. GM.getValue('hide_spotify_now_playing_view', true).then(function (v) {
  595. input.checked = v
  596. })
  597.  
  598. const label = div.appendChild(document.createElement('label'))
  599. label.setAttribute('for', id)
  600. label.appendChild(document.createTextNode('Hide Spotify\'s "Now Playing View"'))
  601.  
  602. const onChange = function onChangeListener () {
  603. GM.setValue('hide_spotify_now_playing_view', input.checked)
  604. }
  605. input.addEventListener('change', onChange)
  606. }
  607.  
  608. function addCss () {
  609. document.head.appendChild(document.createElement('style')).innerHTML = `
  610. .lyricsiframe {
  611. opacity:0.1;
  612. transition:opacity 2s;
  613. margin:0px;
  614. padding:0px;
  615. }
  616. .loadingspinnerholder {
  617. position:absolute;
  618. top:100px;
  619. left:100px;
  620. cursor:progress
  621. }
  622. .lyricsnavbar {
  623. background-color: rgb(80, 80, 80);
  624. background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgb(18, 18, 18));
  625. border-radius: 8px 8px 0px 0px;
  626. margin: 8px 0px 0px 0px;
  627. padding:0px 10px;
  628. }
  629.  
  630. .lyricsnavbar span,.lyricsnavbar a:link,.lyricsnavbar a:visited {
  631. color: rgb(179, 179, 179);
  632. text-decoration:none;
  633. transition:color 400ms;
  634. }
  635. .lyricsnavbar a:hover,.lyricsnavbar span:hover {
  636. color:white;
  637. text-decoration:none;
  638. }
  639. .lyricsnavbar .second-line-separator,.lyricsnavbar .second-line-separator:hover {
  640. padding:0px 10px !important;
  641. color: transparent;
  642. vertical-align: text-bottom;
  643. }
  644. .geniushits li.tracklist-row {
  645. cursor:pointer
  646. }
  647. .geniushits li.tracklist-row:hover {
  648. background-color: #fff5;
  649. border-radius: 5px;
  650. }
  651. .geniushits li .geniushiticonout {
  652. display:inline-block;
  653. }
  654. .geniushits li:hover .geniushiticonout {
  655. display:none
  656. }
  657. .geniushits li .geniushiticonover {
  658. display:none
  659. }
  660. .geniushits li:hover .geniushiticonover {
  661. display:inline-block;
  662. padding-top:5px;
  663. }
  664. .geniushiticon {
  665. width:25px;
  666. height:2em;
  667. display:inline-block;
  668. vertical-align: top;
  669. }
  670. .geniushitname {
  671. display:inline-block;
  672. position: relative;
  673. overflow:hidden
  674. }
  675. .geniushitname .tracklist-name {
  676. font-size: 16px;
  677. font-weight: 400;
  678. color:white;
  679. }
  680. .geniushitname.runningtext .tracklist-name {
  681. display: inline-block;
  682. position: relative;
  683. animation: 3s linear 0s infinite alternate runtext;
  684. }
  685.  
  686. .geniushits .second-line-separator {
  687. opacity: 0.7
  688. }
  689.  
  690. .geniushitname .geniusbadge {
  691. color: #121212;
  692. background-color: hsla(0,0%,100%,.6);
  693. border-radius: 2px;
  694. text-transform: uppercase;
  695. font-size: 9px;
  696. line-height: 10px;
  697. min-width: 16px;
  698. height: 16px;
  699. padding: 0 2px;
  700. margin: 0 3px;
  701. }
  702.  
  703. @keyframes runtext {
  704. 0%, 25% {
  705. transform: translateX(0%);
  706. left: 0%;
  707. }
  708. 75%, 100% {
  709. transform: translateX(-100%);
  710. left: 100%;
  711. }
  712. }
  713.  
  714. `
  715. }
  716.  
  717. function styleIframeContent () {
  718. if (genius.option.themeKey === 'genius' || genius.option.themeKey === 'geniusReact') {
  719. genius.style.enabled = true
  720. genius.style.setup = () => {
  721. genius.style.setup = null // run once; set variables to genius.styleProps
  722. if (genius.option.themeKey !== 'genius' && genius.option.themeKey !== 'geniusReact') {
  723. genius.style.enabled = false
  724. return false
  725. }
  726. return true
  727. }
  728. } else {
  729. genius.style.enabled = false
  730. genius.style.setup = null
  731. }
  732. }
  733.  
  734. function main () {
  735. if (document.querySelector('.Root [data-testid="player-controls"] [data-testid="playback-progressbar"]') && document.querySelector(songTitleQuery)) {
  736. if (genius.option.autoShow) {
  737. addLyrics()
  738. } else {
  739. addLyricsButton()
  740. }
  741. }
  742. }
  743.  
  744. if (document.location.hostname === 'genius.com') {
  745. // https://genius.com/songs/new
  746. fillGeniusForm()
  747. } else {
  748. window.setInterval(function removeAds () {
  749. // Remove "premium" button
  750. try {
  751. const button = document.querySelector('button[class^=Button][aria-label*=Premium]')
  752. if (button) {
  753. button.style.display = 'none'
  754. }
  755. } catch (e) {
  756. console.warn(e)
  757. }
  758. // Remove "install app" button
  759. try {
  760. const button = document.querySelector('a[href*="/download"]')
  761. if (button) {
  762. button.style.display = 'none'
  763. }
  764. } catch (e) {
  765. console.warn(e)
  766. }
  767. // Remove iframe "GET 3 MONTHS FREE"
  768. try {
  769. const iframe = document.querySelector('iframe[data-testid="inAppMessageIframe"]')
  770. if (iframe && iframe.contentDocument && iframe.contentDocument.body) {
  771. iframe.contentDocument.body.querySelectorAll('button').forEach(function (button) {
  772. if (button.parentNode.innerHTML.indexOf('Dismiss_action') !== -1) {
  773. button.click()
  774. }
  775. })
  776. }
  777. } catch (e) {
  778. console.warn(e)
  779. }
  780.  
  781. GM.getValue('hide_spotify_suggestions', true).then(function (hideSuggestions) {
  782. if (hideSuggestions) {
  783. // Remove hints and suggestions
  784. document.querySelectorAll('.encore-announcement-set button[class*="Button-"]').forEach(b => b.click())
  785. // Check "show never again"
  786. document.querySelectorAll('#dont.show.onboarding.npv').forEach(c => (c.checked = true))
  787. // Close bubble
  788. document.querySelectorAll('.tippy-box button[class*="Button-"]').forEach(b => b.click())
  789. }
  790. })
  791.  
  792. GM.getValue('hide_spotify_now_playing_view', true).then(function (hideNowPlaying) {
  793. if (hideNowPlaying) {
  794. // Close "Now Playing View"
  795. // New: 2025-04
  796. document.querySelectorAll('[data-testid="control-button-npv"][data-active="true"]').forEach(function (b) {
  797. b.click()
  798. })
  799. // Old: 2024-10
  800. document.querySelectorAll('#Desktop_PanelContainer_Id [data-testid="PanelHeader_CloseButton"] button[class*="Button-"]').forEach(function (b) {
  801. if (b.parentNode.previousElementSibling && b.parentNode.previousElementSibling.querySelector('button[data-testid="more-button"]')) {
  802. // Second button is the "Now Playing View" button but not in the "Queue view"
  803. b.click()
  804. }
  805. })
  806. }
  807. })
  808. }, 3000)
  809.  
  810. genius = geniusLyrics({
  811. GM,
  812. scriptName,
  813. scriptIssuesURL: 'https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues',
  814. scriptIssuesTitle: 'Report problem: github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues',
  815. domain: 'https://open.spotify.com',
  816. emptyURL: 'https://open.spotify.com/robots.txt',
  817. main,
  818. addCss,
  819. listSongs,
  820. showSearchField,
  821. addLyrics,
  822. hideLyrics,
  823. getCleanLyricsContainer,
  824. setFrameDimensions,
  825. initResize,
  826. onResize,
  827. config: [
  828. configShowSpotifyLyrics,
  829. configSubmitSpotifyLyrics,
  830. configHideSpotifySuggestions,
  831. configHideSpotifyNowPlayingView
  832. ],
  833. toggleLyricsKey: {
  834. shiftKey: true,
  835. ctrlKey: false,
  836. altKey: false,
  837. key: 'L'
  838. },
  839. onNoResults,
  840. onNewSongPlaying
  841. })
  842.  
  843. genius.option.enableStyleSubstitution = true
  844. genius.option.cacheHTMLRequest = true // 1 lyrics page consume 2XX KB [OR 25 ~ 50KB under ]
  845.  
  846. genius.onThemeChanged.push(styleIframeContent)
  847.  
  848. GM.registerMenuCommand(scriptName + ' - Show lyrics', () => addLyrics(true))
  849. GM.registerMenuCommand(scriptName + ' - Options', () => genius.f.config())
  850. GM.registerMenuCommand(scriptName + ' - Submit lyrics to Genius', () => submitLyricsFromMenu())
  851. window.setInterval(updateAutoScroll, 1000)
  852. window.setInterval(improveLyricsPaywall, 10000)
  853. }