Sleazy Fork is available in English.

Hanime1 Batch Downloader

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    }

})();