Feat: Three data sources as backup, JAVDB Cookie is now an optional configuration.
This commit is contained in:
1 parent
1bebc39e16
commit
953e916d27
5 files changed
+274
-88
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "javinfo",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "AV番号命令行查询工具",
|
||||
"main": "./index.js",
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user