hanjutv源增加极速版接口作为兜底 (#219)
* 优化弹幕点赞数显示,修复镂空爱心前数字样式异常;redis包修改成配置后动态引入 * hanjutv源增加tv端接口作为兜底
This commit is contained in:
1 parent
c01df2f483
commit
124e72a436
3 files changed
+193
-5
No files matched your search
@@ -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, // 请求记录最大数量
|
||||
|
||||
@@ -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) {
|
||||
// 捕获请求中的错误
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user