找回密码
 立即注册
查看: 614|回复: 4

找ai搓了个油猴脚本,方便在nyaa搜动画时切换不同译名作为关键词

1

主题

4

回帖

0

VC币

中级会员

Rank: 3Rank: 3

积分
18390
野口笑子 发表于 2026-1-10 23:21:11 | 显示全部楼层 |阅读模式
脚本会根据当前搜索框内容查询并列出相关动画的各种名称,在列表中点击条目可以直接进行搜索,点击条目前的语言标识可以替换搜索框内容

231858.png

查询服务部署在cloudflare workers上,数据用的是anidb提供的离线名称库,要自建的话可以按下面的步骤
1. 新建KV
2. 新建Workers并贴入代码
3. 绑定Workers和KV,变量名称填“ANIME_TITLES”
4. 添加“变量和机密”用作数据更新页面的密码,类型选“密钥”,变量名称填“PASSWORD”,值为要设置的密码

部署完成后,替换油猴中两处域名即可。首次查询会自动从anidb下载数据,后续如果需要更新数据可以自行添加定时任务或者直接打开workers地址手动更新。anidb提供的数据是每日更新,定时间隔也应当不小于一天。





油猴脚本:
  1. // ==UserScript==
  2. // [url=home.php?mod=space&uid=14588]@Name[/url]         Nyaa 搜索建议(动画别名)
  3. // @namespace    http://localhost/
  4. // @version      1.1
  5. // @description  在 Nyaa 页面快捷查询动画译名,方便切换关键词进行搜索
  6. // @author       Claude & Gemini
  7. // @match        *://*.nyaa.si/*
  8. // @grant        GM_xmlhttpRequest
  9. // @connect      anime.titles.workers.dev
  10. // ==/UserScript==

  11. (function () {
  12.     'use strict';

  13.     const style = document.createElement('style');
  14.     style.textContent = `
  15.         #anime-search-btn {
  16.             background: #337ab7;
  17.             color: #fff;
  18.             border: none;
  19.             border-radius: 3px;
  20.             margin-left: 5px;
  21.         }
  22.         #anime-search-btn:hover { background: #286090; }

  23.         #anime-panel {
  24.             position: absolute;
  25.             z-index: 99999;
  26.             background: #fff;
  27.             border-radius: 3px;
  28.             padding: 10px;
  29.             width: 300px;
  30.             max-width: 90vw;
  31.             max-height: 70vh;
  32.             overflow-y: auto;
  33.             box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  34.             display: none;
  35.             font-size: 14px;
  36.         }
  37.         #anime-panel.show { display: block; }

  38.         #anime-panel-header {
  39.             display: flex;
  40.             justify-content: space-between;
  41.             align-items: center;
  42.             margin-bottom: 10px;
  43.             padding-bottom: 8px;
  44.             border-bottom: 1px solid #eee;
  45.         }
  46.         #anime-panel-header span { color: #595959; font-size: 12px; }
  47.         #anime-close { cursor: pointer; font-size: 18px; color: #999; }
  48.         #anime-close:hover { color: #333; }

  49.         .anime-item {
  50.             margin-bottom: 5px;
  51.             padding-bottom: 5px;
  52.             border-bottom: 1px solid #eee;
  53.         }
  54.         .anime-item:last-child { border-bottom: none; margin-bottom: 0; }
  55.         .anime-aid { color: #707070; font-size: 11px; margin-bottom: 4px; text-decoration: none; }
  56.         .anime-aid:hover { text-decoration: underline; }
  57.         .anime-titles { padding-left: 0; margin: 0; list-style: none; }
  58.         .anime-titles li { margin: 2px 0; }
  59.         .anime-titles a {
  60.             color: #337ab7;
  61.             text-decoration: none;
  62.         }
  63.         .anime-titles a:hover { text-decoration: underline; }
  64.         .anime-titles .lang {
  65.             color: #707070;
  66.             font-size: 11px;
  67.             margin: 5px 5px;
  68.             cursor: pointer;
  69.             user-select: none;
  70.             transition: color 0.2s;
  71.         }
  72.         .anime-titles .lang:hover {
  73.             color: #000;
  74.             /* font-weight: bold;*/
  75.         }

  76.     `;
  77.     document.head.appendChild(style);

  78.     function showError(msg) {
  79.         panel.classList.add('show');
  80.         document.getElementById('anime-info').textContent = msg;
  81.         document.getElementById('anime-content').innerHTML = '';
  82.     }

  83.     // 搜索按钮
  84.     const btn = document.createElement('button');
  85.     btn.id = 'anime-search-btn';
  86.     btn.className = 'btn btn-default';
  87.     btn.type = 'button';

  88.     // 创建 Font Awesome 图标
  89.     const icon = document.createElement('i');
  90.     icon.className = 'fa fa-info-circle fa-paw';
  91.     btn.appendChild(icon);

  92.     // 修改插入逻辑
  93.     function insertBtn() {
  94.         const target = document.querySelector('.search-btn');
  95.         if (target) {
  96.             // 创建一个符合 Bootstrap input-group 规范的包装器
  97.             const wrapper = document.createElement('div');
  98.             wrapper.className = 'input-group-btn';
  99.             // 将按钮放入包装器
  100.             wrapper.appendChild(btn);
  101.             // 将包装器插入到搜索按钮组的后面
  102.             target.insertAdjacentElement('afterend', wrapper);
  103.         } else {
  104.             // 兜底方案
  105.             btn.style.cssText = 'position:fixed;top:80px;right:20px;z-index:99999;';
  106.             document.body.appendChild(btn);
  107.         }
  108.     }

  109.     if (document.readyState === 'loading') {
  110.         document.addEventListener('DOMContentLoaded', insertBtn);
  111.     } else {
  112.         insertBtn();
  113.     }

  114.     // 结果面板
  115.     const panel = document.createElement('div');
  116.     panel.id = 'anime-panel';
  117.     panel.innerHTML = `
  118.         <div id="anime-panel-header">
  119.             <span id="anime-info"></span>
  120.             <span id="anime-close">X</span>
  121.         </div>
  122.         <div id="anime-content"></div>
  123.     `;
  124.     document.body.appendChild(panel);

  125.     panel.querySelector('#anime-close').onclick = () => panel.classList.remove('show');

  126.     // 点击语言标签替换输入框内容
  127.     panel.addEventListener('click', function(e) {
  128.         // 检查点击的是不是 class="lang" 的元素
  129.         if (e.target.classList.contains('lang')) {
  130.             const title = e.target.getAttribute('data-title');
  131.             const input = document.querySelector('.search-bar');

  132.             if (title && input) {
  133.                 input.value = title; // 替换内容
  134.                 input.focus(); // 让输入框获得焦点

  135.                 // 视觉反馈,闪烁输入框背景
  136.                 const originalBg = input.style.backgroundColor;
  137.                 input.style.backgroundColor = '#88b5dd';
  138.                 setTimeout(() => {
  139.                     input.style.backgroundColor = originalBg;
  140.                 }, 200);
  141.             }
  142.         }
  143.     });

  144.     const langMap = { 'ja': '日', 'en': '英', 'zh-Hans': '简', 'zh-Hant': '繁', 'x-jat': '罗' };

  145.     btn.onclick = (e) => {
  146.         e.preventDefault();
  147.         e.stopPropagation();

  148.         const input = document.querySelector('.search-bar');
  149.         if (!input) return;

  150.         const rect = btn.getBoundingClientRect();
  151.         const scrollY = window.scrollY || document.documentElement.scrollTop;
  152.         const docWidth = document.documentElement.clientWidth;

  153.         // 设置面板坐标
  154.         panel.style.top = (rect.bottom + scrollY + 5) + 'px';
  155.         panel.style.left = 'auto'; // 清除可能存在的左定位
  156.         panel.style.right = (docWidth - rect.right) + 'px'; // 右对齐

  157.         // 检查关键字
  158.         const keyword = input.value.trim();

  159.         if (!keyword) {
  160.             showError('请输入关键字');
  161.             return;
  162.         }

  163.         // 有关键字,开始搜索流程
  164.         panel.classList.add('show');
  165.         const content = document.getElementById('anime-content');
  166.         const info = document.getElementById('anime-info');

  167.         content.innerHTML = '搜索中...';
  168.         info.textContent = '';

  169.         // 发起请求
  170.         GM_xmlhttpRequest({
  171.             method: 'GET',
  172.             url: `https://anime.titles.workers.dev/?q=${encodeURIComponent(keyword)}&limit=50`,
  173.             onload: function (res) {
  174.                 try {
  175.                     const data = JSON.parse(res.responseText);
  176.                     if (data.results && data.results.length > 0) {
  177.                         info.textContent = `${data.count} 个结果`;  //  (${data.searchTime})
  178.                         content.innerHTML = data.results.map(item => `
  179.                             <div class="anime-item">
  180.                                 <a class="anime-aid" href="https://anidb.net/anime/${item.aid}" target="_blank">AID: ${item.aid}</a>
  181.                                 <ul class="anime-titles">
  182.                                     ${item.titles.map(t => {
  183.                                         // 处理标题中的双引号,防止破坏 HTML 结构
  184.                                         const safeTitle = t.title.replace(/"/g, '&quot;');

  185.                                         // 在 span 中添加 data-title 属性和 title 提示
  186.                                         return `
  187.                                         <li>
  188.                                             <span class="lang" data-title="${safeTitle}" title="填入搜索框">
  189.                                                 [${langMap[t.language] || t.language}]
  190.                                             </span>
  191.                                             <a href="/?f=0&c=0_0&q=${encodeURIComponent(t.title)}" target="_blank">${t.title}</a>
  192.                                         </li>
  193.                                         `;
  194.                                     }).join('')}
  195.                                 </ul>
  196.                             </div>
  197.                         `).join('');
  198.                     } else {
  199.                         content.innerHTML = '未找到结果';
  200.                     }
  201.                 } catch (e) {
  202.                     content.innerHTML = '解析失败: ' + e.message;
  203.                 }
  204.             },
  205.             onerror: () => { content.innerHTML = '请求失败'; }
  206.         });
  207.     };
  208.     document.addEventListener('click', function(e) {
  209.         // 点击空白处关闭面板
  210.         if (!panel.contains(e.target) && e.target !== btn) {
  211.             panel.classList.remove('show');
  212.         }
  213.     });
  214. })();
复制代码






Workers代码:
  1. let cachedTitles = null;
  2. let cacheTime = 0;
  3. const CACHE_TTL = 300000; // 5分钟
  4. const ANIDB_URL = 'http://anidb.net/api/anime-titles.dat.gz';

  5. export default {
  6.   async fetch(request, env) {
  7.     const url = new URL(request.url);

  8.     // 优先处理搜索逻辑
  9.     if (url.searchParams.has('q')) {
  10.       return handleSearch(request, env, url);
  11.     }

  12.     // 不带 q 参数时进入管理
  13.     return handleManagement(request, env, url);
  14.   },

  15.   // 定时任务处理
  16.   async scheduled(event, env, ctx) {
  17.     console.log('开始定时更新任务。', new Date().toISOString());
  18.     try {
  19.       await autoImportData(env);
  20.       console.log('定时更新完成');
  21.     } catch (error) {
  22.       console.error('定时更新失败!', error);
  23.     }
  24.   }
  25. };

  26. // 搜索逻辑
  27. async function handleSearch(request, env, url) {
  28.   const corsHeaders = {
  29.     'Access-Control-Allow-Origin': '*',
  30.     'Access-Control-Allow-Methods': 'GET, OPTIONS',
  31.     'Access-Control-Allow-Headers': 'Content-Type',
  32.   };

  33.   if (request.method === 'OPTIONS') {
  34.     return new Response(null, { headers: corsHeaders });
  35.   }

  36.   const query = url.searchParams.get('q');
  37.   const limit = parseInt(url.searchParams.get('limit') || '100');

  38.   try {
  39.     const startTime = Date.now();
  40.     const now = Date.now();

  41.         // 缓存检查
  42.         if (!cachedTitles || (now - cacheTime) > CACHE_TTL) {
  43.           let allTitlesData;
  44.           
  45.           try {
  46.                 allTitlesData = await env.ANIME_TITLES.get('all_titles');
  47.           } catch (error) {
  48.                 // KV 未绑定或访问失败
  49.                 return jsonResponse({
  50.                   error: 'KV 存储未配置或访问失败',
  51.                   detail: error.message,
  52.                   hint: '请检查 ANIME_TITLES KV 命名空间是否已正确绑定'
  53.                 }, 503, corsHeaders);
  54.           }
  55.           
  56.           if (!allTitlesData) {
  57.                 // 数据未初始化,自动导入
  58.                 console.log('检测到数据未初始化,开始自动导入……');
  59.                 try {
  60.                   await autoImportData(env);
  61.                   // 导入后重新获取数据
  62.                   const newData = await env.ANIME_TITLES.get('all_titles');
  63.                   cachedTitles = JSON.parse(newData);
  64.                   cacheTime = now;
  65.                 } catch (error) {
  66.                   return jsonResponse({
  67.                         error: '数据未初始化且自动导入失败',
  68.                         detail: error.message
  69.                   }, 503, corsHeaders);
  70.                 }
  71.           } else {
  72.                 cachedTitles = JSON.parse(allTitlesData);
  73.                 cacheTime = now;
  74.           }
  75.         }
  76.    
  77.     // 搜索逻辑
  78.     const searchLower = query.toLowerCase();
  79.    
  80.     // 一次遍历完成搜索和聚合
  81.     const results = Object.entries(cachedTitles)
  82.       .filter(([aid, titles]) => {
  83.         // 检查该 AID 下是否有任何标题匹配
  84.         return titles.some(t => t.title?.toLowerCase().includes(searchLower));
  85.       })
  86.       .slice(0, limit) // 限制数量
  87.       .map(([aid, titles]) => ({
  88.         aid: parseInt(aid),
  89.         titles: titles // 直接引用,无需重新构造
  90.       }));
  91.    
  92.     return jsonResponse({
  93.       query,
  94.       count: results.length,
  95.       searchTime: `${Date.now() - startTime}ms`,
  96.       cached: cacheTime !== now,
  97.       results
  98.     }, 200, corsHeaders);

  99.   } catch (error) {
  100.     return jsonResponse({ error: error.message }, 500, corsHeaders);
  101.   }
  102. }

  103. // 管理界面
  104. const COOKIE_NAME = 'AniDB_Updater_Auth';

  105. async function handleManagement(request, env, url) {
  106.   // 仅在进入管理逻辑时才解析 Cookie
  107.   const cookie = request.headers.get('Cookie');
  108.   const authCookie = cookie && cookie.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
  109.   const isLogged = (authCookie && authCookie[1] === env.PASSWORD);

  110.   // 1. 登录处理
  111.   if (url.pathname === '/login' && request.method === 'POST') {
  112.     const formData = await request.formData();
  113.     if (formData.get('password') === env.PASSWORD) {
  114.       const headers = new Headers();
  115.       headers.append('Set-Cookie', `${COOKIE_NAME}=${env.PASSWORD}; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict`);
  116.       headers.append('Location', '/');
  117.       return new Response(null, { status: 302, headers });
  118.     }
  119.     return new Response(null, { status: 302, headers: { 'Location': '/' } });
  120.   }

  121.   // 2. 一键导入处理
  122.   if (url.pathname === '/auto-import' && request.method === 'POST') {
  123.     if (!isLogged) return new Response('Unauthorized', { status: 401 });
  124.     try {
  125.       const count = await autoImportData(env);
  126.       return new Response(`自动导入完成,总条目数: ${count}`);
  127.     } catch (e) {
  128.       return new Response(`自动导入失败! ${e.message}`, { status: 500 });
  129.     }
  130.   }

  131.   // 3. 手动上传处理
  132.   if (request.method === 'POST') {
  133.     if (!isLogged) return new Response('Unauthorized', { status: 401 });
  134.     try {
  135.       const fileBuffer = await request.arrayBuffer();
  136.       if (!fileBuffer || fileBuffer.byteLength === 0) return new Response('无文件内容', { status: 400 });
  137.       
  138.       const count = await processAndSaveData(fileBuffer, env);
  139.       
  140.       // 上传成功后,强制清空搜索缓存
  141.       cachedTitles = null;
  142.       
  143.       return new Response(`更新完成,总条目数: ${count}`);
  144.     } catch (e) {
  145.       return new Response(`失败! ${e.message}`, { status: 500 });
  146.     }
  147.   }

  148.   // 4. 页面渲染
  149.   const html = isLogged ? await renderUploadUI(env) : renderLoginUI();
  150.   return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
  151. }

  152. // ================= 自动导入功能 =================

  153. async function autoImportData(env) {
  154.   // 检查上次更新日期
  155.   const metadata = await env.ANIME_TITLES.get('metadata');
  156.   if (metadata) {
  157.     const meta = JSON.parse(metadata);
  158.     const lastUpdate = new Date(meta.lastUpdate);
  159.     const now = new Date();
  160.    
  161.     // 获取日期部分(忽略时间)
  162.     const lastUpdateDate = lastUpdate.toISOString().split('T')[0];
  163.     const nowDate = now.toISOString().split('T')[0];
  164.    
  165.     // 如果是同一天,拒绝更新
  166.     if (lastUpdateDate === nowDate) {
  167.       throw new Error(`今日已更新(${lastUpdate.toLocaleString('zh-CN')}),请明日再试`);
  168.     }
  169.   }
  170.   
  171.   console.log('正在从 AniDB 获取数据……');
  172.   
  173.   // 从 AniDB 下载 .gz 文件,添加必要的请求头
  174.   const response = await fetch(ANIDB_URL, {
  175.     headers: {
  176.       'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0',
  177.       'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  178.       'Accept-Language': 'en-US,en;q=0.9',
  179.       'Accept-Encoding': 'gzip, deflate, br, zstd',
  180.       'Connection': 'keep-alive',
  181.       'Host': 'anidb.net',
  182.     },
  183.     cf: {
  184.       cacheTtl: 3600,
  185.       cacheEverything: true
  186.     }
  187.   });
  188.   
  189.   if (!response.ok) {
  190.     throw new Error(`下载失败! ${response.status} ${response.statusText}`);
  191.   }
  192.   
  193.   const buffer = await response.arrayBuffer();
  194.   console.log(`下载完成,文件大小: ${buffer.byteLength} 字节`);
  195.   
  196.   // 处理并保存数据
  197.   const count = await processAndSaveData(buffer, env);
  198.   
  199.   // 清空缓存
  200.   cachedTitles = null;
  201.   
  202.   console.log(`数据处理完成,总条目数: ${count}`);
  203.   return count;
  204. }

  205. // ================= 数据处理 (仅上传时调用) =================

  206. async function processAndSaveData(buffer, env) {
  207.   const ALLOWED_LANGS = new Set(['ja', 'en', 'x-jat', 'zh-Hans', 'zh-Hant']);
  208.   
  209.   // 定义语言优先级映射
  210.   const LANG_PRIORITY = {
  211.     'zh-Hans': 1,
  212.     'zh-Hant': 2,
  213.     'ja': 3,
  214.     'x-jat': 4,
  215.     'en': 5
  216.   };
  217.   
  218.   let text = '';
  219.   const view = new Uint8Array(buffer);
  220.   
  221.   if (view[0] === 0x1f && view[1] === 0x8b) {
  222.     try {
  223.       const ds = new DecompressionStream('gzip');
  224.       const stream = new Response(buffer).body.pipeThrough(ds);
  225.       const decompressed = await new Response(stream).arrayBuffer();
  226.       text = new TextDecoder('utf-8').decode(decompressed);
  227.     } catch (e) { throw new Error('解压失败!'); }
  228.   } else {
  229.     text = new TextDecoder('utf-8').decode(buffer);
  230.   }
  231.   
  232.   const titles = [];
  233.   const lines = text.trim().split('\n');
  234.   
  235.   for (const line of lines) {
  236.     if (line.startsWith('#')) continue;
  237.     const parts = line.split('|');
  238.     if (parts.length !== 4) continue;
  239.     const [aid, typeStr, language, title] = parts;
  240.     if (!aid || !title) continue;

  241.     const type = parseInt(typeStr);
  242.     if (type === 3) continue;

  243.     if (ALLOWED_LANGS.has(language)) {
  244.       titles.push({
  245.         aid: parseInt(aid),
  246.         type,
  247.         language,
  248.         title
  249.       });
  250.     }
  251.   }
  252.   
  253.   if (titles.length === 0) throw new Error('无有效数据!');

  254.   // 按 AID 分组
  255.   const groupedByAid = {};
  256.   for (const item of titles) {
  257.     const aid = item.aid.toString(); // 转为字符串作为 key
  258.     if (!groupedByAid[aid]) {
  259.       groupedByAid[aid] = [];
  260.     }
  261.     groupedByAid[aid].push({
  262.       type: item.type,
  263.       language: item.language,
  264.       title: item.title
  265.     });
  266.   }

  267.   // 对每个 AID 的标题按语言优先级排序
  268.   for (const aid in groupedByAid) {
  269.     groupedByAid[aid].sort((a, b) => {
  270.       const langA = LANG_PRIORITY[a.language] || 99;
  271.       const langB = LANG_PRIORITY[b.language] || 99;
  272.       return langA - langB;
  273.     });
  274.   }
  275.   
  276.   // 保存分组后的数据
  277.   await env.ANIME_TITLES.put('all_titles', JSON.stringify(groupedByAid));
  278.   await env.ANIME_TITLES.put('metadata', JSON.stringify({
  279.     lastUpdate: new Date().toISOString(),
  280.     totalTitles: titles.length,
  281.     totalAnime: Object.keys(groupedByAid).length,
  282.     source: 'auto'
  283.   }));
  284.   
  285.   return titles.length;
  286. }

  287. // ================= 辅助函数 =================

  288. function jsonResponse(data, status = 200, headers = {}) {
  289.   return new Response(JSON.stringify(data, null, 2), {
  290.     status,
  291.     headers: { ...headers, 'Content-Type': 'application/json; charset=utf-8' }
  292.   });
  293. }

  294. function renderLoginUI() {
  295.   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>`;
  296. }

  297. async function renderUploadUI(env) {
  298.   // 获取上次更新信息
  299.   let lastUpdateInfo = '';
  300.   try {
  301.     const metadata = await env.ANIME_TITLES.get('metadata');
  302.     if (metadata) {
  303.       const meta = JSON.parse(metadata);
  304.       const updateDate = new Date(meta.lastUpdate);
  305.       lastUpdateInfo = `<div class="info-box">上次更新: ${updateDate.toLocaleString('zh-CN')} | 总条目: ${meta.totalTitles.toLocaleString()}</div>`;
  306.     }
  307.   } catch (e) {
  308.     lastUpdateInfo = '<div class="info-box">暂无更新记录</div>';
  309.   }

  310.   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[type=file]{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[0]?e.target.files[0].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[0]});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>`;
  311. }
复制代码




回复

使用道具 举报

4

主题

69

回帖

208

VC币

中级会员

Rank: 3Rank: 3

积分
8455
SAOKiller 发表于 2026-1-11 00:43:55 | 显示全部楼层
可以,是我想要的功能,之前就总是先去找动画的罗马文和日文是什么,才去nyaa上搜索,这个就省事多了
回复

使用道具 举报

2

主题

25

回帖

10

VC币

高级会员

Rank: 4

积分
22340
HanTaNiA 发表于 2026-1-11 01:19:16 | 显示全部楼层
好帖帮顶
回复

使用道具 举报

35

主题

140

回帖

260

VC币

星辰大海

爱の探求者

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
401240

崭露头角活跃达人

waecy 发表于 2026-1-11 13:32:42 | 显示全部楼层
本帖最后由 waecy 于 2026-3-13 19:04 编辑

支持,现在用AI写游猴脚本,多加调教,试错反馈可以写成不少好用的功能
浏览次数统计/一键复制 / 一键批量打开 / 一键磁力导出 /一键BT文件下载/BT压缩包打包/表格导出…

以前还得花一天写样式,调试脚本,如今只要提需求就能写出来, 确实方便多了

先推荐个一键复制

NYAA复制磁力
https://greasyfork.org/zh-CN/scripts/530242


「ANCG美好,在于代入角色用心感受,感悟,理解不同世界和与众不同的生存方式。
当了解这段话时,这将成汝之宝藏。」
回复

使用道具 举报

35

主题

140

回帖

260

VC币

星辰大海

爱の探求者

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
401240

崭露头角活跃达人

waecy 发表于 2026-1-11 13:57:35 | 显示全部楼层
本帖最后由 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/

「ANCG美好,在于代入角色用心感受,感悟,理解不同世界和与众不同的生存方式。
当了解这段话时,这将成汝之宝藏。」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表