NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Genius.com translate lyrics // @description Shows English or Russian translation next to the lyrics on genius.com. Powered by Yandex.Translate // @namespace cuzi // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @copyright 2019, cuzi (https://github.com/cvzi) // @version 1 // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @connect translate.yandex.net // @include https://genius.com/* // ==/UserScript== const YANDEX_API_KEY = 'trnsl.1.1.20190330T204003Z.a10ff99a15ff49d5.81a81cdd708ab5a0a1748539e579820da8446c9c' const YANDEX_URL = 'https://translate.yandex.net/api/v1.5/tr.json/translate' const YANDEX_HOME = 'https://translate.yandex.com/translate' let DEFAULT_LANG = 'en' const allLangs = { 'en': 'English', 'ru': 'Русский' } const spinner = '<style>.loadingspinner { pointer-events: none; width: 2.5em; height: 2.5em; border: 0.4em solid transparent; border-color: rgb(255, 255, 100) #181818 #181818 #181818; border-radius: 50%; animation: loadingspin 2s ease infinite;} @keyframes loadingspin { 25% { transform: rotate(90deg) } 50% { transform: rotate(180deg) } 75% { transform: rotate(270deg) } 100% { transform: rotate(360deg) }}</style><div class="loadingspinner"></div>' const TAGS = ['A', 'I', 'EM', 'SMALL', 'STRONG'] function retrieveText (node) { let child = node.firstChild let text = '' while (child) { if (child.tagName === 'BR') { text += '\n' } else if (child.nodeType === Node.TEXT_NODE) { text += child.textContent } else if(TAGS.includes(child.tagName)) { text += retrieveText(child) } else if(child.firstChild && child.firstChild.nodeType === Node.TEXT_NODE && child.firstChild.textContent.trim()) { text += child.firstChild.textContent } child = child.nextSibling } return text } function metricPrefix (n, decimals, k) { // http://stackoverflow.com/a/18650828 if (n <= 0) { return String(n) } k = k || 1000 let dm = decimals <= 0 ? 0 : decimals || 2 let sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] let i = Math.floor(Math.log(n) / Math.log(k)) return parseFloat((n / Math.pow(k, i)).toFixed(dm)) + sizes[i] } function askForKey (fromError) { if (fromError) { alert('The API key is incorrect or expired/out of quota!') } else { alert('You have not set your API key yet!') } config() } function config () { Promise.all([ GM.getValue('api_key', false), GM.getValue('lang', DEFAULT_LANG), GM.getValue('requestcache', '{}') ]).then(function (values) { let apiKey = values[0] DEFAULT_LANG = values[1] const requestcacheRaw = values[2] // Window const win = document.createElement('div') win.setAttribute('style', 'position:fixed; top: 10px; left:10px; padding:15px; background:white; border-radius:10%; border:2px solid black; color:black; z-index:100') document.body.appendChild(win) const h1 = document.createElement('h1') win.appendChild(h1).appendChild(document.createTextNode('Options')) let a = document.createElement('a') a.href = 'https://github.com/cvzi/Genius-translate-lyrics-userscript/issues' a.style = 'color:blue' win.appendChild(a).appendChild(document.createTextNode('Report problem: github.com/cvzi/Genius-translate-lyrics-userscript')) // Text: Api Key let div = document.createElement('div') win.appendChild(div) div.appendChild(document.createTextNode('Your Yandex API key: ')) const inputApiKey = div.appendChild(document.createElement('input')) inputApiKey.type = 'text' inputApiKey.size = 90 if (apiKey) { inputApiKey.value = apiKey } else { inputApiKey.style.backgroundColor = '#f6f6a1' inputApiKey.value = '#Not set' } const onApiKeyChange = function onApiKeyChangeListener () { if (inputApiKey.value && (inputApiKey.value.length > 20 || inputApiKey.value === '#Not set')) { GM.setValue('api_key', inputApiKey.value !== '#Not set' ? inputApiKey.value : false) inputApiKey.style.backgroundColor = '#93d89c' } else { alert('Invalid api key') inputApiKey.style.backgroundColor = '#f2c4be' } } inputApiKey.addEventListener('change', onApiKeyChange) div.appendChild(document.createElement('br')) div.appendChild(document.createTextNode('You can get a free API key by registering an account here: ')) div.appendChild(document.createElement('br')) a = document.createElement('a') a.href = 'https://translate.yandex.com/developers/keys' a.style = 'color:blue' div.appendChild(a).appendChild(document.createTextNode('https://translate.yandex.com/developers/keys')) // Select: Language div = document.createElement('div') win.appendChild(div) div.appendChild(document.createTextNode('Your language: ')) const selectLang = div.appendChild(document.createElement('select')) for (let key in allLangs) { const option = selectLang.appendChild(document.createElement('option')) option.value = key if (DEFAULT_LANG === key) { option.selected = true } option.appendChild(document.createTextNode(allLangs[key])) } const onLangChange = function onLangChangeListener () { GM.setValue('lang', selectLang.selectedOptions[0].value) selectLang.style.backgroundColor = '#93d89c' } selectLang.addEventListener('change', onLangChange) // Clear request cache button const bytes = metricPrefix(requestcacheRaw.length - 2, 2, 1024) + 'Bytes' const clearCacheButton = win.appendChild(document.createElement('button')) clearCacheButton.appendChild(document.createTextNode('Clear cache (' + bytes + ')')) clearCacheButton.addEventListener('click', function onClearCacheButtonClick () { GM.setValue('requestcache', '{}').then(function () { clearCacheButton.innerHTML = 'Cleared' }) }) // Close button const closeButton = win.appendChild(document.createElement('button')) closeButton.appendChild(document.createTextNode('Close')) closeButton.style.color = 'black' closeButton.addEventListener('click', function onCloseButtonClick () { win.parentNode.removeChild(win) }) }) } function createArea () { if (document.getElementById('userscripttranslate')) { document.getElementById('userscripttranslate').remove() } // Move lyrics to the right const columnLayout = document.querySelector('routable-page song-page .song_body.column_layout') const minWidth = columnLayout.querySelector('.column_layout-column_span--primary').clientWidth + 50 const rightOffset = document.body.clientWidth - minWidth if (rightOffset > 0) { columnLayout.style.margin = '0 auto 0 ' + minWidth + 'px' } else { columnLayout.style.margin = '0 0 0 auto' } const bodyRect = document.body.getBoundingClientRect() const elemRect = document.querySelector('song-page lyrics div div.lyrics section p').getBoundingClientRect() const offset = elemRect.top - bodyRect.top const width = elemRect.left - bodyRect.left const div = document.createElement('div') div.setAttribute('id', 'userscripttranslate') document.body.appendChild(div) div.style = 'overflow:auto;position:absolute;top:' + offset + 'px;left:0px;max-width:' + width + 'px;padding:0 2.5rem 0 0;margin:0 1rem;white-space: nowrap;font-size: 1.125em;color:#222;background-color:#f7f7f7;font-family: Programme,sans-serif;word-break: break-word;line-height: 1.7em;font-weight: 100;' return div } function translate () { Promise.all([ GM.getValue('api_key', false), GM.getValue('requestcache', '{}') ]).then(function (values) { if (!values[0]) { askForKey() } let requestCache = JSON.parse(values[1]) /* requestCache = { "cachekey0": "121648565.5\njsondata123", ... } */ const now = (new Date()).getTime() const exp = 48 * 60 * 60 * 1000 for (let prop in requestCache) { // Delete cached values, that are older than 2 days const time = requestCache[prop].split('\n')[0] if ((now - (new Date(time)).getTime()) > exp) { delete requestCache[prop] } } yandex(values[0] || YANDEX_API_KEY, requestCache) }) } function yandex (apiKey, requestCache) { const div = createArea() div.innerHTML = spinner + '<br>Collecting lyrics...' const textInput = retrieveText(document.querySelector('song-page lyrics div div.lyrics section p')) const cachekey = JSON.stringify(textInput) if (cachekey in requestCache) { div.innerHTML += ' Found in cache.' const cacheResponse = JSON.parse(requestCache[cachekey].split('\n')[1]) showTranslation(JSON.parse(cacheResponse.responseText), div) return } const requestURL = YANDEX_URL + '?key=' + apiKey + '&lang=' + DEFAULT_LANG + '&format=plain' div.innerHTML += ' OK<br>Opening translate.yandex.net<br>' GM.xmlHttpRequest({ method: 'POST', url: requestURL, data: '&text=' + encodeURIComponent(textInput), headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, onload: function (response) { div.innerHTML += response.status + ' ' + response.statusText if (response.status === 200) { // Cache result: const time = (new Date()).toJSON() // Chrome fix: Otherwise JSON.stringify(requestCache) omits responseText var newobj = {} for (var key in response) { newobj[key] = response[key] } newobj.responseText = response.responseText requestCache[cachekey] = time + '\n' + JSON.stringify(newobj) GM.setValue('requestcache', JSON.stringify(requestCache)) // Show result showTranslation(JSON.parse(response.responseText), div) } else { showError(response, div) } }, onreadystatechange: function (response) { if (response.readyState === 1) { div.innerHTML += 'Opened.' } else if (response.readyState === 2) { div.innerHTML += ' Sent.' } else if (response.readyState === 3) { div.innerHTML += ' Downloading... ' } }, onerror: function (response) { console.log('Error: ' + response.status + '\nURL: ' + requestURL + '\nResponse:\n' + response.responseText) div.innerHTML += '<br><br>' const a = div.appendChild(document.createElement('a')) a.appendChild(document.createTextNode('Try again on translate.yandex.com/translate')) a.href = YANDEX_HOME + '?url=' + encodeURIComponent(document.location.href) + '&lang=' + DEFAULT_LANG a.target = '_blank' div.innerHTML += '<br><br>Error: ' + response.status + '<br>URL: ' + requestURL + '<br>Response:<br>' + response.responseText } }) } function showError (response, div) { try { let data = JSON.parse(response.responseText) if ('code' in data) { div.innerHTML += '<br>Error code: ' + data.code if (data.code === 401) { askForKey(true) } } if ('message' in data) { div.innerHTML += '<br>Error message: ' + data.message } } catch (e) { } div.innerHTML += '<br><br>' const a = div.appendChild(document.createElement('a')) a.appendChild(document.createTextNode('Try again on translate.yandex.com/translate')) a.href = YANDEX_HOME + '?url=' + encodeURIComponent(document.location.href) + '&lang=' + DEFAULT_LANG a.target = '_blank' div.innerHTML += '<br><br>Response body:<br><pre>' + response.responseText + '</pre>' } function showTranslation (data, div) { if (data.code !== 200) { alert('Error ' + data.code + '\n' + JSON.stringify(data)) return } let html = '' + data.text div.innerHTML = html.split('\n').join('<br>\n') div.appendChild(document.createElement('br')) div.appendChild(document.createElement('br')) const a = document.createElement('a') a.href = YANDEX_HOME + '?url=' + encodeURIComponent(document.location.href) + '&lang=' + DEFAULT_LANG a.target = '_blank' a.appendChild(document.createTextNode('\uD83D\uDD17 Powered by Yandex.Translate')) div.appendChild(a) div.appendChild(document.createElement('br')) div.appendChild(document.createElement('br')) const configLink = document.createElement('a') configLink.href = '#' configLink.addEventListener('click', function (ev) { ev.preventDefault() config() }) configLink.appendChild(document.createTextNode('\u2699\uFE0F Userscript options')) div.appendChild(configLink) } function addTranslateButton () { GM.getValue('lang', DEFAULT_LANG).then(function (value) { DEFAULT_LANG = value if (document.querySelector('lyrics div.lyrics_controls')) { const button = document.createElement('button') button.setAttribute('class', 'square_button') button.appendChild(document.createTextNode('Translate')) button.addEventListener('click', translate) button.addEventListener('auxclick', function (event) { window.open(YANDEX_HOME + '?url=' + encodeURIComponent(document.location.href) + '&lang=' + DEFAULT_LANG) }) document.querySelector('lyrics div.lyrics_controls').appendChild(button) } else if (document.querySelector('song-page secondary song-metadata-preview > div')) { const div = document.createElement('div') div.setAttribute('class', 'header_with_cover_art-metadata_preview-unit') const a = document.createElement('a') a.appendChild(document.createTextNode('Translate')) a.addEventListener('click', function (ev) { ev.preventDefault() translate() }) a.href = YANDEX_HOME + '?url=' + encodeURIComponent(document.location.href) + '&lang=' + DEFAULT_LANG document.querySelector('song-page secondary song-metadata-preview > div').appendChild(div) div.appendChild(a) } }) } let iv = window.setInterval(function () { if (document.querySelector('song-page lyrics div div.lyrics section p')) { clearInterval(iv) addTranslateButton() } }, 500)