Steam Workshop Downloader (GGNetwork)

Download Steam Workshop items using GGNetwork API

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Steam Workshop Downloader (GGNetwork)
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  Download Steam Workshop items using GGNetwork API
// @author       Cerulean
// @match        https://steamcommunity.com/sharedfiles/filedetails/*
// @match        https://steamcommunity.com/workshop/filedetails/*
// @grant        GM_xmlhttpRequest
// @connect      api.ggntw.com
// @connect      cdn.ggntw.com
// @connect      ggntw.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Extract Workshop ID from URL
    function getWorkshopId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('id');
    }

    // Get current page URL
    function getCurrentUrl() {
        return window.location.href;
    }

    // Create download button
    function createDownloadButton() {
        const workshopId = getWorkshopId();
        if (!workshopId) return;

        // Create button container
        const btnContainer = document.createElement('div');
        btnContainer.style.cssText = 'margin: 10px 0; padding: 10px; background: #1b2838; border-radius: 4px;';

        // Create button row container
        const buttonRow = document.createElement('div');
        buttonRow.style.cssText = 'display: flex; gap: 10px; flex-wrap: wrap;';

        // Create download button
        const downloadBtn = document.createElement('button');
        downloadBtn.textContent = '📥 Download via GGNetwork';
        downloadBtn.style.cssText = `
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 12px 24px;
            font-size: 14px;
            font-weight: bold;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
        `;

        downloadBtn.onmouseover = () => {
            downloadBtn.style.transform = 'translateY(-2px)';
            downloadBtn.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
        };

        downloadBtn.onmouseout = () => {
            downloadBtn.style.transform = 'translateY(0)';
            downloadBtn.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
        };

        // Create copy link button
        const copyBtn = document.createElement('button');
        copyBtn.textContent = '📋 Copy Download Link';
        copyBtn.style.cssText = `
            background: #2a475e;
            color: white;
            border: none;
            padding: 12px 24px;
            font-size: 14px;
            font-weight: bold;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.3s ease;
        `;

        copyBtn.onmouseover = () => {
            copyBtn.style.background = '#3a5768';
        };

        copyBtn.onmouseout = () => {
            copyBtn.style.background = '#2a475e';
        };

        // Status message element
        const statusMsg = document.createElement('div');
        statusMsg.style.cssText = 'margin-top: 10px; font-size: 13px; color: #c7d5e0;';

        // Store download URL globally for copy button
        let currentDownloadUrl = null;

        // Download button click handler
        downloadBtn.onclick = () => {
            downloadWorkshopItem(workshopId, statusMsg, downloadBtn, copyBtn, (url) => {
                currentDownloadUrl = url;
            });
        };

        // Copy button click handler - gets link without downloading
        copyBtn.onclick = () => {
            getDownloadLink(workshopId, statusMsg, copyBtn, (url) => {
                currentDownloadUrl = url;
                navigator.clipboard.writeText(url).then(() => {
                    statusMsg.textContent = '✅ Download link copied! Paste in IDM quickly - it will expire soon!';
                    statusMsg.style.color = '#90ee90';
                }).catch(() => {
                    statusMsg.textContent = '❌ Failed to copy. Link: ' + url;
                    statusMsg.style.color = '#ff6b6b';
                });
            });
        };

        buttonRow.appendChild(downloadBtn);
        buttonRow.appendChild(copyBtn);
        btnContainer.appendChild(buttonRow);
        btnContainer.appendChild(statusMsg);

        // Find a good place to insert the button
        const detailsBlock = document.querySelector('.workshopItemDetailsHeader') ||
                           document.querySelector('.workshopItemDetails') ||
                           document.querySelector('.rightDetailsBlock');

        if (detailsBlock) {
            detailsBlock.parentNode.insertBefore(btnContainer, detailsBlock.nextSibling);
        }
    }

    // Get download link without starting download
    function getDownloadLink(workshopId, statusElement, button, onUrlReceived) {
        statusElement.textContent = '⏳ Getting download link...';
        statusElement.style.color = '#ffa500';
        button.disabled = true;
        button.style.opacity = '0.6';
        button.style.cursor = 'not-allowed';

        const workshopUrl = getCurrentUrl();
        const apiUrl = 'https://api.ggntw.com/steam.request';

        GM_xmlhttpRequest({
            method: 'POST',
            url: apiUrl,
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json, text/plain, */*',
                'Origin': 'https://ggntw.com',
                'Referer': 'https://ggntw.com/'
            },
            data: JSON.stringify({
                url: workshopUrl
            }),
            timeout: 60000,
            onload: function(response) {
                button.disabled = false;
                button.style.opacity = '1';
                button.style.cursor = 'pointer';

                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        let downloadUrl = data.download_url || data.url || data.link || data.file || data.download;

                        if (!downloadUrl && data.data) {
                            downloadUrl = data.data.download_url || data.data.url || data.data.link || data.data.file;
                        }

                        if (downloadUrl) {
                            onUrlReceived(downloadUrl);
                        } else {
                            statusElement.textContent = '❌ No download URL found';
                            statusElement.style.color = '#ff6b6b';
                        }
                    } catch (e) {
                        statusElement.textContent = '❌ Failed to get link';
                        statusElement.style.color = '#ff6b6b';
                    }
                } else {
                    statusElement.textContent = `❌ API error: ${response.status}`;
                    statusElement.style.color = '#ff6b6b';
                }
            },
            onerror: function() {
                button.disabled = false;
                button.style.opacity = '1';
                button.style.cursor = 'pointer';
                statusElement.textContent = '❌ Network error';
                statusElement.style.color = '#ff6b6b';
            }
        });
    }

    // Download workshop item using GGNetwork API
    function downloadWorkshopItem(workshopId, statusElement, button, copyButton, onUrlReceived) {
        statusElement.textContent = '⏳ Requesting download from GGNetwork...';
        statusElement.style.color = '#ffa500';
        button.disabled = true;
        button.style.opacity = '0.6';
        button.style.cursor = 'not-allowed';

        const workshopUrl = getCurrentUrl();
        const apiUrl = 'https://api.ggntw.com/steam.request';

        GM_xmlhttpRequest({
            method: 'POST',
            url: apiUrl,
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json, text/plain, */*',
                'Origin': 'https://ggntw.com',
                'Referer': 'https://ggntw.com/'
            },
            data: JSON.stringify({
                url: workshopUrl
            }),
            timeout: 60000,
            onload: function(response) {
                button.disabled = false;
                button.style.opacity = '1';
                button.style.cursor = 'pointer';

                console.log('API Response:', response.responseText);

                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        console.log('Parsed data:', data);

                        // Extract download URL from various possible response formats
                        let downloadUrl = data.download_url || data.url || data.link || data.file || data.download;

                        // Check if it's nested in a data object
                        if (!downloadUrl && data.data) {
                            downloadUrl = data.data.download_url || data.data.url || data.data.link || data.data.file;
                        }

                        if (downloadUrl) {
                            // Store URL for copy button
                            onUrlReceived(downloadUrl);

                            statusElement.textContent = '✅ Starting download...';
                            statusElement.style.color = '#90ee90';

                            // Create a hidden anchor element and trigger download
                            const a = document.createElement('a');
                            a.href = downloadUrl;
                            a.download = '';
                            a.style.display = 'none';
                            document.body.appendChild(a);
                            a.click();
                            document.body.removeChild(a);

                            setTimeout(() => {
                                statusElement.textContent = '✅ Download started! Use "Copy Link" button for IDM.';
                            }, 1000);
                        } else {
                            statusElement.textContent = '❌ No download URL in response. Check console for details.';
                            statusElement.style.color = '#ff6b6b';
                        }
                    } catch (e) {
                        statusElement.textContent = '❌ Failed to parse API response. Check console.';
                        statusElement.style.color = '#ff6b6b';
                        console.error('Parse error:', e);
                    }
                } else {
                    statusElement.textContent = `❌ API error: ${response.status}. Check console.`;
                    statusElement.style.color = '#ff6b6b';
                }
            },
            onerror: function(error) {
                button.disabled = false;
                button.style.opacity = '1';
                button.style.cursor = 'pointer';
                statusElement.textContent = '❌ Network error. Check console for details.';
                statusElement.style.color = '#ff6b6b';
                console.error('Network error:', error);
            },
            ontimeout: function() {
                button.disabled = false;
                button.style.opacity = '1';
                button.style.cursor = 'pointer';
                statusElement.textContent = '❌ Request timed out. Please try again.';
                statusElement.style.color = '#ff6b6b';
            }
        });
    }

    // Wait for page to load and add button
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createDownloadButton);
    } else {
        createDownloadButton();
    }
})();