// ==UserScript==
// @name Youtube Music Lyrics
// @namespace https://greasyfork.org/users/102866
// @description Adds lyrics to Youtube Music
// @include https://music.youtube.com/*
// @require https://code.jquery.com/jquery-3.5.1.min.js
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @author TiLied
// @version 0.2.01
// @grant GM_listValues
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant GM.listValues
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.xmlHttpRequest
// ==/UserScript==
const oneSecond = 1000,
oneDay = oneSecond * 60 * 60 * 24,
oneWeek = oneDay * 7,
oneMonth = oneWeek * 4;
var cache = {},
tTitle = 0,
options;
var debug,
contextmenu,
ret,
priority;
//Start
//Function main
void function Main()
{
console.log("Youtube Music Lyrics v" + GM.info.script.version + " initialization");
//Place CSS in head
SetCSS();
//Url handler for changing
UrlHandler();
//Set settings or create
SetSettings(function ()
{
//Disable second mouse click(contextmenu)
if (contextmenu)
document.addEventListener("contextmenu", function (e) { e.button === 2 && e.stopPropagation(); }, true);
//Check music.
setTimeout(function ()
{
//Place UI
SetUI();
SetEvents();
Music();
}, oneSecond * 7);
});
}();
//Function main
//End
//Start
//Class options
class Options
{
constructor(debug, contextmenu, priority)
{
this.debug = debug;
this.contextmenu = contextmenu;
this.priority = priority;
}
get values()
{
let v =
{
debug: this.debug,
contextmenu: this.contextmenu,
priority: this.priority
};
return v;
}
static string(s)
{
switch (s) {
case "one":
return "api - https://github.com/NTag/lyrics.ovh";
case "two":
return "api - https://github.com/rhnvrm/lyric-api";
case "three":
return "api - http://api.lololyrics.com/";
}
}
set values(obj)
{
this.debug = obj["debug"];
this.contextmenu = obj["contextmenu"];
this.priority = obj["priority"];
}
priorityF(p, id, artist, title)
{
let lyrics;
switch (p)
{
case "zero":
{
return new Promise(function (resolve)
{
if ($.isEmptyObject(cache[id]) || typeof cache[id]["musics"][title] === "undefined")
{
resolve(false);
} else
{
AddCache(id, artist, title, lyrics);
DisplayLyrics(id, title);
resolve(true);
}
});
}
case "one":
{
return new Promise(function (resolve)
{
//api - https://github.com/NTag/lyrics.ovh
GM.xmlHttpRequest({
method: "GET",
url: "https://api.lyrics.ovh/v1/" + artist + "/" + title + "",
timeout: oneSecond * 5,
onload: function (response)
{
if (debug) console.log(response);
if (response.status === 200)
{
let r = JSON.parse(response.responseText);
if (Object.keys(r)[0] === "lyrics")
{
lyrics = r["lyrics"];
if (debug) console.log(lyrics);
AddCache(id, artist, title, lyrics);
DisplayLyrics(id, title);
resolve(true);
} else
{
if (debug) console.log(r);
resolve(false);
}
} else
{
resolve(false);
}
},
onerror: function (e)
{
console.error(e);
resolve(false);
}
});
});
}
case "two":
{
return new Promise(function (resolve)
{
//api - https://github.com/rhnvrm/lyric-api
GM.xmlHttpRequest({
method: "GET",
url: "https://lyric-api.herokuapp.com/api/find/" + artist + "/" + title + "",
timeout: oneSecond * 5,
onload: function (response)
{
if (debug) console.log(response);
if (response.status === 200)
{
let r = JSON.parse(response.responseText);
if (r["err"] === "none")
{
lyrics = r["lyric"];
if (debug) console.log(lyrics);
AddCache(id, artist, title, lyrics);
DisplayLyrics(id, title);
resolve(true);
} else
{
if (debug) console.log(r);
resolve(false);
}
}else
{
resolve(false);
}
},
onerror: function (e)
{
console.error(e);
resolve(false);
}
});
});
}
case "three":
{
return new Promise(function (resolve)
{
//api - http://api.lololyrics.com/
GM.xmlHttpRequest({
method: "GET",
url: "http://api.lololyrics.com/0.5/getLyric?artist=" + artist + "&track=" + title + "",
timeout: oneSecond * 5,
onload: function (response)
{
if (debug) console.log(response);
if (response.status === 200)
{
let xml = response.responseXML.all;
if (xml[1].textContent === "OK")
{
lyrics = xml[2].innerHTML;
if (debug) console.log(lyrics);
AddCache(id, artist, title, lyrics);
DisplayLyrics(id, title);
resolve(true);
} else
{
if (debug) console.log(xml);
resolve(false);
}
} else
{
resolve(false);
}
},
onerror: function (e)
{
console.error(e);
resolve(false);
}
});
});
}
case "last":
{
return new Promise(function (resolve)
{
if (debug) console.log("No lyrics found!");
DisplayLyrics(id, title, "No lyrics found!");
if (debug) console.log(priority);
resolve(true);
});
}
}
}
}
//Class options
//End
//Start
//Functions GM_VALUE
async function SetSettings(callBack)
{
try
{
//DeleteValues("yml_options");
//THIS IS ABOUT OPTIONS
if (await HasValue("yml_options", JSON.stringify(options)))
{
let v = JSON.parse(await GM.getValue("yml_options"));
options = new Options(false, true, ["zero", "one", "two", "three", "last"]);
options.values = v;
SetOptionsObj();
} else
{
options = new Options(false, true, ["zero", "one", "two", "three", "last"]);
SetOptionsObj();
}
//THIS IS ABOUT CACHE
if (await HasValue("yml_cache", JSON.stringify(cache)))
{
cache = JSON.parse(await GM.getValue("yml_cache"));
SetCacheObj();
}
//Console log prefs with value
console.log("*prefs:");
console.log("*-----*");
var vals = await GM.listValues();
//Find out that var in for block is not local... Seriously js?
for (let i = 0; i < vals.length; i++)
{
console.log("*" + vals[i] + ":" + await GM.getValue(vals[i]));
}
console.log("*-----*");
if (debug)
{
console.log(options);
console.log(cache);
}
callBack();
} catch (e) { console.log(e); }
}
//Check if value exists or not. optValue = Optional
async function HasValue(nameVal, optValue)
{
var vals = await GM.listValues();
if (vals.length === 0)
{
if (optValue !== undefined)
{
GM.setValue(nameVal, optValue);
return true;
} else
{
return false;
}
}
if (typeof nameVal !== "string")
{
return alert("name of value: '" + nameVal + "' are not string");
}
for (let i = 0; i < vals.length; i++)
{
if (vals[i] === nameVal)
{
return true;
}
}
if (optValue !== undefined)
{
GM.setValue(nameVal, optValue);
return true;
} else
{
return false;
}
}
//Delete Values
async function DeleteValues(nameVal)
{
var vals = await GM.listValues();
if (vals.length === 0 || typeof nameVal !== "string")
{
return;
}
switch (nameVal)
{
case "all":
for (let i = 0; i < vals.length; i++)
{
if (vals[i] !== "adm")
{
GM.deleteValue(vals[i]);
}
}
break;
case "old":
for (let i = 0; i < vals.length; i++)
{
if (vals[i] === "debug" || vals[i] === "debugA")
{
GM.deleteValue(vals[i]);
}
}
break;
default:
for (let i = 0; i < vals.length; i++)
{
if (vals[i] === nameVal)
{
GM.deleteValue(nameVal);
}
}
break;
}
}
///Update gm value what:"cache","options"
function UpdateGM(what)
{
var gmVal;
switch (what)
{
case "cache":
gmVal = JSON.stringify(cache);
GM.setValue("yml_cache", gmVal);
break;
case "options":
gmVal = JSON.stringify(options.values);
GM.setValue("yml_options", gmVal);
break;
default:
alert("fun:UpdateGM(" + what + "). default switch");
break;
}
}
//Functions GM_VALUE
//End
//Start
//Functions create object option and cache
function SetOptionsObj()
{
debug = options.debug;
contextmenu = options.contextmenu;
UpdateGM("options");
}
function SetCacheObj()
{
try
{
var v = String(GM.info.script.version).split('.');
v = v.slice(0, 2);
var ver = v[0] + "." + v[1];
//Version
if (typeof cache.versionCache === "undefined")
{
cache.versionCache = ver;
versionCache = cache.versionCache;
} else
{
versionCache = cache.versionCache;
if (versionCache !== ver)
{
cache.versionCache = ver;
versionCache = cache.versionCache;
for (var prop in cache)
{
if (prop !== "versionCache")
{
delete cache[prop];
}
}
//DeleteValues("yml_cache");
}
}
} catch (e) { console.error(e); }
}
//Functions create object option and cache
//End
//Start
//Functions Get music
function Music()
{
priority = options.priority;
var artist = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.innerText;
var title = document.querySelector(".title.ytmusic-player-bar").innerText;
var id = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.attributes.href.value;
if (debug)
{
console.log(artist);
console.log(title);
console.log(id);
}
if (artist === "" || title === "" || typeof id === "undefined" || typeof artist === "undefined" || typeof title === "undefined" )
return console.error(artist + "-" + title + "-" + id);
if (tTitle === 0 || title !== tTitle)
{
tTitle = title;
GetLyrics(priority, id, artist, title);
} else return;
}
//-------------------------
//CORE STUFF BELOW
//-------------------------
//Start
//Function Add to Cache lyrics
async function AddCache(id, artist, title, lyrics)
{
if (typeof title === "undefined")
{
//TODO DO SOMETHING
var a = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.innerText;
var t = document.querySelector(".title.ytmusic-player-bar").innerText;
var i = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.attributes.href.value;
if ($.isEmptyObject(cache[i]))
{
cache[i] =
{
name: a,
musics: {},
//statistics
gettingLyricsForArtistTimes: 1,
custom: ""
};
}
if (typeof cache[i]["musics"][t] === "undefined")
{
cache[i]["musics"][t] =
{
title: t,
lyrics: lyrics,
dateId: Date.now(),
//statistics
gettingLyricsForMusicTimes: 1,
custom: ""
};
} else
{
cache[i]["musics"][t]["lyrics"] = lyrics;
}
return;
}
if ($.isEmptyObject(cache[id]))
{
cache[id] =
{
name: artist,
musics: {},
//statistics
gettingLyricsForArtistTimes: 1,
custom: ""
};
} else
{
cache[id]["gettingLyricsForArtistTimes"] = cache[id]["gettingLyricsForArtistTimes"] + 1;
}
if (typeof cache[id]["musics"][title] === "undefined")
{
cache[id]["musics"][title] =
{
title:title,
lyrics:lyrics,
dateId:Date.now(),
//statistics
gettingLyricsForMusicTimes:1,
custom:""
};
} else
{
cache[id]["musics"][title]["gettingLyricsForMusicTimes"] = cache[id]["musics"][title]["gettingLyricsForMusicTimes"] + 1;
}
if (debug) console.log(cache);
UpdateGM("cache");
}
//Function Add to Cache lyrics
//End
//-------------------------
//XMLHTTPREQUESTS BELOW
//-------------------------
//Start
//Function xml api
async function GetLyrics(priority, id, artist, title)
{
for (let i = 0; priority.length; i++)
{
let aww = await options.priorityF(priority[i], id, artist, title);
if(aww === true)
break;
}
}
//Function xml api
//End
//-------------------------
//UI AND VISUAL STAFF BELOW
//-------------------------
//Start
//Function set ui
function SetUI()
{
var rightC = $(".middle-controls-buttons");
var mainP = $("#main-panel");
var divP = $("<div id=yml_lyricsPanel class='style-scope ytmusic-player-page'></div>").html("<pre id=yml_lyricsText class='style-scope ytmusic-player-baryt-formatted-string'>Lyrics:</pre>");
var divB = $("<div id=yml_lyricsButton class='right-controls-buttons style-scope ytmusic-player-bar'></div>").html("<a class='yml_Button style-scope ytmusic-player-bar yt-formatted-string' style='color:inherit;'>Lyrics</a>");
var divPB = $("<div id=yml_PanelButtons class='style-scope ytmusic-player-page'></div>").html("<a class='yml_Button' id=yml_addLyricsButton>Add lyrics</a><a class='yml_Button' id=yml_optionButton style='padding-left: 10px;'>Options</a>");
var divPO = $("<div id=yml_optionsPanel class='style-scope ytmusic-player-page'></div>").html("<a class='style-scope ytmusic-player-baryt-formatted-string' style='color:inherit; font-family:inherit;'>Options:</a><form>\
<br>\
<input type=checkbox name=debug id=yml_debug >Debug</input><br> \
<input type=checkbox name=contextmenu id=yml_contextmenu >Context menu</input><br> \
<ul id='image-list1' class='sortable-list'>\
<br>\
<li class='ui-state-default ui-state-disabled' id='zero'>Locale cache</li>\
<li class='ui-state-default' id='one'><span>api-https://github.com/NTag/lyrics.ovh</span></li>\
<li class='ui-state-default' id='two'><span>api-https://github.com/rhnvrm/lyric-api</span></li>\
<li class='ui-state-default' id='three'><span>api-http://api.lololyrics.com/</span></li>\
<li class='ui-state-default ui-state-disabled' id='last' style='display:none;'>Last Not Found!</li>\
</ul >\
</form>");
if (debug)
{
console.log(rightC);
console.log(mainP);
}
$(divP).append(divPB);
$(divP).prepend(divPO);
$(mainP).append(divP);
$(rightC).append(divB);
$(divP).hide();
$(divPO).hide();
UIValues();
}
//Function set ui
//End
//Start
//Function set UI values of settengs/options
function UIValues()
{
$("#yml_debug").prop("checked", debug);
$("#yml_contextmenu").prop("checked", contextmenu);
let li = $(".sortable-list li");
for (let i = 1; i < options.priority.length; i++)
{
$(li[i]).attr("id", options.priority[i]);
$(li[i]).find("span").text(Options.string(options.priority[i]));
}
}
//Function set events
//End
//Start
//Function set css
function SetCSS()
{
$("head").append($("<!--Start of Youtube Music Lyrics v" + GM.info.script.version + " CSS-->"));
$("head").append($("<style type=text/css></style>").text("#yml_lyricsPanel { \
position: absolute;\
z-index: 100;\
background-color: #1d1d1d;\
font-size:16px;\
overflow-y:scroll;\
color:#aaaaaa;\
}"));
$("head").append($("<style type=text/css></style>").text("#yml_lyricsText { \
color: inherit;\
font-family: inherit;\
padding-left: 5%;\
}"));
$("head").append($("<style type=text/css></style>").text("#yml_PanelButtons { \
float: right;\
padding:10px;\
margin:10px;\
border: 3px solid;\
}"));
$("head").append($("<style type=text/css></style>").text("#yml_optionsPanel { \
position: fixed;\
z-index: 111;\
padding-left: 5%;\
padding-bottom: 5%;\
border: 3px solid;\
background-color: #2d2d2d;\
font-size:16px;\
overflow-y:scroll;\
color:#aaaaaa;\
}"));
$("head").append($("<style type=text/css></style>").text(".yml_Button { \
cursor: pointer;\
font-size:16px;\
color:#aaaaaa;\
}"));
$("head").append($("<style type=text/css></style>").text(".yml_Button:hover { \
text-decoration: underline;\
}"));
$("head").append($("<style type=text/css></style>").text(".ui-state-default { \
cursor: move;\
}"));
$("head").append($("<!--End of Youtube Music Lyrics v" + GM.info.script.version + " CSS-->"));
}
//Function set css
//End
//Start
//Function show lyrics
function DisplayLyrics(id, title, lyrics)
{
if (debug) console.log($("#yml_lyricsPanel pre"));
if (typeof lyrics === "string")
return $("#yml_lyricsPanel pre").text(lyrics);
$("#yml_lyricsPanel pre").text(cache[id]["musics"][title]["lyrics"]);
if (debug) console.log(cache[id]["musics"][title]["lyrics"]);
}
//Function show lyrics
//End
//Start
//Function set events
function SetEvents()
{
$("#yml_lyricsButton").click(function ()
{
$("#yml_lyricsPanel").toggle(1000);
let w = $("#main-panel").width();
let h = $("#main-panel").height();
$("#yml_lyricsPanel").attr({
style: "max-height: " + h + "px;max-width:" + (w + 100) + "px;min-width:" + w + "px;"
});
});
$("#yml_optionButton").click(function ()
{
$("#yml_optionsPanel").toggle();
if ($("#yml_optionsPanel").css('display') !== 'none')
{
$(this).text("Save Options");
} else
{
UpdateGM("options");
$(this).text("Options");
}
});
$("#yml_debug").change(function ()
{
options.debug = $(this).prop("checked");
debug = $(this).prop("checked");
});
$("#yml_contextmenu").change(function ()
{
options.contextmenu = $(this).prop("checked");
debug = $(this).prop("checked");
});
$("#yml_addLyricsButton").click(function ()
{
//TODO MAKE BETTER CODE!!!
if (debug) console.log($("#yml_lyricsText").prop("tagName"));
if ($("#yml_lyricsText").prop("tagName") === "PRE")
{
$(this).text("Save Lyrics");
let w = $("#yml_lyricsText").width();
let h = $("#yml_lyricsText").height();
$("#yml_lyricsText").attr({
style: "height: " + h + "px;width:" + (w + 100) + "px;"
});
$('pre#yml_lyricsText').replaceTag('textarea');
document.getElementById("yml_lyricsText").value($("#yml_lyricsText").text());
}
else
{
$(this).text("Add Lyrics");
$("#yml_lyricsText").text(document.getElementById("yml_lyricsText").value);
//console.log(document.getElementById("yml_lyricsText").value);
//cache[id]["musics"][title]["lyrics"] = document.getElementById("yml_lyricsText").value;
AddCache(undefined, undefined, undefined, document.getElementById("yml_lyricsText").value);
UpdateGM("cache");
$("#yml_lyricsText").attr({
style: "height: inherit;width:inherit;"
});
$('textarea#yml_lyricsText').replaceTag('pre');
}
});
$('.sortable-list').sortable({
connectWith: '.sortable-list',
items: "li:not(.ui-state-disabled)",
update: function (event, ui)
{
var order = $(this).sortable('toArray');
order.unshift("zero");
order.push("last");
console.log(order);
options.priority = order;
priority = order;
}
});
}
//Function set events
//End
//-------------------------
//TOOLS BELOW
//-------------------------
//Start
//Handler for url
function UrlHandler()
{
this.oldHash = window.location.search;
this.Check;
var that = this;
var detect = function ()
{
if (that.oldHash !== window.location.search)
{
that.oldHash = window.location.search;
setTimeout(function () { Music(); }, oneSecond + oneSecond);
}
};
this.Check = setInterval(function () { detect(); }, 200);
}
//Handler for url
//End
//Start
//Tool for changing tags https://stackoverflow.com/a/32067355
(function ($)
{
$.fn.replaceTag = function (newTag)
{
var originalElement = this[0]
, originalTag = originalElement.tagName
, startRX = new RegExp('^<' + originalTag, 'i')
, endRX = new RegExp(originalTag + '>$', 'i')
, startSubst = '<' + newTag
, endSubst = newTag + '>'
, newHTML = originalElement.outerHTML
.replace(startRX, startSubst)
.replace(endRX, endSubst);
this.replaceWith(newHTML);
};
})(jQuery);
//Tool for changing tags https://stackoverflow.com/a/32067355
//End