Steam Workshop Links Extractor & Auto-Subscriptions

Extract Links, Subscribe to All, or Unsubscribe from All items on any Steam Workshop page.

// ==UserScript==
// @name         Steam Workshop Links Extractor & Auto-Subscriptions
// @namespace    SteamWorkshopLinksExtractorandAutoSubscriptions
// @version      1.00
// @description  Extract Links, Subscribe to All, or Unsubscribe from All items on any Steam Workshop page.
// @author       Dragonking69
// @match        https://steamcommunity.com/workshop/browse/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const maxPagesToProcess = 100; // <--- SET YOUR MAXIMUM NUMBER OF PAGES TO PROCESS
    const outputFilename = 'steam_workshop_links.txt'; // Filename for extracted links
    // -------------------

    let pageTimeoutId = null; // To keep track of the safety timeout

    // --- ACTION-SPECIFIC LOGIC ---

    function runExtractionProcess() {
        console.log('ACTION: Extracting Links...');
        const storedLinks = JSON.parse(sessionStorage.getItem('workshopExtractedLinks') || '[]');
        const currentPageLinks = new Set(storedLinks);
        const linkElements = document.querySelectorAll('a.ugc[href*="sharedfiles/filedetails/"]');
        linkElements.forEach(el => currentPageLinks.add(el.href));
        sessionStorage.setItem('workshopExtractedLinks', JSON.stringify([...currentPageLinks]));
        console.log(`Found ${linkElements.length} links on this page. Total unique links: ${currentPageLinks.size}`);
        proceedOrStop();
    }

    function runSubscriptionProcess() {
        console.log('ACTION: Subscribing to all...');
        const buttonsToClick = document.querySelectorAll('span[id^="SubscribeItemBtn"]:not(.toggled)');
        if (buttonsToClick.length > 0) {
            console.log(`Found ${buttonsToClick.length} items to subscribe to. Clicking now...`);
            buttonsToClick.forEach(btn => btn.click());
            waitForCompletion('subscribe');
        } else {
            console.log("All items on this page are already subscribed.");
            proceedOrStop();
        }
    }

    function runUnsubscriptionProcess() {
        console.log('ACTION: Unsubscribing from all...');
        const buttonsToClick = document.querySelectorAll('span[id^="SubscribeItemBtn"].toggled');
        if (buttonsToClick.length > 0) {
            console.log(`Found ${buttonsToClick.length} items to unsubscribe from. Clicking now...`);
            buttonsToClick.forEach(btn => btn.click());
            waitForCompletion('unsubscribe');
        } else {
            console.log("No items on this page were subscribed.");
            proceedOrStop();
        }
    }

    // --- CORE & HELPER FUNCTIONS ---

    function waitForCompletion(expectedState) {
        const totalItems = document.querySelectorAll('.workshopItem').length;
        const isSubscribing = expectedState === 'subscribe';
        console.log(`Waiting for ${totalItems} items to reach '${expectedState}' state...`);

        const intervalCheck = setInterval(() => {
            const subscribedItemsCount = document.querySelectorAll('.user_action_history_icon.subscribed[style*="block"]').length;
            const unsubscribedItemsCount = totalItems - subscribedItemsCount;

            const goalReached = isSubscribing ? (subscribedItemsCount >= totalItems) : (unsubscribedItemsCount >= totalItems);

            console.log(`Progress: ${isSubscribing ? subscribedItemsCount : unsubscribedItemsCount} / ${totalItems} completed.`);

            if (goalReached) {
                clearInterval(intervalCheck);
                clearTimeout(pageTimeoutId);
                console.log('All actions confirmed.');
                proceedOrStop();
            }
        }, 2000);

        pageTimeoutId = setTimeout(() => {
            clearInterval(intervalCheck);
            console.warn(`Page timeout reached. Moving to next page anyway.`);
            proceedOrStop();
        }, 30000); // 30-second timeout
    }

    function goToNextPage() {
        const urlParams = new URLSearchParams(window.location.search);
        const currentPage = parseInt(urlParams.get('p'), 10) || 1;
        const nextPage = currentPage + 1;
        console.log(`Navigating from page ${currentPage} to ${nextPage}...`);
        urlParams.set('p', nextPage);
        window.location.search = urlParams.toString();
    }

    function proceedOrStop() {
        clearTimeout(pageTimeoutId);
        const urlParams = new URLSearchParams(window.location.search);
        const currentPage = parseInt(urlParams.get('p'), 10) || 1;

        if (currentPage >= maxPagesToProcess) {
            stopAction(true, `Reached the maximum page limit of ${maxPagesToProcess}.`);
            return;
        }

        const nextPageButton = document.querySelector('.pagebtn:not(.disabled):last-child');
        if (!nextPageButton || nextPageButton.textContent !== '>') {
             stopAction(true, 'Finished scanning all available pages.');
             return;
        }

        console.log('Page complete. Moving to the next page in 2 seconds...');
        setTimeout(goToNextPage, 2000);
    }

    function downloadLinksAsTxt() {
        const links = JSON.parse(sessionStorage.getItem('workshopExtractedLinks') || '[]');
        if (links.length === 0) return;
        console.log(`Preparing to download ${links.length} links.`);
        const fileContent = links.join('\n');
        const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = outputFilename;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a);
    }

    // --- UI AND STATE MANAGEMENT ---

    function startAction(action) {
        const confirmation = confirm(`This will start the "${action}" process and continue automatically up to page ${maxPagesToProcess}.\n\nAre you sure?`);
        if (confirmation) {
            sessionStorage.setItem('workshopToolAction', action);
            if (action === 'extract') {
                sessionStorage.setItem('workshopExtractedLinks', '[]');
            }
            runAction(action);
        }
    }

    function stopAction(finished = false, message = 'Script stopped manually.') {
        const action = sessionStorage.getItem('workshopToolAction');
        if (action === 'extract') {
            if (finished) {
                alert(message + ' Downloading collected links now.');
            } else {
                 alert(message + ' Downloading any links collected so far.');
            }
            downloadLinksAsTxt();
        } else {
            alert(message);
        }

        clearTimeout(pageTimeoutId);
        sessionStorage.removeItem('workshopToolAction');
        sessionStorage.removeItem('workshopExtractedLinks');
        updateUIState(null);
    }

    function updateUIState(runningAction) {
        const panel = document.getElementById('workshopToolPanel');
        if (!panel) return;
        const buttons = panel.querySelectorAll('button');
        buttons.forEach(btn => {
            const action = btn.getAttribute('data-action');
            if (runningAction) {
                // A task is running
                if (action === 'stop') {
                    btn.style.display = 'block';
                    let stopText = 'Stop';
                    if (runningAction === 'extract') stopText += ' & Download';
                    btn.textContent = stopText;
                } else {
                    btn.style.display = 'none';
                }
            } else {
                // No task is running
                btn.style.display = action === 'stop' ? 'none' : 'block';
            }
        });
    }

    function createControllerPanel() {
        const panel = document.createElement('div');
        panel.id = 'workshopToolPanel';
        Object.assign(panel.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '9999',
            backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: '10px', borderRadius: '5px',
            display: 'flex', flexDirection: 'column', gap: '8px'
        });

        const actions = [
            { id: 'extract', text: 'Extract Links', color: '#4c6e81' },
            { id: 'subscribe', text: 'Subscribe All', color: '#5ba32b' },
            { id: 'unsubscribe', text: 'Unsubscribe All', color: '#a03c2a' },
            { id: 'stop', text: 'Stop', color: '#d94126' }
        ];

        actions.forEach(({ id, text, color }) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.setAttribute('data-action', id);
            Object.assign(button.style, {
                padding: '8px 16px', fontSize: '14px', color: 'white', border: 'none',
                borderRadius: '5px', cursor: 'pointer', backgroundColor: color, width: '100%'
            });
            if (id === 'stop') {
                button.addEventListener('click', () => stopAction(false));
            } else {
                button.addEventListener('click', () => startAction(id));
            }
            panel.appendChild(button);
        });

        document.body.appendChild(panel);
    }

    function runAction(action) {
        updateUIState(action);
        switch (action) {
            case 'extract':     runExtractionProcess(); break;
            case 'subscribe':   runSubscriptionProcess(); break;
            case 'unsubscribe': runUnsubscriptionProcess(); break;
        }
    }

    // --- SCRIPT INITIALIZATION ---
    createControllerPanel();
    const activeAction = sessionStorage.getItem('workshopToolAction');
    updateUIState(activeAction);

    if (activeAction) {
        setTimeout(() => runAction(activeAction), 2000);
    }

})();