SUKEBEI PLUS

Add original video preview.

// ==UserScript==
// @name        SUKEBEI PLUS
// @namespace   Violentmonkey Scripts
// @match       *://sukebei.nyaa.si/*
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @version     2.0.0
// @author      Chaewon
// @description Add original video preview.
// @license     Unlicense
// @icon        https://sukebei.nyaa.si/static/favicon.png
// ==/UserScript==

(function () {
	"use strict";

	const css = String.raw;
	const html = String.raw;

	const stylesheet = css`
		.overlay-video-container {
			position: fixed;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background-color: rgba(0, 0, 0, 0.9);
			display: flex;
			justify-content: center;
			align-items: center;
			z-index: 9999;
			overflow: hidden;
		}

		.video-container {
			display: flex;
			justify-content: center;
			align-items: center;
			width: 100%;
			height: 100%;
			max-width: 90%;
			max-height: 90%;
			position: relative;
		}

		.overlay-video {
			width: 100%;
			height: auto;
			max-width: 100%;
			max-height: 100%;
			border: 1px solid #262626;
			background: #000;
		}

		.close-button {
			position: absolute;
			padding: 6px 14px;
			top: -40px;
			right: 0;
			background: none;
			color: white;
			cursor: pointer;
			border: 1px solid #333;
		}

		.close-button:hover {
			background: #333;
		}

		.settings-container {
			position: fixed;
			right: 1em;
			top: 2em;
			max-width: 400px;
			max-height: 92vh;
			width: 100%;
			padding: 10px 20px;
			border: 1px solid #ddd;
			border-radius: 8px;
			background-color: #fff;
			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
			z-index: 9999;
			overflow: auto;
		}

		.settings-container .settings-item:not(:last-of-type) {
			margin-bottom: 1rem;
		}

		.settings-container label {
			display: block;
			margin-bottom: 8px;
			font-weight: 600;
			color: #333;
		}

		.settings-container input[type="text"],
		.settings-container input[type="range"] {
			width: 100%;
			padding: 8px;
			border: 1px solid #ccc;
			border-radius: 4px;
			box-sizing: border-box;
		}

		.settings-container input[type="checkbox"] {
			margin-right: 10px;
		}

		.settings-container .button {
			display: inline-block;
			padding: 10px 15px;
			color: #fff;
			background-color: #007bff;
			border: none;
			border-radius: 4px;
			cursor: pointer;
			text-align: center;
			transition: background-color 0.3s ease;
		}

		.settings-container .button:hover {
			background-color: #0056b3;
		}

		.settings-container range-label {
			margin-bottom: 5px;
			color: #333;
		}

		.settings-container #quality,
		.settings-container #languages {
			color: black;
		}

		.settings-container option,
		.settings-container input {
			color: black;
		}

		.settings-container #resetApiData {
			width: 100%;
			margin: 4px 0;
		}

		.toast {
			font-weight: bolder;
			position: fixed;
			top: 20px;
			left: 50%;
			transform: translateX(-50%);
			padding: 10px 20px;
			background-color: #913030;
			color: #fff;
			border-radius: 4px;
			opacity: 0;
			visibility: hidden;
			transition: opacity 0.5s, visibility 0.5s;
			z-index: 1000;
			border: 2px solid #fff;
		}

		.toast.show {
			opacity: 1;
			visibility: visible;
		}

		table.torrent-list tbody .comments {
			margin-left: 8px;
		}

		.btn-translate {
			margin-left: 8px;
			position: relative;
			float: right;
			border: 1px solid #d7d7d7;
			border-top-color: rgb(215, 215, 215);
			border-right-color: rgb(215, 215, 215);
			border-bottom-color: rgb(215, 215, 215);
			border-left-color: rgb(215, 215, 215);
			border-radius: 3px;
			color: #383838;
			padding: 0 5px;
			font-size: small;
			background-color: #ffffff;
		}

		.btn-translate {
			color: #337ab7;
		}

		body.dark .btn-translate {
			border-color: #212121;
			color: #247fcc;
			background-color: #2f2c2c;
		}

		.translatedText {
			display: block;
			word-break: break-word;
			white-space: normal;
			font-size: 0.9em;
			font-style: italic;
		}

		.languages {
			padding-top: 2px;
			float: right;
			position: relative;
			font-size: 0.9em;
			font-style: italic;
		}
	`;

	GM_addStyle(stylesheet);

	const defaultConfig = {
		darkTheme: false,
		playerAutoplay: true,
		playerVolume: 0.1,
		playerLoop: true,
		playerClickAnywhereToClose: false,
		previewQuality: 2,
		lastUpdated: 0,
		devMode: false,
		translate: false,
		languages: "en",
		data: {
			studios: {
				general: ["PFES", "AVOP"],
				sOne: ["SSIS", "SOE", "SNIS", "SSNI", "OFJE", "SONE", "ONSD"],
				faleno: ["FSDSS", "FCDSS", "FTBLD", "FTKD", "FTHTD", "MGOLD"],
			},
			prefixMap: {
				general: ["general"],
				nmixx: ["sOne"],
				plusOne: ["faleno"],
			},
			prefixMapData: {
				plusOne: "1",
			},
		}, //Basic data, incase of API failure on first run
		url: "https://api.jsonsilo.com/public/445432dd-f613-4528-a45d-71a1efacee95",
	};

	const mergedConfig = { ...defaultConfig, ...GM_getValue("config") };
	const userConfig = mergedConfig;
	console.log("[US:DEBUG] : ", `CONFIG: `, userConfig);

	async function fetchDataFromAPI() {
		try {
			const response = await fetch(userConfig.url);
			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}
			const data = await response.json();
			return data;
		} catch (error) {
			console.log("[US:DEBUG] : ", " Error fetching data:", error);
		}
	}

	async function main() {
		GM_registerMenuCommand("Config", openSettings);

		let studios, prefixMap, prefixMapData;
		const lastFetchTime = parseInt(userConfig.lastUpdated) || 0;
		const oneWeekInMilliseconds = 7 * 24 * 60 * 60 * 1000;
		const now = new Date().getTime();
		const isDataOld = now - lastFetchTime >= oneWeekInMilliseconds;

		if (userConfig.darkTheme && localStorage.getItem("theme") !== "dark") {
			localStorage.setItem("theme", "dark");
			location.reload();
		}

		if (isDataOld) {
			console.log(
				"[US:DEBUG] : ",
				`\n  -- TIMESTAMP:\t\t${new Date().getTime()} \n  -- LAST UPDATED:\t${lastFetchTime} \n  -- EXPIRED:\t\t${isDataOld} \n  -- Data is not available or old. Fetching new data.`,
			);
			const response = await fetchDataFromAPI();

			if (response) {
				userConfig.lastUpdated = now;
				userConfig.data = response;
				studios = response.studios;
				prefixMap = response.prefixMap;
				prefixMapData = response.prefixMapData;
				GM_setValue("config", userConfig);
			} else {
				console.log("[US:DEBUG] : ", "No Response from API. Will try again later.");
				userConfig.lastUpdated = new Date().getTime() - 6 * 24 * 60 * 60 * 1000;
				studios = userConfig.data.studios;
				prefixMap = userConfig.data.prefixMap;
				prefixMapData = userConfig.data.prefixMapData;
				GM_setValue("config", userConfig);
			}
		} else {
			console.log(
				"[US:DEBUG] : ",
				`\n  -- TIMESTAMP:\t\t${new Date().getTime()} \n  -- LAST UPDATED:\t${lastFetchTime} \n  -- EXPIRED:\t\t${isDataOld} \n  -- Data is still fresh. Skipping API call.`,
			);
			studios = userConfig.data.studios;
			prefixMap = userConfig.data.prefixMap;
			prefixMapData = userConfig.data.prefixMapData;
		}

		function mergeStudiosByPrefix(studios, prefixes) {
			const result = {};
			Object.keys(prefixes).forEach((prefix) => {
				result[prefix] = [];
				prefixes[prefix].forEach((key) => {
					if (studios[key]) {
						result[prefix] = [...result[prefix], ...studios[key]];
					}
				});
			});
			return result;
		}

		function valuesToRegex(object) {
			return Object.fromEntries(
				Object.entries(object).map(([object, values]) => [
					object,
					new RegExp(`\\b(?:${values.join("|")})(?:-)?\\d{3,5}(?:.)?\\b`, "i"),
				]),
			);
		}

		const studioMergedForPrefix = mergeStudiosByPrefix(studios, prefixMap);
		const regexMap = valuesToRegex(studioMergedForPrefix);

		if (window.location.pathname.startsWith("/view/")) {
			console.log("[US:DEBUG] : ", "Torrent Detail Page");
			const titlePanel = document.getElementsByClassName("panel-title");
			const titleCell = document.getElementsByClassName("panel-heading");

			if (userConfig.translate) {
				const trButton = document.createElement("a");
				trButton.classList.add("btn", "btn-translate");
				trButton.setAttribute("href", `?translate=${titleCell[0].innerText}`);
				trButton.setAttribute("title", "Translate");
				trButton.innerHTML = `<i class="fa fa-globe" aria-hidden="true"></i>`;
				titleCell[0].prepend(trButton);
				trButton.addEventListener("click", async function (event) {
					event.preventDefault();
					trButton.setAttribute("disabled", "");
					trButton.removeAttribute("title");
					if (!titleCell[0].getAttribute("translated")) {
						titleCell[0].setAttribute("translated", "true");

						try {
							const response = await gTranslate(titleCell[0].innerText);

							if (response.length <= 0) {
								showToast("Translation Service Unavailable");
								return;
							}

							const node = document.createElement("span");
							node.innerText = response;
							node.classList.add("translatedText");
							node.setAttribute("title", response);
							titleCell[0].appendChild(node);
						} catch (error) {
							console.error("Translation failed:", error);
							showToast("Translation Service Unavailable");
						}
					}
				});
			}

			if (
				titlePanel[0].parentElement.nextElementSibling.classList.contains("panel-body") &&
				titlePanel[0].parentElement.nextElementSibling.children[0].classList.contains("row")
			) {
				const code = detectCode(titlePanel[0].innerText, regexMap);
				if (code && code.match) {
					console.log("[US:DEBUG] : ", "Valid studios detected: ", code);
					let codeId = code.match.toLowerCase();
					const buttonDom = document.getElementsByClassName("panel-footer clearfix")[0];
					const newButton = document.createElement("a");
					newButton.setAttribute("href", "?preview=" + codeId);
					newButton.setAttribute("title", "Preview");
					newButton.innerHTML = `<i class="fa fa-video-camera"></i> Preview`;
					const buttonDivider = document.createTextNode(" ・");
					buttonDom.insertBefore(newButton, buttonDom.firstChild);
					buttonDom.insertBefore(buttonDivider, buttonDom.firstChild.nextSibling);
					newButton.addEventListener("click", function (event) {
						event.preventDefault();
						openPreview(codeId, code.cdn);
					});
				}
			} else {
				return;
			}
		} else {
			console.log("[US:DEBUG] : ", "Non Torrent Detail Page");
			const rows = document.querySelectorAll("tr");

			rows.forEach((row, index) => {
				if (!row || index === 0) return;

				const categoryCell = row.querySelector("td:nth-child(1) a")["title"];
				const titleCell = row.querySelector("td:nth-child(2)");
				const linkCell = row.querySelector("td:nth-child(3)");

				// Translate
				if (userConfig.translate) {
					const trButton = document.createElement("a");
					trButton.classList.add("btn", "btn-translate");
					trButton.setAttribute("href", `?translate=${titleCell.innerText}`);
					trButton.setAttribute("title", "Translate");
					trButton.innerHTML = `<i class="fa fa-globe" aria-hidden="true"></i>`;
					titleCell.prepend(trButton);
					trButton.addEventListener("click", async function (event) {
						event.preventDefault();
						trButton.setAttribute("disabled", "");
						trButton.removeAttribute("title");
						if (!titleCell.getAttribute("translated")) {
							titleCell.setAttribute("translated", "true");

							try {
								const response = await gTranslate(titleCell.innerText);

								if (response.length <= 0) {
									showToast("Translation Service Unavailable");
									return;
								}

								const node = document.createElement("span");
								node.innerText = response;
								node.classList.add("translatedText");
								node.setAttribute("title", response);
								titleCell.appendChild(node);
							} catch (error) {
								console.error("Translation failed:", error);
								showToast("Translation Service Unavailable");
							}
						}
					});
				}

				//Preview Button
				if (categoryCell === "Real Life - Videos" || categoryCell === "Art - Pictures") {
					const code = detectCode(titleCell.innerText, regexMap);
					if (code && code.match) {
						console.log("[US:DEBUG] : ", "Valid studios detected: ", code);
						let codeId = code.match.toLowerCase();
						const newButton = document.createElement("a");
						newButton.setAttribute("href", "?preview=" + codeId);
						newButton.setAttribute("title", "Preview");
						newButton.innerHTML = `<i class="fa fa-video-camera"></i>`;
						linkCell.appendChild(newButton);
						newButton.addEventListener("click", function (event) {
							event.preventDefault();
							openPreview(codeId, code.cdn);
						});
					} else {
						return;
					}
				}
			});
		}

		function detectCode(title, patterns) {
			for (const [key, pattern] of Object.entries(patterns)) {
				const match = title.match(pattern);
				if (match) {
					return {
						match: match[0],
						cdn: key,
					};
				}
			}
			return { match: null, cdn: null };
		}

		function openPreview(code, cdn) {
			code = cdn === "nmixx" ? code.replace(/-?(?<!^)(\d+)/, "00$1") : code;
			code = code.replace(/\s+/g, "").replace(/[^\w]/g, "");
			fetchData(code, cdn, (response) => {
				if (response.length <= 0) {
					showToast("No Preview Available!");
					return;
				}
				const video = document.createElement("video");
				video.src = qualitySelector(userConfig.previewQuality, qualitySorter(response));
				//DEV
				if (userConfig.devmode) {
					if (!prefixMapData[cdn]) prefixMapData[cdn] = "";
					const finalCode = prefixMapData[cdn] + code;
					console.log(`[US:DEBUG] : ${code}, ${finalCode}, ${cdn}, ${video.src}`);
					if (!video.src.includes(finalCode)) {
						alert(`DEVMODE: CODE PREFIX MISMATCH : ${code}, ${finalCode}, ${cdn}, ${video.src}`);
					}
				}
				//DEV END
				video.autoplay = userConfig.playerAutoplay;
				video.volume = userConfig.playerVolume;
				video.loop = userConfig.playerLoop;
				video.controls = true;
				video.classList.add("overlay-video");
				video.addEventListener("error", function (event) {
					console.error("Video failed to load:");
					videoContainer.innerHTML = `<p style="color: white; margin: 0; text-align: center;">Preview Not Available.<br>This window will auto close in 3 seconds</p>`;
					setTimeout(() => {
						videoContainerOverlay.remove();
					}, 3000);
				});

				const closeButton = document.createElement("button");
				closeButton.textContent = "Close";
				closeButton.classList.add("close-button");
				closeButton.addEventListener("click", () => {
					videoContainerOverlay.remove();
				});

				const videoContainerOverlay = document.createElement("div");
				const videoContainer = document.createElement("div");
				videoContainerOverlay.classList.add("overlay-video-container");
				videoContainerOverlay.classList.add(`${code}`);
				videoContainer.classList.add("video-container");
				videoContainer.appendChild(video);
				videoContainer.appendChild(closeButton);
				videoContainerOverlay.appendChild(videoContainer);
				document.body.appendChild(videoContainerOverlay);
				if (userConfig.playerClickAnywhereToClose) {
					videoContainerOverlay.addEventListener("click", (event) => {
						if (event.target !== video) {
							videoContainerOverlay.remove();
						}
					});
				}
			});
		}

		function fetchData(id, cdn, callback) {
			let dvdId = id;
			const prefix = prefixMapData[cdn];
			if (prefix) {
				dvdId = prefix + dvdId;
			}

			GM_xmlhttpRequest({
				method: "GET",
				url: "https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=" + dvdId.toLowerCase(),
				onload: function (response) {
					if (response.status === 200) {
						let scriptData, scriptObj;
						let scripts = response.responseXML.scripts;
						for (let i = 0; i < scripts.length; i++) {
							const script = scripts[i];
							if (script.textContent.includes("dmm")) {
								scriptData = script.textContent;
							}
						}
						const regex = /const args = ({.*?});/s;
						const match = scriptData.match(regex);

						if (match) {
							const jsonString = match[1];
							try {
								scriptObj = JSON.parse(jsonString.replace(/\\/g, ""));
							} catch (error) {
								console.error("Error parsing JSON:", error);
							}
							//callback(scriptObj.src);
							callback(scriptObj.bitrates);
						}
					} else {
						console.error("Request failed with status:", response.status);
					}
				},
			});
		}

		function qualitySorter(items) {
			const qualityTiers = {
				5: { min: 1440, max: 2160 },
				4: { min: 1080, max: 1439 },
				3: { min: 720, max: 1079 },
				2: { min: 480, max: 719 },
				1: { min: 0, max: 479 },
			};

			const extractResolution = (bitrate) => {
				const match = bitrate.match(/\((\d+)p\)/);
				return match ? parseInt(match[1], 10) : null;
			};
			const getQualityTier = (resolution) => {
				for (const [tier, { min, max }] of Object.entries(qualityTiers)) {
					if (resolution >= min && resolution <= max) {
						return tier;
					}
				}
				return "Unknown";
			};

			const tierMap = new Map();

			items.forEach((item) => {
				const resolution = extractResolution(item.bitrate);
				if (resolution) {
					const qualityTier = getQualityTier(resolution);
					if (!tierMap.has(qualityTier)) {
						tierMap.set(qualityTier, {
							quality: qualityTier,
							bitrate: item.bitrate,
							src: item.src,
						});
					}
				}
			});

			return Array.from(tierMap.values());
		}

		function qualitySelector(desiredQuality, array) {
			const sortedArray = array.slice().sort((a, b) => b.quality - a.quality);

			for (const item of sortedArray) {
				if (item.quality <= desiredQuality) {
					return item.src;
				}
			}

			return null;
		}

		function gTranslate(string) {
			const languages = userConfig.languages || defaultConfig.languages || "en";
			return new Promise((resolve, reject) => {
				GM_xmlhttpRequest({
					method: "GET",
					url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${languages}&dt=t&q=${encodeURIComponent(
						string,
					)}`,
					responseType: "json",
					onload: function (response) {
						if (response.status === 200) {
							let answer = JSON.parse(response.responseText);
							const finalAnswer = answer[0]?.map((segment) => segment[0]).join("");
							resolve(finalAnswer);
						} else {
							reject(`Request failed with status: ${response.status}`);
						}
					},
				});
			});
		}

		function showToast(string) {
			const toast = document.createElement("div");
			toast.className = "toast";
			toast.textContent = string;
			document.body.appendChild(toast);

			setTimeout(() => {
				toast.classList.add("show");
			}, 0);

			setTimeout(() => {
				toast.classList.remove("show");
				setTimeout(() => {
					document.body.removeChild(toast);
				}, 500);
			}, 2000);
		}

		const footerLink = document.querySelector("footer p");
		const settingButton = document.createElement("a");
		const buttonDivider = document.createTextNode("・");
		settingButton.setAttribute("href", "?setting");
		settingButton.setAttribute("title", "Settings");
		settingButton.innerHTML = `<i class="fa fa-cogs"></i> Settings`;
		footerLink.insertBefore(settingButton, footerLink.firstChild);
		footerLink.insertBefore(buttonDivider, footerLink.firstChild.nextSibling);
		settingButton.addEventListener("click", function (event) {
			event.preventDefault();
			openSettings();
		});

		const menubar = document.querySelector(".navbar-right .dropdown-menu");
		const newLi = document.createElement("li");
		const newLink = document.createElement("a");
		newLink.setAttribute("href", "?setting");
		newLink.setAttribute("title", "Settings");
		newLink.innerHTML = `<i class="fa fa-cogs"></i> Settings`;
		newLi.appendChild(newLink);
		menubar.appendChild(newLi);
		newLink.addEventListener("click", function (event) {
			event.preventDefault();
			openSettings();
		});

		function openSettings() {
			const isExist = document.querySelector(".settings-container");
			if (isExist) return;

			const container = document.createElement("div");
			container.classList.add("settings-container");
			container.setAttribute("aria-label", "Settings");
			const parsePlayerAutoplay = userConfig.playerAutoplay ? "checked" : "";
			const parsePlayerLoop = userConfig.playerLoop ? "checked" : "";
			const parsePlayerClickAnywhereToClose = userConfig.playerClickAnywhereToClose ? "checked" : "";
			const parseDarkTheme = userConfig.darkTheme ? "checked" : "";
			const devmode = userConfig.devmode ? "checked" : "";
			const translate = userConfig.translate ? "checked" : "";
			const parsePlayerWidth = null;
			const parsePlayerVolume = userConfig.playerVolume;
			const parseApiUrl = userConfig.url;

			container.innerHTML = html`
                <div class="settings-item">
                    <!--
                        <label for="video-width">Video Player Width (px) <i>[Default: 320]</i></label>
                        <input type="text" id="video-width" name="video-width" placeholder="Enter width in px" value="${parsePlayerWidth}">
                    </div>
                    -->
                    <div class="settings-item">
                        <label for="volume-slider" class="range-label">Volume:</label>
                        <input type="range" id="volume-slider" name="volume-slider" min="0" max="1" value="${parsePlayerVolume}" step="0.01">
                    </div>
                    <div class="settings-item">
                    <label for="quality">Video Quality (if available):</label>
                    <select name="qualities" id="quality">
                        <option value="5">QHD (2K/1440p)</option>
                        <option value="4">FHD (1080p)</option>
                        <option value="3">HD (720p)</option>
                        <option value="2">Medium</option>
                        <option value="1">Low</option>
                    </select>
                    </div>
                    <div class="settings-item">
                        <label>
                            <input type="checkbox" id="autoplay" name="autoplay" ${parsePlayerAutoplay}>
                            Video Autoplay
                        </label>
                    </div>
                    <div class="settings-item">
                        <label>
                            <input type="checkbox" id="loop" name="loop" ${parsePlayerLoop}>
                            Video Loop
                        </label>
                    </div>
                    <div class="settings-item">
                        <label>
                            <input type="checkbox" id="clickanywhere" name="clickanywhere" ${parsePlayerClickAnywhereToClose}>
                            Click Anywhere To Close Player
                        </label>
                    </div>
                    <div class="settings-item">
                        <label disabled>
                            <input type="checkbox" id="darktheme" name="darktheme" ${parseDarkTheme}>
                            Auto Dark Theme
                        </label>
                    </div>
                    <div class="settings-item">
                        <label disabled>
                            <input type="checkbox" id="translate" name="translate" ${translate}>
                            Translate Button
                        </label>
                    </div>
                    <div class="settings-item">
                        <label for="languages">Translate to:</label>
                        <select id="languages" name="languages">
                            <option value="af">Afrikaans</option>
                            <option value="sq">Albanian</option>
                            <option value="am">Amharic</option>
                            <option value="ar">Arabic</option>
                            <option value="hy">Armenian</option>
                            <option value="az">Azerbaijani</option>
                            <option value="eu">Basque</option>
                            <option value="be">Belarusian</option>
                            <option value="bn">Bengali</option>
                            <option value="bs">Bosnian</option>
                            <option value="bg">Bulgarian</option>
                            <option value="ca">Catalan</option>
                            <option value="ceb">Cebuano</option>
                            <option value="ny">Chichewa</option>
                            <option value="zh-CN">Chinese (Simplified)</option>
                            <option value="zh-TW">Chinese (Traditional)</option>
                            <option value="co">Corsican</option>
                            <option value="hr">Croatian</option>
                            <option value="cs">Czech</option>
                            <option value="da">Danish</option>
                            <option value="nl">Dutch</option>
                            <option value="en">English</option>
                            <option value="eo">Esperanto</option>
                            <option value="et">Estonian</option>
                            <option value="tl">Filipino</option>
                            <option value="fi">Finnish</option>
                            <option value="fr">French</option>
                            <option value="fy">Frisian</option>
                            <option value="gl">Galician</option>
                            <option value="ka">Georgian</option>
                            <option value="de">German</option>
                            <option value="el">Greek</option>
                            <option value="gu">Gujarati</option>
                            <option value="ht">Haitian Creole</option>
                            <option value="ha">Hausa</option>
                            <option value="haw">Hawaiian</option>
                            <option value="iw">Hebrew</option>
                            <option value="hi">Hindi</option>
                            <option value="hmn">Hmong</option>
                            <option value="hu">Hungarian</option>
                            <option value="is">Icelandic</option>
                            <option value="ig">Igbo</option>
                            <option value="id">Indonesian</option>
                            <option value="ga">Irish</option>
                            <option value="it">Italian</option>
                            <option value="ja">Japanese</option>
                            <option value="jw">Javanese</option>
                            <option value="kn">Kannada</option>
                            <option value="kk">Kazakh</option>
                            <option value="km">Khmer</option>
                            <option value="rw">Kinyarwanda</option>
                            <option value="ko">Korean</option>
                            <option value="ku">Kurdish (Kurmanji)</option>
                            <option value="ky">Kyrgyz</option>
                            <option value="lo">Lao</option>
                            <option value="la">Latin</option>
                            <option value="lv">Latvian</option>
                            <option value="lt">Lithuanian</option>
                            <option value="lb">Luxembourgish</option>
                            <option value="mk">Macedonian</option>
                            <option value="mg">Malagasy</option>
                            <option value="ms">Malay</option>
                            <option value="ml">Malayalam</option>
                            <option value="mt">Maltese</option>
                            <option value="mi">Maori</option>
                            <option value="mr">Marathi</option>
                            <option value="mn">Mongolian</option>
                            <option value="my">Myanmar (Burmese)</option>
                            <option value="ne">Nepali</option>
                            <option value="no">Norwegian</option>
                            <option value="or">Odia (Oriya)</option>
                            <option value="ps">Pashto</option>
                            <option value="fa">Persian</option>
                            <option value="pl">Polish</option>
                            <option value="pt">Portuguese</option>
                            <option value="pa">Punjabi</option>
                            <option value="ro">Romanian</option>
                            <option value="ru">Russian</option>
                            <option value="sm">Samoan</option>
                            <option value="gd">Scots Gaelic</option>
                            <option value="sr">Serbian</option>
                            <option value="st">Sesotho</option>
                            <option value="sn">Shona</option>
                            <option value="sd">Sindhi</option>
                            <option value="si">Sinhala</option>
                            <option value="sk">Slovak</option>
                            <option value="sl">Slovenian</option>
                            <option value="so">Somali</option>
                            <option value="es">Spanish</option>
                            <option value="su">Sundanese</option>
                            <option value="sw">Swahili</option>
                            <option value="sv">Swedish</option>
                            <option value="tg">Tajik</option>
                            <option value="ta">Tamil</option>
                            <option value="tt">Tatar</option>
                            <option value="te">Telugu</option>
                            <option value="th">Thai</option>
                            <option value="tr">Turkish</option>
                            <option value="tk">Turkmen</option>
                            <option value="uk">Ukrainian</option>
                            <option value="ur">Urdu</option>
                            <option value="ug">Uyghur</option>
                            <option value="uz">Uzbek</option>
                            <option value="vi">Vietnamese</option>
                            <option value="cy">Welsh</option>
                            <option value="xh">Xhosa</option>
                            <option value="yi">Yiddish</option>
                            <option value="yo">Yoruba</option>
                            <option value="zu">Zulu</option>
                        </select>
                    </div>
                    <div class="settings-item">
                        <label disabled>
                            <input type="checkbox" id="devmode" name="devmode" ${devmode}>
                            Devmode
                        </label>
                    </div>
                    <div class="settings-item last">
                        <label for="apiUrl">API Url:</label>
                        <input type="text" id="apiUrl" name="apiUrl" value="${parseApiUrl}"></input>
                        <button id="resetApiData" class="button" type="button">Force Refresh API Data</button>
                    </div>
                    <div class="settings-item">
                        <button id="saveButton" class="button" type="button">Save Settings</button>
                        <button id="closeButton" class="button" type="button">Close</button>
                    </div>
                    `;

			document.body.appendChild(container);

			const preQuality = document.getElementById("quality");
			if (preQuality) {
				preQuality.value = userConfig.previewQuality || 2;
			}

			const preLanguages = document.getElementById("languages");
			if (preLanguages) {
				preLanguages.value = userConfig.languages || "en";
			}

			let saveButton = document.getElementById("saveButton");
			saveButton.addEventListener("click", saveConfigMenu);

			let apiButton = document.getElementById("resetApiData");
			apiButton.addEventListener("click", () => {
				userConfig.lastUpdated = 0;
				saveConfigMenu();
			});

			let closeButton = document.getElementById("closeButton");
			closeButton.addEventListener("click", () => {
				const toRemove = document.querySelector(".settings-container");
				if (toRemove) {
					toRemove.remove();
				} else {
					console.log("[US:DEBUG] : ", "Bruh.");
				}
			});

			function saveConfigMenu() {
				const preQuality = document.getElementById("quality");
				const newVolume = document.getElementById("volume-slider");
				const autoplay = document.getElementById("autoplay");
				const loop = document.getElementById("loop");
				const darkTheme = document.getElementById("darktheme");
				const clickAnywhere = document.getElementById("clickanywhere");
				const devMode = document.getElementById("devmode");
				const apiUrl = document.getElementById("apiUrl");
				const languages = document.getElementById("languages");
				const translate = document.getElementById("translate");
				let finalVolume;

				if (
					newVolume.value !== null &&
					!isNaN(newVolume.value) &&
					newVolume.value >= 0 &&
					newVolume.value <= 1
				) {
					finalVolume = parseFloat(newVolume.value);
				}

				userConfig.darkTheme = darkTheme.checked;
				userConfig.playerAutoplay = autoplay.checked;
				userConfig.playerVolume = finalVolume;
				userConfig.playerLoop = loop.checked;
				userConfig.playerClickAnywhereToClose = clickAnywhere.checked;
				userConfig.previewQuality = preQuality.value;
				userConfig.devMode = devMode.checked;
				userConfig.url = apiUrl.value;
				userConfig.languages = languages.value;
				userConfig.translate = translate.checked;

				GM_setValue("config", userConfig);

				if (userConfig.darkTheme) {
					localStorage.setItem("theme", "dark");
				}
				location.reload();
			}
		}
	}

	main();
})();