找ai搓了个油猴脚本,方便在nyaa搜动画时切换不同译名作为关键词
脚本会根据当前搜索框内容查询并列出相关动画的各种名称,在列表中点击条目可以直接进行搜索,点击条目前的语言标识可以替换搜索框内容查询服务部署在cloudflare workers上,数据用的是anidb提供的离线名称库,要自建的话可以按下面的步骤
1. 新建KV
2. 新建Workers并贴入代码
3. 绑定Workers和KV,变量名称填“ANIME_TITLES”
4. 添加“变量和机密”用作数据更新页面的密码,类型选“密钥”,变量名称填“PASSWORD”,值为要设置的密码
部署完成后,替换油猴中两处域名即可。首次查询会自动从anidb下载数据,后续如果需要更新数据可以自行添加定时任务或者直接打开workers地址手动更新。anidb提供的数据是每日更新,定时间隔也应当不小于一天。
static/image/hrline/line3.png
油猴脚本:
// ==UserScript==
// @Name Nyaa 搜索建议(动画别名)
// @namespace http://localhost/
// @version 1.1
// @description在 Nyaa 页面快捷查询动画译名,方便切换关键词进行搜索
// @author Claude & Gemini
// @match *://*.nyaa.si/*
// @grant GM_xmlhttpRequest
// @connect anime.titles.workers.dev
// ==/UserScript==
(function () {
'use strict';
const style = document.createElement('style');
style.textContent = `
#anime-search-btn {
background: #337ab7;
color: #fff;
border: none;
border-radius: 3px;
margin-left: 5px;
}
#anime-search-btn:hover { background: #286090; }
#anime-panel {
position: absolute;
z-index: 99999;
background: #fff;
border-radius: 3px;
padding: 10px;
width: 300px;
max-width: 90vw;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
display: none;
font-size: 14px;
}
#anime-panel.show { display: block; }
#anime-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
#anime-panel-header span { color: #595959; font-size: 12px; }
#anime-close { cursor: pointer; font-size: 18px; color: #999; }
#anime-close:hover { color: #333; }
.anime-item {
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.anime-item:last-child { border-bottom: none; margin-bottom: 0; }
.anime-aid { color: #707070; font-size: 11px; margin-bottom: 4px; text-decoration: none; }
.anime-aid:hover { text-decoration: underline; }
.anime-titles { padding-left: 0; margin: 0; list-style: none; }
.anime-titles li { margin: 2px 0; }
.anime-titles a {
color: #337ab7;
text-decoration: none;
}
.anime-titles a:hover { text-decoration: underline; }
.anime-titles .lang {
color: #707070;
font-size: 11px;
margin: 5px 5px;
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
.anime-titles .lang:hover {
color: #000;
/* font-weight: bold;*/
}
`;
document.head.appendChild(style);
function showError(msg) {
panel.classList.add('show');
document.getElementById('anime-info').textContent = msg;
document.getElementById('anime-content').innerHTML = '';
}
// 搜索按钮
const btn = document.createElement('button');
btn.id = 'anime-search-btn';
btn.className = 'btn btn-default';
btn.type = 'button';
// 创建 Font Awesome 图标
const icon = document.createElement('i');
icon.className = 'fa fa-info-circle fa-paw';
btn.appendChild(icon);
// 修改插入逻辑
function insertBtn() {
const target = document.querySelector('.search-btn');
if (target) {
// 创建一个符合 Bootstrap input-group 规范的包装器
const wrapper = document.createElement('div');
wrapper.className = 'input-group-btn';
// 将按钮放入包装器
wrapper.appendChild(btn);
// 将包装器插入到搜索按钮组的后面
target.insertAdjacentElement('afterend', wrapper);
} else {
// 兜底方案
btn.style.cssText = 'position:fixed;top:80px;right:20px;z-index:99999;';
document.body.appendChild(btn);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertBtn);
} else {
insertBtn();
}
// 结果面板
const panel = document.createElement('div');
panel.id = 'anime-panel';
panel.innerHTML = `
<div id="anime-panel-header">
<span id="anime-info"></span>
<span id="anime-close">X</span>
</div>
<div id="anime-content"></div>
`;
document.body.appendChild(panel);
panel.querySelector('#anime-close').onclick = () => panel.classList.remove('show');
// 点击语言标签替换输入框内容
panel.addEventListener('click', function(e) {
// 检查点击的是不是 class="lang" 的元素
if (e.target.classList.contains('lang')) {
const title = e.target.getAttribute('data-title');
const input = document.querySelector('.search-bar');
if (title && input) {
input.value = title; // 替换内容
input.focus(); // 让输入框获得焦点
// 视觉反馈,闪烁输入框背景
const originalBg = input.style.backgroundColor;
input.style.backgroundColor = '#88b5dd';
setTimeout(() => {
input.style.backgroundColor = originalBg;
}, 200);
}
}
});
const langMap = { 'ja': '日', 'en': '英', 'zh-Hans': '简', 'zh-Hant': '繁', 'x-jat': '罗' };
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const input = document.querySelector('.search-bar');
if (!input) return;
const rect = btn.getBoundingClientRect();
const scrollY = window.scrollY || document.documentElement.scrollTop;
const docWidth = document.documentElement.clientWidth;
// 设置面板坐标
panel.style.top = (rect.bottom + scrollY + 5) + 'px';
panel.style.left = 'auto'; // 清除可能存在的左定位
panel.style.right = (docWidth - rect.right) + 'px'; // 右对齐
// 检查关键字
const keyword = input.value.trim();
if (!keyword) {
showError('请输入关键字');
return;
}
// 有关键字,开始搜索流程
panel.classList.add('show');
const content = document.getElementById('anime-content');
const info = document.getElementById('anime-info');
content.innerHTML = '搜索中...';
info.textContent = '';
// 发起请求
GM_xmlhttpRequest({
method: 'GET',
url: `https://anime.titles.workers.dev/?q=${encodeURIComponent(keyword)}&limit=50`,
onload: function (res) {
try {
const data = JSON.parse(res.responseText);
if (data.results && data.results.length > 0) {
info.textContent = `${data.count} 个结果`;//(${data.searchTime})
content.innerHTML = data.results.map(item => `
<div class="anime-item">
<a class="anime-aid" href="https://anidb.net/anime/${item.aid}" target="_blank">AID: ${item.aid}</a>
<ul class="anime-titles">
${item.titles.map(t => {
// 处理标题中的双引号,防止破坏 HTML 结构
const safeTitle = t.title.replace(/"/g, '"');
// 在 span 中添加 data-title 属性和 title 提示
return `
<li>
<span class="lang" data-title="${safeTitle}" title="填入搜索框">
[${langMap || t.language}]
</span>
<a href="/?f=0&c=0_0&q=${encodeURIComponent(t.title)}" target="_blank">${t.title}</a>
</li>
`;
}).join('')}
</ul>
</div>
`).join('');
} else {
content.innerHTML = '未找到结果';
}
} catch (e) {
content.innerHTML = '解析失败: ' + e.message;
}
},
onerror: () => { content.innerHTML = '请求失败'; }
});
};
document.addEventListener('click', function(e) {
// 点击空白处关闭面板
if (!panel.contains(e.target) && e.target !== btn) {
panel.classList.remove('show');
}
});
})();
static/image/hrline/line3.png
Workers代码:
let cachedTitles = null;
let cacheTime = 0;
const CACHE_TTL = 300000; // 5分钟
const ANIDB_URL = 'http://anidb.net/api/anime-titles.dat.gz';
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 优先处理搜索逻辑
if (url.searchParams.has('q')) {
return handleSearch(request, env, url);
}
// 不带 q 参数时进入管理
return handleManagement(request, env, url);
},
// 定时任务处理
async scheduled(event, env, ctx) {
console.log('开始定时更新任务。', new Date().toISOString());
try {
await autoImportData(env);
console.log('定时更新完成');
} catch (error) {
console.error('定时更新失败!', error);
}
}
};
// 搜索逻辑
async function handleSearch(request, env, url) {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
const query = url.searchParams.get('q');
const limit = parseInt(url.searchParams.get('limit') || '100');
try {
const startTime = Date.now();
const now = Date.now();
// 缓存检查
if (!cachedTitles || (now - cacheTime) > CACHE_TTL) {
let allTitlesData;
try {
allTitlesData = await env.ANIME_TITLES.get('all_titles');
} catch (error) {
// KV 未绑定或访问失败
return jsonResponse({
error: 'KV 存储未配置或访问失败',
detail: error.message,
hint: '请检查 ANIME_TITLES KV 命名空间是否已正确绑定'
}, 503, corsHeaders);
}
if (!allTitlesData) {
// 数据未初始化,自动导入
console.log('检测到数据未初始化,开始自动导入……');
try {
await autoImportData(env);
// 导入后重新获取数据
const newData = await env.ANIME_TITLES.get('all_titles');
cachedTitles = JSON.parse(newData);
cacheTime = now;
} catch (error) {
return jsonResponse({
error: '数据未初始化且自动导入失败',
detail: error.message
}, 503, corsHeaders);
}
} else {
cachedTitles = JSON.parse(allTitlesData);
cacheTime = now;
}
}
// 搜索逻辑
const searchLower = query.toLowerCase();
// 一次遍历完成搜索和聚合
const results = Object.entries(cachedTitles)
.filter(() => {
// 检查该 AID 下是否有任何标题匹配
return titles.some(t => t.title?.toLowerCase().includes(searchLower));
})
.slice(0, limit) // 限制数量
.map(() => ({
aid: parseInt(aid),
titles: titles // 直接引用,无需重新构造
}));
return jsonResponse({
query,
count: results.length,
searchTime: `${Date.now() - startTime}ms`,
cached: cacheTime !== now,
results
}, 200, corsHeaders);
} catch (error) {
return jsonResponse({ error: error.message }, 500, corsHeaders);
}
}
// 管理界面
const COOKIE_NAME = 'AniDB_Updater_Auth';
async function handleManagement(request, env, url) {
// 仅在进入管理逻辑时才解析 Cookie
const cookie = request.headers.get('Cookie');
const authCookie = cookie && cookie.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
const isLogged = (authCookie && authCookie === env.PASSWORD);
// 1. 登录处理
if (url.pathname === '/login' && request.method === 'POST') {
const formData = await request.formData();
if (formData.get('password') === env.PASSWORD) {
const headers = new Headers();
headers.append('Set-Cookie', `${COOKIE_NAME}=${env.PASSWORD}; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict`);
headers.append('Location', '/');
return new Response(null, { status: 302, headers });
}
return new Response(null, { status: 302, headers: { 'Location': '/' } });
}
// 2. 一键导入处理
if (url.pathname === '/auto-import' && request.method === 'POST') {
if (!isLogged) return new Response('Unauthorized', { status: 401 });
try {
const count = await autoImportData(env);
return new Response(`自动导入完成,总条目数: ${count}`);
} catch (e) {
return new Response(`自动导入失败! ${e.message}`, { status: 500 });
}
}
// 3. 手动上传处理
if (request.method === 'POST') {
if (!isLogged) return new Response('Unauthorized', { status: 401 });
try {
const fileBuffer = await request.arrayBuffer();
if (!fileBuffer || fileBuffer.byteLength === 0) return new Response('无文件内容', { status: 400 });
const count = await processAndSaveData(fileBuffer, env);
// 上传成功后,强制清空搜索缓存
cachedTitles = null;
return new Response(`更新完成,总条目数: ${count}`);
} catch (e) {
return new Response(`失败! ${e.message}`, { status: 500 });
}
}
// 4. 页面渲染
const html = isLogged ? await renderUploadUI(env) : renderLoginUI();
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
// ================= 自动导入功能 =================
async function autoImportData(env) {
// 检查上次更新日期
const metadata = await env.ANIME_TITLES.get('metadata');
if (metadata) {
const meta = JSON.parse(metadata);
const lastUpdate = new Date(meta.lastUpdate);
const now = new Date();
// 获取日期部分(忽略时间)
const lastUpdateDate = lastUpdate.toISOString().split('T');
const nowDate = now.toISOString().split('T');
// 如果是同一天,拒绝更新
if (lastUpdateDate === nowDate) {
throw new Error(`今日已更新(${lastUpdate.toLocaleString('zh-CN')}),请明日再试`);
}
}
console.log('正在从 AniDB 获取数据……');
// 从 AniDB 下载 .gz 文件,添加必要的请求头
const response = await fetch(ANIDB_URL, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Connection': 'keep-alive',
'Host': 'anidb.net',
},
cf: {
cacheTtl: 3600,
cacheEverything: true
}
});
if (!response.ok) {
throw new Error(`下载失败! ${response.status} ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
console.log(`下载完成,文件大小: ${buffer.byteLength} 字节`);
// 处理并保存数据
const count = await processAndSaveData(buffer, env);
// 清空缓存
cachedTitles = null;
console.log(`数据处理完成,总条目数: ${count}`);
return count;
}
// ================= 数据处理 (仅上传时调用) =================
async function processAndSaveData(buffer, env) {
const ALLOWED_LANGS = new Set(['ja', 'en', 'x-jat', 'zh-Hans', 'zh-Hant']);
// 定义语言优先级映射
const LANG_PRIORITY = {
'zh-Hans': 1,
'zh-Hant': 2,
'ja': 3,
'x-jat': 4,
'en': 5
};
let text = '';
const view = new Uint8Array(buffer);
if (view === 0x1f && view === 0x8b) {
try {
const ds = new DecompressionStream('gzip');
const stream = new Response(buffer).body.pipeThrough(ds);
const decompressed = await new Response(stream).arrayBuffer();
text = new TextDecoder('utf-8').decode(decompressed);
} catch (e) { throw new Error('解压失败!'); }
} else {
text = new TextDecoder('utf-8').decode(buffer);
}
const titles = [];
const lines = text.trim().split('\n');
for (const line of lines) {
if (line.startsWith('#')) continue;
const parts = line.split('|');
if (parts.length !== 4) continue;
const = parts;
if (!aid || !title) continue;
const type = parseInt(typeStr);
if (type === 3) continue;
if (ALLOWED_LANGS.has(language)) {
titles.push({
aid: parseInt(aid),
type,
language,
title
});
}
}
if (titles.length === 0) throw new Error('无有效数据!');
// 按 AID 分组
const groupedByAid = {};
for (const item of titles) {
const aid = item.aid.toString(); // 转为字符串作为 key
if (!groupedByAid) {
groupedByAid = [];
}
groupedByAid.push({
type: item.type,
language: item.language,
title: item.title
});
}
// 对每个 AID 的标题按语言优先级排序
for (const aid in groupedByAid) {
groupedByAid.sort((a, b) => {
const langA = LANG_PRIORITY || 99;
const langB = LANG_PRIORITY || 99;
return langA - langB;
});
}
// 保存分组后的数据
await env.ANIME_TITLES.put('all_titles', JSON.stringify(groupedByAid));
await env.ANIME_TITLES.put('metadata', JSON.stringify({
lastUpdate: new Date().toISOString(),
totalTitles: titles.length,
totalAnime: Object.keys(groupedByAid).length,
source: 'auto'
}));
return titles.length;
}
// ================= 辅助函数 =================
function jsonResponse(data, status = 200, headers = {}) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: { ...headers, 'Content-Type': 'application/json; charset=utf-8' }
});
}
function renderLoginUI() {
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Login</title><style>body{display:flex;justify-content:center;align-items:center;height:100vh;margin:0;font-family:sans-serif;background:#fafafa}form{background:white;padding:2rem;border:1px solid #eaeaea;border-radius:2px;width:280px}input{width:100%;padding:10px;margin-bottom:10px;border:1px solid #ddd;border-radius:2px;box-sizing:border-box;outline:none}input:focus{border-color:#000}button{width:100%;padding:10px;background:#000;color:white;border:none;border-radius:2px;cursor:pointer;font-weight:bold}button:hover{opacity:0.8}.link-area{margin-bottom:15px;text-align:center;font-size:12px;word-break:break-all;}.link-area a{color:#555;}</style></head><body><form action="/login" method="POST"><div class="link-area"><a href="/?q=maruko&limit=50">Example</a></div><input type="password" name="password" placeholder="Password" required autofocus><button type="submit">Enter</button></form></body></html>`;
}
async function renderUploadUI(env) {
// 获取上次更新信息
let lastUpdateInfo = '';
try {
const metadata = await env.ANIME_TITLES.get('metadata');
if (metadata) {
const meta = JSON.parse(metadata);
const updateDate = new Date(meta.lastUpdate);
lastUpdateInfo = `<div class="info-box">上次更新: ${updateDate.toLocaleString('zh-CN')} | 总条目: ${meta.totalTitles.toLocaleString()}</div>`;
}
} catch (e) {
lastUpdateInfo = '<div class="info-box">暂无更新记录</div>';
}
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>导入数据</title><style>body{font-family:sans-serif;max-width:500px;margin:3rem auto;padding:0 1rem;color:#333}.card{border:1px solid #eaeaea;padding:1.5rem;border-radius:2px;background:#fff;margin-bottom:1rem}h3{margin-top:0;font-size:1.1rem;margin-bottom:1rem}.info-box{background:#f5f5f5;padding:10px;border-radius:2px;font-size:13px;color:#666;margin-bottom:1rem}.upload-area{position:relative;height:120px;border:2px dashed #ddd;border-radius:2px;background:#fafafa;display:flex;align-items:center;justify-content:center;text-align:center;transition:border-color 0.2s;margin-bottom:1rem}.upload-area:hover{border-color:#999}.upload-area input{position:absolute;width:100%;height:100%;top:0;left:0;opacity:0;cursor:pointer;z-index:2}.placeholder{pointer-events:none;color:#666;font-size:0.9rem}button{background:#000;color:white;border:none;padding:12px;border-radius:2px;cursor:pointer;width:100%;font-weight:bold;margin-bottom:8px}button:disabled{background:#ccc;cursor:not-allowed}button:hover:not(:disabled){opacity:0.8}#log{margin-top:15px;font-size:13px;color:#555;word-break:break-all}.links{font-size:12px;margin-bottom:1rem;color:#888}.links a{color:#555}.divider{border-top:1px solid #eaeaea;margin:1.5rem 0;position:relative}.divider span{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:white;padding:0 10px;color:#999;font-size:12px}</style></head><body><div class="card"><h3>数据状态</h3>${lastUpdateInfo}</div><div class="card"><h3>一键导入</h3><div class="links">从 AniDB 自动获取最新数据</div><button class="btn-auto" onclick="autoImport()">一键导入最新数据</button><div class="divider"><span>或</span></div><h3>手动上传</h3><div class="links">手动下载:<a href="http://anidb.net/api/anime-titles.dat.gz" target="_blank">anidb.net/api/anime-titles.dat.gz</a></div><div class="upload-area"><input type="file" id="f" accept=".gz,.dat"><div class="placeholder" id="p">点击或拖拽文件 (.gz)</div></div><button onclick="u()">开始上传</button><div id="l"></div></div><script>document.getElementById('f').onchange=e=>{document.getElementById('p').innerHTML='已选择:<br><b>'+(e.target.files?e.target.files.name:'...')+'</b>'};async function autoImport(){const l=document.getElementById('l'),btns=document.querySelectorAll('button');btns.forEach(b=>b.disabled=1);l.innerText='正在从 AniDB 获取数据...';l.style.color='#0070f3';try{const r=await fetch('/auto-import',{method:'POST'});const t=await r.text();l.innerText=(r.ok?'成功!':'失败!')+t;l.style.color=r.ok?'#008000':'#d00';if(r.ok)setTimeout(()=>location.reload(),1500);if(r.status===401)location.reload()}catch(e){l.innerText='ERR: '+e;l.style.color='#d00'}finally{btns.forEach(b=>b.disabled=0)}}async function u(){const f=document.getElementById('f'),l=document.getElementById('l'),btns=document.querySelectorAll('button');if(!f.files.length)return alert('无文件');btns.forEach(b=>b.disabled=1);l.innerText='上传中...';l.style.color='#0070f3';try{const r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:f.files});const t=await r.text();l.innerText=(r.ok?'成功!':'失败!')+t;l.style.color=r.ok?'#008000':'#d00';if(r.ok)setTimeout(()=>location.reload(),1500);if(r.status===401)location.reload()}catch(e){l.innerText='ERR: '+e;l.style.color='#d00'}finally{btns.forEach(b=>b.disabled=0)}}</script></body></html>`;
}
可以,是我想要的功能,之前就总是先去找动画的罗马文和日文是什么,才去nyaa上搜索,这个就省事多了 好帖帮顶 本帖最后由 waecy 于 2026-3-13 19:04 编辑
支持,现在用AI写游猴脚本,多加调教,试错反馈可以写成不少好用的功能
浏览次数统计/一键复制 / 一键批量打开 / 一键磁力导出 /一键BT文件下载/BT压缩包打包/表格导出…
以前还得花一天写样式,调试脚本,如今只要提需求就能写出来, 确实方便多了
先推荐个一键复制
NYAA复制磁力
https://greasyfork.org/zh-CN/scripts/530242
本帖最后由 waecy 于 2026-1-11 14:01 编辑
经过多年测试, AniDB个人也常用找英文或罗马音, 但亲测不全, 实际上找番, 若要外网找到所有番名, 中/日/英的话,需多个搭配, 实际网友搜刮,也只能选其一, 目前没全部搜刮收藏的轮子
三大最全资讯
1. Bangumi (搭配脚本,也可以加载Anidb或MAL的罗马音和英文名,但不全) 主要中文名/日文名
2. AniDB 最常用之一
3. MAL 补充英文名, 一些特典,广播剧英文名啥的, 这里最全
大部分情况,三大网站汇总,就算挺全的,但若要更全的话
1.TMDB 经常搜刮的不陌生,不少中文译名和网友自己添加的罗马音和英文名 用作补齐
2.AS 其实大部分重复,论全不如以上, 但可以部分补充
其他的
ANN
豆瓣
百科
萌娘
维基
...
说实话用的情况反而少, 需要看详细人物介绍和来源倒可以看看, 但收藏番名用于收藏的话,倒没必要, 番名的中/日/英/罗 有以上基本汇总完毕
别的国家语言我这边用的少, 比如俄语啥的,有的松鼠党喜欢收藏各种原盘, 各种语言的也能去搜下,不过这种大部分也有英文名作为标题, 个人感觉除非是个人刚需的话, 否则没必要收藏名称
PS: 不排除终极强迫症,爱好把各国语言的番名也收藏就是了
这是本人写的一个番名去重汇总功能, 适合把不同番名汇总, 安装中文名[简体别名][繁体别名][日文名][英文名] 年份 集数 来收藏,需要的可自取
https://www.bilibili.com/video/BV1Lmn2z8E6P
✨番剧不同译名收藏-文本处理工具✨
https://anilistname.netlify.app/
页:
[1]