// ==UserScript==
// @name         JYNGMN Image Downloader (Stable Sequential ZIP)
// @namespace    http://tampermonkey.net/
// @version      1.33
// @description  Download images from xinggan5.top albums with stable sequential numbering
// @author       ChatGPT
// @match        https://www.xinggan5.top/*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// ==/UserScript==

(function () {
    'use strict';

    const USE_CHUNKED_DOWNLOADS = false;
    const CHUNK_COUNT = 3;
    const PAGE_CONCURRENCY = 5;
    const IMAGE_BATCH_SIZE = 5;

    function makeAbsoluteURL(url, base) {
        try { return new URL(url, base || window.location.origin).href; }
        catch { return url; }
    }

    async function injectScript(url) {
        return new Promise((resolve, reject) => {
            if (document.querySelector(`script[src="${url}"]`)) { resolve(); return; }
            const script = document.createElement('script');
            script.src = url;
            script.onload = () => resolve();
            script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
            document.head.appendChild(script);
        });
    }

    async function fetchImage(url) {
        try {
            const res = await fetch(url, { mode: 'cors', credentials: 'omit' });
            if (!res.ok) throw new Error(`Failed to fetch image: ${url} (${res.status})`);
            return await res.blob();
        } catch (e) {
            console.error(e.message);
            return null;
        }
    }

    function getFileExtension(url) {
        const m = url.match(/\.([a-zA-Z0-9]+)(\?.*)?$/);
        return m ? m[1].toLowerCase() : 'jpg';
    }

    function downloadUsingAnchor(blobUrl, fileName) {
        const a = document.createElement('a');
        a.href = blobUrl;
        a.download = fileName;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    async function createAndDownloadZip(zip, zipName = 'images.zip') {
        try {
            const blob = await zip.generateAsync({ type: 'blob', compression: 'STORE' });
            const blobUrl = URL.createObjectURL(blob);
            downloadUsingAnchor(blobUrl, zipName);
        } catch (e) {
            alert(`ZIP creation failed: ${e.message}`);
        }
    }

    function getPageHTML(url) {
        return new Promise((resolve, reject) => {
            $.get(url).done(html => resolve($(html))).fail(() => reject(new Error(`Failed to load ${url}`)));
        });
    }

    function getPaginationUrls($doc, baseUrl) {
        const urls = [];
        const pagination = $doc.find('div.pagination');
        if (pagination.length) {
            pagination.find('a[href]').each(function () {
                const txt = $(this).text().trim();
                if (/^\d+$/.test(txt)) {
                    const href = $(this).attr('href');
                    if (href) urls.push(makeAbsoluteURL(href, baseUrl));
                }
            });
        }
        if (urls.length === 0) urls.push(baseUrl);
        return urls;
    }

    function extractImageUrlsFromDoc($doc, pageUrl) {
        const imgs = [];
        $doc.find('article.article-content p img[src]').each(function () {
            const src = $(this).attr('src');
            if (src && !src.toLowerCase().endsWith('.gif')) imgs.push(makeAbsoluteURL(src, pageUrl));
        });
        return imgs;
    }

    function chunkArray(arr, size) {
        const result = [];
        for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
        return result;
    }

    async function downloadImagesFromUrl(linkUrl, useCurrentPage = false) {
        const localState = {
            images: [],
            zip: new JSZip(),
            TITLE: '',
            totalImages: 0,
            downloadedCount: 0
        };

        try {
            let $baseDoc = useCurrentPage ? $(document) : await getPageHTML(linkUrl);
            const text = $baseDoc.text();
            const match = text.match(/\[[^\]]+\] .+? \[[^\]]+P\]/);
            if (match && match[0]) localState.TITLE = match[0].trim().replace(/[\\/:*?"<>|]/g, '_');
            else {
                const h1 = $baseDoc.find('h1.article-title').text() || '';
                localState.TITLE = h1.trim().replace(/[\\/:*?"<>|]/g, '_') || 'images';
            }

            const pageUrls = getPaginationUrls($baseDoc, linkUrl);

            // Fetch all pages and collect image URLs
            let index = 0;
            const concurrency = PAGE_CONCURRENCY;
            async function worker() {
                while (index < pageUrls.length) {
                    const i = index++;
                    try {
                        const $pageDoc = (i === 0 && useCurrentPage) ? $baseDoc : await getPageHTML(pageUrls[i]);
                        const imgs = extractImageUrlsFromDoc($pageDoc, pageUrls[i]);
                        localState.images.push(...imgs);
                        console.log(`Page ${i + 1}/${pageUrls.length} fetched, ${imgs.length} images.`);
                    } catch (err) {
                        console.warn(`Failed page ${pageUrls[i]}: ${err.message}`);
                    }
                }
            }
            await Promise.all(Array(concurrency).fill().map(() => worker()));

            // Deduplicate URLs while preserving order
            const seen = new Set();
            localState.images = localState.images.filter(url => {
                if (seen.has(url)) return false;
                seen.add(url);
                return true;
            });
            localState.totalImages = localState.images.length;
            console.log(`Total unique images collected: ${localState.totalImages}`);

            if (localState.totalImages === 0) {
                alert('No images found to download.');
                return;
            }

            // Download images sequentially to ensure proper ordering
            for (let i = 0; i < localState.images.length; i++) {
                const imgUrl = localState.images[i];
                const blob = await fetchImage(imgUrl);

                if (!blob) {
                    console.warn(`Skipping image ${i + 1}: ${imgUrl}`);
                    continue;
                }

                const ext = getFileExtension(imgUrl);
                const validExts = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'tiff'];
                const finalExt = validExts.includes(ext) ? ext : 'jpg';
				let filename;
				if (i === localState.images.length - 1) {
					// last image renamed to 000.ext
					filename = `000.${finalExt}`;
				} else {
					// normal numbering from 001...
					filename = `${String(i + 1).padStart(3, '0')}.${finalExt}`;
				}
				localState.zip.file(filename, blob);
                localState.downloadedCount++;

                console.log(`Added image ${i + 1} to ZIP: ${filename}`);

                // Small delay between downloads to avoid rate limiting
                await new Promise(resolve => setTimeout(resolve, 300));
            }

            if (localState.downloadedCount === 0) {
                alert('No images were successfully downloaded.');
                return;
            }

            await createAndDownloadZip(localState.zip, `${localState.TITLE}.zip`);

        } catch (e) {
            console.error('Download process failed:', e);
            alert('Download error: ' + e.message);
        }
    }

    function addDownloadButton(targetElement, linkUrl, useCurrentPage = false, insertMode = 'after') {
        if (insertMode === 'append') {
            if (targetElement.querySelector('button.download-button')) return;
        } else if (insertMode === 'after') {
            if (targetElement.nextElementSibling?.classList.contains('download-button')) return;
        }

        const button = document.createElement('button');
        button.innerText = 'DOWNLOAD';
        button.classList.add('download-button');

        Object.assign(button.style, {
            marginLeft: '10px',
            padding: '3px 8px',
            cursor: 'pointer',
            backgroundColor: '#FB1C76',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            fontSize: '12px',
            transition: 'background-color 0.3s',
            boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
        });

        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = '#AA124F';
        });
        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = '#FB1C76';
        });

        button.addEventListener('click', async (event) => {
            event.preventDefault();
            button.disabled = true;
            const originalText = button.innerText;
            button.innerText = 'Processing…';

            await downloadImagesFromUrl(linkUrl, useCurrentPage);

            button.innerText = originalText;
            button.disabled = false;
        });

        if (insertMode === 'append') {
            targetElement.appendChild(button);
        } else {
            targetElement.insertAdjacentElement('afterend', button);
        }
    }

    function findAndAddButtons() {
        document.querySelectorAll('div.node p a b').forEach(el => {
            const aTag = el.parentElement;
            if (aTag && !aTag.nextElementSibling?.classList.contains('download-button')) {
                const href = aTag.getAttribute('href');
                if (href) {
                    addDownloadButton(el, makeAbsoluteURL(href, window.location.href), false, 'after');
                }
            }
        });

        document.querySelectorAll('div.toptip').forEach(div => {
            if (!div.querySelector('button.download-button')) {
                addDownloadButton(div, window.location.href, true, 'append');
            }
        });
    }

    async function initializeScript() {
        console.log('Initializing JYNGMN Image Downloader...');
        try {
            await injectScript('https://cdn.jsdelivr.net/npm/jszip@3.10.0/dist/jszip.min.js');
            if (typeof JSZip === 'undefined') throw new Error('JSZip failed to load.');
            findAndAddButtons();

            const observer = new MutationObserver(() => findAndAddButtons());
            observer.observe(document.body, { childList: true, subtree: true });
        } catch (e) {
            console.error('Init failed:', e);
            alert('Failed to initialize the downloader script.');
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }
})();

Please explain the method used to collect the image address links.