Hanime1 Batch Downloader

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();