feat: 人人视频增加调用APP API方式请求弹幕,原先方式作为兜底 (#80)
* 人人视频增加调用APP API方式请求弹幕,原先方式作为兜底 * 个别抽出数据模型 * 修改小版本号
This commit is contained in:
1 parent
68e35e4f97
commit
b7afcf08dd
8 files changed
+536
-72
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+194
-28
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -693,4 +693,33 @@ export function createHmacSha256(key, message) {
|
||||
const hmacBytes = sha256(String.fromCharCode(...oKeyPad) + String.fromCharCode(...innerHash));
|
||||
|
||||
return bytesToBase64(hmacBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 生成随机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);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user