OSRS Wiki Auto-Categorizer with UI, Adaptive Speed, and Global Scope

Adds listed pages to a category upon request with UI, CSRF token, adaptive speed, and global compatibility

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

// ==UserScript==
// @name         OSRS Wiki Auto-Categorizer with UI, Adaptive Speed, and Global Scope
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Adds listed pages to a category upon request with UI, CSRF token, adaptive speed, and global compatibility
// @author       Nick2bad4u
// @match        https://oldschool.runescape.wiki/*
// @grant        GM_xmlhttpRequest
// @icon         https://www.google.com/s2/favicons?sz=64&domain=oldschool.runescape.wiki
// @license      UnLicense
// ==/UserScript==

(function() {
    'use strict';

    // Global variables
    const versionNumber = '1.5';
    let categoryName = '';        // Stores the requested category name
    let pageLinks = [];           // Stores all applicable page links on the current page
    let selectedLinks = [];       // Stores selected page links for categorization
    let currentIndex = 0;         // Tracks current page being processed
    let csrfToken = '';           // Stores CSRF token for authenticated actions
    let isCancelled = false;      // Flag to cancel ongoing categorization
    let isRunning = false;        // Indicates if categorization is in progress
    let requestInterval = 500;    // Initial interval between API requests, adapts based on rate limiting
    const maxInterval = 5000;     // Maximum interval to wait after rate limiting occurs
    const excludedPrefixes = ["Template:", "File:", "Category:", "Module:"]; // Pages to exclude

    // Add UI elements to start, cancel, and display progress of the categorization process
    function addButtonAndProgressBar() {
        console.log("Adding UI elements for categorization.");
        const container = document.createElement('div');
        container.id = 'categorize-ui';
        container.style = `position: fixed; bottom: 20px; right: 20px; z-index: 1000;
            background-color: #2b2b2b; color: #ffffff; padding: 12px;
            border: 1px solid #595959; border-radius: 8px; font-family: Arial, sans-serif; width: 250px;`;

        // Start Button
        const startButton = document.createElement('button');
        startButton.textContent = 'Start Categorizing';
        startButton.style = `background-color: #4caf50; color: #fff; border: none;
            padding: 6px 12px; border-radius: 5px; cursor: pointer;`;
        startButton.onclick = promptCategoryName;
        container.appendChild(startButton);

        // Cancel Button
        const cancelButton = document.createElement('button');
        cancelButton.textContent = 'Cancel';
        cancelButton.style = `background-color: #d9534f; color: #fff; border: none;
            padding: 6px 12px; border-radius: 5px; cursor: pointer; margin-left: 10px;`;
        cancelButton.onclick = cancelCategorization;
        container.appendChild(cancelButton);

        // Progress Bar and Status Text
        const progressBarContainer = document.createElement('div');
        progressBarContainer.style = `width: 100%; margin-top: 10px; background-color: #3d3d3d;
            height: 20px; border-radius: 5px; position: relative;`;
        progressBarContainer.id = 'progress-bar-container';

        const progressBar = document.createElement('div');
        progressBar.style = `width: 0%; height: 100%; background-color: #4caf50; border-radius: 5px;`;
        progressBar.id = 'progress-bar';
        progressBarContainer.appendChild(progressBar);

        const progressText = document.createElement('span');
        progressText.id = 'progress-text';
        progressText.style = `position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
            font-size: 12px; color: #ffffff; white-space: nowrap; overflow: visible; text-align: center;`;
        progressBarContainer.appendChild(progressText);

        container.appendChild(progressBarContainer);
        document.body.appendChild(container);
        console.log("UI elements added successfully.");
    }

    // Prompt user for category name and initiate the link fetching process
    function promptCategoryName() {
        console.log("Prompting user for category name.");
        categoryName = prompt("Enter the category name you'd like to add:");
        if (!categoryName) {
            alert("Category name is required.");
            console.warn("User did not enter a category name.");
            return;
        }

        getPageLinks();
        if (pageLinks.length === 0) {
            alert("No pages found to categorize.");
            console.warn("No applicable pages found.");
            return;
        }

        displayPageSelectionPopup();
    }

    // Retrieve applicable page links on the current page, filtering out excluded prefixes
    function getPageLinks() {
        console.log("Retrieving applicable page links.");
        pageLinks = Array.from(document.querySelectorAll('#mw-content-text a'))
            .map(link => link.getAttribute('href'))
            .filter(href => href && href.startsWith('/w/'))
            .map(href => decodeURIComponent(href.replace('/w/', '')))
            .filter(page => !excludedPrefixes.some(prefix => page.startsWith(prefix)));

        console.log(`Found ${pageLinks.length} page links.`);
    }

    // Display popup for selecting pages to categorize
    function displayPageSelectionPopup() {
        console.log("Displaying page selection popup.");
        const popup = document.createElement('div');
        popup.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
            z-index: 1001; background-color: #2b2b2b; padding: 15px; color: white;
            border-radius: 8px; max-height: 80vh; overflow-y: auto; border: 1px solid #595959;`;

        const title = document.createElement('h3');
        title.textContent = 'Select Pages to Categorize';
        title.style = `margin: 0 0 10px; font-family: Arial, sans-serif;`;
        popup.appendChild(title);

        const listContainer = document.createElement('div');
        listContainer.style = `max-height: 300px; overflow-y: auto;`;
        pageLinks.forEach(link => {
            const listItem = document.createElement('div');
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = true;
            checkbox.value = link;
            listItem.appendChild(checkbox);
            listItem.appendChild(document.createTextNode(` ${link}`));
            listContainer.appendChild(listItem);
        });
        popup.appendChild(listContainer);

        const buttonContainer = document.createElement('div');
        buttonContainer.style = `margin-top: 10px; display: flex; justify-content: space-between;`;

        const confirmButton = document.createElement('button');
        confirmButton.textContent = 'Confirm Selection';
        confirmButton.style = `padding: 5px 10px; background-color: #4caf50;
            border: none; color: white; cursor: pointer; border-radius: 5px;`;
        confirmButton.onclick = () => {
            selectedLinks = Array.from(listContainer.querySelectorAll('input:checked')).map(input => input.value);
            console.log("Selected links confirmed:", selectedLinks);
            document.body.removeChild(popup);
            if (selectedLinks.length > 0) {
                startCategorization();
            } else {
                alert("No pages selected.");
                console.warn("User did not select any pages.");
            }
        };

        const cancelPopupButton = document.createElement('button');
        cancelPopupButton.textContent = 'Cancel';
        cancelPopupButton.style = `padding: 5px 10px; background-color: #d9534f;
            border: none; color: white; cursor: pointer; border-radius: 5px;`;
        cancelPopupButton.onclick = () => {
            document.body.removeChild(popup);
            console.log("Popup canceled by user.");
        };

        buttonContainer.appendChild(confirmButton);
        buttonContainer.appendChild(cancelPopupButton);
        popup.appendChild(buttonContainer);

        document.body.appendChild(popup);
    }

    // Start categorization process and fetch CSRF token for authorized API calls
    function startCategorization() {
        console.log("Starting categorization process.");
        isCancelled = false;
        isRunning = true;
        currentIndex = 0;
        document.getElementById('progress-bar-container').style.display = 'block';
        console.log("Fetching CSRF token...");
        fetchCsrfToken(() => processNextPage());
    }

    // Process each page one at a time, updating progress bar and adjusting speed for rate limits
    function processNextPage() {
        if (isCancelled || currentIndex >= selectedLinks.length) {
            isRunning = false;
            alert(isCancelled ? "Categorization canceled." : "Categorization complete.");
            console.log("Categorization process completed or canceled.");
            return;
        }

        const page = selectedLinks[currentIndex];
        console.log(`Processing page: ${page}`);
        categorizePage(page, () => {
            currentIndex++;
            updateProgress();
            // Adaptive request timing
            requestInterval = adjustRequestInterval();
            setTimeout(processNextPage, requestInterval);
        });
    }

    // Fetch CSRF token from the server for authenticated requests
    function fetchCsrfToken(callback) {
        console.log("Requesting CSRF token...");
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://oldschool.runescape.wiki/api.php?action=query&meta=tokens&type=edit&format=json',
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    csrfToken = data.query.tokens.csrftoken;
                    console.log("CSRF token fetched:", csrfToken);
                    callback();
                } else {
                    console.error("Failed to fetch CSRF token:", response.statusText);
                    alert("Error fetching CSRF token. Please try again.");
                }
            }
        });
    }

    // Categorize a specific page and handle possible errors
    function categorizePage(page, callback) {
        console.log(`Categorizing page: ${page}`);
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'https://oldschool.runescape.wiki/api.php',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            data: `action=edit&title=${encodeURIComponent(page)}&appendtext=[[Category:${encodeURIComponent(categoryName)}]]&token=${encodeURIComponent(csrfToken)}&format=json`,
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    if (data.error) {
                        console.error(`Error categorizing page: ${page} - ${data.error.info}`);
                    } else {
                        console.log(`Successfully categorized page: ${page}`);
                    }
                } else {
                    console.error(`Failed to categorize page: ${page} - ${response.statusText}`);
                }
                callback();
            }
        });
    }

    // Update the progress bar and text based on the current status
    function updateProgress() {
        const progressBar = document.getElementById('progress-bar');
        const progressText = document.getElementById('progress-text');
        const progressPercentage = Math.floor((currentIndex / selectedLinks.length) * 100);
        progressBar.style.width = `${progressPercentage}%`;
        progressText.textContent = `Processing ${currentIndex + 1} of ${selectedLinks.length} pages`;
        console.log(`Progress updated: ${progressText.textContent}`);
    }

    // Cancel the ongoing categorization process
    function cancelCategorization() {
        console.log("Categorization process has been cancelled by the user.");
        isCancelled = true;
    }

    // Adjust the request interval based on API response
    function adjustRequestInterval() {
        // Add logic here based on your requirements, for example, increase delay on errors
        return Math.min(requestInterval * 2, maxInterval); // Example: double the interval
    }

    // Initialize the script by adding UI elements
    addButtonAndProgressBar();
})();

QingJ © 2025

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