Sleazy Fork is available in English.

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.2.6
// @author      Chaewon
// @description Add original video preview.
// @license     Unlicense
// @icon        https://sukebei.nyaa.si/static/favicon.png
// @antifeature referral-link VPN referral links.
// ==/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 {
			color: #333;
			position: fixed;
			right: 1em;
			top: 2em;
			max-width: 500px;
			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;
		}

		.notification {
			display: flex;
			margin: 1em 0;
			align-items: center;
			justify-content: space-between;
			background-color: #f8d7da;
			color: #721c24;
			padding: 8px 10px;
			border: 1px solid #f5c6cb;
			box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
			width: 100%;
			box-sizing: border-box;
		}

		.notification-message {
			flex-grow: 1;
			font-size: 16px;
		}

		.close-btn {
			background: none;
			border: none;
			color: #721c24;
			font-size: 20px;
			font-weight: bold;
			cursor: pointer;
			margin-left: 10px;
		}

		.close-btn:hover {
			color: #501118;
		}

		.notice {
			border: 1px solid #333;
			border-radius: 4px;
			padding: 1rem;
			background-color: #fff9c8;
			margin-bottom: 1rem;
		}
	`;

	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))
                // Fixes
					//.replace(/\.co\.jp/g, ".com")
					//.replace(/cc3001/g, "pv3001")
                    ;
				//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;
						if (scripts.length > 0) {
							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:", "No script found.");
								callback("");
							}
						} else {
							console.error("Request failed:", "No script found.");
							callback("");
						}
					} else {
						console.error("Request failed with status:", response.status);
						callback("");
					}
				},
			});
		}

		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>

    <div class="notice">
        <p>
            <strong>3rd Dec 2024:</strong> DMM has started restricting preview access from outside of Japan. Alternative methods or bypasses will be added if found, but they are actively patching these measures as of now, so they may stop working at any time. For reliable access, consider using a VPN.
        </p>
        <p><strong>Known Bypass (If needed):</strong> Change your request header's User-Agent for DMM urls to Google Bot (<a href="https://i.postimg.cc/Vkc30r7B/image.png" target="_blank" rel="noopener noreferrer">Example</a>). If it's not working anymore, it means they've patched it.</p>
        <p>
            <strong>Recommended VPN:</strong>
            <a href="https://windscribe.com/yo/br5vlcwg" target="_blank" rel="noopener noreferrer">Windscribe</a> (Affiliate link). You can get as low as $3/month from their build-a-plan feature. They also often have $29/year offers throughout the year.
        </p>
    </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();
})();