Nexus Download Collection

Download every mods of a collection in a single click

当前为 2023-12-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Nexus Download Collection
// @namespace    NDC
// @version      0.2
// @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==

(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 */

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

		return data.data.collectionRevision;
	}

	// create a button to open all urls in new tabs
	const button = document.createElement("button");
	button.classList.add("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", "px-4", "py-1", "cursor-pointer", "bg-primary", "fill-font-primary", "text-font-primary", "border-transparent", "focus:bg-primary-lighter", "hover:bg-primary-darker", "add-to-vortex-button");
	button.innerText = "Download all mods";

	const progress = document.createElement("div");
	progress.classList.add("mt-2", "w-full", "bg-gray-200", "rounded-full");
	progress.style.display = "none";
	progress.style.height = "1.1rem";
	progress.style.backgroundColor = "rgb(229 231 235 / 1)";

	const progressBar = document.createElement("div");
	progressBar.classList.add("bg-blue-600", "rounded-full");
	progressBar.style.height = "1.1rem";
	progressBar.style.fontSize = "0.8rem";
	progressBar.style.padding = "0 5px";
	progressBar.style.width = "0%";
	progressBar.style.backgroundColor = "rgb(28 100 242 / 1)";

	progress.appendChild(progressBar);

	button.onclick = async () => {
		// from current document get gameId and collectionId eg. https://next.nexusmods.com/newvegas/collections/jscbqj
		const gameId = document.location.pathname.split("/")[1];
		const collectionId = document.location.pathname.split("/")[3];

		// open all modFiles in new tabs
		const modCollection = await getModCollection(gameId, collectionId);
		const { modFiles, externalResources } = modCollection;

		const modUrls = modFiles.map(modFile => {
			// https://www.nexusmods.com/newvegas/mods/45126?tab=files&file_id=103242&nmm=1
			const modId = modFile.file.mod.modId;
			const gameId = modFile.file.mod.game.domainName;
			const fileId = modFile.file.fileId;
			return `https://www.nexusmods.com/${gameId}/mods/${modId}?tab=files&file_id=${fileId}&nmm=1`;
		});

		// const externalUrls = externalResources.map(externalResource => {
		//   return externalResource.resourceUrl;
		// });

		const urls = [
			...modUrls,
			// ...externalUrls
		];

		progress.style.display = "block";

		let downloadProgress = 0;
		let downloadProgressPercent = 0;

		const downloadMod = async (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");
			const slow = xml.getElementById("slowDownloadButton");
			const downloadUrl = slow.getAttribute("data-download-url");

			document.location.href = downloadUrl; // open downloadUrl to Nexusmods
			console.log(`[NDC] Opened : ${downloadUrl}`);

			downloadProgress += 1;
			downloadProgressPercent = downloadProgress / urls.length * 100;

			progressBar.style.width = `${downloadProgressPercent}%`;
			progressBar.innerText = `${Math.round(downloadProgressPercent)}%`;
			console.log(`[NDC] Progress : ${Math.round(downloadProgressPercent)}%`);
		};

		const downloadAllMods = async () => {
			const promises = urls.map((url, index) => {
				return new Promise((resolve) => {
					setTimeout(async () => {
						await downloadMod(url, index);
						resolve();
					}, index * 250);
				});
			});

			await Promise.all(promises);

			progress.style.display = "none";
			progressBar.style.width = "0%";
		};

		await downloadAllMods();
	}

	const observerCallback = () => {
		// on element div id="tabcontent-mods" appear append button
		const tabcontentMods = document.querySelector("#tabcontent-mods");
		if (tabcontentMods) {
			document.querySelector("#tabcontent-mods > div > div > div").prepend(progress);
			document.querySelector("#tabcontent-mods > div > div > div").prepend(button);
		}
	};

	// on element div id="tabcontent-mods" appear append button
	const observer = new MutationObserver(observerCallback);

	const sectionWrapper = document.querySelector("#collection-tabs > section.py-4.section.bg-background-primary > div");
	// sectionWrapper child is the content of a tab, so sometime its (id="tabcontent-mods") sometime its (id="tabcontent-comments")
	observer.observe(sectionWrapper, { childList: true });
	// trigger the observer callback for the first time
	observerCallback();

})();