Steam Workshop Downloader (Skymods/Modsbase)

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

目前為 2024-10-17 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Steam Workshop Downloader (Skymods/Modsbase)
// @namespace    http://tampermonkey.net/
// @version      0.0.5
// @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() {
    const collectionsLink = document.querySelector('a[href*="/workshop/browse/?section=collections"]');
    return collectionsLink !== null; // Returns true if the link is found
}

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;
}

.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;
}

.steam-button.not-available {
    background-color: #d32f2f;
}

.steam-button.not-available:hover {
    background-color: #c62828;
}

.game_area_purchase_game > div {
    height: 30px; /* Replace 30px with the actual height of the Subscribe button */
    display: flex;
    align-items: center;
}

.game_area_purchase_game > div > a#SubscribeItemBtn + button.steam-button {
    margin-left: -10px; /* Adjust this value as needed for left alignment */
    margin-right: 0; /* Ensure no right margin */
}


@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.classList.add("not-available");
                                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.classList.add("not-available");
                                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.classList.add("not-available");
                    btnNode.addEventListener("click", function() {
                        gotoRequestPage(publishedfileid);
                    });
                }
            });

     const subscribeButton = document.getElementById("SubscribeItemBtn");
    if (subscribeButton) {
        subscribeButton.parentNode.insertBefore(btnNode, subscribeButton.nextSibling);
    } else {
        // Fallback if Subscribe button not found (e.g., due to Steam layout changes):
        const subscriptionControls = document.querySelector('.subscriptionControls');
    if (subscriptionControls) {
        subscriptionControls.classList.add('single-item-controls'); // Add a unique class
        subscriptionControls.insertBefore(btnNode, subscribeButton.nextSibling); // insertBefore, as before
        } else {
            console.error("Neither Subscribe button nor Subscription Controls found. Appending to body.");
            document.body.appendChild(btnNode);
        }
    }

        }
        console.log("----------- Init successfully");
    });
}

(function() {
    "use strict";

    init();
})();

QingJ © 2025

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