From b7afcf08dd20a43671fa7a30766360760c2e29bf Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Wed, 29 Oct 2025 23:57:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=BA=E4=BA=BA=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B0=83=E7=94=A8APP=20API=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=BC=B9=E5=B9=95=EF=BC=8C=E5=8E=9F=E5=85=88?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E4=BD=9C=E4=B8=BA=E5=85=9C=E5=BA=95=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 人人视频增加调用APP API方式请求弹幕,原先方式作为兜底 * 个别抽出数据模型 * 修改小版本号 --- danmu_api/apis/dandan-api.js | 76 +++++----- danmu_api/configs/globals.js | 2 +- danmu_api/models/dandan-model.js | 248 +++++++++++++++++++++++++++++++ danmu_api/sources/renren.js | 222 +++++++++++++++++++++++---- danmu_api/utils/cache-util.js | 4 +- danmu_api/utils/codec-util.js | 31 +++- danmu_api/utils/common-util.js | 17 +++ danmu_api/utils/http-util.js | 8 +- 8 files changed, 536 insertions(+), 72 deletions(-) create mode 100644 danmu_api/models/dandan-model.js diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 034ccd7..98a49bb 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -19,6 +19,7 @@ import MangoSource from "../sources/mango.js"; import BilibiliSource from "../sources/bilibili.js"; import YoukuSource from "../sources/youku.js"; import OtherSource from "../sources/other.js"; +import {Anime, AnimeMatch, Episodes, Bangumi} from "../models/dandan-model.js"; // ===================== // 兼容弹弹play接口 @@ -82,7 +83,7 @@ export async function searchAnime(url) { // 链接弹幕解析 const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:\d+)?(\/[^\s]*)?$/; if (urlRegex.test(queryTitle)) { - const tmpAnime = { + const tmpAnime = Anime.fromJson({ "animeId": 111, "bangumiId": "string", "animeTitle": queryTitle, @@ -93,7 +94,7 @@ export async function searchAnime(url) { "episodeCount": 1, "rating": 0, "isFavorited": true - }; + }); let platform = "unknown"; if (queryTitle.includes(".qq.com")) { @@ -116,7 +117,7 @@ export async function searchAnime(url) { "title": `【${platform}】 ${pageTitle}` }]; curAnimes.push(tmpAnime); - addAnime({...tmpAnime, links: links}); + addAnime(Anime.fromJson({...tmpAnime, links: links})); if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime(); // 如果有新的anime获取到,则更新redis @@ -160,7 +161,10 @@ export async function searchAnime(url) { }); // 解构出返回的结果 - const { vod: animesVodResults, 360: animes360, renren: animesRenren, hanjutv: animesHanjutv, bahamut: animesBahamut, tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili } = resultData; + const { + vod: animesVodResults, 360: animes360, renren: animesRenren, hanjutv: animesHanjutv, bahamut: animesBahamut, + tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili + } = resultData; // 按顺序处理每个来源的结果 for (const key of globals.sourceOrderArr) { @@ -482,7 +486,7 @@ export async function matchAnime(url, req) { if (resEpisode) { resData["isMatched"] = true; resData["matches"] = [ - { + AnimeMatch.fromJson({ "episodeId": resEpisode.episodeId, "animeId": resAnime.animeId, "animeTitle": resAnime.animeTitle, @@ -491,7 +495,7 @@ export async function matchAnime(url, req) { "typeDescription": resAnime.typeDescription, "shift": 0, "imageUrl": resAnime.imageUrl - } + }) ] } @@ -574,7 +578,7 @@ export async function searchEpisodes(url) { // 只有当过滤后还有集数时才添加到结果中 if (filteredEpisodes.length > 0) { - resultAnimes.push({ + resultAnimes.push(Episodes.fromJson({ animeId: animeItem.animeId, animeTitle: animeItem.animeTitle, type: animeItem.type, @@ -583,7 +587,7 @@ export async function searchEpisodes(url) { episodeId: ep.episodeId, episodeTitle: ep.episodeTitle })) - }); + })); } } } @@ -624,33 +628,6 @@ export async function getBangumi(path) { } log("info", `Fetched details for anime ID: ${idParam}`); - let resData = { - errorCode: 0, - success: true, - errorMessage: "", - bangumi: { - animeId: anime.animeId, - bangumiId: anime.bangumiId, - animeTitle: anime.animeTitle, - imageUrl: anime.imageUrl, - isOnAir: true, - airDay: 1, - isFavorited: anime.isFavorited, - rating: anime.rating, - type: anime.type, - typeDescription: anime.typeDescription, - seasons: [ - { - id: `season-${anime.animeId}`, - airDate: anime.startDate, - name: "Season 1", - episodeCount: anime.episodeCount, - }, - ], - episodes: [], - }, - }; - // 构建 episodes 列表 let episodesList = []; for (let i = 0; i < anime.links.length; i++) { @@ -681,9 +658,34 @@ export async function getBangumi(path) { } } - resData["bangumi"]["episodes"] = episodesList; + const bangumi = Bangumi.fromJson({ + animeId: anime.animeId, + bangumiId: anime.bangumiId, + animeTitle: anime.animeTitle, + imageUrl: anime.imageUrl, + isOnAir: true, + airDay: 1, + isFavorited: anime.isFavorited, + rating: anime.rating, + type: anime.type, + typeDescription: anime.typeDescription, + seasons: [ + { + id: `season-${anime.animeId}`, + airDate: anime.startDate, + name: "Season 1", + episodeCount: anime.episodeCount, + }, + ], + episodes: episodesList, + }); - return jsonResponse(resData); + return jsonResponse({ + errorCode: 0, + success: true, + errorMessage: "", + bangumi: bangumi + }); } // Extracted function for GET /api/v2/comment/:commentId diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index 55a4f12..7627df3 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -11,7 +11,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.6.2', + VERSION: '1.6.3', MAX_LOGS: 500, // 日志存储,最多保存 500 行 MAX_ANIMES: 100, MAX_LAST_SELECT_MAP: 100, diff --git a/danmu_api/models/dandan-model.js b/danmu_api/models/dandan-model.js new file mode 100644 index 0000000..880495f --- /dev/null +++ b/danmu_api/models/dandan-model.js @@ -0,0 +1,248 @@ +import { validateType } from "../utils/common-util.js"; + +// ===================== +// 数据模型:Anime +// ===================== +export class Anime { + constructor({ animeId = 111, bangumiId = "", animeTitle = "", type = "", + typeDescription = "", imageUrl = "", startDate = "", episodeCount = 1, + rating = 0, isFavorited = true, links = [] } = {}) { + // ---- 类型检查 ---- + validateType(animeId, "number"); + validateType(bangumiId, "string"); + validateType(animeTitle, "string"); + validateType(type, "string", "type"); + validateType(typeDescription, "string"); + validateType(imageUrl, "string"); + validateType(startDate, "string"); + validateType(episodeCount, "number"); + validateType(rating, "number"); + validateType(isFavorited, "boolean"); + validateType(links, "array"); + + // 直接解构并赋值给 this + Object.assign(this, { animeId, bangumiId, animeTitle, type, typeDescription, imageUrl, startDate, + episodeCount, rating, isFavorited, links }); + } + + // ---- 静态方法:从 JSON 创建 User 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new Anime(json); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { ...this }; + } +} + +// ===================== +// 数据模型:AnimeMatch +// ===================== +export class AnimeMatch { + constructor({ episodeId = 10001, animeId = 111, animeTitle = "", episodeTitle = "", + type = "", typeDescription = "", shift = 1, imageUrl = "" } = {}) { + // ---- 类型检查 ---- + validateType(episodeId, "number"); + validateType(animeId, "number"); + validateType(animeTitle, "string"); + validateType(episodeTitle, "string"); + validateType(type, "string", "type"); + validateType(typeDescription, "string"); + validateType(shift, "number"); + validateType(imageUrl, "string"); + + // 直接解构并赋值给 this + Object.assign(this, { episodeId, animeId, animeTitle, episodeTitle, type, typeDescription, shift, imageUrl }); + } + + // ---- 静态方法:从 JSON 创建 User 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new AnimeMatch(json); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { ...this }; + } +} + +// ===================== +// 数据模型:Episode +// ===================== +export class Episode { + constructor({ episodeId = "", episodeTitle = "" } = {}) { + this.episodeId = episodeId; + this.episodeTitle = episodeTitle; + } +} + +// Episode 的 toJson 方法 +Episode.prototype.toJson = function () { + return { + episodeId: this.episodeId, + episodeTitle: this.episodeTitle + }; +}; + +// ===================== +// 数据模型:Episodes +// ===================== +export class Episodes { + constructor({ animeId = 111, animeTitle = "", type = "", typeDescription = "", + episodes = [] } = {}) { + // ---- 类型检查 ---- + validateType(animeId, "number"); + validateType(animeTitle, "string"); + validateType(type, "string"); + validateType(typeDescription, "string"); + validateType(episodes, "array"); + + // 直接解构并赋值给 this + Object.assign(this, { animeId, animeTitle, type, typeDescription, + episodes: episodes.map(ep => new Episode(ep)) }); + } + + // ---- 静态方法:从 JSON 创建 Episodes 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new Episodes(json); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { + animeId: this.animeId, + animeTitle: this.animeTitle, + type: this.type, + typeDescription: this.typeDescription, + episodes: this.episodes.map(ep => ep.toJson()) + }; + } +} + +// ===================== +// 数据模型:Season +// ===================== +export class Season { + constructor({ id = "", airDate = "", name = "", episodeCount = 0 } = {}) { + validateType(id, "string"); + validateType(airDate, "string"); + validateType(name, "string"); + validateType(episodeCount, "number"); + + // 直接解构并赋值给 this + Object.assign(this, { id, airDate, name, episodeCount }); + } + + // ---- 静态方法:从 JSON 创建 Season 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new Season(json); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { ...this }; + } +} + +// ===================== +// 数据模型:BangumiEpisode +// ===================== +export class BangumiEpisode { + constructor({ seasonId = "", episodeId = "", episodeTitle = "", episodeNumber = "", + airDate = "" } = {}) { + validateType(seasonId, "string"); + validateType(episodeId, "number"); + validateType(episodeTitle, "string"); + validateType(episodeNumber, "string"); + validateType(airDate, "string"); + + // 直接解构并赋值给 this + Object.assign(this, { seasonId, episodeId, episodeTitle, episodeNumber, airDate }); + } + + // ---- 静态方法:从 JSON 创建 BangumiEpisode 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new BangumiEpisode(json); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { ...this }; + } +} + +// ===================== +// 数据模型:Bangumi +// ===================== +export class Bangumi { + constructor({ animeId = 111, bangumiId = "", animeTitle = "", imageUrl = "", + isOnAir = true, airDay = 1, isFavorited = true, rating = 0, + type = "", typeDescription = "", seasons = [], episodes = [] } = {}) { + validateType(animeId, "number"); + validateType(bangumiId, "string"); + validateType(animeTitle, "string"); + validateType(imageUrl, "string"); + validateType(isOnAir, "boolean"); + validateType(airDay, "number"); + validateType(isFavorited, "boolean"); + validateType(rating, "number"); + validateType(type, "string"); + validateType(typeDescription, "string"); + validateType(seasons, "array"); + validateType(episodes, "array"); + + // 将 seasons 转换为 Season 实例数组 + const seasonInstances = seasons.map(seasonData => Season.fromJson(seasonData)); + + // 直接解构并赋值给 this + Object.assign(this, { animeId, bangumiId, animeTitle, imageUrl, isOnAir, airDay, isFavorited, rating, + type, typeDescription, seasons: seasonInstances, episodes }); + } + + // ---- 静态方法:从 JSON 创建 Bangumi 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + + // 将 episodes 转换为 BangumiEpisode 实例数组 + const episodes = json.episodes.map(ep => BangumiEpisode.fromJson(ep)); + + // 创建 Bangumi 实例 + return new Bangumi({ ...json, episodes }); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { + animeId: this.animeId, + bangumiId: this.bangumiId, + animeTitle: this.animeTitle, + imageUrl: this.imageUrl, + isOnAir: this.isOnAir, + airDay: this.airDay, + isFavorited: this.isFavorited, + rating: this.rating, + type: this.type, + typeDescription: this.typeDescription, + seasons: this.seasons.map(season => season.toJson()), // 转换每个 season 为 JSON + episodes: this.episodes.map(ep => ep.toJson()) // 转换每个 episode 为 JSON + }; + } +} diff --git a/danmu_api/sources/renren.js b/danmu_api/sources/renren.js index aeeb082..fc0086e 100644 --- a/danmu_api/sources/renren.js +++ b/danmu_api/sources/renren.js @@ -2,7 +2,7 @@ import BaseSource from './base.js'; import { globals } from '../configs/globals.js'; import { log } from "../utils/log-util.js"; import { getPathname, httpGet, sortedQueryString, updateQueryString } from "../utils/http-util.js"; -import { autoDecode, createHmacSha256 } from "../utils/codec-util.js"; +import { autoDecode, createHmacSha256, generateRandomSid, generateSign, generateXCaSign } from "../utils/codec-util.js"; import { generateValidStartDate } from "../utils/time-util.js"; import { addAnime, removeEarliestAnime } from "../utils/cache-util.js"; import { titleMatches } from "../utils/common-util.js"; @@ -11,6 +11,157 @@ import { titleMatches } from "../utils/common-util.js"; // 获取人人视频弹幕 // ===================== export default class RenrenSource extends BaseSource { + API_CONFIG = { + SECRET_KEY: "cf65GPholnICgyw1xbrpA79XVkizOdMq", + SEARCH_HOST: "api.qwdjapp.com", + DRAMA_HOST: "api.zhimeisj.top", + DANMU_HOST: "static-dm.qwdjapp.com", + USER_AGENT: 'Mozilla/5.0 (Linux; Android 15; PJC110 Build/AP3A.240617.008; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.207 Mobile Safari/537.36 App/RRSPApp platform/android AppVersion/10.27.4' + }; + + generateAppCommonHeaders(timestamp, sign, xCaSign = null) { + const headers = { + 'User-Agent': this.API_CONFIG.USER_AGENT, + 'deviceId': 'fG1vO5jzBm22vJ5mfcCYGp2NrBii5SPysgiy%2FaUb63EOTrtXyXdxHm1cUajUR1zbszl62ApHyWc1GKZtH%2FbmF0UMZWgEetdDy9QVXd9WvPU%3D', + 'aliId': 'aPuaf9shK3QDAL6WwVdhc7cC', + 'umId': '380998657e22ed51b5a21f2b519aa5beod', + 'clientType': 'android_rrsp_xb_RRSP', + 't': timestamp.toString(), + 'sign': sign, + 'isAgree': '1', + 'cv': '10.27.4' + }; + + if (xCaSign) { + headers['x-ca-sign'] = xCaSign; + headers['x-ca-method'] = '1'; + } + + return headers; + } + + async searchAppContent(keyword, size = 15) { + try { + const timestamp = Date.now(); + const path = "/search/content"; + const queryParams = { + keywords: keyword, + size: size, + search_after: "", + order: "match", + isAgeLimit: false + }; + + const sign = generateSign(path, timestamp, queryParams, this.API_CONFIG.SECRET_KEY); + const queryString = Object.entries(queryParams) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join('&'); + const xCaSign = generateXCaSign(path, timestamp, queryString, this.API_CONFIG.SECRET_KEY); + + const headers = this.generateAppCommonHeaders(timestamp, sign, xCaSign); + headers['Host'] = this.API_CONFIG.SEARCH_HOST; + + + const resp = await httpGet(`https://${this.API_CONFIG.SEARCH_HOST}${path}?${queryString}`, { + headers: headers, + }); + + if (!resp.data) return []; + + const list = resp?.data?.data?.searchDramaList || []; + return list.map((item, idx) => ({ + provider: "renren", + mediaId: String(item.id), + title: String(item.title || "").replace(/<[^>]+>/g, "").replace(/:/g, ":"), + type: "tv_series", + season: null, + year: item.year, + imageUrl: item.cover, + episodeCount: item.episodeTotal, + currentEpisodeIndex: null, + })); + } catch (error) { + log("error", "getRenrenAppAnimes error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return []; + } + } + + async getAppDramaDetail(dramaId, episodeSid = "") { + try { + if (!episodeSid) episodeSid = generateRandomSid(); + + const timestamp = Date.now(); + const path = "/app/drama/page"; + const queryParams = { + isAgeLimit: false, + dramaId: dramaId, + episodeSid: episodeSid, + quality: "SD", + subtitle: 3, + hsdrOpen: 1, + hevcOpen: 1, + tria4k: 1 + }; + + const sign = generateSign(path, timestamp, queryParams, this.API_CONFIG.SECRET_KEY); + const queryString = Object.entries(queryParams) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join('&'); + + const headers = this.generateAppCommonHeaders(timestamp, sign); + headers['Host'] = this.API_CONFIG.DRAMA_HOST; + headers['ignore'] = 'false'; + + const resp = await httpGet(`https://${this.API_CONFIG.DRAMA_HOST}${path}?${queryString}`, { + headers: headers + }); + + if (!resp.data) return null; + + return resp.data; + } catch (error) { + log("error", "getRenrenAppDramaDetail error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return null; + } + } + + // ========== 弹幕API ========== + async getAppDanmu(episodeSid) { + try { + const timestamp = Date.now(); + const path = `/v1/produce/danmu/emo/EPISODE/${episodeSid}`; + + const sign = generateSign(path, timestamp, {}, this.API_CONFIG.SECRET_KEY); + const xCaSign = generateXCaSign(path, timestamp, "", this.API_CONFIG.SECRET_KEY); + + const headers = this.generateAppCommonHeaders(timestamp, sign, xCaSign); + headers['Host'] = this.API_CONFIG.DANMU_HOST; + + const resp = await httpGet(`https://${this.API_CONFIG.DANMU_HOST}${path}`, { + headers: headers + }); + + if (!resp.data) return null; + + return resp.data; + } catch (error) { + log("error", "getRenrenDramaDetail error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return null; + } + } + parseRRSPPFields(pField) { const parts = String(pField).split(","); const num = (i, cast, dft) => { try { return cast(parts[i]); } catch { return dft; } }; @@ -142,9 +293,14 @@ export default class RenrenSource extends BaseSource { const searchTitle = parsedKeyword.title; const searchSeason = parsedKeyword.season; - const lock = { value: false }; - const lastRequestTime = { value: 0 }; - let allResults = await this.performNetworkSearch(searchTitle, { lockRef: lock, lastRequestTimeRef: lastRequestTime, minInterval: 400 }); + let allResults = []; + // 先使用APP API进行搜索 + allResults = await this.searchAppContent(searchTitle); + if (allResults.length === 0) { + const lock = { value: false }; + const lastRequestTime = { value: 0 }; + allResults = await this.performNetworkSearch(searchTitle, { lockRef: lock, lastRequestTimeRef: lastRequestTime, minInterval: 400 }); + } if (searchSeason == null) return allResults; @@ -153,12 +309,17 @@ export default class RenrenSource extends BaseSource { } async getDetail(id) { - const url = `https://api.rrmj.plus/m-station/drama/page`; - const params = { hsdrOpen:0,isAgeLimit:0,dramaId:String(id),hevcOpen:1 }; - const resp = await this.renrenRequest("GET", url, params); - if (!resp.data) return null; - const decoded = autoDecode(resp.data); - return decoded?.data || null; + const resp = await this.getAppDramaDetail(String(id)); + if (!resp) { + const url = `https://api.rrmj.plus/m-station/drama/page`; + const params = { hsdrOpen:0,isAgeLimit:0,dramaId:String(id),hevcOpen:1 }; + const resp = await this.renrenRequest("GET", url, params); + if (!resp.data) return null; + const decoded = autoDecode(resp.data); + return decoded?.data || null; + } else { + return resp.data; + } } async getEpisodes(id) { @@ -228,24 +389,29 @@ export default class RenrenSource extends BaseSource { } async getEpisodeDanmu(id) { - const ClientProfile = { - user_agent: "Mozilla/5.0", - origin: "https://rrsp.com.cn", - referer: "https://rrsp.com.cn/", - }; - const url = `https://static-dm.rrmj.plus/v1/produce/danmu/EPISODE/${id}`; - const headers = { - "Accept": "application/json", - "User-Agent": ClientProfile.user_agent, - "Origin": ClientProfile.origin, - "Referer": ClientProfile.referer, - }; - const resp = await this.renrenHttpGet(url, { headers }); - if (!resp.data) return null; - const data = autoDecode(resp.data); - if (Array.isArray(data)) return data; - if (data?.data && Array.isArray(data.data)) return data.data; - return null; + const resp = await this.getAppDanmu(id); + if (!resp) { + const ClientProfile = { + user_agent: "Mozilla/5.0", + origin: "https://rrsp.com.cn", + referer: "https://rrsp.com.cn/", + }; + const url = `https://static-dm.rrmj.plus/v1/produce/danmu/EPISODE/${id}`; + const headers = { + "Accept": "application/json", + "User-Agent": ClientProfile.user_agent, + "Origin": ClientProfile.origin, + "Referer": ClientProfile.referer, + }; + const resp = await this.renrenHttpGet(url, { headers }); + if (!resp.data) return null; + const data = autoDecode(resp.data); + if (Array.isArray(data)) return data; + if (data?.data && Array.isArray(data.data)) return data.data; + return null; + } else { + return resp; + } } formatComments(comments) { diff --git a/danmu_api/utils/cache-util.js b/danmu_api/utils/cache-util.js index 85506f4..a3cbada 100644 --- a/danmu_api/utils/cache-util.js +++ b/danmu_api/utils/cache-util.js @@ -1,5 +1,6 @@ import { globals } from '../configs/globals.js'; import { log } from './log-util.js' +import { Anime } from "../models/dandan-model.js"; // ===================== // cache数据结构处理函数 @@ -140,6 +141,7 @@ export function findTitleById(id) { // 添加 anime 对象到 animes,并将其 links 添加到 episodeIds export function addAnime(anime) { + anime = Anime.fromJson(anime); try { // 确保 anime 有 links 属性且是数组 if (!anime.links || !Array.isArray(anime.links)) { @@ -161,7 +163,7 @@ export function addAnime(anime) { }); // 创建新的 anime 副本 - const animeCopy = { ...anime, links: newLinks }; + const animeCopy = Anime.fromJson({ ...anime, links: newLinks }); // 检查是否已存在相同 animeId 的 anime const existingAnimeIndex = globals.animes.findIndex(a => a.animeId === anime.animeId); diff --git a/danmu_api/utils/codec-util.js b/danmu_api/utils/codec-util.js index a150e34..2fa69d4 100644 --- a/danmu_api/utils/codec-util.js +++ b/danmu_api/utils/codec-util.js @@ -693,4 +693,33 @@ export function createHmacSha256(key, message) { const hmacBytes = sha256(String.fromCharCode(...oKeyPad) + String.fromCharCode(...innerHash)); return bytesToBase64(hmacBytes); -} \ No newline at end of file +} + +// ========== 生成随机SID ========== +export function generateRandomSid() { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + return Array.from({length: 12}, () => chars[Math.floor(Math.random() * chars.length)]).join(''); +} + +// ========== 签名生成函数 ========== +export function generateSign(path, timestamp, params, secretKey) { + let signStr = path + "t" + timestamp; + + if (params) { + const sortedKeys = Object.keys(params).sort(); + sortedKeys.forEach(key => { + signStr += key + params[key]; + }); + } + + signStr += secretKey; + return md5(signStr); + } + +// ========== X-CA-Sign 生成函数 ========== +export function generateXCaSign(path, timestamp, queryString, secretKey) { + let signStr = `GET\n*/*\ngzip\n\nx-ca-method:1\n${path}`; + if (queryString) signStr += `?${queryString}`; + + return createHmacSha256(secretKey, signStr); + } \ No newline at end of file diff --git a/danmu_api/utils/common-util.js b/danmu_api/utils/common-util.js index e24eb41..c482a84 100644 --- a/danmu_api/utils/common-util.js +++ b/danmu_api/utils/common-util.js @@ -225,4 +225,21 @@ export function titleMatches(title, query) { // 宽松模糊匹配 return String(title).includes(String(query)); } +} + +/** + * 数据类型校验 + * @param {string} value - 值 + * @param {string} expectedType - 期望类型 + * @param {string} fieldName - 参数名称 + */ +export function validateType(value, expectedType) { + const fieldName = value?.constructor?.name; // 获取字段名 + if (expectedType === "array") { + if (!Array.isArray(value)) { + throw new TypeError(`${fieldName} 必须是一个数组`); + } + } else if (typeof value !== expectedType) { + throw new TypeError(`${fieldName} 必须是 ${expectedType}`); + } } \ No newline at end of file diff --git a/danmu_api/utils/http-util.js b/danmu_api/utils/http-util.js index a551e1c..23a5fda 100644 --- a/danmu_api/utils/http-util.js +++ b/danmu_api/utils/http-util.js @@ -65,7 +65,7 @@ export async function httpGet(url, options = {}) { try { decodedData = await decompressedStream.text(); } catch (e) { - log("error", "[iOS模拟] 解压缩失败", e); + log("error", "[请求模拟] 解压缩失败", e); throw e; } @@ -110,14 +110,14 @@ export async function httpGet(url, options = {}) { // 检查是否是超时错误 if (error.name === 'AbortError') { - log("error", `[iOS模拟] 请求超时:`, error.message); + log("error", `[请求模拟] 请求超时:`, error.message); log("error", '详细诊断:'); log("error", '- URL:', url); log("error", '- 超时时间:', `${timeout}ms`); throw new Error(`Request timeout after ${timeout}ms`); } - log("error", `[iOS模拟] 请求失败:`, error.message); + log("error", `[请求模拟] 请求失败:`, error.message); log("error", '详细诊断:'); log("error", '- URL:', url); log("error", '- 错误类型:', error.name); @@ -170,7 +170,7 @@ export async function httpPost(url, body, options = {}) { }; } catch (error) { - log("error", `[iOS模拟] 请求失败:`, error.message); + log("error", `[请求模拟] 请求失败:`, error.message); log("error", '详细诊断:'); log("error", '- URL:', url); log("error", '- 错误类型:', error.name);