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

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

  • TA的每日心情
    无聊
    2026-1-28 18:16
  • 签到天数: 82 天

    [LV.6]常住居民II

    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. }
    复制代码




    回复

    使用道具 举报

  • TA的每日心情
    开心
    6 天前
  • 签到天数: 118 天

    [LV.6]常住居民II

    6

    主题

    75

    回帖

    208

    VC币

    中级会员

    Rank: 3Rank: 3

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

    使用道具 举报

  • TA的每日心情
    无聊
    2026-2-16 14:25
  • 签到天数: 27 天

    [LV.4]偶尔看看III

    3

    主题

    33

    回帖

    10

    VC币

    高级会员

    Rank: 4

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

    使用道具 举报

  • TA的每日心情
    慵懒
    昨天 09:15
  • 签到天数: 2049 天

    [LV.Master]伴坛终老

    41

    主题

    157

    回帖

    260

    VC币

    星辰大海

    爱の探求者

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

    积分
    457047

    崭露头角活跃达人

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

    使用道具 举报

  • TA的每日心情
    慵懒
    昨天 09:15
  • 签到天数: 2049 天

    [LV.Master]伴坛终老

    41

    主题

    157

    回帖

    260

    VC币

    星辰大海

    爱の探求者

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

    积分
    457047

    崭露头角活跃达人

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

    使用道具 举报

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

    本版积分规则

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