Youtube Music Genius Lyrics

Mostra i testi delle canzoni di genius.com su Youtube Music

Installa questo script?
Script suggerito dall'autore

Potresti essere interessato/a anche a Youtube Genius Lyrics

Installa questo script
  1. // ==UserScript==
  2. // @name Youtube Music Genius Lyrics
  3. // @description Shows lyrics/songtexts from genius.com on Youtube music next to music videos
  4. // @description:es Mostra la letra de genius.com de las canciones en Youtube Music
  5. // @description:de Zeigt den Songtext von genius.com auf Youtube Music an
  6. // @description:fr Présente les paroles de chansons de genius.com sur Youtube Music
  7. // @description:pl Pokazuje teksty piosenek z genius.com na Youtube Music
  8. // @description:pt Mostra letras de genius.com no Youtube Music
  9. // @description:it Mostra i testi delle canzoni di genius.com su Youtube Music
  10. // @description:ja YouTube Music(ユーチューブ ミュージック)プレーヤーで、スクリプトが genius.com の歌詞を表示する
  11. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  12. // @copyright 2020, cuzi (https://github.com/cvzi)
  13. // @author cuzi
  14. // @icon https://music.youtube.com/img/favicon_144.png
  15. // @supportURL https://github.com/cvzi/Youtube-Music-Genius-Lyrics-userscript/issues
  16. // @version 4.0.31
  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_addValueChangeListener
  24. // @connect genius.com
  25. // @match https://music.youtube.com/*
  26. // @namespace https://greasyfork.org/users/20068
  27. // ==/UserScript==
  28.  
  29. /*
  30. Copyright (C) 2020 cuzi (cuzi@openmail.cc)
  31.  
  32. This program is free software: you can redistribute it and/or modify
  33. it under the terms of the GNU General Public License as published by
  34. the Free Software Foundation, either version 3 of the License, or
  35. (at your option) any later version.
  36.  
  37. This program is distributed in the hope that it will be useful,
  38. but WITHOUT ANY WARRANTY; without even the implied warranty of
  39. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  40. GNU General Public License for more details.
  41.  
  42. You should have received a copy of the GNU General Public License
  43. along with this program. If not, see <https://www.gnu.org/licenses/>.
  44. */
  45.  
  46. /* global GM, genius, geniusLyrics, GM_addValueChangeListener, HTMLMediaElement, MutationObserver */ // eslint-disable-line no-unused-vars
  47. /* jshint asi: true, esversion: 8 */
  48.  
  49. 'use strict'
  50.  
  51. const SCRIPT_NAME = 'Youtube Music Genius Lyrics'
  52. let lyricsDisplayState = 'hidden'
  53. let lyricsWidth = '40%'
  54.  
  55. const elmBuild = (tag, ...contents) => {
  56. /** @type {HTMLElement} */
  57. const elm = typeof tag === 'string' ? document.createElement(tag) : tag
  58. for (const content of contents) {
  59. if (!content || typeof content !== 'object' || (content instanceof Node)) { // eslint-disable-line no-undef
  60. elm.append(content)
  61. } else if (content.length > 0) {
  62. elm.appendChild(elmBuild(...content))
  63. } else if (content.style) {
  64. Object.assign(elm.style, content.style)
  65. } else if (content.classList) {
  66. elm.classList.add(...content.classList)
  67. } else if (content.attr) {
  68. for (const [attr, val] of Object.entries(content.attr)) elm.setAttribute(attr, val)
  69. } else {
  70. Object.assign(elm, content)
  71. }
  72. }
  73. return elm
  74. }
  75.  
  76. function addCss () {
  77. // Spotify
  78. const style = document.createElement('style')
  79. style.id = 'youtube-music-genius-lyrics-style'
  80. style.textContent = `
  81. #lyricscontainer {
  82. position:fixed;
  83. right:0px;
  84. margin:0px;
  85. padding:0px;
  86. background:#000;
  87. color:#fff;
  88. z-index:101;
  89. font-size:1.4rem;
  90. border:none;
  91. border-radius:none;
  92. }
  93. .lyricsiframe {
  94. opacity:0.1;
  95. transition:opacity 2s;
  96. margin:0px;
  97. padding:0px;
  98. }
  99. .lyricsnavbar {
  100. font-size : 0.7em;
  101. text-align:right;
  102. padding-right:10px;
  103. background:#212121;
  104. }
  105. .lyricsnavbar span,.lyricsnavbar a:link,.lyricsnavbar a:visited {
  106. color:#d5d5d5;
  107. text-decoration:none;
  108. transition:color 400ms;
  109. }
  110. .lyricsnavbar a:hover,.lyricsnavbar span:hover {
  111. color:#fff;
  112. text-decoration:none;
  113. }
  114. .loadingspinner {
  115. color:white;
  116. font-size:1em;
  117. line-height:2.5em;
  118. }
  119. .loadingspinnerholder {
  120. z-index:101;
  121. background-color:transparent;
  122. position:absolute;
  123. top:120px;
  124. right:100px;
  125. cursor:progress
  126. }
  127. .lorem {padding:10px 0px 0px 15px; font-size: 1.4rem;line-height: 2.2rem;letter-spacing: 0.3rem;}
  128. .lorem .white {background:black;color:black}
  129. .lorem .gray {background:#7f7f7f;color:#7f7f7f}
  130. #lyricscontainer.geniusSearch {
  131. background:#212121;
  132. }
  133. #lyricscontainer.geniusSearch a:link, #lyricscontainer.geniusSearch a:visited {
  134. color:#909090;
  135. transition:color 300ms;
  136. text-decoration:none;
  137. font-size:16px
  138. }
  139. #lyricscontainer.geniusSearch a:hover{
  140. color:white;
  141. }
  142. .geniussearchinput {
  143. background-color:#212121;
  144. color:white;
  145. border:1px solid #333;
  146. font-size:17px;
  147. padding:7px;
  148. min-width: 60%;
  149. }
  150. input.geniussearchinput:focus {
  151. outline:0;
  152. }
  153. `
  154. document.head.appendChild(style)
  155. }
  156.  
  157. function calcContainerWidthTop () {
  158. const playerBar = document.querySelector('ytmusic-nav-bar')
  159. const playerPage = document.querySelector('ytmusic-player-page#player-page')
  160. const lyricsBar = document.querySelector('#lyricscontainer .lyricsnavbar')
  161. const playerPageDim = playerPage.getBoundingClientRect()
  162. const playerBarDim = playerBar.getBoundingClientRect()
  163.  
  164. const left = playerPageDim.left + playerPageDim.width
  165. const top = playerBarDim.height - (lyricsBar ? lyricsBar.getBoundingClientRect().height : 11)
  166. return [left, top]
  167. }
  168.  
  169. function setFrameDimensions (container, iframe) {
  170. const bar = container.querySelector('.lyricsnavbar')
  171. const ytmusicPlayerBarDim = document.querySelector('ytmusic-player-bar').getBoundingClientRect()
  172. const progressContainer = document.getElementById('progressContainer')
  173. const width = iframe.style.width = container.clientWidth - 1 + 'px'
  174. const height = iframe.style.height = window.innerHeight - 2 -
  175. (bar ? bar.getBoundingClientRect().height : 11) -
  176. container.getBoundingClientRect().top -
  177. (progressContainer ? progressContainer.getBoundingClientRect().height : 3) -
  178. ytmusicPlayerBarDim.height + 'px'
  179.  
  180. if (genius.option.themeKey === 'spotify') {
  181. iframe.style.backgroundColor = 'black'
  182. } else {
  183. iframe.style.backgroundColor = ''
  184. }
  185.  
  186. return [width, height]
  187. }
  188.  
  189. function onResize () {
  190. window.setTimeout(function () {
  191. document.body.dispatchEvent(new CustomEvent('genius-resize-requested'))
  192. }, 200)
  193. }
  194.  
  195. function resize () {
  196. const container = document.getElementById('lyricscontainer')
  197. const iframe = document.getElementById('lyricsiframe')
  198.  
  199. if (!container) {
  200. return
  201. }
  202.  
  203. const [left, top] = calcContainerWidthTop()
  204.  
  205. container.style.top = top + 'px'
  206. container.style.left = left + 'px'
  207.  
  208. if (iframe) {
  209. setFrameDimensions(container, iframe)
  210. }
  211. }
  212.  
  213. function getCleanLyricsContainer () {
  214. let container
  215.  
  216. const playerPage = document.querySelector('ytmusic-player-page#player-page')
  217. const playerPageDiv = playerPage.querySelector('.ytmusic-player-page')
  218. playerPage.style.width = `calc(100% - ${lyricsWidth})`
  219.  
  220. playerPageDiv.dataset.paddingRight = window.getComputedStyle(playerPageDiv).paddingRight
  221. playerPageDiv.style.paddingRight = '0px'
  222.  
  223. const [left, top] = calcContainerWidthTop()
  224.  
  225. if (!document.getElementById('lyricscontainer')) {
  226. container = document.createElement('div')
  227. container.id = 'lyricscontainer'
  228. document.body.appendChild(container)
  229. } else {
  230. container = document.getElementById('lyricscontainer')
  231. container.textContent = ''
  232. }
  233. container.style = ''
  234. container.style.top = top + 'px'
  235. container.style.left = left + 'px'
  236. container.className = ''
  237.  
  238. return document.getElementById('lyricscontainer')
  239. }
  240.  
  241. function getSongInfoNodes () {
  242. let playerBars = [...document.querySelectorAll('ytmusic-player-bar.ytmusic-app')].filter(e => !e.closest('[hidden]') && !e.closest('[disabled]'))
  243. if (playerBars.length === 0) playerBars = [...document.querySelectorAll('ytmusic-player-bar')].filter(e => !e.closest('[hidden]') && !e.closest('[disabled]'))
  244. let titleNode = null
  245. let artistNodes = []
  246. if (playerBars.length === 1) {
  247. const playerBar = playerBars[0]
  248. const key = '__shady_native_querySelector' in playerBar && typeof playerBar.__shady_native_querySelector === 'function' && typeof playerBar.__shady_native_querySelectorAll === 'function' ? '__shady_native_querySelector' : 'querySelector'
  249. titleNode = playerBar[key]('.title.ytmusic-player-bar')
  250. artistNodes = [...playerBar[`${key}All`]('.ytmusic-player-bar.subtitle a[href*="channel/"]')]
  251. }
  252. return {
  253. titleNode,
  254. artistNodes,
  255. isSongQueuedOrPlaying: artistNodes.length > 0 && artistNodes[0].textContent.trim() && titleNode && titleNode.textContent.trim()
  256. }
  257. }
  258.  
  259. function hideLyrics () {
  260. document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove())
  261. if (document.getElementById('lyricscontainer')) {
  262. document.getElementById('lyricscontainer').remove()
  263. }
  264.  
  265. const playerPage = document.querySelector('ytmusic-player-page#player-page')
  266. const playerPageDiv = playerPage.querySelector('.ytmusic-player-page')
  267.  
  268. playerPage.style.width = ''
  269. playerPageDiv.style.paddingRight = playerPageDiv.dataset.paddingRight
  270.  
  271. addLyricsButton()
  272. }
  273.  
  274. function addLyricsButton () {
  275. if (document.getElementById('showlyricsbutton')) {
  276. return
  277. }
  278. const b = document.body.appendChild(document.createElement('div'))
  279. b.setAttribute('id', 'showlyricsbutton')
  280. b.setAttribute('style', 'position: absolute; min-width: 22px; top: 1px; right: 0px; cursor: pointer; z-index: 3000; background: transparent; text-align: right;')
  281. b.setAttribute('title', 'Load lyrics from genius.com')
  282. b.addEventListener('click', function onShowLyricsButtonClick () {
  283. genius.option.autoShow = true // Temporarily enable showing lyrics automatically on song change
  284. window.clearInterval(genius.iv.main)
  285. genius.iv.main = window.setInterval(main, 2000)
  286. b.remove()
  287. addLyrics(true)
  288. })
  289. const g = b.appendChild(document.createElement('span'))
  290. g.setAttribute('style', 'display:inline; color: #ffff64; background: black; border-radius: 50%; margin: auto; font-size: 15px; line-height: 15px;padding: 0px 2px;')
  291. g.appendChild(document.createTextNode('🅖'))
  292. if (g.getBoundingClientRect().width < 10) { // in case the font doesn't have "🅖" symbol
  293. g.setAttribute('style', 'border: 2px solid #ffff64; border-radius: 100%; padding: 0px 3px; font-size: 11px; background-color: black; color: #ffff64; font-weight: 700;')
  294. g.textContent = 'G'
  295. }
  296. }
  297.  
  298. let lastSong = null
  299. function addLyrics (force, beLessSpecific) {
  300. const { titleNode, artistNodes, isSongQueuedOrPlaying } = getSongInfoNodes()
  301. if (!isSongQueuedOrPlaying) {
  302. // No song is playing
  303. lastSong = null
  304. hideLyrics()
  305. return
  306. }
  307.  
  308. let songTitle = titleNode.textContent
  309. const songArtistsArr = Array.from(artistNodes).map(e => e.textContent)
  310.  
  311. const song = `${songArtistsArr.join(', ')}-${songTitle}#${genius.option.themeKey}@${genius.option.fontSize}@${lyricsWidth}`
  312.  
  313. if (lastSong === song && document.getElementById('lyricscontainer')) {
  314. // Same video id and same theme and lyrics are showing -> stop here
  315. return
  316. } else {
  317. lastSong = song
  318. }
  319.  
  320. songTitle = songTitle.replace(/[([]\w+\s*\w*\s*video[)\]]/i, '').trim()
  321. songTitle = songTitle.replace(/[([]\w*\s*audio[)\]]/i, '').trim()
  322. songTitle = genius.f.cleanUpSongTitle(songTitle)
  323.  
  324. const video = getYoutubeMainVideo()
  325. console.log('debug: Youtube Music Genius Lyrics - getYoutubeMainVideo()', video)
  326. genius.f.loadLyrics(force, beLessSpecific, songTitle, songArtistsArr, true)
  327. }
  328.  
  329. function getYoutubeMainVideo () {
  330. const activeMedia_ = activeMedia
  331. if (activeMedia_) {
  332. const moviePlayer = activeMedia_.closest('#movie_player')
  333. const mediaList = moviePlayer ? moviePlayer.querySelectorAll('audio, video') : null
  334. if (mediaList && mediaList.length === 1 && mediaList[0] === activeMedia_) {
  335. return activeMedia_
  336. }
  337. if (activeMedia_.classList.contains('html5-main-video')) {
  338. return activeMedia_
  339. }
  340. }
  341. let video = document.querySelector('#movie_player video[src]')
  342. if (video !== null) {
  343. return video
  344. }
  345. video = document.querySelector('video[src]')
  346. if (video !== null) {
  347. return video
  348. }
  349. return null
  350. }
  351.  
  352. let lastPos = null
  353. function updateAutoScroll (video, force) { // eslint-disable-line no-unused-vars
  354. let pos = null
  355. if (!video) {
  356. video = getYoutubeMainVideo()
  357. }
  358. if (video) {
  359. pos = video.currentTime / video.duration
  360. }
  361. if (pos !== null && pos >= 0 && `${lastPos}` !== `${pos}`) {
  362. lastPos = pos
  363. genius.f.scrollLyrics(pos)
  364. }
  365. }
  366.  
  367. function showSearchField (query) {
  368. const b = getCleanLyricsContainer()
  369.  
  370. b.style.border = '1px solid black'
  371. b.style.borderRadius = '3px'
  372. b.style.padding = '5px'
  373.  
  374. b.appendChild(document.createTextNode('Search genius.com: '))
  375. b.style.paddingRight = '15px'
  376. const input = b.appendChild(document.createElement('input'))
  377. input.className = 'geniussearchinput'
  378. input.placeholder = 'Search genius.com...'
  379.  
  380. const span = b.appendChild(document.createElement('span'))
  381. span.style = 'cursor:pointer'
  382. span.appendChild(document.createTextNode(' \uD83D\uDD0D'))
  383.  
  384. // Hide button
  385. const hideButton = b.appendChild(document.createElement('span'))
  386. hideButton.style = 'cursor:pointer;opacity: 0.8;padding-left: 10px;color: white;font-size: larger;vertical-align: top;'
  387. hideButton.title = 'Hide'
  388. hideButton.appendChild(document.createTextNode('\uD83C\uDD87'))
  389. hideButton.addEventListener('click', function hideButtonClick (ev) {
  390. ev.preventDefault()
  391. hideLyrics()
  392. })
  393.  
  394. if (query) {
  395. input.value = query
  396. } else if (genius.current.compoundTitle) {
  397. input.value = genius.current.compoundTitle.replace('\t', ' ')
  398. } else if (genius.current.artists && genius.current.title) {
  399. input.value = genius.current.artists + ' ' + genius.current.title
  400. } else if (genius.current.artists) {
  401. input.value = genius.current.artists
  402. }
  403. input.addEventListener('change', function onSearchLyricsButtonClick () {
  404. if (input.value) {
  405. genius.f.searchByQuery(input.value, b)
  406. }
  407. })
  408. input.addEventListener('keyup', function onSearchLyricsKeyUp (ev) {
  409. if (ev.code === 'Enter' || ev.code === 'NumpadEnter') {
  410. ev.preventDefault()
  411. if (input.value) {
  412. genius.f.searchByQuery(input.value, b)
  413. }
  414. }
  415. })
  416. span.addEventListener('click', function onSearchLyricsKeyUp (ev) {
  417. if (input.value) {
  418. genius.f.searchByQuery(input.value, b)
  419. }
  420. })
  421.  
  422. document.body.appendChild(b)
  423. input.focus()
  424. }
  425.  
  426. function listSongs (hits, container, query) {
  427. if (!container) {
  428. container = getCleanLyricsContainer()
  429. }
  430.  
  431. container.classList.add('geniusSearch')
  432.  
  433. // Back to search button
  434. const backToSearchButton = document.createElement('a')
  435. backToSearchButton.href = '#'
  436. backToSearchButton.appendChild(document.createTextNode('Back to search'))
  437. backToSearchButton.addEventListener('click', function backToSearchButtonClick (ev) {
  438. ev.preventDefault()
  439. if (query) {
  440. showSearchField(query)
  441. } else if (genius.current.compoundTitle) {
  442. showSearchField(genius.current.compoundTitle.replace('\t', ' '))
  443. } else if (genius.current.artists && genius.current.title) {
  444. showSearchField(genius.current.artists + ' ' + genius.current.title)
  445. } else if (genius.current.artists) {
  446. showSearchField(genius.current.artists)
  447. } else {
  448. showSearchField()
  449. }
  450. })
  451.  
  452. const separator = document.createElement('span')
  453. separator.setAttribute('class', 'second-line-separator')
  454. separator.setAttribute('style', 'padding:0px 3px')
  455. separator.appendChild(document.createTextNode('•'))
  456.  
  457. // Hide button
  458. const hideButton = document.createElement('a')
  459. hideButton.href = '#'
  460. hideButton.appendChild(document.createTextNode('Hide'))
  461. hideButton.addEventListener('click', function hideButtonClick (ev) {
  462. ev.preventDefault()
  463. hideLyrics()
  464. })
  465.  
  466. elmBuild(container, ['ol', { classList: ['tracklist'] }, { style: { width: '99%', fontSize: '1.15em' } }])
  467.  
  468. container.style.border = '1px solid black'
  469. container.style.borderRadius = '3px'
  470.  
  471. container.insertBefore(hideButton, container.firstChild)
  472. container.insertBefore(separator, container.firstChild)
  473. container.insertBefore(backToSearchButton, container.firstChild)
  474.  
  475. const ol = container.querySelector('ol.tracklist')
  476. ol.style.listStyle = 'none'
  477. const searchresultsLengths = hits.length
  478. const compoundTitle = genius.current.compoundTitle
  479. const onclick = function onclick () {
  480. genius.f.rememberLyricsSelection(compoundTitle, null, this.dataset.hit)
  481. genius.f.showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths)
  482. }
  483. const mouseover = function onmouseover () {
  484. this.querySelector('.onhover').style.display = 'block'
  485. this.querySelector('.onout').style.display = 'none'
  486. this.style.backgroundColor = '#666'
  487. }
  488. const mouseout = function onmouseout () {
  489. this.querySelector('.onhover').style.display = 'none'
  490. this.querySelector('.onout').style.display = 'block'
  491. this.style.backgroundColor = '#333'
  492. }
  493.  
  494. hits.sort(function compareFn (a, b) {
  495. if (genius.current.compoundTitle) {
  496. if (genius.current.compoundTitle.toLowerCase() === (a.result.artist_names + '\t' + a.result.title_with_featured).toLowerCase()) {
  497. return -1
  498. }
  499. if (genius.current.compoundTitle.toLowerCase() === (b.result.artist_names + '\t' + b.result.title_with_featured).toLowerCase()) {
  500. return 1
  501. }
  502. } else if (genius.current.artists && genius.current.title) {
  503. if (genius.current.artists.toLowerCase() === a.result.artist_names.toLowerCase() && genius.current.title.toLowerCase() === a.result.title_with_featured.toLowerCase()) {
  504. return -1
  505. }
  506. if (genius.current.artists.toLowerCase() === b.result.artist_names.toLowerCase() && genius.current.title.toLowerCase() === b.result.title_with_featured.toLowerCase()) {
  507. return 1
  508. }
  509. if (genius.current.title.toLowerCase() === a.result.title_with_featured.toLowerCase()) {
  510. return -1
  511. }
  512. if (genius.current.title.toLowerCase() === b.result.title_with_featured.toLowerCase()) {
  513. return 1
  514. }
  515. }
  516. return 0
  517. })
  518.  
  519. hits.forEach(function forEachHit (hit) {
  520. const li = document.createElement('li')
  521. li.style.cursor = 'pointer'
  522. li.style.transition = 'background-color 350ms'
  523. li.style.padding = '3px'
  524. li.style.margin = '2px'
  525. li.style.borderRadius = '3px'
  526. li.style.backgroundColor = '#333'
  527.  
  528. elmBuild(li,
  529. ['div',
  530. {
  531. style: {
  532. float: 'left'
  533. }
  534. },
  535. ['div', { classList: ['onhover'] }, {
  536. style: {
  537. marginTop: '-0.25em',
  538. display: 'none'
  539. }
  540. }, ['span', '🅖', {
  541. style: {
  542. color: '#222',
  543. fontSize: '2.0em'
  544. }
  545. }]],
  546. ['div', { classList: ['onout'] }, ['span', '📄', {
  547. style: {
  548. fontSize: '1.5em'
  549. }
  550. }]]
  551. ],
  552. ['div', {
  553. style: {
  554. float: 'left',
  555. marginLeft: '5px'
  556. }
  557. },
  558. `${hit.result.primary_artist.name} ${hit.result.title_with_featured}`,
  559. ['br'],
  560. ['span', { style: { fontSize: '0.7em' } }, `👁 ${genius.f.metricPrefix(hit.result.stats.pageviews, 1)} ${hit.result.lyrics_state}`]
  561. ],
  562. ['div', { style: { clear: 'left' } }]
  563. )
  564.  
  565. li.dataset.hit = JSON.stringify(hit)
  566.  
  567. li.addEventListener('click', onclick)
  568. li.addEventListener('mouseover', mouseover)
  569. li.addEventListener('mouseout', mouseout)
  570. ol.appendChild(li)
  571. })
  572. }
  573.  
  574. function loremIpsum () {
  575. const random = (x) => 1 + parseInt(Math.random() * x)
  576.  
  577. // Create a container for the entire content
  578. const container = document.createElement('div')
  579.  
  580. for (let v = 0; v < Math.max(3, random(5)) + 4; v++) {
  581. for (let b = 0; b < random(6); b++) {
  582. const lineContainer = document.createElement('span')
  583. lineContainer.classList.add('gray')
  584.  
  585. for (let l = 0; l < random(9); l++) {
  586. for (let w = 0; w < 1 + random(10); w++) {
  587. for (let i = 0; i < 1 + random(7); i++) {
  588. // Create and append 'x' text node
  589. const xTextNode = document.createTextNode('x')
  590. lineContainer.appendChild(xTextNode)
  591. }
  592.  
  593. // Add the whitespace span
  594. const whiteSpaceSpan = document.createElement('span')
  595. whiteSpaceSpan.classList.add('white')
  596. whiteSpaceSpan.textContent = '\u00A0' // Non-breaking space
  597. lineContainer.appendChild(whiteSpaceSpan)
  598. }
  599.  
  600. // Add line break (br) after each set
  601. lineContainer.appendChild(document.createElement('br'))
  602. }
  603.  
  604. // Append the line container to the main container
  605. container.appendChild(lineContainer)
  606.  
  607. // Add a line break after each section
  608. container.appendChild(document.createElement('br'))
  609. }
  610. }
  611.  
  612. return container // Return the main container with all generated elements
  613. }
  614.  
  615. function createSpinner (spinnerHolder) {
  616. const lyricscontainer = document.getElementById('lyricscontainer')
  617.  
  618. const rect = lyricscontainer.getBoundingClientRect()
  619. spinnerHolder.style.left = ''
  620. spinnerHolder.style.right = '0px'
  621. spinnerHolder.style.top = (lyricscontainer.style.top ? (parseInt(lyricscontainer.style.top) + 50) + 'px' : 0) || '120px'
  622. spinnerHolder.style.width = lyricscontainer.style.width || (rect.width - 1 + 'px')
  623. spinnerHolder.style.maxHeight = (lyricscontainer.getBoundingClientRect().height - 50) + 'px'
  624. spinnerHolder.style.overflow = 'hidden'
  625.  
  626. const spinner = spinnerHolder.appendChild(document.createElement('div'))
  627. spinner.classList.add('loadingspinner')
  628. spinner.style.marginLeft = (rect.width / 2) + 'px'
  629.  
  630. const lorem = loremIpsum()
  631. lorem.classList.add('lorem')
  632. spinnerHolder.appendChild(lorem)
  633.  
  634. function resizeSpinner () {
  635. const spinnerHolder = document.querySelector('.loadingspinnerholder')
  636. const lyricscontainer = document.getElementById('lyricscontainer')
  637. if (spinnerHolder && lyricscontainer) {
  638. const rect = lyricscontainer.getBoundingClientRect()
  639. spinnerHolder.style.top = (lyricscontainer.style.top ? (parseInt(lyricscontainer.style.top) + 50) + 'px' : 0) || '120px'
  640. spinnerHolder.style.width = lyricscontainer.style.width || (rect.width - 1 + 'px')
  641. const loadingSpinner = spinnerHolder.querySelector('.loadingspinner')
  642. if (loadingSpinner) {
  643. loadingSpinner.style.marginLeft = (rect.width / 2) + 'px'
  644. }
  645. } else {
  646. window.clearInterval(resizeSpinnerIV)
  647. }
  648. }
  649. const resizeSpinnerIV = window.setInterval(resizeSpinner, 1000)
  650.  
  651. return spinner
  652. }
  653.  
  654. function configLyricsWidth (div) {
  655. // Input: lyrics width
  656. const label = div.appendChild(document.createElement('label'))
  657. label.setAttribute('for', 'input85654')
  658. label.appendChild(document.createTextNode('Lyrics width: '))
  659.  
  660. const input = div.appendChild(document.createElement('input'))
  661. input.type = 'text'
  662. input.id = 'input85654'
  663. input.size = 4
  664. GM.getValue('lyricswidth', '40%').then(function (v) {
  665. input.value = v
  666. })
  667.  
  668. const onChange = function onChangeListener () {
  669. const m = input.value.match(/\d+%/)
  670. if (m && m[0]) {
  671. lyricsWidth = m[0]
  672. GM.setValue('lyricswidth', lyricsWidth).then(function () {
  673. addLyrics(true)
  674. })
  675. input.value = lyricsWidth
  676. } else {
  677. window.alert('Please set a percentage e.g. 40%')
  678. }
  679. }
  680. input.addEventListener('change', onChange)
  681. }
  682.  
  683. const getNodeHTML = (e) => {
  684. if (e) {
  685. return e.__shady_native_innerHTML || e.innerHTML || ''
  686. }
  687. return ''
  688. }
  689. let activeMedia = null
  690. async function setupMain () {
  691. let resizeRequested = false
  692. lyricsWidth = await GM.getValue('lyricswidth', '40%')
  693. let runid = 0
  694. let lastNodeString = ''
  695.  
  696. const mutationObserver = new MutationObserver(() => {
  697. const songInfoNodes = getSongInfoNodes()
  698. const nodeString = `${(getNodeHTML(songInfoNodes?.titleNode) || '')}|${(songInfoNodes?.artistNodes?.map(e => getNodeHTML(e))?.join(',') || '')}`
  699. if (lastNodeString === nodeString) return
  700. lastNodeString = nodeString
  701. if (nodeString.length > 1 && songInfoNodes.isSongQueuedOrPlaying) {
  702. console.log('debug: Youtube Music Genius Lyrics - Song Info', songInfoNodes, nodeString)
  703. if (genius.option.autoShow) {
  704. addLyrics(true)
  705. } else {
  706. addLyricsButton()
  707. }
  708. if (resizeRequested) {
  709. resizeRequested = false
  710. resize()
  711. }
  712. }
  713. })
  714.  
  715. const onMediaChanged_ = (runid_) => {
  716. if (runid_ !== runid) return
  717. const songInfoNodes = getSongInfoNodes()
  718. const titleNode = songInfoNodes?.titleNode
  719. if (titleNode) {
  720. mutationObserver.observe(titleNode, { attributes: true, childList: true, subtree: true, characterData: true, attributeFilter: ['media-changed-at', 'title'] })
  721. titleNode.setAttribute('media-changed-at', Date.now())
  722. } else {
  723. activeMedia = null
  724. }
  725. }
  726.  
  727. const onMediaChanged = (evt) => {
  728. const target = evt?.target
  729. if (!(target instanceof HTMLMediaElement)) return
  730. if (runid > 1e9) runid = 9
  731. const runid_ = ++runid
  732. activeMedia = target
  733. Promise.resolve(runid_).then(onMediaChanged_).catch(console.warn)
  734. }
  735.  
  736. const onResizeRequested = (evt) => {
  737. if (runid > 1e9) runid = 9
  738. const runid_ = ++runid
  739. lastNodeString = ''
  740. resizeRequested = true
  741. Promise.resolve(runid_).then(onMediaChanged_).catch(console.warn)
  742. }
  743.  
  744. document.addEventListener('durationchange', onMediaChanged, true)
  745. document.addEventListener('loadedmetadata', onMediaChanged, true)
  746. document.addEventListener('canplay', onMediaChanged, true)
  747. document.addEventListener('canplaythrough', onMediaChanged, true)
  748. document.addEventListener('emptied', onMediaChanged, true)
  749. document.addEventListener('abort', onMediaChanged, true)
  750. document.addEventListener('error', onMediaChanged, true)
  751. document.addEventListener('ended', onMediaChanged, true)
  752. document.addEventListener('genius-resize-requested', onResizeRequested, true)
  753. Promise.resolve(++runid).then(onMediaChanged_)
  754. }
  755.  
  756. function main () {
  757. // do nothing
  758. }
  759.  
  760. function styleIframeContent () {
  761. if (genius.option.themeKey === 'genius') {
  762. genius.style.enabled = true
  763. genius.style.setup = () => {
  764. genius.style.setup = null // run once; set variables to genius.styleProps
  765. if (genius.option.themeKey !== 'genius') {
  766. genius.style.enabled = false
  767. return false
  768. }
  769.  
  770. const ytdApp = document.querySelector('ytmusic-app') || document.body
  771. if (!ytdApp) return
  772.  
  773. const cStyle = window.getComputedStyle(ytdApp)
  774. let background = cStyle.getPropertyValue('--ytmusic-general-background-c')
  775. let color = cStyle.getPropertyValue('--ytmusic-text-primary')
  776. let slbc = cStyle.getPropertyValue('--ytd-searchbox-legacy-button-color')
  777. const linkColor = cStyle.getPropertyValue('--yt-spec-call-to-action') || cStyle.getPropertyValue('--ytmusic-text-primary')
  778. const annotatedSpanBgColor = cStyle.getPropertyValue('--yt-spec-static-overlay-icon-inactive') || cStyle.getPropertyValue('--yt-spec-static-overlay-text-secondary') || ''
  779. const annotatedSpanBgColorActive = cStyle.getPropertyValue('--yt-spec-static-overlay-button-hover') || cStyle.getPropertyValue('--yt-spec-static-overlay-button-primary') || ''
  780.  
  781. if (typeof background === 'string' && typeof color === 'string' && background.length > 3 && color.length > 3) {
  782. // do nothing
  783. } else {
  784. background = null
  785. color = null
  786. }
  787.  
  788. if (typeof slbc === 'string') {
  789. // do nothing
  790. } else {
  791. slbc = null
  792. }
  793.  
  794. Object.assign(genius.styleProps, {
  795. '--egl-background': (background === null ? '' : `${background}`),
  796. '--egl-color': (color === null ? '' : `${color}`),
  797. '--egl-infobox-background': (slbc === null ? '' : `${slbc}`),
  798. '--egl-link-color': (`${linkColor}`),
  799. '--egl-annotated-span-bgcolor': (`${annotatedSpanBgColor}`),
  800. '--egl-annotated-span-bgcolor-active': (`${annotatedSpanBgColorActive}`)
  801. })
  802. return true
  803. }
  804. } else {
  805. genius.style.enabled = false
  806. genius.style.setup = null
  807. }
  808. }
  809.  
  810. const isRobotsTxt = document.location.href.indexOf('robots.txt') >= 0
  811. const defaultOptions = {
  812. enableStyleSubstitution: true,
  813. normalizeClassV2: true,
  814. cacheHTMLRequest: true
  815. }
  816.  
  817. const genius = geniusLyrics({
  818. GM,
  819. scriptName: SCRIPT_NAME,
  820. scriptIssuesURL: 'https://github.com/cvzi/Youtube-Music-Genius-Lyrics-userscript/issues',
  821. scriptIssuesTitle: 'Report problem: github.com/cvzi/Youtube-Music-Genius-Lyrics-userscript/issues',
  822. domain: 'https://music.youtube.com/',
  823. emptyURL: 'https://music.youtube.com/robots.txt',
  824. config: [configLyricsWidth],
  825. main,
  826. setupMain,
  827. addCss,
  828. listSongs,
  829. showSearchField,
  830. addLyrics,
  831. hideLyrics,
  832. getCleanLyricsContainer,
  833. setFrameDimensions,
  834. onResize,
  835. createSpinner,
  836. defaultOptions
  837. })
  838.  
  839. genius.onThemeChanged.push(styleIframeContent)
  840.  
  841. if (isRobotsTxt === false) {
  842. GM.registerMenuCommand(SCRIPT_NAME + ' - Show lyrics', () => addLyrics(true))
  843. GM.registerMenuCommand(SCRIPT_NAME + ' - Options', () => genius.f.config())
  844.  
  845. function videoTimeUpdate (ev) {
  846. if (genius.f.isScrollLyricsEnabled()) {
  847. if ((ev || 0).target.nodeName === 'VIDEO') updateAutoScroll()
  848. }
  849. }
  850.  
  851. window.addEventListener('message', function (e) {
  852. const data = ((e || 0).data || 0)
  853. if (data.iAm === SCRIPT_NAME && data.type === 'lyricsDisplayState') {
  854. let isScrollLyricsEnabled = false
  855. if (data.visibility === 'loaded' && data.lyricsSuccess === true) {
  856. isScrollLyricsEnabled = genius.f.isScrollLyricsEnabled()
  857. }
  858. lyricsDisplayState = data.visibility
  859. if (isScrollLyricsEnabled === true) {
  860. document.addEventListener('timeupdate', videoTimeUpdate, true)
  861. } else {
  862. document.removeEventListener('timeupdate', videoTimeUpdate, true)
  863. }
  864. }
  865. })
  866.  
  867. function autoscrollenabledChanged () {
  868. // when value is configurated in any tab, this function will be triggered in all tabs by Userscript Manager
  869. if (typeof genius.f.updateAutoScrollEnabled !== 'function') return
  870. window.requestAnimationFrame(() => {
  871. // not execute for all foreground and background tabs, only execute when the tab is visibile / when the tab shows
  872. genius.f.updateAutoScrollEnabled().then(() => {
  873. let isScrollLyricsEnabled = false
  874. if (lyricsDisplayState === 'loaded') {
  875. isScrollLyricsEnabled = genius.f.isScrollLyricsEnabled()
  876. }
  877. if (isScrollLyricsEnabled === true) {
  878. document.addEventListener('timeupdate', videoTimeUpdate, true)
  879. } else {
  880. document.removeEventListener('timeupdate', videoTimeUpdate, true)
  881. }
  882. })
  883. })
  884. }
  885.  
  886. if (typeof GM_addValueChangeListener === 'function') {
  887. GM_addValueChangeListener('autoscrollenabled', autoscrollenabledChanged)
  888. }
  889. }