Steam Workshop Downloader (Skymods/Modsbase)

Download mod via skymods.ru and modsbase.com directly from steam workshop

  1. // ==UserScript==
  2. // @name Steam Workshop Downloader (Skymods/Modsbase)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.06
  5. // @description Download mod via skymods.ru and modsbase.com directly from steam workshop
  6. // @author Skrylor - Maintainer
  7. // @author Namkazt ( nam.kazt.91@gmail.com ) - Original Author
  8. // @match https://steamcommunity.com/sharedfiles/filedetails/*
  9. // @match https://steamcommunity.com/workshop/filedetails/*
  10. // @match https://steamcommunity.com/workshop/browse/*
  11. // @connect smods.ru
  12. // @connect modsbase.com
  13. // @run-at document-end
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js
  15. // @require http://code.jquery.com/jquery-3.6.0.min.js
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_addStyle
  18. // @grant GM_notification
  19. // @grant GM_download
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_deleteValue
  23. // @license MIT
  24. // ==/UserScript==
  25.  
  26. function createElementFromHTML(htmlString) {
  27. var div = document.createElement("div");
  28. div.innerHTML = htmlString.trim();
  29. return div.firstChild;
  30. }
  31.  
  32. function getAppId() {
  33. return document.querySelector(".apphub_OtherSiteInfo a").getAttribute('data-appid');
  34. }
  35.  
  36. function isCitiesSkylines() {
  37. return (
  38. document.querySelector(".apphub_HeaderTop .apphub_AppName").innerText ===
  39. "Cities: Skylines"
  40. );
  41. }
  42.  
  43. function isCV6() {
  44. return (
  45. document.querySelector(".apphub_HeaderTop .apphub_AppName").innerText ===
  46. "Sid Meier's Civilization VI"
  47. );
  48. }
  49.  
  50. function isCollectionPage() {
  51. const collectionsLink = document.querySelector('a[href*="/workshop/browse/?section=collections"]');
  52. return collectionsLink !== null; // Returns true if the link is found
  53. }
  54.  
  55. function getDownloadId(downloadUrl) {
  56. console.log("----------- parsing download url: " + downloadUrl);
  57. var regex = /\/[^\/]*\//gm;
  58. var m;
  59. var downloadId = "";
  60. while ((m = regex.exec(downloadUrl)) !== null) {
  61. if (m.index === regex.lastIndex) {
  62. regex.lastIndex++;
  63. }
  64. if (m.index > 6) {
  65. downloadId = m[0].substr(1, m[0].length - 2);
  66. }
  67. }
  68. return downloadId;
  69. }
  70.  
  71. function getDownloadLinkFromModsBase(downloadId, referer, callback) {
  72. const formData = new FormData();
  73. formData.append("op", "download2");
  74. formData.append("id", downloadId);
  75. formData.append("rand", "");
  76. formData.append("referer", "");
  77. formData.append("method_free", "");
  78. formData.append("method_premium", "");
  79.  
  80. GM_xmlhttpRequest({
  81. method: "POST",
  82. url: "https://modsbase.com/",
  83. headers: {
  84. "Content-Type": "application/x-www-form-urlencoded",
  85. "Referer": referer,
  86. },
  87. data: new URLSearchParams(formData).toString(),
  88. onload: function(response) {
  89. if (response.status === 200) {
  90. const parser = new DOMParser();
  91. const doc = parser.parseFromString(response.responseText, "text/html");
  92. const downloadLinkElement = doc.querySelector('.download-details a');
  93.  
  94. if (downloadLinkElement) {
  95. const directDownloadLink = downloadLinkElement.href;
  96. callback(null, directDownloadLink);
  97. } else {
  98. callback("Download link not found in response", null);
  99. }
  100.  
  101. } else {
  102. callback(`Request failed with status ${response.status}`, null);
  103. }
  104. },
  105. onerror: function(error) {
  106. callback(`Request error: ${error.statusText}`, null);
  107. }
  108. });
  109. }
  110.  
  111.  
  112. function searchForMod(id, callback) {
  113. var appId = getAppId();
  114. var url = "http://catalogue.smods.ru/?s=" + id + "&app=" + appId;
  115.  
  116. console.log("----------- URL: " + url);
  117.  
  118. GM_xmlhttpRequest({
  119. anonymous: true,
  120. method: "GET",
  121. url: url,
  122. headers: {
  123. "Referer": "http://catalogue.smods.ru"
  124. },
  125. onload: function(e) {
  126. doc = new DOMParser().parseFromString(e.responseText, "text/html");
  127. if (doc.getElementsByClassName("post-inner").length > 0) {
  128. var downloadUrl = doc.querySelector(".post-inner .skymods-excerpt-btn").href;
  129. var downloadId = getDownloadId(downloadUrl);
  130. if (downloadId != undefined || downloadId != null || downloadId != "") {
  131. console.log("----------- download id: " + downloadId);
  132. var rDateStr = doc.querySelector(".post-inner .skymods-item-date").innerText;
  133. var updated = moment(rDateStr, "DD MMM at HH:mm YYYY").format(
  134. "DD MMM, YYYY"
  135. );
  136. let titleElement = doc.querySelector(".post-inner h2 a");
  137. let title = titleElement ? titleElement.textContent.trim() : "Unknown Mod Title";
  138. callback(true, downloadId, downloadUrl, updated, title);
  139. } else {
  140. callback(false, downloadId, downloadUrl, "");
  141. }
  142. } else {
  143. callback(false, downloadId, downloadUrl, "");
  144. }
  145. },
  146. onerror: function(error) {
  147. console.error("Request failed:", error);
  148. callback(false, null, null, "Error fetching mod info");
  149. }
  150. });
  151. }
  152.  
  153. function gotoRequestPage(id) {
  154. var url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + id;
  155. if (isCitiesSkylines()) {
  156. window.open('https://docs.google.com/forms/d/e/1FAIpQLSdXlq9OAWVwX5lRLNvpkMSmpKbEDY50Bl-UU3f6P7OBI2Ny3Q/viewform?c=0&w=1&entry.417177883=' + url, '_blank');
  157. } else {
  158. window.open('https://docs.google.com/forms/d/e/1FAIpQLSe7MisYbKNUlTXBcSR2clHxpwaoo0HiZ3zWto0osemubdDP1g/viewform?entry.417177883=' + url, '_blank');
  159. }
  160. }
  161.  
  162. function changeButtonGradient(btn, color1, color2) {
  163. var gradient =
  164. "linear-gradient(42deg, #" + color1 + " 35%, #" + color2 + " 65%)";
  165. btn.style.background = gradient;
  166. btn.querySelector("#DownloadTxt").style.background = gradient;
  167. }
  168.  
  169. function searchForDownloadLink(btn, downloadId, downloadUrl, modTitle) {
  170. let textNode = btn.querySelector("#DownloadTxt");
  171. let spinner = btn.querySelector(".loading-spinner");
  172. spinner.style.display = "inline-block";
  173. textNode.style.opacity = 0;
  174. btn.classList.add('loading');
  175. getDownloadLinkFromModsBase(downloadId, downloadUrl, function(err, directDownloadLink) {
  176. spinner.style.display = "none";
  177. textNode.style.opacity = 1;
  178. btn.classList.remove('loading');
  179. if (err) {
  180. console.error(err);
  181. textNode.innerText = "Failed to get link";
  182. return;
  183. }
  184.  
  185. textNode.innerText = "Downloading...";
  186. spinner.style.display = "inline-block";
  187. textNode.style.opacity = 0;
  188.  
  189.  
  190. let fileName = modTitle.replace(/[^a-zA-Z0-9_.-]/g, '_') + ".zip";
  191. fileName = fileName.substring(0, 250);
  192.  
  193. GM_download({
  194. url: directDownloadLink,
  195. name: fileName,
  196. onload: function() {
  197. spinner.style.display = "none";
  198. textNode.style.opacity = 1;
  199. textNode.innerHTML = "Downloaded!";
  200. },
  201. onerror: function(error) {
  202. spinner.style.display = "none";
  203. textNode.style.opacity = 1;
  204. console.error("Download error:", error);
  205. textNode.innerText = "Download Failed";
  206. }
  207. });
  208. });
  209. }
  210.  
  211. var DOWNLOAD_BTN_TEMPLATE = `
  212. <button id="DownloadBtn" class="steam-button">
  213. <span id="DownloadTxt">Download</span>
  214. <span class="loading-spinner" style="display: none;">
  215. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  216. <circle cx="8" cy="8" r="7" stroke="#fff" stroke-width="2" style="animation: rotate 1s linear infinite;"/>
  217. </svg>
  218. </span>
  219. </button>
  220. `;
  221.  
  222. GM_addStyle(`
  223. .steam-button {
  224. background-color: #7cb342;
  225. border: none;
  226. color: white;
  227. padding: 6px 12px;
  228. border-radius: 4px;
  229. cursor: pointer;
  230. text-decoration: none;
  231. font-weight: bold;
  232. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  233. transition: background-color 0.2s ease, transform 0.1s ease;
  234. display: inline-block;
  235. position: relative;
  236. }
  237.  
  238. .steam-button:hover {
  239. background-color: #669933;
  240. transform: scale(1.02);
  241. }
  242.  
  243. .steam-button.loading #DownloadTxt {
  244. opacity: 0;
  245. transition: opacity 0.2s ease;
  246. }
  247.  
  248. .steam-button.loading .loading-spinner {
  249. display: inline-block;
  250. position: absolute;
  251. top: 50%;
  252. left: 50%;
  253. transform: translate(-50%, -50%);
  254. }
  255.  
  256. .steam-button .loading-spinner svg {
  257. animation: rotate 1s linear infinite;
  258. }
  259.  
  260. .steam-button.not-available {
  261. background-color: #d32f2f;
  262. }
  263.  
  264. .steam-button.not-available:hover {
  265. background-color: #c62828;
  266. }
  267.  
  268. .game_area_purchase_game > div {
  269. height: 30px; /* Replace 30px with the actual height of the Subscribe button */
  270. display: flex;
  271. align-items: center;
  272. }
  273.  
  274. .game_area_purchase_game > div > a#SubscribeItemBtn + button.steam-button {
  275. margin-left: -10px; /* Adjust this value as needed for left alignment */
  276. margin-right: 0; /* Ensure no right margin */
  277. }
  278.  
  279.  
  280. @keyframes rotate {
  281. from { transform: rotate(0deg); }
  282. to { transform: rotate(360deg); }
  283. }
  284.  
  285. .steam-button.loading {
  286. opacity: 0.7;
  287. pointer-events: none;
  288. }
  289.  
  290. /* Floating Downloader Styles */
  291. .floating-downloader {
  292. position: fixed;
  293. right: 20px;
  294. top: 50%;
  295. transform: translateY(-50%);
  296. background: #1b2838;
  297. border: 1px solid #4582a5;
  298. border-radius: 4px;
  299. padding: 15px;
  300. width: 300px;
  301. color: #fff;
  302. z-index: 9999;
  303. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  304. display: none; /* Initially hidden */
  305. }
  306.  
  307. .floating-downloader-header {
  308. display: flex;
  309. justify-content: space-between;
  310. align-items: center;
  311. margin-bottom: 10px;
  312. border-bottom: 1px solid #4582a5;
  313. padding-bottom: 10px;
  314. }
  315.  
  316. .floating-downloader-title {
  317. font-weight: bold;
  318. font-size: 16px;
  319. }
  320.  
  321. .floating-downloader-close {
  322. background: none;
  323. border: none;
  324. color: #fff;
  325. cursor: pointer;
  326. font-size: 18px;
  327. }
  328.  
  329. .download-progress {
  330. margin: 10px 0;
  331. }
  332.  
  333. .progress-bar {
  334. width: 100%;
  335. height: 20px;
  336. background: #2a475e;
  337. border-radius: 10px;
  338. overflow: hidden;
  339. }
  340.  
  341. .progress-bar-fill {
  342. height: 100%;
  343. background: #66c0f4;
  344. transition: width 0.3s ease;
  345. }
  346.  
  347. .download-stats {
  348. display: flex;
  349. justify-content: space-between;
  350. margin-top: 5px;
  351. font-size: 12px;
  352. }
  353.  
  354. .download-list {
  355. max-height: 200px;
  356. overflow-y: auto;
  357. margin: 10px 0;
  358. }
  359.  
  360. .download-item {
  361. display: flex;
  362. justify-content: space-between;
  363. align-items: center;
  364. padding: 5px 0;
  365. border-bottom: 1px solid #2a475e;
  366. }
  367.  
  368. .download-item-status {
  369. font-size: 12px;
  370. color: #66c0f4;
  371. }
  372. `);
  373.  
  374. const FLOATING_DOWNLOADER_TEMPLATE = `
  375. <div class="floating-downloader" id="batchDownloader">
  376. <div class="floating-downloader-header">
  377. <div class="floating-downloader-title">Batch Downloader</div>
  378. <button class="floating-downloader-close">×</button>
  379. </div>
  380. <div class="download-progress">
  381. <div class="progress-bar">
  382. <div class="progress-bar-fill" style="width: 0%"></div>
  383. </div>
  384. <div class="download-stats">
  385. <span class="downloads-completed">0/0 completed</span>
  386. <span class="download-size">0 MB</span>
  387. </div>
  388. </div>
  389. <div class="download-list"></div>
  390. <button id="startBatchDownload" class="steam-button">Download All</button>
  391. </div>
  392. `;
  393.  
  394. class BatchDownloadManager {
  395. constructor() {
  396. this.downloads = new Map();
  397. this.completed = 0;
  398. this.total = 0;
  399. this.currentlyDownloading = false;
  400. this.downloadQueue = [];
  401. this.initializeUI();
  402. }
  403.  
  404. initializeUI() {
  405. const downloaderEl = createElementFromHTML(FLOATING_DOWNLOADER_TEMPLATE);
  406. document.body.appendChild(downloaderEl);
  407.  
  408. downloaderEl.querySelector('.floating-downloader-close').addEventListener('click', () => {
  409. downloaderEl.style.display = 'none';
  410. });
  411.  
  412. document.getElementById('startBatchDownload').addEventListener('click', () => {
  413. this.startBatchDownload();
  414. });
  415. }
  416.  
  417. addDownload(workshopId, modTitle, downloadId, downloadUrl) {
  418. this.downloads.set(workshopId, { modTitle, downloadId, downloadUrl, status: 'pending' });
  419. this.updateUI();
  420. }
  421.  
  422. async startBatchDownload() {
  423. if (this.currentlyDownloading) return;
  424. this.currentlyDownloading = true;
  425. this.downloadQueue = Array.from(this.downloads.entries()).filter(([_, info]) => info.status === 'pending');
  426. this.total = this.downloadQueue.length;
  427. this.completed = 0;
  428. this.processNextDownload();
  429. }
  430.  
  431. async processNextDownload() {
  432. if (this.downloadQueue.length === 0) {
  433. this.currentlyDownloading = false;
  434. this.updateUI();
  435. return;
  436. }
  437.  
  438. const [workshopId, info] = this.downloadQueue.shift();
  439.  
  440. try {
  441. await this.downloadMod(workshopId, info);
  442. this.completed++;
  443. info.status = 'completed';
  444. } catch (error) {
  445. console.error(`Failed to download ${info.modTitle}:`, error);
  446. info.status = 'failed';
  447. }
  448.  
  449. this.updateUI();
  450. this.processNextDownload();
  451. }
  452.  
  453. async downloadMod(workshopId, info) {
  454. return new Promise((resolve, reject) => {
  455. getDownloadLinkFromModsBase(info.downloadId, info.downloadUrl, (err, directDownloadLink) => {
  456. if (err) {
  457. reject(err);
  458. return;
  459. }
  460.  
  461. let fileName = info.modTitle.replace(/[^a-zA-Z0-9_.-]/g, '_') + ".zip";
  462. fileName = fileName.substring(0, 250);
  463.  
  464. GM_download({
  465. url: directDownloadLink,
  466. name: fileName,
  467. onload: resolve,
  468. onerror: reject
  469. });
  470. });
  471. });
  472. }
  473.  
  474. updateUI() {
  475. const progress = (this.completed / this.total) * 100 || 0;
  476. const progressBar = document.querySelector('.progress-bar-fill');
  477. const statsEl = document.querySelector('.downloads-completed');
  478. const downloadList = document.querySelector('.download-list');
  479.  
  480. progressBar.style.width = `${progress}%`;
  481. statsEl.textContent = `${this.completed}/${this.total} completed`;
  482.  
  483. downloadList.innerHTML = '';
  484. this.downloads.forEach((info, workshopId) => {
  485. const itemEl = document.createElement('div');
  486. itemEl.className = 'download-item';
  487. itemEl.innerHTML = `<span class="download-item-title">${info.modTitle}</span><span class="download-item-status">${info.status}</span>`;
  488. downloadList.appendChild(itemEl);
  489. });
  490. }
  491. }
  492.  
  493.  
  494. function init() {
  495. $(document).ready(function() {
  496. const batchManager = new BatchDownloadManager(); // Initialize here for collection pages
  497. if (window.location.href.indexOf("appid=") >= 0) {
  498. console.log("----------- Workshop browser page");
  499. var itemList = document.querySelectorAll(".workshopItemPreviewHolder");
  500.  
  501. for (var item of itemList) {
  502. var itemDownloadId = item.id.replace("sharedfile_", "");
  503. var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
  504.  
  505. searchForMod(itemDownloadId,
  506. (function() {
  507. var workshopId = itemDownloadId;
  508. var btn = btnNode;
  509. var textNode = btn.querySelector("#DownloadTxt");
  510. textNode.innerText = "Checking for mod";
  511. return function(found, downloadId, downloadUrl, updated, modTitle) {
  512. if (found) {
  513. textNode.innerText = "Download - " + updated;
  514. btn.addEventListener("click", function() {
  515. searchForDownloadLink(btn, downloadId, downloadUrl, modTitle);
  516. });
  517. } else {
  518. textNode.innerText = "Not Available (REQUEST)";
  519. btn.classList.add("not-available");
  520. btn.addEventListener("click", function() {
  521. gotoRequestPage(workshopId);
  522. });
  523. }
  524. };
  525. })()
  526. );
  527.  
  528. var subscriptionControls = item.parentNode.querySelector('.subscriptionControls');
  529. if (subscriptionControls) subscriptionControls.appendChild(btnNode);
  530. }
  531. } else if (isCollectionPage()) {
  532. console.log("----------- Collection page");
  533. var itemList = document.querySelectorAll(".collectionItem");
  534. for (var item of itemList) {
  535. var itemDownloadId = item.id.replace("sharedfile_", "");
  536. var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
  537. searchForMod(itemDownloadId,
  538. (function() {
  539. var workshopId = itemDownloadId;
  540. var btn = btnNode;
  541. var textNode = btn.querySelector("#DownloadTxt");
  542. textNode.innerText = "Checking for mod";
  543. return function(found, downloadId, downloadUrl, updated, modTitle) {
  544. if (found) {
  545. textNode.innerText = "Download - " + updated;
  546. btn.addEventListener("click", function() {
  547. searchForDownloadLink(btn, downloadId, downloadUrl, modTitle);
  548. });
  549. } else {
  550. textNode.innerText = "Not Available (REQUEST)";
  551. btn.classList.add("not-available");
  552. btn.addEventListener("click", function() {
  553. gotoRequestPage(workshopId);
  554. });
  555. }
  556. };
  557. })()
  558. );
  559. var subscriptionControls = item.querySelector('.subscriptionControls');
  560. if (subscriptionControls) subscriptionControls.appendChild(btnNode);
  561.  
  562. }
  563. // Add items to batch manager for collection page
  564. document.querySelectorAll('.collectionItem').forEach(item => {
  565. const itemDownloadId = item.id.replace("sharedfile_", "");
  566. searchForMod(itemDownloadId, (found, downloadId, downloadUrl, updated, modTitle) => {
  567. if (found) {
  568. batchManager.addDownload(itemDownloadId, modTitle, downloadId, downloadUrl);
  569. }
  570. });
  571. });
  572.  
  573. // Show the floating downloader after processing all items
  574. document.getElementById('batchDownloader').style.display = 'block';
  575. } else {
  576. console.log("----------- Single item page");
  577. var publishedfileid = window.location.href.match(/id=(\d+)/)[1];
  578. var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
  579. var textNode = btnNode.querySelector("#DownloadTxt");
  580. textNode.innerText = "Checking for mod";
  581. searchForMod(publishedfileid, function(
  582. found,
  583. downloadId,
  584. downloadUrl,
  585. updated,
  586. modTitle
  587. ) {
  588. if (found) {
  589. textNode.innerText = "Download - " + updated;
  590. btnNode.addEventListener("click", function() {
  591. searchForDownloadLink(btnNode, downloadId, downloadUrl, modTitle);
  592. });
  593. } else {
  594. textNode.innerText = "Not Available (REQUEST)";
  595. btnNode.classList.add("not-available");
  596. btnNode.addEventListener("click", function() {
  597. gotoRequestPage(publishedfileid);
  598. });
  599. }
  600. });
  601.  
  602. const subscribeButton = document.getElementById("SubscribeItemBtn");
  603. if (subscribeButton) {
  604. subscribeButton.parentNode.insertBefore(btnNode, subscribeButton.nextSibling);
  605. } else {
  606. const subscriptionControls = document.querySelector('.subscriptionControls');
  607. if (subscriptionControls) {
  608. subscriptionControls.insertBefore(btnNode, subscriptionControls.firstChild);
  609. } else {
  610. console.error("Neither Subscribe button nor Subscription Controls found. Appending to body.");
  611. document.body.appendChild(btnNode);
  612. }
  613. }
  614. }
  615. console.log("----------- Init successfully");
  616. });
  617. }
  618.  
  619. (function() {
  620. "use strict";
  621.  
  622. init();
  623. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址