自动在搜索页面批量获取下载链接并发送到aria2.
// ==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);
}
})();