// ==UserScript== // @name Nexus No Wait ++ // @description Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features. // @namespace NexusNoWaitPlusPlus // @author Torkelicious // @version 1.1.9 // @include https://*.nexusmods.com/* // @run-at document-idle // @iconURL https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png // @icon https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @license GPL-3.0-or-later // @downloadURL none // ==/UserScript== /* global GM_getValue, GM_setValue, GM_deleteValue, GM_xmlhttpRequest, GM_info GM */ (function () { const DEFAULT_CONFIG = { autoCloseTab: true, // Close tab after download starts skipRequirements: true, // Skip requirements popup/tab showAlerts: true, // Show errors as browser alerts refreshOnError: false, // Refresh page on error requestTimeout: 30000, // Request timeout (30 sec) closeTabTime: 1000, // Wait before closing tab (1 sec) debug: false, // Show debug messages as alerts playErrorSound: true, // Play a sound on error }; // === Settings Management === function validateSettings(settings) { if (!settings || typeof settings !== "object") return { ...DEFAULT_CONFIG }; const validated = { ...settings }; // Keep all existing settings for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) { if (typeof validated[key] !== typeof defaultValue) { validated[key] = defaultValue; } } return validated; } function loadSettings() { try { const saved = GM_getValue("nexusNoWaitConfig", null); const parsed = saved ? JSON.parse(saved) : DEFAULT_CONFIG; return validateSettings(parsed); } catch (error) { console.warn("GM storage load failed:", error); return { ...DEFAULT_CONFIG }; } } function saveSettings(settings) { try { GM_setValue("nexusNoWaitConfig", JSON.stringify(settings)); logMessage("Settings saved to GM storage", false, true); } catch (e) { console.error("Failed to save settings:", e); } } const config = Object.assign({}, DEFAULT_CONFIG, loadSettings()); // Global sound instance const errorSound = new Audio( "https://github.com/torkelicious/nexus-no-wait-pp/raw/refs/heads/main/errorsound.mp3" ); errorSound.load(); // Preload sound // Plays error sound if enabled function playErrorSound() { if (!config.playErrorSound) return; errorSound.play().catch((e) => { console.warn("Error playing sound:", e); }); } // === Error Handling === function logMessage(message, showAlert = false, isDebug = false) { if (isDebug) { console.log( "[Nexus No Wait ++]: " + message + "\nPage:" + window.location.href ); if (config.debug) { alert("[Nexus No Wait ++] (Debug):\n" + message); } return; } playErrorSound(); // Play sound before alert console.error( "[Nexus No Wait ++]: " + message + "\nPage:" + window.location.href ); if (showAlert && config.showAlerts) { alert("[Nexus No Wait ++] \n" + message); } if (config.refreshOnError) { location.reload(); } } // === URL and Navigation Handling === if ( window.location.href.includes("tab=requirements") && config.skipRequirements ) { const newUrl = window.location.href.replace( "tab=requirements", "tab=files" ); window.location.replace(newUrl); return; } // === AJAX Setup and Configuration === let ajaxRequestRaw; if (typeof GM_xmlhttpRequest !== "undefined") { ajaxRequestRaw = GM_xmlhttpRequest; } else if ( typeof GM !== "undefined" && typeof GM.xmlHttpRequest !== "undefined" ) { ajaxRequestRaw = GM.xmlHttpRequest; } // Wrapper for AJAX requests function ajaxRequest(obj) { if (!ajaxRequestRaw) { logMessage( "AJAX functionality not available (Your browser or userscript manager may not support these requests!)", true ); return; } ajaxRequestRaw({ method: obj.type, url: obj.url, data: obj.data, headers: obj.headers, timeout: config.requestTimeout, onload: function (response) { if (response.status >= 200 && response.status < 300) { obj.success(response.responseText); } else { obj.error(response); } }, onerror: function (response) { obj.error(response); }, ontimeout: function (response) { obj.error(response); }, }); } // === Button Management === function btnError(button, error) { try { if (button && button.style) button.style.color = "red"; let errorMessage = "Download failed: "; if (error) { if (typeof error === "string") { errorMessage += error; } else if (error.message) { errorMessage += error.message; } else if (error.status) { errorMessage += `HTTP ${error.status} ${error.statusText || ""}`; } else if (typeof error.responseText === "string") { errorMessage += error.responseText; } else { errorMessage += JSON.stringify(error); } } else { errorMessage += "Unknown error"; } if (button && "innerText" in button) { button.innerText = "ERROR: " + errorMessage; } logMessage(errorMessage, true); } catch (e) { logMessage( "Unknown error while handling button error: " + e.message, true ); } } function btnSuccess(button) { if (button && button.style) button.style.color = "green"; if (button && "innerText" in button) { button.innerText = "Downloading!"; } logMessage("Download started.", false, true); } function btnWait(button) { if (button && button.style) button.style.color = "yellow"; if (button && "innerText" in button) { button.innerText = "Wait..."; } logMessage("Loading...", false, true); } // Closes tab after download starts (if enabled) function closeOnDL() { if (config.autoCloseTab && !isArchiveDownload) { setTimeout(() => window.close(), config.closeTabTime); } } // fix download buttons in the action bar // determine a primary/selected file_id from the action bar or page function getPrimaryFileId() { try { // Prefer the Vortex action button in the header action bar const vortexAction = document.querySelector( '#action-nmm a[href*="file_id="]' ); if (vortexAction) { const fid = new URL(vortexAction.href, location.href).searchParams.get( "file_id" ); if (fid) return fid; } // Fallback to visible download link with file_id on page const anyFileLink = document.querySelector('a[href*="file_id="]'); if (anyFileLink) { const fid = new URL(anyFileLink.href, location.href).searchParams.get( "file_id" ); if (fid) return fid; } // Fallback to data-id on file headers (common on Files tab) const header = document.querySelector(".file-expander-header[data-id]"); if (header) { const fid = header.getAttribute("data-id"); if (fid) return fid; } } catch (e) { // ignore & return null } return null; } function isManualActionButton(el) { try { if (!el || !(el instanceof HTMLElement)) return false; if (el.classList && el.classList.contains("download-open-tab")) return true; const li = el.closest("li"); if (li && li.id === "action-manual") return true; return false; } catch { return false; } } // === Download Handling === function clickListener(event) { // Skip if this is an archive download if (isArchiveDownload) { isArchiveDownload = false; // Reset the flag return; } const selfIsElement = this && this.tagName; const href = (selfIsElement && this.href) || window.location.href; const params = new URL(href, location.href).searchParams; // Treat manual action bar button as a direct download button if (selfIsElement && isManualActionButton(this)) { if (event && typeof event.preventDefault === "function") { event.preventDefault(); } let button = this; btnWait(button); const section = document.getElementById("section"); const gameId = section ? section.dataset.gameId : this.current_game_id; let fileId = getPrimaryFileId(); if (!fileId) { btnError(button, { message: "Could not determine file ID for download (no link or file list found).", }); return; } ajaxRequest({ type: "POST", url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl", data: "fid=" + encodeURIComponent(fileId) + "&game_id=" + encodeURIComponent(gameId || ""), headers: { Origin: "https://www.nexusmods.com", Referer: href, "Sec-Fetch-Site": "same-origin", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }, success(data) { console.log("Nexus No Wait ++ [POST] raw response (preview):", String(data).slice(0, 1200)); if (!data) { btnError(button, { message: "Empty response from server" }); return; } let parsed = null; try { parsed = JSON.parse(data); } catch (e) { parsed = null; } if (parsed && parsed.url) { btnSuccess(button); document.location.href = parsed.url; closeOnDL(); return; } // Fallback looking for nxm:// or https? link in the response body const text = String(data); const nxmMatch = text.match(/(nxm:\/\/[^\s"'<>]+)/i); if (nxmMatch) { btnSuccess(button); document.location.href = nxmMatch[1]; closeOnDL(); return; } const httpMatch = text.match(/\bhttps?:\/\/[^\s"'<>]+/i); if (httpMatch) { btnSuccess(button); document.location.href = httpMatch[0]; closeOnDL(); return; } btnError(button, { message: "Could not extract download URL from server response." }); }, error(xhr) { btnError(button, xhr); }, }); return; } if (params.get("file_id")) { let button = event; if (selfIsElement && this.href) { button = this; if (event && typeof event.preventDefault === "function") { event.preventDefault(); } } btnWait(button); const section = document.getElementById("section"); const gameId = section ? section.dataset.gameId : this.current_game_id; let fileId = params.get("file_id"); if (!fileId) { fileId = params.get("id"); } const ajaxOptions = { type: "POST", url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl", data: "fid=" + fileId + "&game_id=" + gameId, headers: { Origin: "https://www.nexusmods.com", Referer: href, "Sec-Fetch-Site": "same-origin", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }, success(data) { console.log("NNW++ [POST] raw response (preview):", String(data).slice(0, 1200)); if (!data) { btnError(button, { message: "Empty response from server" }); return; } let parsed = null; try { parsed = JSON.parse(data); } catch (e) { parsed = null; } if (parsed && parsed.url) { btnSuccess(button); document.location.href = parsed.url; closeOnDL(); return; } // Fallbacks const text = String(data); const nxmMatch = text.match(/(nxm:\/\/[^\s"'<>]+)/i); if (nxmMatch) { btnSuccess(button); document.location.href = nxmMatch[1]; closeOnDL(); return; } const httpMatch = text.match(/\bhttps?:\/\/[^\s"'<>]+/i); if (httpMatch) { btnSuccess(button); document.location.href = httpMatch[0]; closeOnDL(); return; } btnError(button, { message: "No download URL returned from server" }); }, error(xhr) { btnError(button, xhr); }, }; if (!params.get("nmm")) { ajaxRequest(ajaxOptions); } else { // extract the slowDownloadButton data-download-url ajaxRequest({ type: "GET", url: href, headers: { Origin: "https://www.nexusmods.com", Referer: document.location.href, "Sec-Fetch-Site": "same-origin", "X-Requested-With": "XMLHttpRequest", }, success(data) { console.log("NNW++ [nmm GET] raw response (preview):", String(data).slice(0, 1200)); if (!data) { btnError(button, { message: "Empty response from server" }); return; } try { const doc = new DOMParser().parseFromString(data, "text/html"); const slow = doc.getElementById("slowDownloadButton"); if (slow) { const downloadUrl = slow.getAttribute("data-download-url") || slow.dataset?.downloadUrl; if (downloadUrl) { btnSuccess(button); document.location.href = downloadUrl; closeOnDL(); return; } } // fallback to JSON.parse or link extraction before handing it back to the page let parsed = null; try { parsed = JSON.parse(data); } catch (_) { parsed = null; } if (parsed && parsed.url) { btnSuccess(button); document.location.href = parsed.url; closeOnDL(); return; } const text = String(data); const nxmMatch = text.match(/(nxm:\/\/[^\s"'<>]+)/i); if (nxmMatch) { btnSuccess(button); document.location.href = nxmMatch[1]; closeOnDL(); return; } // let the site handle the link (open mod manager) btnSuccess(button); window.location.href = href; } catch (e) { btnError(button, e); } }, error(xhr) { btnError(button, xhr); }, }); } const popup = selfIsElement ? this.parentNode : null; if (popup && popup.classList.contains("popup")) { popup.getElementsByTagName("button")[0]?.click(); const popupButton = document.getElementById("popup" + fileId); if (popupButton) { btnSuccess(popupButton); closeOnDL(); } } } else if (/ModRequirementsPopUp/.test(href)) { const fileId = params.get("id"); if (fileId && selfIsElement) { this.setAttribute("id", "popup" + fileId); } } } // === Event Listeners === function addClickListener(el) { el.addEventListener("click", clickListener, true); } function addClickListeners(els) { for (let i = 0; i < els.length; i++) { addClickListener(els[i]); } } // === Automatic Downloading === function autoStartFileLink() { if (/file_id=/.test(window.location.href)) { clickListener(document.getElementById("slowDownloadButton")); } } // Automatically skips file requirements popup and downloads function autoClickRequiredFileDownload() { const observer = new MutationObserver(() => { const downloadButton = document.querySelector( ".popup-mod-requirements a.btn" ); if (downloadButton) { downloadButton.click(); const popup = document.querySelector(".popup-mod-requirements"); if (!popup) { logMessage("Popup closed", false, true); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "class"], }); } // === Archived Files Handling === const ICON_PATHS = { nmm: "https://www.nexusmods.com/assets/images/icons/icons.svg#icon-nmm", manual: "https://www.nexusmods.com/assets/images/icons/icons.svg#icon-manual", }; let isArchiveDownload = false; function archivedFile() { try { // Only run in the archived category if (!window.location.href.includes("category=archived")) { return; } // DOM queries and paths const path = `${location.protocol}//${location.host}${location.pathname}`; const downloadTemplate = (fileId) => `