Hanime1 Batch Downloader

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
    }

})();