diff --git a/index.js b/index.js index de6dc3a..90dd453 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,11 @@ #!/usr/bin/env node +import { createInterface } from 'readline'; import { program } from 'commander'; import { createRequire } from 'module'; import { search } from './lib/fetcher.js'; import { display } from './lib/display.js'; -import { clearCache } from './lib/cache.js'; +import { clearCache, setConfig } from './lib/cache.js'; import ora from 'ora'; import chalk from 'chalk'; @@ -18,8 +19,40 @@ program .argument('[番号]', '要查询的番号,例如: SSIS-001') .option('-r, --raw', '显示原始详细数据') .option('--clear-cache', '清空本地缓存') + .option('--setup', '配置JAVDB Cookie(可选,提高查询覆盖率)') .action(async (id, options) => { + if (options.setup) { + console.log(''); + console.log(chalk.yellow('=== 配置 JAVDB Cookie(可选)===')); + console.log(''); + console.log('不配置也可以正常使用,配置后覆盖率更高。'); + console.log(''); + console.log('获取步骤:'); + console.log(' 1. Chrome 打开 https://javdb.com 并登录账号'); + console.log(' 2. 安装插件 "Get cookies.txt LOCALLY"'); + console.log(' 3. 导出 Cookie 文件,找到 _jdb_session 那行'); + console.log(' 4. 复制最后一列的值粘贴到下面'); + console.log(''); + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + await new Promise((resolve) => { + rl.question(chalk.cyan('请粘贴 _jdb_session 的值(直接回车跳过): '), (session) => { + rl.close(); + if (!session.trim()) { + console.log(chalk.gray('\n已跳过,使用 JAVBUS + JavLibrary 作为数据源。')); + } else { + setConfig({ session: session.trim() }); + console.log(chalk.green('\n✅ 配置保存成功!')); + console.log(chalk.gray('保存位置: ~/.config/javinfo/config.json')); + } + console.log(''); + resolve(); + }); + }); + process.exit(0); + } + if (options.clearCache) { clearCache(); process.exit(0); diff --git a/lib/cache.js b/lib/cache.js index ddd1e10..c47aa54 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -52,4 +52,22 @@ export function clearCache() { writeFileSync(CACHE_FILE, '{}', 'utf-8'); console.log('缓存已清空'); } +} + +export function getConfig() { + const configFile = join(CACHE_DIR, 'config.json'); + if (!existsSync(configFile)) return {}; + try { + return JSON.parse(readFileSync(configFile, 'utf-8')); + } catch { + return {}; + } +} + +export function setConfig(data) { + if (!existsSync(CACHE_DIR)) { + mkdirSync(CACHE_DIR, { recursive: true }); + } + const configFile = join(CACHE_DIR, 'config.json'); + writeFileSync(configFile, JSON.stringify(data, null, 2), 'utf-8'); } \ No newline at end of file diff --git a/lib/display.js b/lib/display.js index b736e30..a9b5fbf 100644 --- a/lib/display.js +++ b/lib/display.js @@ -48,6 +48,10 @@ export function display(info, raw = false) { console.log(' ' + chalk.cyan('封面  ') + ' ' + chalk.underline.gray(info.coverUrl)); } + if (info.source) { + console.log(' ' + chalk.gray(`数据来源: ${info.source}`)); + } + console.log(DIVIDER); console.log(''); } \ No newline at end of file diff --git a/lib/fetcher.js b/lib/fetcher.js index 946b83a..6ee004a 100644 --- a/lib/fetcher.js +++ b/lib/fetcher.js @@ -1,68 +1,192 @@ import { execSync } from 'child_process'; import * as cheerio from 'cheerio'; -import { getCache, setCache } from './cache.js'; +import { getCache, setCache, getConfig } from './cache.js'; -const SESSION = decodeURIComponent('wJkH2678EN3cZzzL2Pqbyr158quff0CaGzuh5hRxUXXADcKOQm1zKdOnlwow3wDfL5AX2NeKne52BgUZT6JpBGp5prKgUmKyBReopEABv30rItmWnxowjJ04Mki%2BO0jQ3OO%2F4F2BYE7TTZ8GlgpA3kuesTAsX8bAcL0HqWF4LDRdMeo9JYqzDn%2FoPAUFxnmwXBieXtMXqCP8JnudI2vQ44jtIhFRvSrzf832gRMi7STwMG7K8O9%2BH3AoJfMKbuBomnLMFuBNBbPbB3SShbi3SBja8bX8nudBILUX0ZiJtD%2FEOl%2BN3r6dVPw5--do%2Bi7mvLi5xyZ0HZ--gZT%2Fl7%2BX9Vr3GjRNK7FXOw%3D%3D'); - -const COOKIE = `list_mode=h; theme=auto; locale=zh; over18=1; _jdb_session=${SESSION}`; -const BASE_URL = 'https://javdb.com'; - -// 每次请求都重新构造 cookie,避免 session 过期问题 -function fetchHtml(url) { +// ─── 通用请求函数 ──────────────────────────────────────── +function fetchHtml(url, cookie = '') { + const cookieHeader = cookie + ? `-H "Cookie: ${cookie}"` + : ''; const result = execSync( `curl -sL "${url}" \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" \ - -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" \ + -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" \ -H "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8" \ - -H "Cookie: ${COOKIE}"`, + ${cookieHeader}`, { timeout: 15000, maxBuffer: 1024 * 1024 * 10 } ); return result.toString(); } -export async function search(id) { - // 先查缓存 - const cached = getCache(id); - if (cached) return cached; +// ─── 数据源一:JAVBUS ──────────────────────────────────── +function searchJavbus(id) { + try { + const url = `https://www.javbus.com/${id}`; + const html = fetchHtml(url); + const $ = cheerio.load(html); - // 使用精确番号搜索(f=code 只搜番号,结果更准确) - const searchUrl = `${BASE_URL}/search?q=${encodeURIComponent(id)}&f=all`; - const searchHtml = fetchHtml(searchUrl); - const $ = cheerio.load(searchHtml); - - // 在搜索结果里找番号完全匹配的那一项 - let detailPath = null; - $('.movie-list .item a.box').each((_, el) => { - const titleEl = $(el).find('.video-title strong'); - const foundId = titleEl.text().trim().toUpperCase(); - if (foundId === id.toUpperCase()) { - detailPath = $(el).attr('href'); - return false; // 找到了就停止循环 + // 判断是否找到(404页面包含特定文字) + if ($('title').text().includes('404') || !$('.container .row').length) { + return null; } - }); - // 如果精确匹配没找到,退回到取第一个结果 - if (!detailPath) { - detailPath = $('.movie-list .item a.box').first().attr('href'); + const result = { + id, + source: 'JAVBUS', + title: $('h3').first().text().trim(), + actresses: [], + actors: [], + releaseDate: '', + duration: '', + studio: '', + label: '', + director: '', + series: '', + tags: [], + coverUrl: $('.screencap img').attr('src') || '', + score: '', + }; + + // 遍历详情字段 + $('.info p').each((_, el) => { + const text = $(el).text().trim(); + const labelEl = $(el).find('span.header'); + const label = labelEl.text().trim(); + + if (/發行日期|发行日期/.test(label)) { + result.releaseDate = text.replace(label, '').trim(); + } else if (/長度|长度/.test(label)) { + result.duration = text.replace(label, '').trim(); + } else if (/導演|导演/.test(label)) { + result.director = $(el).find('a').text().trim(); + } else if (/製作商|制作商/.test(label)) { + result.studio = $(el).find('a').text().trim(); + } else if (/發行商|发行商/.test(label)) { + result.label = $(el).find('a').text().trim(); + } else if (/系列/.test(label)) { + result.series = $(el).find('a').text().trim(); + } + }); + + // 类别 + $('span.genre a').each((_, el) => { + const tag = $(el).text().trim(); + if (tag) result.tags.push(tag); + }); + + // 女优 + $('.star-name a').each((_, el) => { + const name = $(el).text().trim(); + if (name) result.actresses.push(name); + }); + + return result.title ? result : null; + } catch { + return null; } - - if (!detailPath) return null; - - // 抓详情页 - const detailUrl = `${BASE_URL}${detailPath}`; - const detailHtml = fetchHtml(detailUrl); - const result = parseDetail(detailHtml, id); - - setCache(id, result); - return result; } -function parseDetail(html, queryId) { - const $ = cheerio.load(html); +// ─── 数据源二:JavLibrary ──────────────────────────────── +function searchJavlibrary(id) { + try { + // 中文版搜索页 + const searchUrl = `https://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${encodeURIComponent(id)}`; + const searchHtml = fetchHtml(searchUrl); + const $ = cheerio.load(searchHtml); + // 如果直接跳转到详情页(只有一个结果) + let detailHtml = searchHtml; + + // 如果是搜索结果列表,找第一个匹配的 + const firstResult = $('.videos .video a').first(); + if (firstResult.length) { + const detailPath = firstResult.attr('href'); + detailHtml = fetchHtml(`https://www.javlibrary.com/cn/${detailPath}`); + } + + const $d = cheerio.load(detailHtml); + + // 确认找到了正确页面 + if (!$d('#video_id .text').length) return null; + + const foundId = $d('#video_id .text').text().trim().toUpperCase(); + if (foundId !== id.toUpperCase()) return null; + + const result = { + id, + source: 'JavLibrary', + title: $d('#video_title h3').text().trim(), + actresses: [], + actors: [], + releaseDate: $d('#video_date .text').text().trim(), + duration: $d('#video_length .text').text().trim(), + studio: $d('#video_maker .text a').text().trim(), + label: $d('#video_label .text a').text().trim(), + director: $d('#video_director .text a').text().trim(), + series: $d('#video_series .text a').text().trim(), + tags: [], + coverUrl: $d('#video_jacket_img').attr('src') || '', + score: $d('#video_review .score').text().trim(), + }; + + // 类别 + $d('#video_genres .genre a').each((_, el) => { + const tag = $d(el).text().trim(); + if (tag) result.tags.push(tag); + }); + + // 演员 + $d('#video_cast .cast .star a').each((_, el) => { + const name = $d(el).text().trim(); + if (name) result.actresses.push(name); + }); + + return result.title ? result : null; + } catch { + return null; + } +} + +// ─── 数据源三:JAVDB(需要Cookie) ────────────────────── +function searchJavdb(id) { + try { + const config = getConfig(); + if (!config.session) return null; // 没配置Cookie就跳过 + + const session = decodeURIComponent(config.session); + const cookie = `locale=zh; over18=1; _jdb_session=${session}`; + + const searchUrl = `https://javdb.com/search?q=${encodeURIComponent(id)}&f=all`; + const searchHtml = fetchHtml(searchUrl, cookie); + const $ = cheerio.load(searchHtml); + + let detailPath = null; + $('.movie-list .item a.box').each((_, el) => { + const foundId = $(el).find('.video-title strong').text().trim().toUpperCase(); + if (foundId === id.toUpperCase()) { + detailPath = $(el).attr('href'); + return false; + } + }); + + if (!detailPath) { + detailPath = $('.movie-list .item a.box').first().attr('href'); + } + if (!detailPath) return null; + + const detailHtml = fetchHtml(`https://javdb.com${detailPath}`, cookie); + return parseJavdb(detailHtml, id); + } catch { + return null; + } +} + +function parseJavdb(html, queryId) { + const $ = cheerio.load(html); const result = { id: queryId, - title: '', + source: 'JAVDB', + title: $('strong.current-title').text().trim(), actresses: [], actors: [], releaseDate: '', @@ -72,63 +196,39 @@ function parseDetail(html, queryId) { director: '', series: '', tags: [], - coverUrl: '', - score: '', + coverUrl: $('.video-cover img').attr('src') || '', + score: $('.score .value').first().text().trim(), }; - // 标题 - result.title = $('h2.title strong.current-title').text().trim() || - $('strong.current-title').text().trim() || - $('h2.title').first().text().trim(); - - // 封面 - result.coverUrl = $('.video-cover img').attr('src') || ''; - - // 评分 - result.score = $('.score .value').first().text().trim(); - - // 遍历详情字段 $('.movie-panel-info .panel-block').each((_, el) => { - const labelText = $(el).find('strong').first().text().trim().replace(/:|:/g, ''); + const label = $(el).find('strong').first().text().replace(/:|:/g, '').trim(); const valueEl = $(el).find('.value'); const valueText = valueEl.text().trim(); - if (/日期|發行日期|发行日期/.test(labelText)) { - // 发售日期格式为 YYYY-MM-DD,用正则提取避免误匹配 - const dateMatch = valueText.match(/\d{4}-\d{2}-\d{2}/); - result.releaseDate = dateMatch ? dateMatch[0] : valueText; - - } else if (/時長|时长/.test(labelText)) { + if (/日期/.test(label)) { + const m = valueText.match(/\d{4}-\d{2}-\d{2}/); + result.releaseDate = m ? m[0] : valueText; + } else if (/時長|时长/.test(label)) { result.duration = valueText; - - } else if (/導演|导演/.test(labelText)) { + } else if (/導演|导演/.test(label)) { result.director = valueText; - - } else if (/片商/.test(labelText)) { + } else if (/片商/.test(label)) { result.studio = valueText; - - } else if (/發行商|发行商/.test(labelText)) { + } else if (/發行商|发行商/.test(label)) { result.label = valueText; - - } else if (/系列/.test(labelText)) { + } else if (/系列/.test(label)) { result.series = valueText; - - } else if (/類別|类别/.test(labelText)) { + } else if (/類別|类别/.test(label)) { valueEl.find('a').each((_, a) => { - const tag = $(a).text().trim(); - if (tag) result.tags.push(tag); + const t = $(a).text().trim(); + if (t) result.tags.push(t); }); - - } else if (/演員|演员/.test(labelText)) { + } else if (/演員|演员/.test(label)) { valueEl.find('a').each((_, a) => { const name = $(a).text().trim(); - // 排除男优(通常男优名字旁边有性别标记) - const isMale = $(a).find('.male').length > 0 || - $(a).parent().find('.male').length > 0; - if (name && !isMale) result.actresses.push(name); + if (name) result.actresses.push(name); }); - - } else if (/男優|男优/.test(labelText)) { + } else if (/男優|男优/.test(label)) { valueEl.find('a').each((_, a) => { const name = $(a).text().trim(); if (name) result.actors.push(name); @@ -136,5 +236,36 @@ function parseDetail(html, queryId) { } }); - return result; + return result.title ? result : null; +} + +// ─── 主入口:依次尝试三个数据源 ───────────────────────── +export async function search(id) { + const cached = getCache(id); + if (cached) return cached; + + let result = null; + + // 第一优先:JAVBUS(无需Cookie,最快) + result = searchJavbus(id); + if (result) { + setCache(id, result); + return result; + } + + // 第二优先:JavLibrary(无需Cookie,数据较全) + result = searchJavlibrary(id); + if (result) { + setCache(id, result); + return result; + } + + // 最后备用:JAVDB(需要Cookie,数据最全) + result = searchJavdb(id); + if (result) { + setCache(id, result); + return result; + } + + return null; } \ No newline at end of file diff --git a/package.json b/package.json index d6f1302..15fbfce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javinfo", - "version": "1.0.5", + "version": "1.0.6", "description": "AV番号命令行查询工具", "main": "./index.js", "type": "module",