Feat: Three data sources as backup, JAVDB Cookie is now an optional configuration.

This commit is contained in:
Unknow committed 2026-03-21 00:44:28 +08:00
1 parent 1bebc39e16
commit 953e916d27
5 files changed
+274 -88

No files matched your search

+34 -1
View File
@@ -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);
+18
View File
@@ -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');
}
+4
View File
@@ -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('');
}
+217 -86
View File
@@ -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;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "javinfo",
"version": "1.0.5",
"version": "1.0.6",
"description": "AV番号命令行查询工具",
"main": "./index.js",
"type": "module",