// ==UserScript==
// @name SkyMods downloader for steam
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Download mod via skymods.ru and modsbase.com directly from steam workshop
// @author Skrylor - Maintainer
// @author Namkazt ( [email protected] ) - Original Author
// @match https://steamcommunity.com/sharedfiles/filedetails/*
// @match https://steamcommunity.com/workshop/filedetails/*
// @match https://steamcommunity.com/workshop/browse/*
// @connect smods.ru
// @connect modsbase.com
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js
// @require http://code.jquery.com/jquery-3.6.0.min.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @license MIT
// ==/UserScript==
function createElementFromHTML(htmlString) {
var div = document.createElement("div");
div.innerHTML = htmlString.trim();
return div.firstChild;
}
function getAppId() {
return document.querySelector(".apphub_OtherSiteInfo a").getAttribute('data-appid');
}
function isCitiesSkylines() {
return (
document.querySelector(".apphub_HeaderTop .apphub_AppName").innerText ===
"Cities: Skylines"
);
}
function isCV6() {
return (
document.querySelector(".apphub_HeaderTop .apphub_AppName").innerText ===
"Sid Meier's Civilization VI"
);
}
function isCollectionPage() {
return $("mainContentsCollection") != null;
}
function getDownloadId(downloadUrl) {
console.log("----------- parsing download url: " + downloadUrl);
var regex = /\/[^\/]*\//gm;
var m;
var downloadId = "";
while ((m = regex.exec(downloadUrl)) !== null) {
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
if (m.index > 6) {
downloadId = m[0].substr(1, m[0].length - 2);
}
}
return downloadId;
}
function getDownloadLinkFromModsBase(downloadId, referer, callback) {
const formData = new FormData();
formData.append("op", "download2");
formData.append("id", downloadId);
formData.append("rand", "");
formData.append("referer", "");
formData.append("method_free", "");
formData.append("method_premium", "");
GM_xmlhttpRequest({
method: "POST",
url: "https://modsbase.com/",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": referer,
},
data: new URLSearchParams(formData).toString(),
onload: function(response) {
if (response.status === 200) {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, "text/html");
const downloadLinkElement = doc.querySelector('.download-details a');
if (downloadLinkElement) {
const directDownloadLink = downloadLinkElement.href;
callback(null, directDownloadLink);
} else {
callback("Download link not found in response", null);
}
} else {
callback(`Request failed with status ${response.status}`, null);
}
},
onerror: function(error) {
callback(`Request error: ${error.statusText}`, null);
}
});
}
function searchForMod(id, callback) {
var appId = getAppId();
var url = "http://catalogue.smods.ru/?s=" + id + "&app=" + appId;
console.log("----------- URL: " + url);
GM_xmlhttpRequest({
anonymous: true,
method: "GET",
url: url,
headers: {
"Referer": "http://catalogue.smods.ru"
},
onload: function(e) {
doc = new DOMParser().parseFromString(e.responseText, "text/html");
if (doc.getElementsByClassName("post-inner").length > 0) {
var downloadUrl = doc.querySelector(".post-inner .skymods-excerpt-btn").href;
var downloadId = getDownloadId(downloadUrl);
if (downloadId != undefined || downloadId != null || downloadId != "") {
console.log("----------- download id: " + downloadId);
var rDateStr = doc.querySelector(".post-inner .skymods-item-date").innerText;
var updated = moment(rDateStr, "DD MMM at HH:mm YYYY").format(
"DD MMM, YYYY"
);
let titleElement = doc.querySelector(".post-inner h2 a");
let title = titleElement ? titleElement.textContent.trim() : "Unknown Mod Title";
callback(true, downloadId, downloadUrl, updated, title);
} else {
callback(false, downloadId, downloadUrl, "");
}
} else {
callback(false, downloadId, downloadUrl, "");
}
},
onerror: function(error) {
console.error("Request failed:", error);
callback(false, null, null, "Error fetching mod info");
}
});
}
function gotoRequestPage(id) {
var url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + id;
if (isCitiesSkylines()) {
window.open('https://docs.google.com/forms/d/e/1FAIpQLSdXlq9OAWVwX5lRLNvpkMSmpKbEDY50Bl-UU3f6P7OBI2Ny3Q/viewform?c=0&w=1&entry.417177883=' + url, '_blank');
} else {
window.open('https://docs.google.com/forms/d/e/1FAIpQLSe7MisYbKNUlTXBcSR2clHxpwaoo0HiZ3zWto0osemubdDP1g/viewform?entry.417177883=' + url, '_blank');
}
}
function changeButtonGradient(btn, color1, color2) {
var gradient =
"linear-gradient(42deg, #" + color1 + " 35%, #" + color2 + " 65%)";
btn.style.background = gradient;
btn.querySelector("#DownloadTxt").style.background = gradient;
}
function searchForDownloadLink(btn, downloadId, downloadUrl, modTitle) {
let textNode = btn.querySelector("#DownloadTxt");
let spinner = btn.querySelector(".loading-spinner");
spinner.style.display = "inline-block";
textNode.style.opacity = 0;
btn.classList.add('loading');
getDownloadLinkFromModsBase(downloadId, downloadUrl, function(err, directDownloadLink) {
spinner.style.display = "none";
textNode.style.opacity = 1;
btn.classList.remove('loading');
if (err) {
console.error(err);
textNode.innerText = "Failed to get link";
return;
}
textNode.innerText = "Downloading...";
spinner.style.display = "inline-block";
textNode.style.opacity = 0;
let fileName = modTitle.replace(/[^a-zA-Z0-9_.-]/g, '_') + ".zip";
fileName = fileName.substring(0, 250);
GM_download({
url: directDownloadLink,
name: fileName,
onload: function() {
spinner.style.display = "none";
textNode.style.opacity = 1;
textNode.innerHTML = "Downloaded!";
},
onerror: function(error) {
spinner.style.display = "none";
textNode.style.opacity = 1;
console.error("Download error:", error);
textNode.innerText = "Download Failed";
}
});
});
}
var DOWNLOAD_BTN_TEMPLATE = `
<button id="DownloadBtn" class="steam-button">
<span id="DownloadTxt">Download</span>
<span class="loading-spinner" style="display: none;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="7" stroke="#fff" stroke-width="2" style="animation: rotate 1s linear infinite;"/>
</svg>
</span>
</button>
`;
GM_addStyle(`
.steam-button {
background-color: #7cb342;
border: none;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
transition: background-color 0.2s ease, transform 0.1s ease;
display: inline-block;
position: relative;
margin-left: 10px;
}
.steam-button:hover {
background-color: #669933;
transform: scale(1.02);
}
.steam-button.loading #DownloadTxt {
opacity: 0;
transition: opacity 0.2s ease;
}
.steam-button.loading .loading-spinner {
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.steam-button .loading-spinner svg {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.steam-button.loading {
opacity: 0.7;
pointer-events: none;
}
`);
function init() {
$(document).ready(function() {
if (window.location.href.indexOf("appid=") >= 0) {
console.log("----------- Workshop browser page");
var itemList = document.querySelectorAll(".workshopItemPreviewHolder");
for (var item of itemList) {
var itemDownloadId = item.id.replace("sharedfile_", "");
var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
searchForMod(itemDownloadId,
(function() {
var workshopId = itemDownloadId;
var btn = btnNode;
var textNode = btn.querySelector("#DownloadTxt");
textNode.innerText = "Checking for mod";
return function(found, downloadId, downloadUrl, updated, modTitle) {
if (found) {
textNode.innerText = "Download - " + updated;
btn.addEventListener("click", function() {
searchForDownloadLink(btn, downloadId, downloadUrl, modTitle);
});
} else {
textNode.innerText = "Not Available (REQUEST)";
btn.addEventListener("click", function() {
gotoRequestPage(workshopId);
});
}
};
})()
);
var subscriptionControls = item.parentNode.querySelector('.subscriptionControls');
if(subscriptionControls) subscriptionControls.appendChild(btnNode);
}
} else if (isCollectionPage()) {
console.log("----------- Collection page");
var itemList = document.querySelectorAll(".collectionItem");
for (var item of itemList) {
var itemDownloadId = item.id.replace("sharedfile_", "");
var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
searchForMod(itemDownloadId,
(function() {
var workshopId = itemDownloadId;
var btn = btnNode;
var textNode = btn.querySelector("#DownloadTxt");
textNode.innerText = "Checking for mod";
return function(found, downloadId, downloadUrl, updated, modTitle) {
if (found) {
textNode.innerText = "Download - " + updated;
btn.addEventListener("click", function() {
searchForDownloadLink(btn, downloadId, downloadUrl, modTitle);
});
} else {
textNode.innerText = "Not Available (REQUEST)";
btn.addEventListener("click", function() {
gotoRequestPage(workshopId);
});
}
};
})()
);
var subscriptionControls = item.querySelector('.subscriptionControls');
if(subscriptionControls) subscriptionControls.appendChild(btnNode);
}
} else {
console.log("----------- Single item page");
var publishedfileid = window.location.href.match(/id=(\d+)/)[1];
var btnNode = createElementFromHTML(DOWNLOAD_BTN_TEMPLATE);
var textNode = btnNode.querySelector("#DownloadTxt");
textNode.innerText = "Checking for mod";
searchForMod(publishedfileid, function(
found,
downloadId,
downloadUrl,
updated,
modTitle
) {
if (found) {
textNode.innerText = "Download - " + updated;
btnNode.addEventListener("click", function() {
searchForDownloadLink(btnNode, downloadId, downloadUrl, modTitle);
});
} else {
textNode.innerText = "Not Available (REQUEST)";
btnNode.addEventListener("click", function() {
gotoRequestPage(publishedfileid);
});
}
});
var subscriptionControls = document.querySelector('.subscriptionControls');
if(subscriptionControls) subscriptionControls.appendChild(btnNode);
}
console.log("----------- Init successfully");
});
}
(function() {
"use strict";
init();
})();