SkyMods downloader for steam

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

目前为 2024-10-16 提交的版本。查看 最新版本

// ==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();
})();

QingJ © 2025

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