您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Download every mods of a collection in a single click
当前为
// ==UserScript== // @name Nexus Download Collection // @namespace NDC // @version 0.5 // @description Download every mods of a collection in a single click // @author Drigtime // @match https://next.nexusmods.com/*/collections* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // ==/UserScript== (async function () { 'use strict'; /** CORSViaGM BEGINING */ const CORSViaGM = document.body.appendChild(Object.assign(document.createElement('div'), { id: 'CORSViaGM' })) addEventListener('fetchViaGM', e => GM_fetch(e.detail.forwardingFetch)) CORSViaGM.init = function (window) { if (!window) throw 'The `window` parameter must be passed in!' window.fetchViaGM = fetchViaGM.bind(window) // Support for service worker window.forwardingFetch = new BroadcastChannel('forwardingFetch') window.forwardingFetch.onmessage = async e => { const req = e.data const { url } = req const res = await fetchViaGM(url, req) const response = await res.blob() window.forwardingFetch.postMessage({ type: 'fetchResponse', url, response }) } window._CORSViaGM && window._CORSViaGM.inited.done() const info = '🙉 CORS-via-GM initiated!' console.info(info) return info } function GM_fetch(p) { GM_xmlhttpRequest({ ...p.init, url: p.url, method: p.init.method || 'GET', onload: responseDetails => p.res(new Response(responseDetails.response, responseDetails)) }) } function fetchViaGM(url, init) { let _r const p = new Promise(r => _r = r) p.res = _r p.url = url p.init = init || {} dispatchEvent(new CustomEvent('fetchViaGM', { detail: { forwardingFetch: p } })) return p } CORSViaGM.init(window); /** CORSViaGM END */ function createButton(classes, text, content) { const button = document.createElement('button'); button.type = 'button'; for (const className of classes) { button.classList.add(className); } button.innerHTML = text; if (content) { button.appendChild(content); } return button; } // Function to create a link with specified classes, text, and href function createLink(classes, text, href) { const link = document.createElement('a'); link.className = classes; link.innerHTML = text; link.href = href; return link; } function createButtonGroup(options) { const defaultOptions = { text: '', btnGroupClasses: [], btnClasses: [], callbacks: [], dropdownItems: [] }; options = { ...defaultOptions, ...options }; // Create the button group container const buttonGroup = document.createElement('div'); const buttonGroupClasses = [ ...'inline-flex position-relative vertical-align-middle align-middle w-full'.split(" "), ...options.btnGroupClasses ]; buttonGroup.className = buttonGroupClasses.join(" "); // Create the main button const mainButton = createButton([ ..."font-montserrat font-semibold text-sm leading-none tracking-wider uppercase leading-none flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 rounded-l px-2 py-1 cursor-pointer bg-primary fill-font-primary text-font-primary border-transparent focus:bg-primary-lighter hover:bg-primary-darker w-full justify-between".split(" "), ...options.btnClasses ], options.text); for (const [eventName, callback] of Object.entries(options.callbacks)) { mainButton.addEventListener(eventName, callback); } // Append the buttons and dropdown to the button group buttonGroup.appendChild(mainButton); buttonGroup.mainButton = mainButton; if (options.dropdownItems.length > 0) { // Create the button with the caret icon const dropdownButton = createButton([ ..."font-montserrat font-semibold text-sm leading-none tracking-wider uppercase leading-none flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 rounded-r px-2 py-1 cursor-pointer bg-primary fill-font-primary text-font-primary border-transparent focus:bg-primary-lighter hover:bg-primary-darker".split(" "), ], ''); const caretIcon = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); caretIcon.setAttribute('viewBox', '0 0 24 24'); caretIcon.style.width = '1.5rem'; caretIcon.style.height = '1.5rem'; caretIcon.setAttribute('role', 'presentation'); const caretIconPath = document.createElementNS("http://www.w3.org/2000/svg", 'path'); caretIconPath.style.fill = 'currentColor'; caretIconPath.setAttribute('d', 'M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z'); caretIcon.appendChild(caretIconPath); dropdownButton.appendChild(caretIcon); // Create the dropdown container const dropdownContainer = document.createElement('div'); dropdownContainer.className = 'absolute z-10 hidden min-w-48 py-1 px-0 mt-1 text-base text-gray-600 border-secondary-lighter bg-secondary border border-gray-200 rounded-md shadow-lg outline-none'; for (const dropdownItem of options.dropdownItems) { // Create the dropdown item const dropdownItemText = dropdownItem.text; const dropdownItemElement = createButton([ ...'font-montserrat text-sm font-semibold uppercase leading-none tracking-wider first:rounded-t last:rounded-b relative flex w-full items-center gap-x-2 p-2 text-left font-normal hover:bg-secondary-lighter hover:text-primary focus:shadow-accent focus:z-10 focus:outline-none text-start justify-between'.split(" "), ], dropdownItemText); for (const [eventName, callback] of Object.entries(dropdownItem.callbacks)) { dropdownItemElement.addEventListener(eventName, callback); } // Append the elements to the dropdown container dropdownContainer.appendChild(dropdownItemElement); } buttonGroup.appendChild(dropdownButton); buttonGroup.appendChild(dropdownContainer); buttonGroup.dropdownButton = dropdownButton; buttonGroup.dropdownContainer = dropdownContainer; // Add event listener to toggle dropdown visibility dropdownButton.addEventListener('click', () => { // we need to add tranform translate to the dropdown container, so we need to get the width of the main button and add it to the dropdown container const mainButtonWidth = mainButton.offsetWidth; const mainButtonHeight = mainButton.offsetHeight; const dropdownButtonWidth = dropdownButton.offsetWidth; dropdownContainer.classList.toggle('hidden'); const dropdownContainerWidth = dropdownContainer.offsetWidth; dropdownContainer.style.transform = `translate(${(mainButtonWidth + dropdownButtonWidth) - dropdownContainerWidth}px, ${mainButtonHeight}px)`; }); document.addEventListener('click', (event) => { if (!dropdownButton.contains(event.target) && !dropdownContainer.contains(event.target)) { // transform translate to the dropdown button dropdownContainer.style dropdownContainer.classList.add('hidden'); } }); } // return the button group and its children return buttonGroup; } function createProgressBar() { const progressBar = document.createElement("div"); progressBar.classList.add("mt-2", "w-full", "bg-gray-200", "rounded-full"); progressBar.style.display = "none"; progressBar.style.height = "1.1rem"; progressBar.style.backgroundColor = "rgb(229 231 235 / 1)"; const bar = document.createElement("div"); bar.classList.add("bg-blue-600", "rounded-full"); bar.style.height = "1.1rem"; bar.style.fontSize = "0.8rem"; bar.style.padding = "0 5px"; bar.style.width = "0%"; bar.style.backgroundColor = "rgb(28 100 242 / 1)"; progressBar.appendChild(bar); progressBar.bar = bar; return progressBar; } const getModCollection = async (gameId, collectionId) => { const response = await fetch("https://next.nexusmods.com/api/graphql", { "headers": { "accept": "*/*", "accept-language": "fr;q=0.5", "api-version": "2023-09-05", "content-type": "application/json", "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Brave\";v=\"120\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "sec-gpc": "1" }, "referrer": `https://next.nexusmods.com/${gameId}/collections/${collectionId}?tab=mods`, "referrerPolicy": "strict-origin-when-cross-origin", "body": JSON.stringify({ "query": "query CollectionRevisionMods ($revision: Int, $slug: String!, $viewAdultContent: Boolean) { collectionRevision (revision: $revision, slug: $slug, viewAdultContent: $viewAdultContent) { externalResources { id, name, resourceType, resourceUrl }, modFiles { fileId, optional, file { fileId, name, scanned, size, sizeInBytes, version, mod { adult, author, category, modId, name, pictureUrl, summary, version, game { domainName }, uploader { avatar, memberId, name } } } } } }", "variables": { "slug": collectionId, "viewAdultContent": true }, "operationName": "CollectionRevisionMods" }), "method": "POST", "mode": "cors", "credentials": "include" }); const data = await response.json(); data.data.collectionRevision.modFiles = data.data.collectionRevision.modFiles.map(modFile => { modFile.file.url = `https://www.nexusmods.com/${gameId}/mods/${modFile.file.mod.modId}?tab=files&file_id=${modFile.file.fileId}`; return modFile; }); return data.data.collectionRevision; } const getSlowDownloadModLink = async (mod, nmm) => { let downloadUrl = ''; const url = nmm ? mod.file.url + '&nmm=1' : mod.file.url; const response = await fetchViaGM(url, { "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "accept-language": "fr;q=0.6", "cache-control": "max-age=0", "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Brave\";v=\"120\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1", "sec-gpc": "1", "upgrade-insecure-requests": "1" }, "referrer": url, "referrerPolicy": "strict-origin-when-cross-origin", "body": null, "method": "GET", "mode": "cors", "credentials": "include" }); const text = await response.text(); const xml = new DOMParser().parseFromString(text, "text/html"); if (nmm) { const slow = xml.getElementById("slowDownloadButton"); downloadUrl = slow.getAttribute("data-download-url"); } else { // const current_game_id = text.split("\n").find(line => line.includes("window.current_game_id = ")).split("window.current_game_id = ")[1].split(";")[0]; // const response = await fetchViaGM("https://www.nexusmods.com/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl", { // "headers": { // "accept": "*/*", // "accept-language": "fr;q=0.5", // "content-type": "application/x-www-form-urlencoded; charset=UTF-8", // "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Brave\";v=\"120\"", // "sec-ch-ua-mobile": "?0", // "sec-ch-ua-platform": "\"Windows\"", // "sec-fetch-dest": "empty", // "sec-fetch-mode": "no-cors", // "sec-fetch-site": "same-origin", // "sec-gpc": "1", // "x-requested-with": "XMLHttpRequest" // }, // // set cookie // "referrer": url, // "referrerPolicy": "strict-origin-when-cross-origin", // "body": `fid=${mod.file.fileId}&game_id${current_game_id}`, // "method": "POST", // "mode": "cors", // "credentials": "include" // }); // const data = await response.json(); // downloadUrl = data.url; downloadUrl = url; } return downloadUrl; }; const addModToVortex = async (mod) => { const downloadUrl = await getSlowDownloadModLink(mod, true); document.location.href = downloadUrl; }; const downloadModFile = async (mod) => { const downloadUrl = await getSlowDownloadModLink(mod, false); window.open(downloadUrl, "_blank"); }; const downloadMods = async (mods, callback) => { let downloadProgress = 0; let downloadProgressPercent = 0; progressBar.style.display = "block"; for (const mod of mods) { await callback(mod); console.log(`[NDC] Opened : ${mod.file.url}`); downloadProgress += 1; downloadProgressPercent = downloadProgress / mods.length * 100; progressBar.bar.style.width = `${downloadProgressPercent}%`; progressBar.bar.innerText = `${Math.round(downloadProgressPercent)}%`; console.log(`[NDC] Progress : ${Math.round(downloadProgressPercent)}%`); } progressBar.style.display = "none"; progressBar.bar.style.width = "0%"; }; const manualDownloadMods = async (mods) => { // prompt message to inform that the action needs preparation const message = 'This action requires a few preparations so that everything works fine.\n\n' + '1. In your browser, allow pop-ups for "next.nexusmods.com".\n' + '2. In your browser settings, disable "Ask where to save each file before downloading."\n\n' + 'When the script is gonna start it will open a lot of tabs (one for each mod), don\'t close them, they will close automatically when the download start.\n\n' + 'Are you ready to start ?'; const confirm = window.confirm(message); if (!confirm) { return; } await downloadMods(mods, downloadModFile); }; const progressBar = createProgressBar(); const loadingContainer = createButtonGroup({ text: `Loading...`, btnClasses: ["text-center", "rounded"] }); const NDCContainer = document.createElement("div"); NDCContainer.id = "NDCContainer"; NDCContainer.classList.add("flex", "flex-col", "gap-y-2", "w-full", "mb-2"); // add addToVortexButtonGroup and downloadFilesButtonGroup in a row const NDCContainerButtonGroup = document.createElement("div"); NDCContainerButtonGroup.classList.add("flex", "gap-2", "sm:flex-row", "flex-col"); NDCContainer.appendChild(progressBar); let previousHash = null; let mods = []; let mandatoryMods = []; let optionalMods = []; const handleHashChange = async () => { if (next.router.state.route === "/[gameDomain]/collections/[collectionSlug]") { const { gameDomain, collectionSlug, tab } = next.router.query; if (previousHash !== `${gameDomain}/${collectionSlug}`) { if (tab === "mods") { const tabcontentMods = document.querySelector("#tabcontent-mods > div > div > div"); tabcontentMods.prepend(loadingContainer); } mods = await getModCollection(gameDomain, collectionSlug); mandatoryMods = mods.modFiles.filter(mod => !mod.optional); optionalMods = mods.modFiles.filter(mod => mod.optional); if (tab === "mods") { loadingContainer.remove(); } } previousHash = `${gameDomain}/${collectionSlug}`; if (tab === "mods") { // Example usage: const addToVortexButtonGroup = createButtonGroup({ text: `Add all mods to vortex <span class="p-2 bg-secondary rounded-full text-xs text-white whitespace-nowrap">${mods.modFiles.length} mods</span>`, callbacks: { click: () => downloadMods([...mandatoryMods, ...optionalMods], addModToVortex), }, dropdownItems: [ { text: `Add all mandatory mods <span class="p-2 bg-primary rounded-full text-xs text-white whitespace-nowrap">${mandatoryMods.length} mods</span>`, callbacks: { click: () => downloadMods(mandatoryMods, addModToVortex), }, }, { text: `Add all optional mods <span class="p-2 bg-primary rounded-full text-xs text-white whitespace-nowrap">${optionalMods.length} mods</span>`, callbacks: { click: () => downloadMods(optionalMods, addModToVortex), }, }, ] }); // const downloadFilesButtonGroup = createButtonGroup({ // text: `Download all mods files <span class="p-2 bg-secondary rounded-full text-xs text-white whitespace-nowrap">${mods.modFiles.length} mods</span>`, // callbacks: { // click: () => manualDownloadMods([...mandatoryMods, ...optionalMods]), // }, // dropdownItems: [ // { // text: `Download all mandatory mods files <span class="p-2 bg-primary rounded-full text-xs text-white whitespace-nowrap">${mandatoryMods.length} mods</span>`, // callbacks: { // click: () => manualDownloadMods(mandatoryMods), // }, // }, // { // text: `Download all optional mods files <span class="p-2 bg-primary rounded-full text-xs text-white whitespace-nowrap">${optionalMods.length} mods</span>`, // callbacks: { // click: () => manualDownloadMods(optionalMods), // }, // }, // ] // }); const tabcontentMods = document.querySelector("#tabcontent-mods > div > div > div"); NDCContainer.innerHTML = ""; NDCContainerButtonGroup.innerHTML = ""; NDCContainerButtonGroup.appendChild(addToVortexButtonGroup); // NDCContainerButtonGroup.appendChild(downloadFilesButtonGroup); NDCContainer.appendChild(NDCContainerButtonGroup); NDCContainer.appendChild(progressBar); // add progress bar tabcontentMods.prepend(NDCContainer); } } } // Add an event listener for the hashchange event next.router.events.on('routeChangeComplete', handleHashChange); handleHashChange(); })();