Youtube Music Lyrics

Adds lyrics to Youtube Music

// ==UserScript==
// @name        Youtube Music Lyrics
// @namespace   https://greasyfork.org/users/102866
// @description Adds lyrics to Youtube Music
// @match     https://music.youtube.com/*
// @require     https://code.jquery.com/jquery-3.7.1.min.js
// @require     https://code.jquery.com/ui/1.14.1/jquery-ui.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js
// @author      TiLied
// @version     0.3.02
// @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==

let oldTitle = "";

const oneSecond = 1000,
	oneDay = oneSecond * 60 * 60 * 24,
	oneWeek = oneDay * 7,
	oneMonth = oneWeek * 4;

let setupSearchOnce = true;	

class Options2 
{
	constructor(version)
	{
		this.version = version;
		this.debug = false;
		this.contextmenu = false;

		this.providers = [];
		this["providers"].push(
			{
				priority: 0,
				name: "Local cache YML",
				getLyrics: function ()
				{
					return;
				},
				custom: ""
			});
		this["providers"].push(
			{
				priority: 1,
				name: "Local website Youtube",
				getLyrics: function (artist, title)
				{
					return new Promise(function (resolve)
					{
						let _bs = $(".tab-header");

						if (_bs.length === 0)
							resolve("Lyrics not found!");

						if ($(_bs[1]).attr("aria-selected") === "true")
						{
							let _m = $("[page-type='MUSIC_PAGE_TYPE_TRACK_LYRICS']");
							if (typeof _m === "undefined")
								resolve("Lyrics not found!");

							setTimeout(() =>
							{
								let _l = $(_m).find(".non-expandable.description:visible");

								if (typeof _l === "undefined" || _l === null || _l.length === 0 || $(_l).attr("hidden") === true)
								{
									resolve("Lyrics not found!");
								}

								resolve($(_l).text());
							}, oneSecond + oneSecond);

						} else
						{
							resolve("Lyrics not found!");
						}
					});
				},
				custom: ""
			});
		this["providers"].push(
			{
				priority: 2,
				name: "Api - https://github.com/NTag/lyrics.ovh",
				getLyrics: function (artist, title)
				{
					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)
							{
								console.log(response);
								if (response.status === 200)
								{
									let r = JSON.parse(response.responseText);
									if (Object.keys(r)[0] === "lyrics")
									{
										let lyrics = r["lyrics"];
										if (lyrics)
										{
											resolve(lyrics);
										} else
										{
											resolve("Lyrics not found!");
										}
									} else
									{
										resolve("Lyrics not found!");
									}
								} else
								{
									resolve("Lyrics not found!");
								}
							},
							onerror: function (e)
							{
								resolve("Lyrics not found!");
							}
						});
					});
				},
				custom: ""
			});
		this["providers"].push(
			{
				priority: 3,
				name: "Api - https://github.com/rhnvrm/lyric-api",
				getLyrics: function (artist, title)
				{
					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)
							{
								console.log(response);
								if (response.status === 200)
								{
									let r = JSON.parse(response.responseText);
									if (r["err"] === "none")
									{
										let lyrics = r["lyric"];

										lyrics = HtmlDecode(lyrics);

										resolve(lyrics);
									} else
									{
										resolve("Lyrics not found!");
									}
								} else
								{
									resolve("Lyrics not found!");
								}
							},
							onerror: function (e)
							{
								console.warn(e);
								resolve("Lyrics not found!");
							}
						});
					});
				},
				custom: ""
			});
		this["providers"].push(
			{
				priority: 4,
				name: "Api - http://api.lololyrics.com/",
				getLyrics: function (artist, title)
				{
					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)
							{
								console.log(response);
								if (response.status === 200)
								{
									let xml = response.responseXML.all;

									if (xml[1].textContent === "OK")
									{
										let lyrics = xml[2].innerHTML;
										resolve(lyrics);
									} else
									{
										resolve("Lyrics not found!");
									}
								} else
								{
									resolve("Lyrics not found!");
								}
							},
							onerror: function (e)
							{
								console.warn(e);
								resolve("Lyrics not found!");
							}
						});
					});
				},
				custom: ""
			});
	}

	then(resolve)
	{
		console.time("Options2.then");
		console.timeLog("Options2.then");

		Options2._GMHasValue("yml_options2").then((r) =>
		{
			if (r === true)
			{
				GM.getValue("yml_options2").then((v) =>
				{
					let _v = JSON.parse(v);
					this.SetOptions = _v;
				});
			} else
			{
				let stringStorage =
				{
					version: this.version,
					debug: this.debug,
					contextmenu: this.contextmenu,
					providers: this.providers
				};

				Options2._GMUpdate("options2", stringStorage);
			}

			console.timeEnd("Options2.then");
			resolve("done");
		});

	}

	//Start
	//Functions GM_VALUE
	//Check if value exists or not.  optValue = Optional
	static async _GMHasValue(nameVal, optValue)
	{
		return new Promise((resolve, reject) =>
		{
			GM.listValues().then(vals =>
			{

				if (vals.length === 0)
				{
					if (optValue !== undefined)
					{
						GM.setValue(nameVal, optValue);
						resolve(true);
					} else
					{
						resolve(false);
					}
				}

				if (typeof nameVal !== "string")
				{
					reject(console.error("name of value: '" + nameVal + "' are not string"));
				}

				for (let i = 0; i < vals.length; i++)
				{
					if (vals[i] === nameVal)
					{
						resolve(true);
					}
				}

				if (optValue !== undefined)
				{
					GM.setValue(nameVal, optValue);
					resolve(true);
				} else
				{
					resolve(false);
				}
			});
		});

	}

	//Delete Values
	static async _GMDeleteValues(nameVal)
	{
		let 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]);
					}
				}
				return;
			default:
				for (let i = 0; i < vals.length; i++)
				{
					if (vals[i] === nameVal)
					{
						GM.deleteValue(nameVal);
					}
				}
				return;
		}
	}

	///Update gm value what:"cache","options"
	static _GMUpdate(what, _v)
	{
		let _l = JSON.stringify(_v);
		switch (what)
		{
			case "cache2":
				GM.setValue("yml_cache2", _l);
				break;
			case "options2":
				GM.setValue("yml_options2", _l);
				break;
			default:
				console.error("method:_GMUpdate(" + what + "," + _v + "). default switch");
				break;
		}
	}
	//Functions GM_VALUE
	//End

	set SetOptions(obj)
	{
		this.debug = obj.debug;
		this.contextmenu = obj.contextmenu;
		for (let i = 0; i < obj["providers"].length; i++)
		{
			this["providers"][i]["priority"] = obj["providers"][i]["priority"];
		}
	}

	set UpdatePriority(arr)
	{
		for (let i = 1; i < arr.length; i++)
		{
			for (let j = 1; j < this.providers.length; j++)
			{
				if (arr[i] === this.providers[j]["name"])
				{
					this.providers[j]["priority"] = i;
					break;
				}
			}
		}
	}
}

class Cache2
{
	constructor(versionCache)
	{
		this.versionCache = versionCache;
	}

	then(resolve)
	{
		console.time("Cache2.then");
		console.timeLog("Cache2.then");

		Options2._GMHasValue("yml_cache2").then((r) =>
		{
			if (r === true)
			{
				GM.getValue("yml_cache2").then((v) =>
				{
					this.SetCache = JSON.parse(v);
				});
			} else
			{
				let stringStorage = this;

				Options2._GMUpdate("cache2", stringStorage);
			}

			console.timeEnd("Cache2.then");
			resolve("done");
		});

	}

	set SetCache(obj)
	{
		if (obj["versionCache"] === this.versionCache)
		{
			let _k = Object.keys(obj)
			for (let i = 0; i < _k.length; i++)
			{
				this[_k[i]] = obj[_k[i]];
			}
		}
		//todo update cache
	}

	CheckData(_data)
	{
		//check if data exist
		let _keys = Object.keys(this);
		for (let i = 0; i <= _keys.length; i++)
		{
			if (i === _keys.length)
			{
				this[_data["id"]] = _data;
				return;
			}
			if (_data["id"] === _keys[i])
			{
				this[_keys[i]]["gettingLyricsForArtistTimes"] += 1;

				let _k = Object.keys(_data["musics"])[0]; 
				if (typeof this[_keys[i]]["musics"][_k] === "undefined") 
				{
					this[_keys[i]]["musics"][_k] = _data["musics"][_k]
					return;
				}

				this[_keys[i]]["musics"][_k]["gettingLyricsForMusicTimes"] += 1;
				//
				//delete this? or update lyrics after one month
				//or add stats TODO
				//if (this[_keys[i]]["dateId"] + oneMonth <= Date.now())
				//{
				//	this[_keys[i]] = _data;
				//	return;
				//}
				return;
			}
		}
	}

	AddLyrics(id, title, lyrics)
	{
		this[id]["musics"][title]["lyrics"] = lyrics;
	}

}

class MusicData
{
	constructor(url, artist, title, id)
	{
		this.url = url;

		this.gettingLyricsForArtistTimes = 1;

		this.id = id;
		this.artist = artist;

		this.musics = {};
		this.musics[title] =
		{
			dateId: Date.now(),
			title: title,
			lyrics: "none",
			gettingLyricsForMusicTimes: 1,
		}

	}

	then(resolve)
	{
		//
	}
}

//Start
//Function main2
void function Main2()
{
	//Options2._GMDeleteValues("all");
	console.log("Youtube Music Lyrics v" + GM.info.script.version + " initialization");

	//Set css
	SetCSS();

	//Set cache
	let cache2 = new Cache2(0.1);
	cache2.then(() =>
	{
		console.log(cache2);

		//Set options
		let options2 = new Options2(GM.info.script.version);
		options2.then(() =>
		{
			console.log(options2);

			//Console log prefs with value
			GM.listValues().then(async (_v) =>
			{
				console.log("*prefs:");
				console.log("*-----*");

				for (let i = 0; i < _v.length; i++)
				{
					let str = await GM.getValue(_v[i]);
					console.log("*" + _v[i] + ":" + str);
					console.log(JSON.parse(str));
					const byteSize = str => new Blob([str]).size;
					console.log("Size " + _v[i] + ": " + FormatBytes(byteSize(str)) + "");
				}

				console.log("*-----*");

				//Disable second mouse click(contextmenu)
				if (options2.contextmenu)
					document.addEventListener("contextmenu", function (e) { e.button === 2 && e.stopPropagation(); }, true);

				//Place UI
				SetUI(options2);

				//events
				SetEvents(options2, cache2);

				//core!
				setTimeout(() =>
				{
					Music2(options2, cache2);

					//Url handler for changing
					UrlHandler(options2, cache2);
				}, oneSecond * 5);
			});
		});
	});
}();
//Function main2
//End

async function Music2(options2, cache2)
{
	let _p = [];

	let artist = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.innerText;

	let title = document.querySelector(".title.ytmusic-player-bar").innerText;

	let _h = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.attributes.href;

	if (typeof _h === "undefined")
		return console.warn(".subtitle.ytmusic-player-bar: " + _h);

	let id = _h.value;
	
	if (artist === "" || title === "" || typeof id === "undefined" || typeof artist === "undefined" || typeof title === "undefined")
		return console.warn(artist + "-" + title + "-" + id);

	//check if we listen same song
	if (oldTitle === title)
		return;

	oldTitle = title;

	let _data = new MusicData(document.URL, artist, title, id);
	console.log(_data);
	cache2.CheckData(_data);
	console.log(cache2);

	if (cache2[id]["musics"][title]["lyrics"] === "none")
	{
		for (let i = 0; i < options2["providers"].length; i++)
		{
			for (let j = 0; j < 5; j++)
			{
				if (options2["providers"][i]["priority"] === j)
					_p.push(options2["providers"][i]);
			}
		}

		//there is no local, this is why i = 1
		for (let i = 1; i <= _p.length; i++)
		{
			if (i === _p.length)
			{
				cache2.AddLyrics(id, title, "No lyrics found!");
				break;
			}

			let _l = await _p[i]["getLyrics"](artist, title);
			console.log(_l);

			if (_l !== "Lyrics not found!")
			{
				cache2.AddLyrics(id, title, _l);
				break;
			}

			cache2.AddLyrics(id, title, _l);
		}
	}

	//display
	$("#yml_musicName").text(artist + " - " + title + ":");
	$("#yml_lyricsPanel pre").text("\r\n\r\n" + cache2[id]["musics"][title]["lyrics"]);

	//save lyrics
	Options2._GMUpdate("cache2", cache2);
}

//-------------------------
//UI AND VISUAL STAFF BELOW
//-------------------------

//Start
//Function set ui 
function SetUI(options2)
{
	let rightC = $(".middle-controls-buttons");
	let mainP = $("#main-panel");

	let divP = $("<div id=yml_lyricsPanel class='style-scope ytmusic-player-page'></div>").html("<header id=yml_musicName></header><pre id=yml_lyricsText class='style-scope ytmusic-player-baryt-formatted-string'>Lyrics:</pre>");
	let 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>");

	let 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><a class='yml_Button' id=yml_searchButton style='padding-left: 10px;'>Search</a>");

	let 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='0'>" + options2["providers"][0]["name"] + "</li>\
	<li class='ui-state-default' id='1'><span>" + options2["providers"][1]["name"] + "</span></li>\
	<li class='ui-state-default' id='2'><span>" + options2["providers"][2]["name"] + "</span></li>\
	<li class='ui-state-default' id='3'><span>" + options2["providers"][3]["name"] + "</span></li>\
	<li class='ui-state-default' id='4'><span>" + options2["providers"][4]["name"] + "</span></li>\
</ul >\
		<br>\
		<a class='yml_Button' id=yml_clearCache >Clear cache</a><br> \
</form>");

	let divSearchPanel = document.createElement("div");
	divSearchPanel.id = "yml_searchPanel";
	divSearchPanel.classList += "style-scope ytmusic-player-page";
	
	divSearchPanel.innerHTML = "<div id='yml_searchMusics'>\
		<button id=yml_closeSearch>Close</button>\
	<div id=yml_filters>\
		<div id=yml_filterZeroGrid>\
			<input class='filter yml_searchArtist' type='text' placeholder='Search Artist'/>\
			<input class='filter yml_searchTitle' type='text' placeholder='Search Title' />\
			<input class='filter yml_searchLyrics' type='text' placeholder='Search Lyrics' />\
		</div>\
		<hr size='1' noshade=''>\
		<div id=yml_filterOneGrid>\
			<input class='filter yml_filterGettingLyricsForArtistTimes' type='text' pattern='(>|<|) \\d + ' placeholder='Filter GettingLyricsForArtistTimes(x,> x, <x)' />\
			<input class='filter yml_filterGettingLyricsForMusicTimes' type='text' pattern='(>|<|) \\d + ' placeholder='Filter GettingLyricsForMusicTimes(x,> x, <x)' />\
		</div>\
		<hr size='1' noshade=''>\
		<div id=yml_filterTwoGrid>\
			<label for='yml_filterAddedDateA'>Added After:</label>\
			<input id='yml_filterAddedDateA' class='filter yml_filterAddedDateA' type='date' placeholder='Added After' />\
			<label for='yml_filterAddedDateB'>Added Before:</label>\
			<input id='yml_filterAddedDateB' class='filter yml_filterAddedDateB' type='date' placeholder='Added Before' />\
		</div>\
		<hr size='1' noshade=''>\
	</div>\
	<div id=yml_sortGrid>\
		<button class='yml_sort' data-sort='artist'>Sort by artist</button >\
		<button class='yml_sort' data-sort='title'>Sort by title</button >\
		<button class='yml_sort' data-sort='dateId'>Sort by date</button >\
		<button class='yml_sort' data-sort='gettingLyricsForArtistTimes'>Sort by gettingLyricsForArtistTimes</button >\
		<button class='yml_sort' data-sort='gettingLyricsForMusicTimes'>Sort by gettingLyricsForMusicTimes</button >\
	</div>\
	<input class='yml_search' placeholder='Global Search' />\
	<span id=yml_resultCount></span>\
	<ul class='paginationTop pagination'></ul>\
	<hr size='1' noshade=''>\
	<ul class='yml_list' ></ul>\
	<hr size='1' noshade=''>\
	<ul class='paginationBottom pagination'></ul>\
</div>\
";

	$(divP).append(divPB);
	$(divP).prepend(divPO);
	$(mainP).append(divP);
	
	document.body.append(divSearchPanel);
	
	$(rightC).append(divB);

	$(divP).hide();
	$(divPO).hide();
	
	divSearchPanel.style.visibility = "hidden";
	
	UIValues(options2);
}
//Function set ui 
//End


//Start
//Function set UI values of settengs/options
function UIValues(options2)
{
	$("#yml_debug").prop("checked", options2.debug);
	$("#yml_contextmenu").prop("checked", options2.contextmenu);

	let li = $(".sortable-list li");

	for (let j = 1; j < options2["providers"].length; j++)
	{
		$(li[options2["providers"][j]["priority"]]).attr("id", options2["providers"][j]["priority"]);
		$(li[options2["providers"][j]["priority"]]).find("span").text(options2["providers"][j]["name"]);
	}
}
//Function set UI values of settengs/options
//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;\
	font-family: inherit;\
}"));

	$("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_musicName { \
	font-family: inherit;\
		font-size:20px;\
	padding-left: 15%;\
		padding-top: 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_searchPanel { \
	position: fixed;\
	z-index: 111;\
	padding-left: 5%;\
	padding-right: 5%;\
	padding-bottom: 5%;\
	border: 1px solid;\
	background-color: #2d2d2d;\
	font-size:16px;\
	overflow-y:scroll;\
	color:#aaaaaa;\
	max-height: 50%;\
}"));
	
	$("head").append($("<style type=text/css></style>").text(".yml_searchItem { \
	display: grid;\
  	grid-gap: 5px;\
  	border: 1px solid;\
	padding: 5px;\
}"));
	
	$("head").append($("<style type=text/css></style>").text(".yml_subItem { \
	border: 1px solid;\
	display: grid;\
  	grid-template-columns: 20% 30% 20% 30%;\
}"));
	
	$("head").append($("<style type=text/css></style>").text('.yml_sort {\
	padding: 8px 30px;\
	border-radius: 6px;\
	border: none;\
	display: inline-block;\
	color: #fff;\
	text-decoration: none;\
	background-color: #28a8e0;\
	height: 50px;\
}\
.yml_sort:hover {\
	text-decoration: none;\
	background-color:#1b8aba;\
}\
.yml_sort:focus {\
	outline: none;\
}\
.yml_sort:after {\
	width: 0;\
	height: 0;\
	border-left: 5px solid transparent;\
	border-right: 5px solid transparent;\
	border-bottom: 5px solid transparent;\
	content: "";\
	position: relative;\
	top: -10px;\
	right: -5px;\
}\
.yml_sort.asc:after {\
	width: 0;\
	height: 0;\
	border-left: 5px solid transparent;\
	border-right: 5px solid transparent;\
	border-top: 5px solid #fff;\
	content: "";\
	position: relative;\
	top: 13px;\
	right: -5px;\
}\
.yml_sort.desc:after {\
	width: 0;\
	height: 0;\
	border-left: 5px solid transparent;\
	border-right: 5px solid transparent;\
	border-bottom: 5px solid #fff;\
	content: "";\
	position: relative;\
	top: -10px;\
	right: -5px;\
}'));
	
	$("head").append($("<style type=text/css></style>").text('.yml_search {\
	width: 75%;\
	margin-bottom: 5px;\
	text-align: center;\
	background: linear-gradient(#eee, #fff);\
	border: 1px solid rgba(255, 255, 255, 0.6);\
	box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.4);\
	padding: 5px;\
	position: relative;\
	display: block;\
	margin-top: 5px;\
	margin-right: auto;\
	margin-bottom: 5px;\
	margin-left: auto;}'));
	
	$("head").append($("<style type=text/css></style>").text('#yml_sortGrid {display: grid;\
	grid-template-columns: repeat(5, 1fr);\
	grid-gap: 5px;}'));
	
	$("head").append($("<style type=text/css></style>").text('.highlight{background-color: purple;}'));
	
	$("head").append($("<style type=text/css></style>").text(".pagination li { \
		cursor: pointer;\
		display: inline-block;\
		padding: 5px;\
		margin-top: 5px;\
		margin-bottom: 5px;\
		align-content: center;\
	}"));
	
	$("head").append($("<style type=text/css></style>").text('.pagination {display: flex;\
		justify-content: center;}'));
	
	$("head").append($("<style type=text/css></style>").text('.active {font-size: 20px;'));
	
	$("head").append($("<style type=text/css></style>").text('#yml_resultCount {display: flex;\
			justify-content: center;\
			font-size: 25px;\
			background-color: #4e4d4d;\
			color: white;\
				}'));

	$("head").append($("<style type=text/css></style>").text('#yml_filterZeroGrid {display: grid;\
					grid-template-columns: repeat(3, 1fr);\
					grid-gap: 5px;\
					margin: 5px;}'));
	
	$("head").append($("<style type=text/css></style>").text('#yml_filterOneGrid {display: grid;\
						grid-template-columns: repeat(2, 1fr);\
						grid-gap: 5px;\
						margin: 5px;}'));

	$("head").append($("<style type=text/css></style>").text('#yml_filterTwoGrid {display: grid;\
							grid-template-columns: repeat(4, 0.2fr);\
							grid-gap: 5px;\
							margin: 5px;}'));
	
	$("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 set events
function SetEvents(options2, cache2)
{
	$("#yml_lyricsButton").on("click", function ()
	{
		$("#yml_lyricsPanel").toggle(500);

		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").on("click", function ()
	{
		$("#yml_optionsPanel").toggle();

		if ($("#yml_optionsPanel").css('display') !== 'none')
		{
			$(this).text("Save Options");
		} else
		{
			Options2._GMUpdate("options2", options2);
			$(this).text("Options");
		}
	});
	
	document.getElementById("yml_closeSearch").addEventListener("click", () =>
	{
		document.getElementById("yml_searchPanel").style.visibility = "hidden";	
	});
	
	document.getElementById("yml_searchButton").addEventListener("click", () =>
	{
		let _panel = document.getElementById("yml_searchPanel");
		if (_panel.style.visibility == "hidden")
			_panel.style.visibility = "visible";
		else
		{
			_panel.style.visibility = "hidden";
			
		}
		
		if (setupSearchOnce)
		{
			let options = {
				valueNames: [
					'artist',
					'gettingLyricsForArtistTimes',
					'id',
					'url',
					{ name: 'urlHref', attr: 'href' },
					{ name: 'dateId', attr: 'data-xutime' },
					'date',
					'gettingLyricsForMusicTimes',
					'lyrics',
					'title'],
				
				page: 25,
				
				pagination: [{
					name: "paginationTop",
					paginationClass: "paginationTop",
					outerWindow: 2,
					innerWindow: 3,
					item: "<li><a class='page'></a></li>"
				}, {
					name: "paginationBottom",
					paginationClass: "paginationBottom",
					outerWindow: 2,
					innerWindow: 3,
					item: "<li><a class='page'></a></li>"
					}],
				
				listClass: "yml_list",
				searchClass: "yml_search",
				sortClass: "yml_sort",
				item: "<div class='yml_searchItem'>\
							<div class='yml_subItem' style='font-size:x-large; color:#fff; display:block;'><span class='artist'></span> - <span class='title'></span></div>\
							<div class='yml_subItem' style='font-size:small;'>Getting Lyrics For Artist Times: <span class='gettingLyricsForArtistTimes'></span> Getting Lyrics For Music Times: <span class='gettingLyricsForMusicTimes'></span></div>\
							<div class='yml_subItem'>Id: <span class='id'></span> Url: <a class='url urlHref' style='color:inherit;'></a></div >\
							<div class='yml_subItem' style='font-size:small;'>Added Date: <span class='date dateId'></span></div>\
							<div>Lyrics: </br><span class='lyrics'></span></div>\
						</div>"
			};
			
			let _searchItems = [];
			let _values = Object.values(cache2);
			let _y = 0;
			for (let i = 1; i < _values.length; i++)
			{
				let _musicValues = Object.values(_values[i].musics);
				for (let j = 0; j < _musicValues.length; j++)
				{
					_searchItems.push({});
					
					_searchItems[_y]["artist"] = _values[i].artist;
					_searchItems[_y]["gettingLyricsForArtistTimes"] = _values[i].gettingLyricsForArtistTimes;
					_searchItems[_y]["id"] = _values[i].id;
					_searchItems[_y]["url"] = _values[i].url;
					_searchItems[_y]["urlHref"] = _values[i].url;
					
					_searchItems[_y]["dateId"] = _musicValues[j].dateId;
					_searchItems[_y]["date"] = new Date(_musicValues[j].dateId).toString()
					_searchItems[_y]["gettingLyricsForMusicTimes"] = _musicValues[j].gettingLyricsForMusicTimes;
					_searchItems[_y]["lyrics"] = _musicValues[j].lyrics;
					_searchItems[_y]["title"] = _musicValues[j].title;
					
					_y++;
				}
			}
			
			//TODO!
			//Erase elements when hiding and add them when showing search panel
			let musicList = new List('yml_searchMusics', options, _searchItems);
			
			$('.yml_searchArtist').on("keyup", function ()
			{
				let searchString = $(this).val();
				musicList.search(searchString, ['artist']);
			});

			$('.yml_searchTitle').on("keyup", function ()
			{
				let searchString = $(this).val();
				musicList.search(searchString, ['title']);
			});
			
			$('.yml_searchLyrics').on("keyup", function ()
			{
				let searchString = $(this).val();
				musicList.search(searchString, ['lyrics']);
			});

			musicList.on("updated", function ()
			{
				$(".yml_searchItem").unhighlight();
				let search = $(".yml_search").val() || $(".yml_searchArtist").val() || $(".yml_searchTitle").val() || $(".yml_searchLyrics").val();
				let words = search.split(" ");
				$(".yml_searchItem").highlight(words);
				$("#yml_resultCount").text(musicList.matchingItems.length);
			});
			
			$('.yml_filterGettingLyricsForArtistTimes, .yml_filterGettingLyricsForMusicTimes, .yml_filterAddedDateA, .yml_filterAddedDateB').on('keyup change', function ()
			{
				let number = [];

				let raw = [$(".yml_filterGettingLyricsForArtistTimes").val(),
					$(".yml_filterGettingLyricsForMusicTimes").val(),
					$(".yml_filterAddedDateA").val(),
					$(".yml_filterAddedDateB").val(),
				];

				let fsp = ["gettingLyricsForArtistTimes",
					"gettingLyricsForMusicTimes",
					"dateId",
					"dateId"];

				let im = [];

				for (let i = 0; i < raw.length; i++)
				{
					if (raw[i].match(">"))
					{
						number[i] = Number(raw[i].substr(1));
					} else if (raw[i].match("<"))
					{
						number[i] = Number(raw[i].substr(1));
					} else { number[i] = Number(raw[i]); }

					if (i == 2 || i == 3)
					{
						if (raw[i] === "")
							number[i] = 0;
						else
							number[i] = new Date(raw[i]).getTime();
					}
				}

				musicList.filter(function (item)
				{
					for (let i = 0; i < raw.length; i++)
					{
						if (raw[i] === "") continue;

						if (i == 2)
						{
							if (item.values()[fsp[i]] >= number[i])
							{
								im.push(true);
							}
							else
							{
								return false;
							}
						 
						}else if(i == 3)
						{
							if (item.values()[fsp[i]] <= number[i])
							{
								im.push(true);
							}
							else
							{
								return false;
							}
						} else
						{
							if (raw[i].match(">"))
							{
								if (item.values()[fsp[i]] >= number[i])
								{
									im.push(true);
								}
								else
								{
									return false;
								}
							} else if (raw[i].match("<"))
							{
								if (item.values()[fsp[i]] <= number[i])
									im.push(true);
								else
									return false;
							} else if (item.values()[fsp[i]] === number[i])
							{
								im.push(true);
							} else
								return false;
						}
					}

					if (im.every(e => e === true))
						return true;
					else
						return false;

				}); // Only items with id > 1 are shown in list

				if (raw.every(e => e === ""))
				{
					musicList.filter();
				}

				$("#yml_resultCount").text(musicList.matchingItems.length);
			});
			
			$("#yml_resultCount").text(musicList.size());
			
			setupSearchOnce = false;
		}
	});
	
	$("#yml_debug").on("change", function ()
	{
		options2.debug = $(this).prop("checked");
	});

	$("#yml_contextmenu").on("change", function ()
	{
		options2.contextmenu = $(this).prop("checked");
	});

	$("#yml_addLyricsButton").on("click", function ()
	{
		//TODO MAKE BETTER CODE!!!
		let _text = document.getElementById("yml_lyricsText");

		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 + 100) + "px;width:" + (w + 100) + "px;"
			});

			$('pre#yml_lyricsText').replaceTag('textarea');

			_text.value($("#yml_lyricsText").text());
		}
		else
		{
			$(this).text("Add Lyrics");

			let title = document.querySelector(".title.ytmusic-player-bar").innerText;

			let id = document.querySelector(".subtitle.ytmusic-player-bar").firstElementChild.firstElementChild.attributes.href.value;

			$("#yml_lyricsText").text(_text.value);

			cache2.AddLyrics(id, title, _text.value);

			$("#yml_lyricsText").attr({
				style: "height: inherit;width:inherit;"
			});

			Options2._GMUpdate("cache2", cache2);

			$('textarea#yml_lyricsText').replaceTag('pre');
		}
	});

	$('.sortable-list').sortable({
		connectWith: '.sortable-list',
		items: "li:not(.ui-state-disabled)",
		update: function ()
		{
			let order = $(this).sortable('toArray');
			let names = [];
			for (let i = 0; i < order.length; i++)
			{
				names.push($("#" + order[i]).find("span").text());
			}

			names.unshift(0);
			order.unshift(0);
			console.log(names);
			console.log(order);

			//update priority!
			options2.UpdatePriority = names;
			console.log(options2);
			Options2._GMUpdate("options2", options2);
		}
	});

	$('#yml_clearCache').on("click", function ()
	{
		Options2._GMDeleteValues("yml_cache2");

		//todo how?
		let cache2 = new Cache2(0.1);
		cache2.then(() =>
		{
			console.log(cache2);
		});
	});

}
//Function set events
//End

//-------------------------
//TOOLS BELOW
//-------------------------

//Start
//Handler for url
function UrlHandler(options2, cache2)
{
	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 () { Music2(options2, cache2); }, 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

//Start
//Tool for decoding stuff like &#xxxx; https://stackoverflow.com/a/2808386
function HtmlDecode(input)
{
	var e = document.createElement('div');
	e.innerHTML = input;
	return e.childNodes[0].nodeValue;
}
//Tool for decoding stuff like &#xxxx; https://stackoverflow.com/a/2808386
//End

//Start
//Format bytes https://stackoverflow.com/a/18650828
function FormatBytes(bytes, decimals = 2)
{
	if (bytes === 0) return '0 Bytes';

	const k = 1024;
	const dm = decimals < 0 ? 0 : decimals;
	const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

	const i = Math.floor(Math.log(bytes) / Math.log(k));

	return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
//Format bytes https://stackoverflow.com/a/18650828
//End

/*
 * jQuery Highlight plugin
 *
 * Based on highlight v3 by Johann Burkard
 * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
 *
 * Code a little bit refactored and cleaned (in my humble opinion).
 * Most important changes:
 *  - has an option to highlight only entire words (wordsOnly - false by default),
 *  - has an option to be case sensitive (caseSensitive - false by default)
 *  - highlight element tag and class names can be specified in options
 *
 * Usage:
 *   // wrap every occurrence of text 'lorem' in content
 *   // with <span class='highlight'> (default options)
 *   $('#content').highlight('lorem');
 *
 *   // search for and highlight more terms at once
 *   // so you can save some time on traversing DOM
 *   $('#content').highlight(['lorem', 'ipsum']);
 *   $('#content').highlight('lorem ipsum');
 *
 *   // search only for entire word 'lorem'
 *   $('#content').highlight('lorem', { wordsOnly: true });
 *
 *   // search only for the entire word 'C#'
 *   // and make sure that the word boundary can also
 *   // be a 'non-word' character, as well as a regex latin1 only boundary:
 *   $('#content').highlight('C#', { wordsOnly: true , wordsBoundary: '[\\b\\W]' });
 *
 *   // don't ignore case during search of term 'lorem'
 *   $('#content').highlight('lorem', { caseSensitive: true });
 *
 *   // wrap every occurrence of term 'ipsum' in content
 *   // with <em class='important'>
 *   $('#content').highlight('ipsum', { element: 'em', className: 'important' });
 *
 *   // remove default highlight
 *   $('#content').unhighlight();
 *
 *   // remove custom highlight
 *   $('#content').unhighlight({ element: 'em', className: 'important' });
 *
 *
 * Copyright (c) 2009 Bartek Szopka
 *
 * Licensed under MIT license.
 *
 */

(function (factory)
{
	if (typeof define === 'function' && define.amd)
	{
		// AMD. Register as an anonymous module.
		define(['jquery'], factory);
	} else if (typeof exports === 'object')
	{
		// Node/CommonJS
		factory(require('jquery'));
	} else
	{
		// Browser globals
		factory(jQuery);
	}
}(function (jQuery)
{
	jQuery.extend({
		highlight: function (node, re, nodeName, className, callback)
		{
			if (node.nodeType === 3)
			{
				var match = node.data.match(re);
				if (match)
				{
					// The new highlight Element Node
					var highlight = document.createElement(nodeName || 'span');
					highlight.className = className || 'highlight';
					// Note that we use the captured value to find the real index
					// of the match. This is because we do not want to include the matching word boundaries
					var capturePos = node.data.indexOf(match[1], match.index);

					// Split the node and replace the matching wordnode
					// with the highlighted node
					var wordNode = node.splitText(capturePos);
					wordNode.splitText(match[1].length);

					var wordClone = wordNode.cloneNode(true);
					highlight.appendChild(wordClone);
					wordNode.parentNode.replaceChild(highlight, wordNode);
					if (typeof callback === 'function')
					{
						callback(highlight);
					}
					return 1; //skip added node in parent
				}
			} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
				!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
				!(node.tagName === nodeName.toUpperCase() && node.className === className))
			{ // skip if already highlighted
				for (var i = 0; i < node.childNodes.length; i++)
				{
					i += jQuery.highlight(node.childNodes[i], re, nodeName, className, callback);
				}
			}
			return 0;
		}
	});

	jQuery.fn.unhighlight = function (options)
	{
		var settings = {
			className: 'highlight',
			element: 'span'
		};

		jQuery.extend(settings, options);

		return this.find(settings.element + '.' + settings.className).each(function ()
		{
			var parent = this.parentNode;
			parent.replaceChild(this.firstChild, this);
			parent.normalize();
		}).end();
	};

	jQuery.fn.highlight = function (words, options, callback)
	{
		var settings = {
			className: 'highlight',
			element: 'span',
			caseSensitive: false,
			wordsOnly: false,
			wordsBoundary: '\\b'
		};

		jQuery.extend(settings, options);

		if (typeof words === 'string')
		{
			words = [words];
		}
		words = jQuery.grep(words, function (word, i)
		{
			return word !== '';
		});
		words = jQuery.map(words, function (word, i)
		{
			return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
		});

		if (words.length === 0)
		{
			return this;
		}

		var flag = settings.caseSensitive ? '' : 'i';
		// The capture parenthesis will make sure we can match
		// only the matching word
		var pattern = '(' + words.join('|') + ')';
		if (settings.wordsOnly)
		{
			pattern =
				(settings.wordsBoundaryStart || settings.wordsBoundary) +
				pattern +
				(settings.wordsBoundaryEnd || settings.wordsBoundary);
		}
		var re = new RegExp(pattern, flag);

		return this.each(function ()
		{
			jQuery.highlight(this, re, settings.element, settings.className, callback);
		});
	};
}));