🏠 返回首頁 

Greasy Fork is available in English.

Youtube Music Genius Lyrics

Présente les paroles de chansons de genius.com sur Youtube Music


Installer ce script?
Script suggéré par l'auteur

Vous pourriez également aimer Youtube Genius Lyrics.


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