Factorio Mod Downloader

让你无需登录 Factorio 官方 Mod 网站,也能批量下载模组的辅助脚本。

Versão de: 20/11/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Factorio Mod Downloader
// @namespace    http://tampermonkey.net/
// @version      5.21
// @description  让你无需登录 Factorio 官方 Mod 网站,也能批量下载模组的辅助脚本。
// @license      CC-BY-4.0
// @author       gggz
// @match        https://mods.factorio.com/mod/*
// @match        https://mods.factorio.com/browse/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const MIRROR_BASE = 'https://mods-storage.re146.dev/';
    const STORAGE_KEY = 'factorioModQueue';

    function loadQueue() {
        return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    }

    function saveQueue(queue) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(queue));
    }

    let downloadQueue = loadQueue();

    function getModNameFromURL(url) {
        const pathname = new URL(url, window.location.origin).pathname;
        const match = pathname.match(/\/mod\/([^\/]+)/);
        return match ? match[1] : null;
    }

    async function fetchLatestRelease(modName) {
        try {
            const resp = await fetch(`https://mods.factorio.com/api/mods/${modName}/full`);
            const data = await resp.json();
            if(!data.releases || data.releases.length === 0) return null;
            const sorted = data.releases.sort((a,b)=> a.version < b.version ? 1 : -1);
            return sorted[0].version;
        } catch(e) {
            console.error('API fetch failed', e);
            return null;
        }
    }

    function updateButtonState(btn, modName) {
        if(downloadQueue.some(item => item.modName === modName)) {
            btn.textContent = '已添加';
        } else {
            btn.textContent = '添加';
        }
    }

    function addDownloadButton(container, modName) {
        if(container.querySelector('.latest-download-btn')) return;

        const btn = document.createElement('button');
        btn.className = 'latest-download-btn';
        btn.style.marginTop = '4px';
        btn.style.cursor = 'pointer';
        btn.style.padding = '2px 6px';
        container.appendChild(btn);

        updateButtonState(btn, modName);

        btn.addEventListener('click', async () => {
            const index = downloadQueue.findIndex(item => item.modName === modName);
            if(index >= 0){
                downloadQueue.splice(index, 1);
                saveQueue(downloadQueue);
                btn.textContent = '添加';
            } else {
                btn.textContent = '已添加';
                const version = await fetchLatestRelease(modName);
                if(!version) {
                    alert(`未获取到最新版本: ${modName}`);
                    btn.textContent = '添加';
                    return;
                }
                if(!downloadQueue.some(item=>item.modName===modName)){
                    downloadQueue.push({modName, version});
                    saveQueue(downloadQueue);
                }
            }
        });
    }

    function createGlobalQueueButton() {
        if(document.getElementById('batch-download-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'batch-download-btn';
        btn.textContent = '下载队列';
        btn.style.position = 'fixed';
        btn.style.right = '20px';
        btn.style.bottom = '20px';
        btn.style.zIndex = 9999;
        btn.style.padding = '6px 12px';
        btn.style.backgroundColor = '#28a745';
        btn.style.color = '#fff';
        btn.style.border = 'none';
        btn.style.borderRadius = '4px';
        btn.style.cursor = 'pointer';
        document.body.appendChild(btn);

        btn.addEventListener('click', () => {
            downloadQueue = loadQueue();
            if(downloadQueue.length === 0) return alert('下载队列为空');

            const oldModal = document.getElementById('queue-confirm-modal');
            if(oldModal) oldModal.remove();

            const modal = document.createElement('div');
            modal.id = 'queue-confirm-modal';
            modal.style.position = 'fixed';
            modal.style.left = '50%';
            modal.style.top = '50%';
            modal.style.transform = 'translate(-50%, -50%)';
            modal.style.backgroundColor = '#fff';
            modal.style.border = '1px solid #ccc';
            modal.style.padding = '20px';
            modal.style.zIndex = 10000;
            modal.style.maxHeight = '70%';
            modal.style.overflowY = 'auto';
            modal.style.width = '300px';
            modal.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';

            const title = document.createElement('h3');
            title.textContent = `确认下载 (${downloadQueue.length} 个)`;
            title.style.marginTop = '0';
            modal.appendChild(title);

            const list = document.createElement('ul');
            list.style.paddingLeft = '20px';
            downloadQueue.forEach((item, idx) => {
                const li = document.createElement('li');
                li.textContent = `${item.modName} ${item.version}`;
                li.style.cursor = 'pointer';
                li.title = '点击取消';
                li.addEventListener('click', () => {
                    downloadQueue.splice(idx, 1);
                    saveQueue(downloadQueue);
                    li.remove();
                    const btns = document.querySelectorAll('.latest-download-btn');
                    btns.forEach(b=>{
                        const parentModName = getModNameFromURL(b.closest('div').querySelector('a[href^="/mod/"]')?.href || '');
                        if(parentModName===item.modName) b.textContent='添加';
                    });
                });
                list.appendChild(li);
            });
            modal.appendChild(list);

            const downloadBtn = document.createElement('button');
            downloadBtn.textContent = '确认下载';
            downloadBtn.style.marginTop = '10px';
            downloadBtn.style.padding = '4px 8px';
            downloadBtn.style.cursor = 'pointer';
            modal.appendChild(downloadBtn);

            const clearBtn = document.createElement('button');
            clearBtn.textContent = '清空队列';
            clearBtn.style.marginLeft = '10px';
            clearBtn.style.marginTop = '10px';
            clearBtn.style.padding = '4px 8px';
            clearBtn.style.cursor = 'pointer';
            modal.appendChild(clearBtn);

            const closeBtn = document.createElement('button');
            closeBtn.textContent = '关闭';
            closeBtn.style.marginLeft = '10px';
            closeBtn.style.padding = '4px 8px';
            closeBtn.style.cursor = 'pointer';
            modal.appendChild(closeBtn);

            closeBtn.addEventListener('click', () => modal.remove());

            clearBtn.addEventListener('click', () => {
                if(confirm('确定要清空整个下载队列吗?')) {
                    downloadQueue = [];
                    saveQueue(downloadQueue);
                    list.innerHTML = '';
                    const btns = document.querySelectorAll('.latest-download-btn');
                    btns.forEach(b=>b.textContent='添加');
                }
            });

            document.body.appendChild(modal);

            downloadBtn.addEventListener('click', async () => {
                for(let i=0;i<downloadQueue.length;i++){
                    const item = downloadQueue[i];
                    const url = `${MIRROR_BASE}${encodeURIComponent(item.modName)}/${encodeURIComponent(item.version)}.zip`;
                    const link = document.createElement('a');
                    link.href = url;
                    link.target = '_blank';
                    link.rel = 'noopener';
                    document.body.appendChild(link);
                    link.click();
                    link.remove();
                    await new Promise(r=>setTimeout(r,300));
                }
                modal.remove();
            });
        });
    }

    function processDetailPage() {
        const downloadBtn = document.querySelector('.button-green[title*="You need to own Factorio"]');
        if(downloadBtn){
            const container = downloadBtn.parentNode;
            const modName = getModNameFromURL(window.location.pathname);
            addDownloadButton(container, modName);
        }
    }

    function processListPage() {
        const modCards = document.querySelectorAll('a[href^="/mod/"]');
        modCards.forEach(link => {
            const container = link.closest('div');
            const modName = getModNameFromURL(link.getAttribute('href'));
            if(modName) addDownloadButton(container, modName);
        });
    }

    const observer = new MutationObserver(() => {
        if(window.location.pathname.startsWith('/mod/')) {
            processDetailPage();
        } else if(window.location.pathname.startsWith('/browse/')) {
            processListPage();
        }
        createGlobalQueueButton();
    });
    observer.observe(document.body, {childList: true, subtree: true});

    window.addEventListener('storage', e => {
        if(e.key === STORAGE_KEY){
            downloadQueue = loadQueue();
            const btns = document.querySelectorAll('.latest-download-btn');
            btns.forEach(b=>{
                const parentModName = getModNameFromURL(b.closest('div').querySelector('a[href^="/mod/"]')?.href || '');
                updateButtonState(b, parentModName);
            });
        }
    });

    if(window.location.pathname.startsWith('/mod/')) {
        processDetailPage();
    } else if(window.location.pathname.startsWith('/browse/')) {
        processListPage();
    }
    createGlobalQueueButton();

})();