hanjutv源增加极速版接口作为兜底 (#219)

* 优化弹幕点赞数显示,修复镂空爱心前数字样式异常;redis包修改成配置后动态引入

* hanjutv源增加tv端接口作为兜底
This commit is contained in:
huangxd- authored and GitHub committed 2026-03-03 23:40:32 +08:00
1 parent c01df2f483
commit 124e72a436
3 files changed
+193 -5

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ export const Globals = {
accessedEnvVars: {},
// 静态常量
VERSION: '1.15.4',
VERSION: '1.15.5',
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
MAX_ANIMES: 100,
MAX_RECORDS: 100, // 请求记录最大数量
+141 -4
View File
@@ -7,7 +7,7 @@ import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { titleMatches } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
import { createHanjutvUid, createHanjutvSearchHeaders, decodeHanjutvEncryptedPayload } from "../utils/hanjutv-util.js";
import { createHanjutvUid, createHanjutvSearchHeaders, decodeHanjutvEncryptedPayload, buildLiteHeaders } from "../utils/hanjutv-util.js";
// =====================
// 获取韩剧TV弹幕
@@ -17,6 +17,7 @@ export default class HanjutvSource extends BaseSource {
super();
this.webHost = "https://hxqapi.hiyun.tv";
this.appHost = "https://hxqapi.hiyun.tv";
this.tvHost = "https://api.xiawen.tv";
this.oldDanmuHost = "https://hxqapi.zmdcq.com";
this.defaultRefer = "2JGztvGjRVpkxcr0T4ZWG2k+tOlnHmDGUNMwAGSeq548YV2FMbs0h0bXNi6DJ00L";
this.webUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
@@ -91,7 +92,7 @@ export default class HanjutvSource extends BaseSource {
}
extractSearchItems(data) {
const list = data?.seriesData?.seriesList || data?.seriesList || [];
const list = data?.seriesData?.seriesList || data?.seriesList || data?.seriesData?.series || [];
return this.normalizeSearchItems(list);
}
@@ -210,6 +211,41 @@ export default class HanjutvSource extends BaseSource {
return this.extractSearchItems(resp?.data);
}
async searchWithTvApi(keyword) {
const q = encodeURIComponent(keyword);
const makeHeaders = await buildLiteHeaders(Date.now());
const headerInfo = await makeHeaders(Date.now());
const headers = headerInfo.headers;
const resp = await httpGet(`https://api.xiawen.tv/api/v1/aggregate/search?key=${q}&scope=101&page=1`, {
headers,
timeout: 10000,
retries: 1,
});
const payload = resp?.data;
if (!payload || typeof payload !== "object") {
throw new Error("tv 响应为空");
}
if (typeof payload.data === "string" && payload.data.length > 0) {
let decoded;
try {
decoded = await decodeHanjutvEncryptedPayload(payload, headerInfo.uid);
} catch (error) {
throw new Error(`tv 响应解密失败: ${error.message}`);
}
const items = this.extractSearchItems(decoded);
if (items.length === 0) throw new Error("tv 解密后无有效结果");
return items;
}
const plainItems = this.extractSearchItems(payload);
if (plainItems.length === 0) throw new Error("tv 无有效结果");
return plainItems;
}
async search(keyword) {
try {
const key = String(keyword || "").trim();
@@ -217,7 +253,9 @@ export default class HanjutvSource extends BaseSource {
let s5List = [];
let webList = [];
let tvList = [];
let s5Error = null;
let webError = null;
try {
s5List = await this.searchWithS5Api(key);
@@ -235,14 +273,31 @@ export default class HanjutvSource extends BaseSource {
try {
webList = await this.searchWithLegacyApi(key);
} catch (error) {
webError = error;
log("warn", `[Hanjutv] 旧搜索接口失败: ${error.message}`);
}
const webMatchedCount = this.countMatchedItems(webList, key);
const needTvSearch = webList.length === 0 || webMatchedCount === 0;
if (needTvSearch) {
if (!webError && webList.length > 0 && webMatchedCount === 0) {
log("warn", `[Hanjutv] web 返回 ${webList.length} 条但标题零命中,触发 TV 补偿检索`);
}
// 如果web接口也失败,则尝试TV端接口作为最终降级方案
try {
log("info", `[Hanjutv] 尝试TV端搜索接口作为降级方案`);
tvList = await this.searchWithTvApi(key);
} catch (tvError) {
log("warn", `[Hanjutv] TV端搜索接口也失败: ${tvError.message}`);
}
}
}
const { resultList, stats } = this.mergeSearchCandidates(key, s5List, webList);
const { resultList, stats } = this.mergeSearchCandidates(key, s5List, [...webList, ...tvList]);
if (resultList.length === 0) {
log("info", "hanjutvSearchresp: s5 与旧接口均无有效结果");
log("info", "hanjutvSearchresp: s5、旧接口和TV端接口均无有效结果");
return [];
}
@@ -293,6 +348,27 @@ export default class HanjutvSource extends BaseSource {
}
}
if (!detail) {
const makeHeaders = await buildLiteHeaders(Date.now());
const headerInfo = await makeHeaders(Date.now());
const headers = headerInfo.headers;
try {
const tvResp = await httpGet(`${this.tvHost}/api/v1/series/detail/query?sid=${sid}`, {
headers: headers,
timeout: 10000,
retries: 1,
});
const decoded = await decodeHanjutvEncryptedPayload(tvResp?.data, headerInfo.uid);
detail = decoded?.series || null;
} catch (error) {
log("error", "getHanjutvDetail error:", {
message: error.message,
name: error.name,
stack: error.stack,
});
}
}
if (!detail) {
log("info", "getHanjutvDetail: series 不存在");
return [];
@@ -370,6 +446,27 @@ export default class HanjutvSource extends BaseSource {
}
}
if (episodes.length === 0) {
const makeHeaders = await buildLiteHeaders(Date.now());
const headerInfo = await makeHeaders(Date.now());
const headers = headerInfo.headers;
try {
const tvResp = await httpGet(`${this.tvHost}/api/v1/series/detail/query?sid=${sid}`, {
headers: headers,
timeout: 10000,
retries: 1,
});
const decoded = await decodeHanjutvEncryptedPayload(tvResp?.data, headerInfo.uid);
episodes = this.normalizeEpisodes(decoded?.episodes || []);
} catch {
log("error", "getHanjutvDetail episodes:", {
message: error.message,
name: error.name,
stack: error.stack,
});
}
}
if (episodes.length === 0) {
log("info", "getHanjutvEposides: episodes 不存在");
return [];
@@ -452,8 +549,11 @@ export default class HanjutvSource extends BaseSource {
async getEpisodeDanmu(id) {
let allDanmus = [];
let prevId = 0;
let fromAxis = 0;
let pageCount = 0;
const maxAxis = 100000000;
const maxPages = 120;
try {
while (fromAxis < maxAxis) {
@@ -481,6 +581,43 @@ export default class HanjutvSource extends BaseSource {
fromAxis = nextAxis;
}
if (allDanmus.length === 0) {
while (fromAxis < maxAxis && pageCount < maxPages) {
const makeHeaders = await buildLiteHeaders(Date.now());
const headerInfo = await makeHeaders(Date.now());
const headers = headerInfo.headers;
try {
const tvResp = await httpGet(`${this.tvHost}/api/v1/bulletchat/episode/get?eid=${id}&prevId=${prevId}&fromAxis=${fromAxis}&toAxis=${maxAxis}&offset=0`, {
headers: headers,
timeout: 10000,
retries: 1,
});
const data = await decodeHanjutvEncryptedPayload(tvResp?.data, headerInfo.uid);
pageCount += 1;
allDanmus.push(...data.bulletchats);
const hasMore = Number(data.more ?? 0) === 1 || data.more === true || data.more === "1";
const nextAxis = Number(data.nextAxis ?? maxAxis);
const lastId = Number(data.lastId ?? prevId);
if (Number.isFinite(lastId) && lastId > prevId) prevId = lastId;
if (!Number.isFinite(nextAxis) || nextAxis <= fromAxis) break;
if (nextAxis >= maxAxis) break;
fromAxis = nextAxis;
if (!hasMore) prevId = 0;
} catch {
log("error", "fetchHanjutvEpisodeDanmu:", {
message: error.message,
name: error.name,
stack: error.stack,
});
}
}
}
return allDanmus;
} catch (error) {
// 捕获请求中的错误
+51
View File
@@ -1,5 +1,6 @@
import { md5, stringToUtf8Bytes, utf8BytesToString, bytesToBase64, base64ToBytes } from "./codec-util.js";
// 移动端参数
const HANJUTV_VERSION = "6.5.3";
const HANJUTV_VC = "a_7980";
const HANJUTV_UA = "HanjuTV/6.5.3 (Pixel 2 XL; Android 11; Scale/2.00)";
@@ -8,6 +9,12 @@ const HANJUTV_UK_IV = "d3w8hf94fidk38lk";
const HANJUTV_RESPONSE_SECRET = "34F9Q53w/HJW8E6Q";
const UID_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// TV版本参数
const UK_KEY = "f349wghhe784tqwh";
const UK_IV = "d3w8hf94fidk38lk";
const RESPONSE_SECRET = "34F9Q53w/HJW8E6Q";
const SAID = "fb3597b87601d5a7";
const SBOX = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
@@ -305,6 +312,13 @@ export function createHanjutvUid(length = 20) {
return uid;
}
function randomFrom(chars, len) {
let s = "";
for (let i = 0; i < len; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
}
// 移动端headers
export async function createHanjutvSearchHeaders(uid, timestamp = Date.now()) {
const ts = Number(timestamp);
const uidMd5 = md5(uid);
@@ -327,6 +341,43 @@ export async function createHanjutvSearchHeaders(uid, timestamp = Date.now()) {
};
}
// TV端headers
export async function buildLiteHeaders(sessionInitTs = Date.now()) {
const uid = randomFrom("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 20);
const oa = randomFrom("0123456789abcdef", 16);
return async function makeHeaders(reqTs = Date.now()) {
const uidMd5 = md5(uid);
const rpPayload = JSON.stringify({
emu: 0, ou: 0, it: sessionInitTs, iit: sessionInitTs, bs: 0, uid,
isp: "", pc: 0, tm: 50, d8m: "0,0,0,0,0,0,14,7", md: "23127PN0CC",
dn: "", osv: "16", br: 50, rpc: 0, scc: 1, plc: 1, toc: 5, tsc: 7,
ts: reqTs, nw: 2, px: "0", ai: SAID, oa, dpc: 0, dsc: 0, qpc: 0, apad: 0,
});
const di = await aesCbcEncryptToBase64(uid, UK_KEY, UK_IV);
const rp = await aesCbcEncryptToBase64(rpPayload, uidMd5.slice(0, 16), uidMd5.slice(16, 32));
return {
uid,
headers: {
version: "a_22570",
"version-name": "1.7.2",
channel: "xiaomi",
"app-type": "ztv",
"User-Agent": "ZTV/1.7.2 (23127PN0CC; Android 16; Scale/2.00)",
said: SAID,
di,
token: "",
uid: "",
rp,
"Accept-Encoding": "gzip",
Connection: "Keep-Alive",
},
};
};
}
export async function decodeHanjutvEncryptedPayload(payload, uid = "") {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
if (typeof payload.data !== "string" || payload.data.length === 0) return payload;