Stys-NovelUpdates-Batch-Upload

You know what it is, its in the name. @Stying on discord for contact

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        Stys-NovelUpdates-Batch-Upload
// @namespace   Stying Scripts
// @match       https://www.novelupdates.com/add-release/
// @grant       GM_xmlhttpRequest
// @connect     www.novelupdates.com
// @version     1.2
// @author      Sty
// @license     GNU GPL V3
// @description You know what it is, its in the name. @Stying on discord for contact
// ==/UserScript==

(function() {
    'use strict';

    // --- UI Construction ---
    const container = document.createElement('div');
    container.id = 'nu-batch-ui';
    // Style the outer container
    Object.assign(container.style, {
        position: 'fixed',
        top: '10px',
        right: '10px',
        width: '320px',
        backgroundColor: '#fff',
        border: '2px solid #333',
        zIndex: '9999',
        fontFamily: 'sans-serif',
        boxShadow: '0 4px 10px rgba(0,0,0,0.3)',
        borderRadius: '8px',
        color: '#333',
        userSelect: 'none'
    });

    container.innerHTML = `
        <div id="nu-drag-handle" style="padding: 10px; background: rgb(44, 62, 80); color: white; cursor: move; border-top-left-radius: 5px; border-top-right-radius: 5px; font-weight: bold; display: flex;">
            <span>Sty's  NU Batch Submitter</span>
        </div>
        <div style="padding: 15px; user-select: auto;">
            <div style="margin-bottom: 10px;">
                <label style="display:block; font-size:12px; font-weight:bold;">Series Search:</label>
                <input type="text" id="nu-series-search" placeholder="Type title..." style="width: 100%; box-sizing: border-box; padding:4px;">
                <div id="nu-series-results" style="max-height: 100px; overflow-y: auto; border: 1px solid #ddd; display: none; background: #fefefe;"></div>
                <input type="hidden" id="nu-series-id">
                <small id="nu-series-selected" style="color: green; font-size:11px;"></small>
            </div>

            <div style="margin-bottom: 10px;">
                <label style="display:block; font-size:12px; font-weight:bold;">Group Search:</label>
                <input type="text" id="nu-group-search" placeholder="Type group name..." style="width: 100%; box-sizing: border-box; padding:4px;">
                <div id="nu-group-results" style="max-height: 100px; overflow-y: auto; border: 1px solid #ddd; display: none; background: #fefefe;"></div>
                <input type="hidden" id="nu-group-id">
                <small id="nu-group-selected" style="color: green; font-size:11px;"></small>
            </div>

            <div style="margin-bottom: 10px;">
                <label style="display:block; font-size:12px; font-weight:bold;">URL Template (use {n}):</label>
                <input type="text" id="nu-url-template" placeholder="https://site.com/ch-{n}" style="width: 100%; box-sizing: border-box; padding:4px;">
            </div>

            <div style="display: flex; gap: 5px; margin-bottom: 10px;">
                <div style="flex: 1;">
                    <label style="display:block; font-size:12px; font-weight:bold;">Start Ch:</label>
                    <input type="number" id="nu-start-ch" style="width: 100%; padding:4px;">
                </div>
                <div style="flex: 1;">
                    <label style="display:block; font-size:12px; font-weight:bold;">End Ch:</label>
                    <input type="number" id="nu-end-ch" style="width: 100%; padding:4px;">
                </div>
            </div>

            <button id="nu-submit-btn" style="width: 100%; padding: 10px; background: rgb(44, 62, 80); color: white; border: none; cursor: pointer; font-weight: bold; border-radius: 4px;">Start Batch Submit</button>
            <div id="nu-status" style="margin-top: 10px; font-size: 11px; white-space: pre-wrap; height: 100px; overflow-y: auto; background: #f1f1f1; padding: 5px; border: 1px solid #ccc;">Idle...</div>
        </div>
        <style>
            .change_list { padding: 5px; cursor: pointer; border-bottom: 1px solid #eee; font-size: 12px; color: #333; }
            .change_list:hover { background: #e3f2fd; }
            .search_hl { font-weight: bold; color: #d32f2f; }
        </style>
    `;
    document.body.appendChild(container);

    // --- Drag Logic ---
    let isDragging = false;
    let offset = { x: 0, y: 0 };
    const handle = document.getElementById('nu-drag-handle');

    handle.addEventListener('mousedown', (e) => {
        isDragging = true;
        offset.x = e.clientX - container.offsetLeft;
        offset.y = e.clientY - container.offsetTop;
        handle.style.cursor = 'grabbing';
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        container.style.left = (e.clientX - offset.x) + 'px';
        container.style.top = (e.clientY - offset.y) + 'px';
        container.style.right = 'auto';
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        handle.style.cursor = 'move';
    });

    // --- Logging ---
    const log = (msg) => {
        const el = document.getElementById('nu-status');
        el.textContent += msg + '\n';
        el.scrollTop = el.scrollHeight;
    };

    // --- Universal AJAX Search (GM_xmlhttpRequest) ---
    function performSearch(query, type, resultDiv, idInput, selectedLabel) {
        if (query.length < 2) { resultDiv.style.display = 'none'; return; }

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://www.novelupdates.com/wp-admin/admin-ajax.php",
            data: `action=nd_ajaxsearch&str=${encodeURIComponent(query)}&strID=100&strType=${type}`,
            headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
            onload: function(response) {
                let html = response.responseText.replace(/0$/, '');
                resultDiv.innerHTML = html;
                resultDiv.style.display = 'block';

                const items = resultDiv.querySelectorAll('.change_list');
                items.forEach(item => {
                    item.onclick = function() {
                        const match = this.getAttribute('onclick').match(/'(\d+)'/g);
                        const id = match[1].replace(/'/g, '');
                        idInput.value = id;
                        selectedLabel.textContent = "Selected: " + this.textContent + " (ID: " + id + ")";
                        resultDiv.style.display = 'none';
                    };
                });
            }
        });
    }

    // --- Event Listeners ---
    document.getElementById('nu-series-search').addEventListener('input', (e) => {
        performSearch(e.target.value, 'series', document.getElementById('nu-series-results'), document.getElementById('nu-series-id'), document.getElementById('nu-series-selected'));
    });

    document.getElementById('nu-group-search').addEventListener('input', (e) => {
        performSearch(e.target.value, 'group', document.getElementById('nu-group-results'), document.getElementById('nu-group-id'), document.getElementById('nu-group-selected'));
    });

    document.getElementById('nu-submit-btn').addEventListener('click', async () => {
        const seriesId = document.getElementById('nu-series-id').value;
        const groupId = document.getElementById('nu-group-id').value;
        const urlTemplate = document.getElementById('nu-url-template').value;
        const start = parseInt(document.getElementById('nu-start-ch').value);
        const end = parseInt(document.getElementById('nu-end-ch').value);

        if (!seriesId || !groupId || !urlTemplate || isNaN(start) || isNaN(end)) {
            alert("Please fill all fields and select items from search results.");
            return;
        }

        document.getElementById('nu-submit-btn').disabled = true;
        document.getElementById('nu-status').textContent = "Starting batch...\n";

        for (let i = start; i <= end; i++) {
            const chUrl = urlTemplate.replace('{n}', i);
            log(`Submitting Ch ${i}...`);

            const postData = `artitle=${seriesId}&arrelease=c${i}&arlink=${encodeURIComponent(chUrl)}&argroup=${groupId}&ardate=&submit=Submit`;

            await new Promise(resolve => {
                GM_xmlhttpRequest({
                    method: "POST",
                    url: "https://www.novelupdates.com/add-release/",
                    data: postData,
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    onload: function(res) {
                        log(res.status === 200 ? `Ch ${i} Sent` : `Ch ${i} Failed (${res.status})`);
                        resolve();
                    },
                    onerror: () => { log(`Error on Ch ${i}`); resolve(); }
                });
            });

            if (i < end) {
                log(`Waiting 3s...`);
                await new Promise(r => setTimeout(r, 3000));
            }
        }

        log("Batch Processing Finished!");
        document.getElementById('nu-submit-btn').disabled = false;
    });

})();