JAVBUS PLUS

Add video preview.

// ==UserScript==
// @name        JAVBUS PLUS
// @namespace   Violentmonkey Scripts
// @match       *://*.javbus.com/*
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @version     1.4.4
// @author      Chaewon
// @description Add video preview.
// @license     Unlicense
// @antifeature referral-link VPN referral links.
// @icon        https://www.google.com/s2/favicons?sz=256&domain_url=https%3A%2F%2Fwww.javbus.com%2Fen
// ==/UserScript==

//document.cookie='dv=1;path=/;expires=Tue, 01 Jan 2030 00:00:00 GMT;domain=javbus.com;';

(function () {
	"use strict";

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

	const defaultConfig = {
		// darkTheme: false,
		playerAutoplay: true,
		playerVolume: 0.1,
		playerLoop: true,
		playerClickAnywhereToClose: false,
		previewQuality: 2,
		lastUpdated: 0,
		languages: "en",
		devmode: false,
		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",
			},
		},
		url: "https://api.jsonsilo.com/public/445432dd-f613-4528-a45d-71a1efacee95",
	};
	const userConfig = GM_getValue("config", defaultConfig);
	console.log("[US:DEBUG] : ", `CONFIG: `, userConfig);

	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: 500px;
			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;
		}

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

		.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 {
			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;
		}

		.preview-btn {
			padding: 4px 10px;
			font-size: 0.8em;
			width: 100%;
			display: block;
			border: 1px solid #d59c9c;
			border-radius: 4px;
			text-align: center;
			background: #e1d2d2;
			color: #5b1818;
			margin: 0 0 10px 0;
		}

		.preview-btn:hover {
			background: #d59c9c;
			color: #5b1818;
		}

		#star-div {
			display: unset !important;
		}

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

	GM_addStyle(stylesheet);

	function createCookie(name, value, days, domain) {
		let expires;
		if (days) {
			const date = new Date();
			date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
			expires = "; expires=" + date.toGMTString();
		} else {
			expires = "";
		}
		document.cookie = name + "=" + value + expires + "; path=/" + "; domain=" + domain + ";";
	}

	function getCookie(name) {
		const nameKey = encodeURIComponent(name) + "=";
		const cookies = document.cookie.split(";");

		for (let i = 0; i < cookies.length; i++) {
			let c = cookies[i].trim();
			if (c.indexOf(nameKey) === 0) {
				return decodeURIComponent(c.substring(nameKey.length));
			}
		}
		return null;
	}

	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() {
		// COOKIE + SEARCH BYPASS
		const cookieDomain = location.hostname;
		const referer = new URLSearchParams(window.location.search).get("referer");

		const cookies = {
			dv: getCookie("dv"),
			age: getCookie("age"),
		};

		if (!cookies.dv || !cookies.age) {
			if (!cookies.dv) {
				createCookie("dv", "1", 365, cookieDomain);
			}
			if (!cookies.age) {
				createCookie("age", "verified", 365, cookieDomain);
			}

			location.assign(referer || window.location.origin);
			return;
		} else {
			console.log("Cookies DV and Age already set.");
		}

		if (window.location.href.includes("doc/driver-verify")) {
			window.location.href = window.location.origin;
		}

		//Search
		if (typeof unsafeWindow.searchs === "function") {
			const originalSearchs = unsafeWindow.searchs;
			unsafeWindow.searchs = function () {
				const searchValue = document.getElementById("search-input").value;
				if (searchValue) {
					window.location.pathname = `${userConfig.languages}/search/${searchValue}`;
				}
			};
		}

		//Languages
		const languagePrefixes = ["en", "ja", "ko"];
		const pathSegments = window.location.pathname.split("/").filter(Boolean);
		if (languagePrefixes.includes(pathSegments[0])) {
			if (pathSegments[0] !== userConfig.languages) {
				pathSegments[0] = userConfig.languages;
				window.location.pathname = "/" + pathSegments.join("/");
			}
		} else {
			if (userConfig.languages !== "") {
				window.location.pathname = userConfig.languages + window.location.pathname;
			}
		}

		// PREVIEW
		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 (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);

		//Default Pages
		const waterfall = document.querySelector("#waterfall");
		const waterfallStyle = css`
			#waterfall {
				display: flex;
				flex-wrap: wrap;
				justify-content: center;
			}

			#waterfall .item {
				display: flex;
				margin: 10px;
				padding: 0;
				align-items: stretch;
			}

			.row:not(.footer-bar) {
				display: flex !important;
				flex-direction: column;
			}

			.movie-box {
				width: 250px !important;
			}

			.photo-frame {
				height: auto !important;
			}

			.movie-box img {
				height: auto !important;
			}
		`;

		//Disable masonry on resize
		if (jQuery.Mason) {
			console.log("[US:DEBUG] :  Masonry Disabled");
			jQuery.Mason.prototype.resize = function () {
				// Do nothing
			};
		}

		// Main

		if (waterfall) {
			if (!window.location.href.includes("actresses")) {
				const waterfall = document.querySelector("#waterfall");
				waterfall.style = "";
				GM_addStyle(waterfallStyle);
			}

			const rows = document.querySelectorAll(".item");
			rows.forEach((row, index) => {
				row.style.position = "relative";
				row.style.removeProperty("top");
				row.style.removeProperty("left");

				// //if masonry js is not disabled, this reset the masonry positioning on resize.
				// let resizeTimeout;
				// window.addEventListener("resize", function () {
				// 	clearTimeout(resizeTimeout);
				// 	resizeTimeout = setTimeout(function () {
				// 	row.style.position = "relative";
				// 	row.style.removeProperty("top");
				// 	row.style.removeProperty("left");
				// 	}, 200);
				// });

				if (!row) return;
				const categoryCell = row.querySelector("date:first-of-type");
				if (!categoryCell) return;
				const code = detectCode(categoryCell.innerText, regexMap);
				if (code && code.match) {
					console.log("[US:DEBUG] : ", "Valid studios detected: ", code);
					let codeId = code.match.toLowerCase();
					const buttonDom = row.querySelector(".photo-info");
					const newButton = document.createElement("a");
					newButton.classList.add("preview-btn");
					newButton.setAttribute("href", "?preview=" + codeId);
					newButton.setAttribute("title", "Original DVD Preview (Not Torrent Preview!)");
					newButton.innerHTML = `Preview`;
					//buttonDom.appendChild(newButton);
					buttonDom.prepend(newButton);
					newButton.addEventListener("click", function (event) {
						event.preventDefault();
						openPreview(codeId, code.cdn);
					});
				} else {
					return;
				}
			});
		}

		// Movie Page
		const moviePage = document.querySelector(".row.movie");

		if (moviePage) {
			const code = detectCode(
				//document.querySelector('.col-md-3.info p span[style="color:#CC0000;"]').innerText,
				document.querySelector(".col-md-3.info p span:not(.header)").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.classList.add("preview-btn");
				newButton.setAttribute("href", "?preview=" + codeId);
				newButton.setAttribute("title", "Original DVD Preview (Not Torrent Preview!)");
				newButton.innerHTML = `Preview`;
				moviePage.querySelector(".col-md-3.info").prepend(newButton);
				newButton.addEventListener("click", function (event) {
					event.preventDefault();
					openPreview(codeId, code.cdn);
				});
			}
		}

		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();

					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("No match found in script data.");
								callback("");
							}
						} else {
							console.error("Request failed with status:", response.status);
							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 showToast() {
			const toast = document.createElement("div");
			toast.className = "toast";
			toast.textContent = "No Preview Available!";
			document.body.appendChild(toast);

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

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

		const menubar = document.querySelector("#navbar");
		const newLi = document.createElement("ul");
		newLi.classList.add("nav");
		newLi.classList.add("navbar-nav");
		newLi.classList.add("navbar-right");
		newLi.innerHTML = `<li><i class="fa fa-cogs"></i><a href="?setting" title="Settings">Settings</a></li>`;
		menubar.appendChild(newLi);
		newLi.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 alwaysEnglish = userConfig.alwaysEnglish ? "checked" : "";
			const devmode = userConfig.devmode ? "checked" : "";

			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="${userConfig.playerWidth}">
                    </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="${userConfig.playerVolume}"
            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 for="languages">Auto Select Language:</label>
        <select name="languages" id="languages">
            <option value="">中文</option>
            <option value="ko">한국의</option>
            <option value="ja">日本语</option>
            <option value="en">English</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="${userConfig.url}"></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 languages = document.getElementById("languages");
			if (languages) {
				languages.value = userConfig.languages || "cn";
			}

			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 languages = document.getElementById("languages");
				const apiUrl = document.getElementById("apiUrl");
				const devmode = document.getElementById("devmode");
				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.languages = languages.value;
				userConfig.url = apiUrl.value;
				userConfig.alwaysEnglish = alwaysEnglish.checked;
				userConfig.devmode = devmode.checked;

				GM_setValue("config", userConfig);

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

				location.reload();
			}
		}
	}

	main();
})();