Hanime1 Batch Downloader

自动在搜索页面批量获取下载链接并发送到aria2.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Hanime1 Batch Downloader
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  自动在搜索页面批量获取下载链接并发送到aria2.
// @license MIT
// @match        https://hanime1.me/search*
// @match        https://hanime1.me/download*
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// ==/UserScript==

(function () {
    'use strict';

    const ARIA2_RPC = "http://localhost:6800/jsonrpc";
    const MAX_OPEN = 2;

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function randomJitter(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }


    /* ================= aria2 ================= */

    function aria2Rpc(data) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: ARIA2_RPC,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify(data),
                onload: r => resolve(JSON.parse(r.responseText)),
                onerror: reject
            });
        });
    }

    async function sendToAria2(url) {
        await aria2Rpc({
            jsonrpc: "2.0",
            id: Date.now().toString(),
            method: "aria2.addUri",
            params: [[url]]
        });
    }

    /* ============================================================
       DOWNLOAD 页面
    ============================================================ */

    if (location.pathname.startsWith("/download")) {

        (async () => {

            const batchMode = await GM_getValue("hanime_batch_mode", false);
            if (!batchMode) return;

            function getMp4Links() {
                return [...document.querySelectorAll("a[data-url]")]
                    .map(a => a.dataset.url)
                    .filter(u => u.includes("vdownload") && u.includes(".mp4"));
            }

            function pickBestQuality(urls) {
                const parsed = urls.map(u => {
                    const match = u.match(/-(\d+)p\.mp4/);
                    return {
                        url: u,
                        quality: match ? parseInt(match[1]) : 0
                    };
                });

                parsed.sort((a, b) => b.quality - a.quality);
                return parsed.length ? parsed[0].url : null;
            }

            const allLinks = getMp4Links();
            const best = pickBestQuality(allLinks);

            if (best) {
                await GM_setValue("hanime_result", {
                    link: best,
                    time: Date.now()
                });
            }

            window.close();

        })();

        return;
    }

    /* ============================================================
       SEARCH 页面
    ============================================================ */

    if (location.pathname.startsWith("/search")) {

        let listenerRegistered = false;
        let lastHandledTime = 0;
        let collectedLinks = [];

        function getWatchLinks() {
            return [...document.querySelectorAll("a.video-link")]
                .map(a => a.href)
                .filter(link => link.includes("/watch?v="));
        }

        function watchToDownload(url) {
            return url.replace("/watch", "/download");
        }

        function createOverlay() {
            if (document.getElementById("hanime-overlay")) return;

            const div = document.createElement("div");
            div.id = "hanime-overlay";
            div.style.cssText = `
                position:fixed;
                top:0;left:0;
                width:100%;height:100%;
                background:rgba(0,0,0,0.9);
                color:#fff;
                z-index:99999;
                padding:20px;
                font-family:monospace;
                overflow:auto;
            `;

            div.innerHTML = `
                <h2>Hanime 批量抓取</h2>
                <div id="status">初始化...</div>
                <div style="width:100%;height:10px;background:#333;margin-top:10px;">
                    <div id="progress-inner" style="width:0%;height:100%;background:#5cb85c;"></div>
                </div>
                <div id="result" style="margin-top:15px;"></div>
                <button id="start-download">开始下载</button>
                <button id="close-overlay">关闭</button>
            `;

            document.body.appendChild(div);

            document.getElementById("close-overlay").onclick = () => div.remove();

            document.getElementById("start-download").onclick = async () => {
                document.getElementById("status").textContent = "发送到 aria2...";
                for (const link of collectedLinks) {
                    await sendToAria2(link);
                }
                document.getElementById("status").textContent = "下载任务已提交";
            };
        }

        function updateProgress(done, total) {
            const percent = Math.round(done / total * 100);
            document.getElementById("status").textContent =
                `抓取中 ${done}/${total}`;
            document.getElementById("progress-inner").style.width =
                percent + "%";
        }

        function appendResult(url) {
            const box = document.getElementById("result");
            const p = document.createElement("div");
            p.textContent = url;
            box.appendChild(p);
        }

        async function startBatch() {

            createOverlay();

            const watchLinks = getWatchLinks();
            const downloadLinks = watchLinks.map(watchToDownload);
            const total = downloadLinks.length;

            let done = 0;
            let index = 0;
            const queue = [];

            collectedLinks = [];

            await GM_setValue("hanime_batch_mode", true);
            await GM_setValue("hanime_result", null);

            async function openNext() {
                while (queue.length < MAX_OPEN && index < total) {

                    const delay = randomJitter(2500, 5000); // 2.5s - 5s 随机
                    await sleep(delay);

                    const url = downloadLinks[index++];
                    const win = window.open(url, "_blank");
                    queue.push(win);
                }
            }


            if (!listenerRegistered) {

                GM_addValueChangeListener("hanime_result", async (name, oldVal, newVal) => {

                    if (!newVal) return;
                    if (newVal.time === lastHandledTime) return;

                    lastHandledTime = newVal.time;

                    const link = newVal.link;

                    collectedLinks.push(link);
                    appendResult(link);

                    done++;
                    updateProgress(done, total);

                    queue.shift();
                    openNext();

                    if (done >= total) {
                        await GM_setValue("hanime_batch_mode", false);
                        document.getElementById("status").textContent =
                            "抓取完成,点击开始下载";
                    }
                });

                listenerRegistered = true;
            }

            openNext();
        }

        function insertButton() {
            if (document.getElementById("download-page-btn")) return;

            const nav = document.getElementById("search-nav-desktop");
            if (!nav) return;

            const btn = document.createElement("button");
            btn.id = "download-page-btn";
            btn.type = "button";
            btn.textContent = "批量抓取本页";
            btn.style.cssText = `
                margin-left:15px;
                padding:8px 16px;
                background:#d9534f;
                color:#fff;
                border:0;
                border-radius:4px;
                cursor:pointer;
            `;
            btn.onclick = startBatch;
            nav.appendChild(btn);
        }

        const observer = new MutationObserver(insertButton);
        observer.observe(document.body, { childList: true, subtree: true });
        window.addEventListener("load", insertButton);
    }

})();